Skip to content

Commit f91f3c7

Browse files
committed
feat(frontend): add featured server option to MCP server forms and views
1 parent 8632f73 commit f91f3c7

File tree

11 files changed

+250
-147
lines changed

11 files changed

+250
-147
lines changed

services/frontend/src/components/admin/mcp-catalog/BasicInfoStep.vue

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { Badge } from '@/components/ui/badge'
1515
import { Button } from '@/components/ui/button'
1616
import { Alert, AlertDescription } from '@/components/ui/alert'
17+
import { Switch } from '@/components/ui/switch'
1718
import { X, Plus, CheckCircle } from 'lucide-vue-next'
1819
import type { BasicInfoFormData, McpCategory } from '@/views/admin/mcp-server-catalog/types'
1920
import { McpCategoriesCache } from '@/services/mcpCatalogService'
@@ -46,6 +47,18 @@ const localValue = computed({
4647
set: (value) => emit('update:modelValue', value)
4748
})
4849
50+
// Specific computed for featured field to handle boolean updates properly
51+
const featuredValue = computed({
52+
get: () => props.modelValue.featured,
53+
set: (value: boolean) => {
54+
const updatedValue = {
55+
...props.modelValue,
56+
featured: value
57+
}
58+
emit('update:modelValue', updatedValue)
59+
}
60+
})
61+
4962
// Check if data was auto-populated from GitHub
5063
const isAutoPopulated = computed(() => {
5164
return props.formData.github?.auto_populated || false
@@ -183,6 +196,22 @@ onMounted(() => {
183196
</p>
184197
</div>
185198

199+
<!-- Featured Server -->
200+
<div class="space-y-2">
201+
<div class="flex items-center space-x-3">
202+
<Switch
203+
id="featured"
204+
v-model="featuredValue"
205+
/>
206+
<Label for="featured" class="text-sm font-medium">
207+
{{ t('mcpCatalog.form.basic.featured.label') }}
208+
</Label>
209+
</div>
210+
<p class="text-xs text-muted-foreground">
211+
{{ t('mcpCatalog.form.basic.featured.description') }}
212+
</p>
213+
</div>
214+
186215
<!-- Author Information -->
187216
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
188217
<!-- Author Name -->

services/frontend/src/components/admin/mcp-catalog/McpServerAddFormWizard.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface McpServerAddFormData {
5757
organization: string
5858
license: string
5959
tags: string[]
60+
featured: boolean
6061
}
6162
}
6263
@@ -170,7 +171,8 @@ const formData = ref<McpServerAddFormData>({
170171
author_contact: '',
171172
organization: '',
172173
license: '',
173-
tags: []
174+
tags: [],
175+
featured: false
174176
}
175177
})
176178

services/frontend/src/components/admin/mcp-catalog/McpServerEditFormWizard.vue

Lines changed: 138 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ interface Props {
2222
initialData?: Partial<McpServerFormData>
2323
submitButtonText?: string
2424
cancelButtonText?: string
25+
serverId?: string
2526
}
2627
2728
const props = withDefaults(defineProps<Props>(), {
2829
mode: 'create',
2930
submitButtonText: '',
30-
cancelButtonText: ''
31+
cancelButtonText: '',
32+
serverId: ''
3133
})
3234
3335
// Emits
@@ -41,6 +43,10 @@ const emit = defineEmits<{
4143
const { t } = useI18n()
4244
const eventBus = useEventBus()
4345
46+
// Storage key for form drafts
47+
const FORM_DRAFTS_KEY = 'mcp_edit_drafts'
48+
const DRAFT_EXPIRY_HOURS = 24
49+
4450
// Form steps configuration
4551
const steps = [
4652
{
@@ -155,7 +161,8 @@ const formData = ref<McpServerFormData>({
155161
author_contact: '',
156162
organization: '',
157163
license: '',
158-
tags: []
164+
tags: [],
165+
featured: false
159166
},
160167
repository: {
161168
github_url: '',
@@ -337,7 +344,8 @@ const autoPopulateFromGitHub = (githubData: any) => {
337344
author_contact: githubData.owner?.email || githubData.author_contact || '',
338345
organization: githubData.owner?.type === 'Organization' ? githubData.owner.login : (githubData.organization || ''),
339346
license: githubData.license?.spdx_id || githubData.license || '',
340-
tags: githubData.topics || githubData.tags || []
347+
tags: githubData.topics || githubData.tags || [],
348+
featured: formData.value.basic.featured // Keep user selection for featured status
341349
},
342350
repository: {
343351
github_url: githubData.html_url || githubData.github_url || formData.value.github.github_url,
@@ -404,20 +412,118 @@ const parseInstallationMethods = (githubData: any): string[] => {
404412
return methods.length > 0 ? methods : ['manual']
405413
}
406414
407-
// Form persistence using event bus
408-
const saveFormData = () => {
409-
eventBus.emit('mcp-form-data-updated', {
410-
step: currentStep.value,
411-
data: formData.value
415+
// Draft management functions with proper typing
416+
interface FormDraft {
417+
data: McpServerFormData
418+
lastModified: string
419+
currentStep: number
420+
}
421+
422+
interface FormDrafts {
423+
[serverId: string]: FormDraft
424+
}
425+
426+
const getDrafts = (): FormDrafts => {
427+
return eventBus.getState<FormDrafts>(FORM_DRAFTS_KEY, {}) || {}
428+
}
429+
430+
const saveDraft = () => {
431+
if (!props.serverId) return
432+
433+
const drafts = getDrafts()
434+
drafts[props.serverId] = {
435+
data: formData.value,
436+
lastModified: new Date().toISOString(),
437+
currentStep: currentStep.value
438+
}
439+
440+
eventBus.setState(FORM_DRAFTS_KEY, drafts)
441+
442+
// Emit specific events for real-time updates
443+
eventBus.emit('mcp-edit-draft-updated', {
444+
serverId: props.serverId,
445+
data: formData.value,
446+
step: currentStep.value
412447
})
413448
}
414449
415-
const loadFormData = () => {
416-
// Try to load persisted form data
417-
// This would be implemented with localStorage or session storage
418-
// For now, we'll keep the default empty form
450+
const loadDraft = (): boolean => {
451+
if (!props.serverId) return false
452+
453+
const drafts = getDrafts()
454+
const draft = drafts[props.serverId]
455+
456+
if (draft) {
457+
// Check if draft is not expired
458+
const draftAge = Date.now() - new Date(draft.lastModified).getTime()
459+
const maxAge = DRAFT_EXPIRY_HOURS * 60 * 60 * 1000
460+
461+
if (draftAge < maxAge) {
462+
formData.value = draft.data
463+
currentStep.value = draft.currentStep || 0
464+
return true
465+
} else {
466+
// Remove expired draft
467+
clearDraft()
468+
}
469+
}
470+
471+
return false
472+
}
473+
474+
const clearDraft = () => {
475+
if (!props.serverId) return
476+
477+
const drafts = getDrafts()
478+
if (drafts[props.serverId]) {
479+
delete drafts[props.serverId]
480+
eventBus.setState(FORM_DRAFTS_KEY, drafts)
481+
482+
eventBus.emit('mcp-edit-draft-cleared', {
483+
serverId: props.serverId
484+
})
485+
}
486+
}
487+
488+
const cleanupExpiredDrafts = () => {
489+
const drafts = getDrafts()
490+
const maxAge = DRAFT_EXPIRY_HOURS * 60 * 60 * 1000
491+
let hasChanges = false
492+
493+
Object.keys(drafts).forEach(serverId => {
494+
const draft = drafts[serverId]
495+
if (draft && draft.lastModified) {
496+
const draftAge = Date.now() - new Date(draft.lastModified).getTime()
497+
498+
if (draftAge >= maxAge) {
499+
delete drafts[serverId]
500+
hasChanges = true
501+
}
502+
}
503+
})
504+
505+
if (hasChanges) {
506+
eventBus.setState(FORM_DRAFTS_KEY, drafts)
507+
}
419508
}
420509
510+
// Enhanced form data watcher for real-time storage
511+
watch(
512+
formData,
513+
() => {
514+
saveDraft()
515+
},
516+
{ deep: true }
517+
)
518+
519+
// Watch current step changes
520+
watch(
521+
currentStep,
522+
() => {
523+
saveDraft()
524+
}
525+
)
526+
421527
// Form submission
422528
const submitForm = async () => {
423529
try {
@@ -436,11 +542,28 @@ const submitForm = async () => {
436542
437543
// Lifecycle
438544
onMounted(() => {
439-
loadFormData()
545+
// Clean up expired drafts on mount
546+
cleanupExpiredDrafts()
547+
548+
// In edit mode, check for existing draft and clear it before loading initial data
549+
if (props.mode === 'edit' && props.serverId) {
550+
clearDraft() // Clear any existing draft for this server
551+
}
552+
553+
// Try to load draft (mainly for create mode or if user refreshed page)
554+
const draftLoaded = loadDraft()
555+
556+
// If no draft was loaded and we have initial data, use it
557+
if (!draftLoaded && props.initialData) {
558+
// The initialData watcher will handle this
559+
}
440560
})
441561
442562
onUnmounted(() => {
443-
saveFormData()
563+
// Save current state as draft when component unmounts (unless submitting)
564+
if (!isSubmitting.value) {
565+
saveDraft()
566+
}
444567
})
445568
</script>
446569

@@ -478,6 +601,7 @@ onUnmounted(() => {
478601
v-else
479602
v-model="formData[currentStepData.key]"
480603
:form-data="formData"
604+
@update:modelValue="(newValue: any) => formData[currentStepData.key] = newValue"
481605
/>
482606
</div>
483607

0 commit comments

Comments
 (0)