Skip to content

Commit a2e1523

Browse files
author
Lasim
committed
Replace language column with category in MCP server table
- Add category display with dynamic icons in server table - Replace language column with category information - Add category fetching logic and state management - Include fallback text for servers without assigned categories - Add new translation key for "No category assigned"
1 parent d8026fb commit a2e1523

File tree

5 files changed

+688
-21
lines changed

5 files changed

+688
-21
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<script setup lang="ts">
2+
import { ref, watch, onMounted, shallowRef, type Component } from 'vue'
3+
import { Tag } from 'lucide-vue-next'
4+
5+
interface Props {
6+
/** Icon name to load (case-insensitive). Supports all Lucide icon names. */
7+
name?: string | null
8+
/** CSS classes for styling the icon */
9+
class?: string
10+
}
11+
12+
const props = withDefaults(defineProps<Props>(), {
13+
name: null,
14+
class: 'h-4 w-4'
15+
})
16+
17+
// Use shallowRef for better performance with components
18+
const IconComponent = shallowRef<Component>(Tag)
19+
const isLoading = ref<boolean>(false)
20+
21+
// Global cache to store loaded icons across all instances
22+
const globalIconCache = new Map<string, Component>()
23+
24+
/**
25+
* Generate icon name variations to try
26+
*/
27+
function getIconVariations(iconName: string): string[] {
28+
const clean = iconName.trim()
29+
30+
// Generate different naming patterns
31+
const variations = [
32+
clean, // Exact: "Database"
33+
clean.toLowerCase(), // Lowercase: "database"
34+
clean.charAt(0).toUpperCase() + clean.slice(1).toLowerCase(), // PascalCase: "Database"
35+
clean.charAt(0).toLowerCase() + clean.slice(1), // camelCase: "database"
36+
]
37+
38+
// Add kebab-case version for compound words
39+
const kebabCase = clean.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
40+
if (kebabCase !== clean.toLowerCase()) {
41+
variations.push(kebabCase) // "shopping-cart"
42+
}
43+
44+
// Add space-to-dash version
45+
const spaceToDash = clean.replace(/\s+/g, '-').toLowerCase()
46+
if (spaceToDash !== clean.toLowerCase() && !variations.includes(spaceToDash)) {
47+
variations.push(spaceToDash) // "globe-lock"
48+
}
49+
50+
// Add versions without spaces
51+
const noSpaces = clean.replace(/\s+/g, '').toLowerCase()
52+
if (noSpaces !== clean.toLowerCase() && !variations.includes(noSpaces)) {
53+
variations.push(noSpaces) // "globelock"
54+
}
55+
56+
// Add Icon suffix versions
57+
variations.push(clean + 'Icon') // "DatabaseIcon"
58+
variations.push(`Lucide${clean}`) // "LucideDatabase"
59+
60+
// Remove duplicates and return
61+
return [...new Set(variations)]
62+
}
63+
64+
/**
65+
* Load icon with proper error handling and no blocking
66+
*/
67+
async function loadIcon(iconName: string): Promise<void> {
68+
if (!iconName || iconName.trim() === '') {
69+
IconComponent.value = Tag
70+
return
71+
}
72+
73+
const trimmedName = iconName.trim()
74+
75+
// Check global cache first
76+
if (globalIconCache.has(trimmedName)) {
77+
const cachedIcon = globalIconCache.get(trimmedName)
78+
if (cachedIcon) {
79+
IconComponent.value = cachedIcon
80+
}
81+
return
82+
}
83+
84+
// Prevent multiple simultaneous loads of the same icon
85+
if (isLoading.value) return
86+
87+
isLoading.value = true
88+
89+
try {
90+
// Import the entire lucide-vue-next module
91+
const lucideModule = await import('lucide-vue-next') as Record<string, any>
92+
93+
// Try different name variations
94+
const variations = getIconVariations(trimmedName)
95+
let foundIcon: Component | null = null
96+
97+
for (const variation of variations) {
98+
if (lucideModule[variation]) {
99+
foundIcon = lucideModule[variation] as Component
100+
break
101+
}
102+
}
103+
104+
if (foundIcon) {
105+
// Cache the successful result globally
106+
globalIconCache.set(trimmedName, foundIcon)
107+
IconComponent.value = foundIcon
108+
} else {
109+
// Icon not found, cache and use fallback
110+
globalIconCache.set(trimmedName, Tag)
111+
IconComponent.value = Tag
112+
}
113+
} catch (error) {
114+
// Import failed, cache and use fallback
115+
globalIconCache.set(trimmedName, Tag)
116+
IconComponent.value = Tag
117+
} finally {
118+
isLoading.value = false
119+
}
120+
}
121+
122+
// Watch for prop changes
123+
watch(() => props.name, (newName: string | null) => {
124+
if (newName) {
125+
loadIcon(newName)
126+
} else {
127+
IconComponent.value = Tag
128+
}
129+
}, { immediate: false })
130+
131+
// Load initial icon
132+
onMounted(() => {
133+
if (props.name) {
134+
loadIcon(props.name)
135+
}
136+
})
137+
</script>
138+
139+
<template>
140+
<component
141+
:is="IconComponent"
142+
:class="props.class"
143+
:data-icon-name="props.name"
144+
:data-loading="isLoading"
145+
/>
146+
</template>

0 commit comments

Comments
 (0)