import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Radio } from '@/components/ui/radio'; import { toast } from '@/components/ui'; import { Icon } from '@/components/icon/Icon'; import { SortableTabsStrip, type SortableTabsStripItem } from '@/components/ui/sortable-tabs-strip'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { usePluginsStore, type PluginScope } from '@/stores/usePluginsStore'; type TabKey = 'npm' | 'path' | 'file'; interface AddPluginDialogProps { open: boolean; onOpenChange: (open: boolean) => void; defaultScope?: PluginScope; } const FILENAME_PATTERN = /^[a-z0-9][a-z0-9-_.]*\.(js|ts|mjs|cjs)$/; function parseOptions(raw: string): { ok: true; value?: Record } | { ok: false } { const trimmed = raw.trim(); if (trimmed === '') return { ok: true, value: undefined }; try { const parsed = JSON.parse(trimmed) as unknown; if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { return { ok: false }; } return { ok: true, value: parsed as Record }; } catch { return { ok: false }; } } export const AddPluginDialog: React.FC = ({ open, onOpenChange, defaultScope = 'user', }) => { const { t } = useI18n(); const createEntry = usePluginsStore((s) => s.createEntry); const createFile = usePluginsStore((s) => s.createFile); const [tab, setTab] = React.useState('npm'); const [spec, setSpec] = React.useState(''); const [optionsJson, setOptionsJson] = React.useState(''); const [fileName, setFileName] = React.useState(''); const [content, setContent] = React.useState(''); const [scope, setScope] = React.useState(defaultScope); const [submitting, setSubmitting] = React.useState(false); const resetForm = React.useCallback(() => { setSpec(''); setOptionsJson(''); setFileName(''); setContent(''); setScope(defaultScope); }, [defaultScope]); React.useEffect(() => { if (open) { setTab('npm'); resetForm(); } }, [open, resetForm]); const handleTabChange = (next: TabKey) => { if (next === tab) return; setTab(next); resetForm(); }; const optionsResult = React.useMemo(() => parseOptions(optionsJson), [optionsJson]); const optionsInvalid = !optionsResult.ok; const fileNameInvalid = tab === 'file' && fileName.trim() !== '' && !FILENAME_PATTERN.test(fileName.trim()); const specEmpty = (tab === 'npm' || tab === 'path') && spec.trim() === ''; const contentEmpty = tab === 'file' && content.trim() === ''; const fileNameEmpty = tab === 'file' && fileName.trim() === ''; const submitDisabled = submitting || optionsInvalid || (tab === 'npm' && specEmpty) || (tab === 'path' && specEmpty) || (tab === 'file' && (fileNameEmpty || fileNameInvalid || contentEmpty)); const handleSubmit = async () => { if (submitDisabled) return; setSubmitting(true); try { let result; if (tab === 'file') { result = await createFile({ fileName: fileName.trim(), content, scope }); } else { result = await createEntry({ spec: spec.trim(), options: optionsResult.ok ? optionsResult.value : undefined, scope, }); } if (result.ok) { toast.success(result.message || t('settings.plugins.toast.created')); if (result.reloadFailed) { toast.warning(t('settings.plugins.toast.reloadFailed')); } onOpenChange(false); } else { toast.error(result.message || t('settings.plugins.sidebar.toast.deleteFailed')); } } finally { setSubmitting(false); } }; const tabs = React.useMemo(() => [ { id: 'npm', label: t('settings.plugins.dialog.add.tab.npm') }, { id: 'path', label: t('settings.plugins.dialog.add.tab.path') }, { id: 'file', label: t('settings.plugins.dialog.add.tab.file') }, ], [t]); return ( { if (!next && submitting) return; onOpenChange(next); }} > {t('settings.plugins.dialog.add.title')} {t('settings.plugins.sidebar.empty.description')} handleTabChange(id as TabKey)} layoutMode="fit" variant="active-pill" activePillLowercase={false} className="h-10" />
{(tab === 'npm' || tab === 'path') && ( <>
setSpec(e.target.value)} placeholder={t('settings.plugins.page.field.spec.placeholder')} aria-invalid={specEmpty ? false : undefined} disabled={submitting} /> {specEmpty && (

{t('settings.plugins.validation.specRequired')}

)}