import { useState, useMemo, useCallback } from 'react'; import Modal from './common/ResizableDraggableModal'; import { Button, Input, List, Tag, Popconfirm, message, Collapse, Typography } from 'antd'; import { PlusOutlined, DeleteOutlined, UndoOutlined, SaveOutlined, CodeOutlined, } from '@ant-design/icons'; import { v4 as uuidv4 } from 'uuid'; import type { SqlSnippet } from '../types'; import { useStore } from '../store'; import { useI18n } from '../i18n/provider'; import { BUILTIN_SNIPPET_MAP } from '../utils/sqlSnippetDefaults'; import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; interface SnippetSettingsModalProps { open: boolean; onClose: () => void; onBack?: () => void; darkMode: boolean; overlayTheme: OverlayWorkbenchTheme; embedded?: boolean; } type DraftSnippet = Omit & { createdAt?: number }; const emptyDraft = (): DraftSnippet => ({ id: uuidv4(), prefix: '', name: '', description: '', syntaxHelp: '', body: '', isBuiltin: false, }); export default function SnippetSettingsModal({ open, onClose, onBack, darkMode, overlayTheme, embedded = false, }: SnippetSettingsModalProps) { const { t } = useI18n(); const sqlSnippets = useStore((s) => s.sqlSnippets); const saveSqlSnippet = useStore((s) => s.saveSqlSnippet); const deleteSqlSnippet = useStore((s) => s.deleteSqlSnippet); const resetBuiltinSqlSnippet = useStore((s) => s.resetBuiltinSqlSnippet); const [selectedId, setSelectedId] = useState(null); const [draft, setDraft] = useState(emptyDraft()); const [isCreating, setIsCreating] = useState(false); const shellStyle = useMemo( () => ({ background: overlayTheme.shellBg, border: overlayTheme.shellBorder, boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter, }), [overlayTheme], ); const panelStyle = useMemo( () => ({ padding: 16, borderRadius: 14, border: overlayTheme.sectionBorder, background: overlayTheme.sectionBg, }), [overlayTheme], ); const textColor = darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(16,24,40,0.9)'; const mutedColor = darkMode ? 'rgba(255,255,255,0.5)' : 'rgba(16,24,40,0.55)'; const selectedBg = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)'; const newSnippetAction = t('snippet_settings.action.new'); const snippetModalBodyMaxHeight = 'calc(100vh - 128px)'; const snippetModalEmbeddedBodyMaxHeight = '100%'; const snippetSyntaxReferenceMaxHeight = 'min(220px, 32vh)'; const sortedSnippets = useMemo( () => [...sqlSnippets].sort((a, b) => a.prefix.localeCompare(b.prefix)), [sqlSnippets], ); const selectedSnippet = useMemo( () => sqlSnippets.find((s) => s.id === selectedId) ?? null, [sqlSnippets, selectedId], ); const handleSelect = useCallback( (snippet: SqlSnippet) => { setIsCreating(false); setSelectedId(snippet.id); setDraft({ ...snippet }); }, [], ); const handleNew = useCallback(() => { setIsCreating(true); setSelectedId(null); setDraft(emptyDraft()); }, []); const handleSave = useCallback(() => { const prefix = draft.prefix.toLowerCase().replace(/[^a-z0-9_]/g, '').slice(0, 20); if (!prefix) { void message.warning(t('snippet_settings.message.prefix_required')); return; } if (!draft.name.trim()) { void message.warning(t('snippet_settings.message.name_required')); return; } if (!draft.body.trim()) { void message.warning(t('snippet_settings.message.body_required')); return; } const duplicate = sqlSnippets.find( (s) => s.prefix.toLowerCase() === prefix && s.id !== draft.id, ); if (duplicate) { void message.warning(t('snippet_settings.message.prefix_duplicate', { prefix })); return; } const toSave: SqlSnippet = { id: draft.id, prefix, name: draft.name.trim(), description: draft.description?.trim() || undefined, syntaxHelp: draft.syntaxHelp?.trim() || undefined, body: draft.body, isBuiltin: draft.isBuiltin, createdAt: draft.createdAt ?? Date.now(), }; saveSqlSnippet(toSave); setSelectedId(toSave.id); setIsCreating(false); void message.success(t('snippet_settings.message.saved')); }, [draft, sqlSnippets, saveSqlSnippet, t]); const handleDelete = useCallback( (id: string) => { deleteSqlSnippet(id); if (selectedId === id) { setSelectedId(null); setDraft(emptyDraft()); } void message.success(t('snippet_settings.message.deleted')); }, [deleteSqlSnippet, selectedId, t], ); const handleReset = useCallback( (id: string) => { resetBuiltinSqlSnippet(id); const original = BUILTIN_SNIPPET_MAP[id]; if (original && selectedId === id) { setDraft({ ...original, syntaxHelp: original.syntaxHelp || '' }); } void message.success(t('snippet_settings.message.reset_default')); }, [resetBuiltinSqlSnippet, selectedId, t], ); const syntaxHelpItems = useMemo( () => [ { key: 'snippet-help', label: t('snippet_settings.syntax_help.label'), children: ( setDraft((d) => ({ ...d, syntaxHelp: e.target.value }))} placeholder={t('snippet_settings.syntax_help.placeholder')} maxLength={1000} autoSize={{ minRows: 4, maxRows: 8 }} style={{ fontSize: 12, resize: 'none', fontFamily: 'var(--gn-font-mono)', }} /> ), }, { key: 'syntax', label: t('snippet_settings.syntax_reference.label'), children: (
{t('snippet_settings.syntax_reference.first_tabstop')}
{t('snippet_settings.syntax_reference.second_tabstop')}
{t('snippet_settings.syntax_reference.final_cursor')}
{t('snippet_settings.syntax_reference.linked_tabstop')}
{t('snippet_settings.syntax_reference.builtin_variables')}
{t('snippet_settings.syntax_reference.current_date')}
{t('snippet_settings.syntax_reference.current_time')}
{t('snippet_settings.syntax_reference.unix_seconds')}
{t('snippet_settings.syntax_reference.uuid')}
{t('snippet_settings.syntax_reference.random')}
{t('snippet_settings.syntax_reference.example')}
), }, ], [draft.syntaxHelp, mutedColor, snippetSyntaxReferenceMaxHeight, t, textColor], ); const showEditor = isCreating || selectedSnippet; return (
{t('app.tools.entry.snippets.title')}
{t('app.tools.entry.snippets.description')}
)} open={open} embedded={embedded} closable={embedded ? false : undefined} onCancel={onClose} width={820} styles={{ content: shellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, paddingBottom: 0, display: 'flex', flexDirection: 'column', height: embedded ? '100%' : undefined, maxHeight: embedded ? snippetModalEmbeddedBodyMaxHeight : snippetModalBodyMaxHeight, minHeight: 0, overflow: 'hidden', }, }} footer={null} >
{/* Left: snippet list */}
{t('snippet_settings.list.title')}
( handleSelect(snippet)} style={{ cursor: 'pointer', padding: '6px 12px', background: selectedId === snippet.id ? selectedBg : 'transparent', borderLeft: selectedId === snippet.id ? `3px solid ${overlayTheme.iconBg}` : '3px solid transparent', transition: 'all 0.15s', }} >
{snippet.prefix} {snippet.name} {snippet.isBuiltin && ( {t('snippet_settings.tag.builtin')} )}
)} />
{/* Right: editor */}
{showEditor ? (
{t('snippet_settings.field.prefix.label')}
setDraft((d) => ({ ...d, prefix: e.target.value.toLowerCase() })) } placeholder={t('snippet_settings.field.prefix.placeholder')} maxLength={20} size="small" />
{t('snippet_settings.field.name.label')}
setDraft((d) => ({ ...d, name: e.target.value }))} placeholder={t('snippet_settings.field.name.placeholder')} maxLength={60} size="small" />
{t('snippet_settings.field.description.label')}
setDraft((d) => ({ ...d, description: e.target.value }))} placeholder={t('snippet_settings.field.description.placeholder')} maxLength={200} size="small" />
{t('snippet_settings.field.body.label')}
setDraft((d) => ({ ...d, body: e.target.value }))} placeholder={'SELECT ${1:columns} FROM ${2:table_name}$0;'} style={{ height: 220, minHeight: 160, maxHeight: 260, fontFamily: 'var(--gn-font-mono)', fontSize: 13, resize: 'none', }} />
) : (
{t('snippet_settings.empty_state', { action: newSnippetAction })}
)}
{showEditor && draft.isBuiltin && draft.createdAt && ( handleReset(draft.id)} > )} {showEditor && !draft.isBuiltin && !isCreating && ( handleDelete(draft.id)} > )} {showEditor && ( )} {onBack ? ( ) : null}
); }