mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-21 22:14:02 +08:00
🐛 fix(sql-snippet): 修复片段管理编辑与按钮布局
- 支持自定义 SQL 片段按 id 更新,避免修改时重复新增 - 将片段语法说明改为可编辑并随片段持久化 - 将保存、删除、重置、关闭按钮统一收敛到底部操作栏 - 调整操作按钮为大号尺寸并增加最小宽度和底部间距 - 补充片段编辑、布局结构和持久化回归测试
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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('新说明:目标表、数据源、关联字段均可修改');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user