🐛 fix(sql-snippet): 修复片段管理编辑与按钮布局

- 支持自定义 SQL 片段按 id 更新,避免修改时重复新增
- 将片段语法说明改为可编辑并随片段持久化
- 将保存、删除、重置、关闭按钮统一收敛到底部操作栏
- 调整操作按钮为大号尺寸并增加最小宽度和底部间距
- 补充片段编辑、布局结构和持久化回归测试
This commit is contained in:
Syngnat
2026-06-15 17:28:48 +08:00
parent a611c1c04b
commit 891c8c1200
3 changed files with 135 additions and 69 deletions

View File

@@ -4367,7 +4367,14 @@ describe('QueryEditor external SQL save', () => {
const modalSource = readFileSync(new URL('./SnippetSettingsModal.tsx', import.meta.url), 'utf8');
const source = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
expect(modalSource).toContain('片段语法说明(可');
expect(modalSource).toContain('片段语法说明(可编辑');
expect(modalSource).toContain('data-sql-snippet-syntax-help-editor="true"');
expect(modalSource).toContain("defaultActiveKey={['snippet-help']}");
expect(modalSource).toContain('footer={null}');
expect(modalSource).toContain('data-sql-snippet-action-row="true"');
expect(modalSource).toContain('body: { paddingTop: 8, paddingBottom: 24 }');
expect(modalSource).toContain("size=\"large\"");
expect(modalSource).toContain('minWidth: 96');
expect(modalSource).toContain('syntaxHelp');
expect(modalSource).toContain('占位符语法参考');
expect(source).toContain('s.syntaxHelp || s.description || s.body');

View File

@@ -159,29 +159,51 @@ export default function SnippetSettingsModal({
[resetBuiltinSqlSnippet, selectedId],
);
const syntaxHelpItems = [
{
key: 'syntax',
label: '占位符语法参考',
children: (
<div style={{ fontSize: 12, lineHeight: 1.8, color: mutedColor, fontFamily: 'var(--gn-font-mono)' }}>
<div>{'${1:占位符} 第一个 Tab 位,占位符为提示文字'}</div>
<div>{'${2:默认值} 第二个 Tab 位,默认值可直接确认'}</div>
<div>{'$0 最终光标位置'}</div>
<div>{'${1:表名} 同一数字在多处出现时会同步编辑'}</div>
<div style={{ marginTop: 6, fontWeight: 600, color: textColor }}>{'内置变量(展开时自动替换为实际值):'}</div>
<div>{'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} 当前日期'}</div>
<div>{'${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} 当前时间'}</div>
<div>{'${CURRENT_SECONDS_UNIX} Unix 时间戳'}</div>
<div>{'${UUID} 随机 UUID'}</div>
<div>{'${RANDOM} 6 位随机数'}</div>
<div style={{ marginTop: 8, fontFamily: 'inherit', color: textColor }}>
{'示例SELECT ${1:列名} FROM ${2:表名} WHERE date >= \'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}\';$0'}
const syntaxHelpItems = useMemo(
() => [
{
key: 'snippet-help',
label: '片段语法说明(可编辑)',
children: (
<Input.TextArea
data-sql-snippet-syntax-help-editor="true"
value={draft.syntaxHelp || ''}
onChange={(e) => setDraft((d) => ({ ...d, syntaxHelp: e.target.value }))}
placeholder="展示在补全详情中的用法说明,例如占位符含义、参数约定或注意事项"
maxLength={1000}
autoSize={{ minRows: 4, maxRows: 8 }}
style={{
fontSize: 12,
resize: 'none',
fontFamily: 'var(--gn-font-mono)',
}}
/>
),
},
{
key: 'syntax',
label: '占位符语法参考',
children: (
<div style={{ fontSize: 12, lineHeight: 1.8, color: mutedColor, fontFamily: 'var(--gn-font-mono)' }}>
<div>{'${1:占位符} 第一个 Tab 位,占位符为提示文字'}</div>
<div>{'${2:默认值} 第二个 Tab 位,默认值可直接确认'}</div>
<div>{'$0 最终光标位置'}</div>
<div>{'${1:表名} 同一数字在多处出现时会同步编辑'}</div>
<div style={{ marginTop: 6, fontWeight: 600, color: textColor }}>{'内置变量(展开时自动替换为实际值):'}</div>
<div>{'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} 当前日期'}</div>
<div>{'${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND} 当前时间'}</div>
<div>{'${CURRENT_SECONDS_UNIX} Unix 时间戳'}</div>
<div>{'${UUID} 随机 UUID'}</div>
<div>{'${RANDOM} 6 位随机数'}</div>
<div style={{ marginTop: 8, fontFamily: 'inherit', color: textColor }}>
{'示例SELECT ${1:列名} FROM ${2:表名} WHERE date >= \'${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}\';$0'}
</div>
</div>
</div>
),
},
];
),
},
],
[draft.syntaxHelp, mutedColor, textColor],
);
const showEditor = isCreating || selectedSnippet;
@@ -217,14 +239,9 @@ export default function SnippetSettingsModal({
styles={{
content: shellStyle,
header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 },
body: { paddingTop: 8 },
footer: { background: 'transparent', borderTop: 'none', paddingTop: 40 },
body: { paddingTop: 8, paddingBottom: 24 },
}}
footer={[
<Button key="close" type="primary" onClick={onClose}>
</Button>,
]}
footer={null}
>
<div style={{ display: 'flex', gap: 16, minHeight: 420 }}>
{/* Left: snippet list */}
@@ -353,18 +370,6 @@ export default function SnippetSettingsModal({
/>
</div>
<div>
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}></div>
<Input.TextArea
value={draft.syntaxHelp || ''}
onChange={(e) => setDraft((d) => ({ ...d, syntaxHelp: e.target.value }))}
placeholder="展示在补全详情中的用法说明,例如占位符含义、参数约定或注意事项"
maxLength={1000}
autoSize={{ minRows: 2, maxRows: 4 }}
style={{ fontSize: 12, resize: 'none' }}
/>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<div style={{ fontSize: 12, color: mutedColor, marginBottom: 4 }}></div>
<Input.TextArea
@@ -381,38 +386,12 @@ export default function SnippetSettingsModal({
/>
<Collapse
size="small"
defaultActiveKey={['snippet-help']}
items={syntaxHelpItems}
style={{ marginTop: 8, background: 'transparent' }}
/>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 4 }}>
{draft.isBuiltin && draft.createdAt && (
<Popconfirm
title="重置为默认"
description="将恢复此内置片段的原始内容"
onConfirm={() => handleReset(draft.id)}
>
<Button icon={<UndoOutlined />} size="small">
</Button>
</Popconfirm>
)}
{!draft.isBuiltin && !isCreating && (
<Popconfirm
title="删除片段"
description="确定要删除此片段吗?"
onConfirm={() => handleDelete(draft.id)}
>
<Button danger icon={<DeleteOutlined />} size="small">
</Button>
</Popconfirm>
)}
<Button type="primary" icon={<SaveOutlined />} size="small" onClick={handleSave}>
</Button>
</div>
</div>
) : (
<div
@@ -430,6 +409,49 @@ export default function SnippetSettingsModal({
)}
</div>
</div>
<div
data-sql-snippet-action-row="true"
style={{
display: 'flex',
gap: 12,
justifyContent: 'flex-end',
alignItems: 'center',
paddingTop: 18,
marginTop: 18,
borderTop: overlayTheme.sectionBorder,
}}
>
{showEditor && draft.isBuiltin && draft.createdAt && (
<Popconfirm
title="重置为默认"
description="将恢复此内置片段的原始内容"
onConfirm={() => handleReset(draft.id)}
>
<Button icon={<UndoOutlined />} size="large" style={{ minWidth: 118 }}>
</Button>
</Popconfirm>
)}
{showEditor && !draft.isBuiltin && !isCreating && (
<Popconfirm
title="删除片段"
description="确定要删除此片段吗?"
onConfirm={() => handleDelete(draft.id)}
>
<Button danger icon={<DeleteOutlined />} size="large" style={{ minWidth: 96 }}>
</Button>
</Popconfirm>
)}
{showEditor && (
<Button type="primary" icon={<SaveOutlined />} size="large" style={{ minWidth: 96 }} onClick={handleSave}>
</Button>
)}
<Button size="large" style={{ minWidth: 96 }} onClick={onClose}>
</Button>
</div>
</Modal>
);
}

View File

@@ -1195,4 +1195,41 @@ describe('store appearance persistence', () => {
windows: { combo: 'Enter', enabled: true },
});
});
it('updates an existing custom SQL snippet by id and persists editable syntax help', async () => {
const { useStore } = await importStore();
const original = {
id: 'custom-merge',
prefix: 'mrg',
name: 'MERGE INTO',
description: 'Oracle merge 模板',
syntaxHelp: '旧说明',
body: 'MERGE INTO t USING s ON (t.id = s.id)$0',
isBuiltin: false,
createdAt: 1710000000000,
};
useStore.getState().saveSqlSnippet(original);
useStore.getState().saveSqlSnippet({
...original,
name: 'MERGE INTO 更新',
syntaxHelp: '新说明:目标表、数据源、关联字段均可修改',
body: 'MERGE INTO ${1:目标表} t USING ${2:源表} s ON (${3:关联条件})$0',
});
const snippets = useStore.getState().sqlSnippets.filter((s) => s.id === original.id);
expect(snippets).toHaveLength(1);
expect(snippets[0]).toMatchObject({
prefix: 'mrg',
name: 'MERGE INTO 更新',
syntaxHelp: '新说明:目标表、数据源、关联字段均可修改',
body: 'MERGE INTO ${1:目标表} t USING ${2:源表} s ON (${3:关联条件})$0',
isBuiltin: false,
});
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
const persistedSnippets = persisted.state.sqlSnippets.filter((s: { id: string }) => s.id === original.id);
expect(persistedSnippets).toHaveLength(1);
expect(persistedSnippets[0].syntaxHelp).toBe('新说明:目标表、数据源、关联字段均可修改');
});
});