mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-21 05:53:46 +08:00
✨ feat(query-editor): 支持还原 SQL 美化前内容
- 美化 SQL 前保存最近一次原始内容快照 - 在格式设置菜单提供还原上次美化入口 - 持久化查询标签页的美化恢复快照 - 补充编辑器与状态恢复回归测试
This commit is contained in:
@@ -1527,6 +1527,43 @@ describe('QueryEditor external SQL save', () => {
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(storeState.updateQueryTabDraft).toHaveBeenCalledWith('tab-1', {
|
||||
formatRestoreSnapshot: {
|
||||
query: 'select * from users where id=1',
|
||||
createdAt: expect.any(Number),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('restores the last pre-beautify SQL snapshot after reopening a query tab', async () => {
|
||||
let renderer!: ReactTestRenderer;
|
||||
const originalSql = 'select * from users where id=1';
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<QueryEditor
|
||||
tab={createTab({
|
||||
query: 'SELECT\n *\nFROM\n users\nWHERE\n id = 1',
|
||||
formatRestoreSnapshot: {
|
||||
query: originalSql,
|
||||
createdAt: 123,
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const restoreButton = findButton(renderer, '还原上次美化');
|
||||
await act(async () => {
|
||||
await restoreButton.props.onClick();
|
||||
});
|
||||
|
||||
expect(editorState.value).toBe(originalSql);
|
||||
expect(storeState.updateQueryTabDraft).toHaveBeenCalledWith('tab-1', {
|
||||
query: originalSql,
|
||||
formatRestoreSnapshot: undefined,
|
||||
});
|
||||
expect(messageApi.success).toHaveBeenCalledWith('已还原到美化前 SQL');
|
||||
});
|
||||
|
||||
it('formats postgres window-function SQL with cast syntax through Monaco edits', async () => {
|
||||
|
||||
@@ -3948,12 +3948,22 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
? connectionsRef.current.find(c => c.id === tabConnectionId)
|
||||
: undefined);
|
||||
const formatterLanguage = resolveQueryEditorFormatterLanguage(conn);
|
||||
const formatted = format(getCurrentQuery(), { language: formatterLanguage, keywordCase: sqlFormatOptions.keywordCase });
|
||||
const sourceSql = getCurrentQuery();
|
||||
const formatted = format(sourceSql, { language: formatterLanguage, keywordCase: sqlFormatOptions.keywordCase });
|
||||
if (sourceSql === formatted) {
|
||||
return;
|
||||
}
|
||||
updateQueryTabDraft(tab.id, {
|
||||
formatRestoreSnapshot: {
|
||||
query: sourceSql,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
});
|
||||
const editor = editorRef.current;
|
||||
const monaco = monacoRef.current;
|
||||
const model = editor?.getModel?.();
|
||||
if (editor && monaco && model) {
|
||||
const currentValue = String(model.getValue?.() || '');
|
||||
const currentValue = String(model.getValue?.() || sourceSql);
|
||||
if (currentValue === formatted) {
|
||||
return;
|
||||
}
|
||||
@@ -3977,6 +3987,21 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestoreLastFormat = () => {
|
||||
const previousQuery = tab.formatRestoreSnapshot?.query;
|
||||
if (!previousQuery) {
|
||||
void message.info('没有可还原的美化前 SQL');
|
||||
return;
|
||||
}
|
||||
syncQueryToEditor(previousQuery);
|
||||
updateQueryTabDraft(tab.id, {
|
||||
query: previousQuery,
|
||||
formatRestoreSnapshot: undefined,
|
||||
});
|
||||
refreshObjectDecorations();
|
||||
void message.success('已还原到美化前 SQL');
|
||||
};
|
||||
|
||||
const handleAIAction = (action: 'generate' | 'explain' | 'optimize' | 'schema') => {
|
||||
const editor = editorRef.current;
|
||||
const selection = editor?.getModel()?.getValueInRange(editor.getSelection()) || '';
|
||||
@@ -4013,6 +4038,13 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
onClick: () => setSqlFormatOptions({ keywordCase: 'lower' })
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'restore-last-format',
|
||||
label: '还原上次美化',
|
||||
disabled: !tab.formatRestoreSnapshot?.query,
|
||||
onClick: handleRestoreLastFormat,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: 'snippet-settings',
|
||||
label: '代码片段管理...',
|
||||
|
||||
@@ -879,6 +879,10 @@ describe('store appearance persistence', () => {
|
||||
query: 'select * from orders where status = "paid";',
|
||||
connectionId: 'conn-2',
|
||||
dbName: 'reporting',
|
||||
formatRestoreSnapshot: {
|
||||
query: 'select * from orders where status="paid";',
|
||||
createdAt: 123,
|
||||
},
|
||||
});
|
||||
|
||||
const persisted = JSON.parse(storage.getItem('lite-db-storage') || '{}');
|
||||
@@ -890,6 +894,10 @@ describe('store appearance persistence', () => {
|
||||
connectionId: 'conn-2',
|
||||
dbName: 'reporting',
|
||||
query: 'select * from orders where status = "paid";',
|
||||
formatRestoreSnapshot: {
|
||||
query: 'select * from orders where status="paid";',
|
||||
createdAt: 123,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(persisted.state.activeTabId).toBe('query-tab-1');
|
||||
@@ -903,9 +911,19 @@ describe('store appearance persistence', () => {
|
||||
connectionId: 'conn-2',
|
||||
dbName: 'reporting',
|
||||
query: 'select * from orders where status = "paid";',
|
||||
formatRestoreSnapshot: {
|
||||
query: 'select * from orders where status="paid";',
|
||||
createdAt: 123,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
expect(reloaded.useStore.getState().activeTabId).toBe('query-tab-1');
|
||||
|
||||
reloaded.useStore.getState().updateQueryTabDraft('query-tab-1', {
|
||||
formatRestoreSnapshot: undefined,
|
||||
});
|
||||
|
||||
expect(reloaded.useStore.getState().tabs[0].formatRestoreSnapshot).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates activeContext when switching between tabs with different host or database', async () => {
|
||||
|
||||
@@ -1279,7 +1279,12 @@ interface AppState {
|
||||
draft: Partial<
|
||||
Pick<
|
||||
TabData,
|
||||
"query" | "connectionId" | "dbName" | "title" | "resultPanelVisible"
|
||||
| "query"
|
||||
| "connectionId"
|
||||
| "dbName"
|
||||
| "title"
|
||||
| "resultPanelVisible"
|
||||
| "formatRestoreSnapshot"
|
||||
>
|
||||
>,
|
||||
) => void;
|
||||
@@ -1483,6 +1488,15 @@ const sanitizeQueryTabs = (value: unknown): TabData[] => {
|
||||
const query = typeof raw.query === "string" ? raw.query.slice(0, MAX_PERSISTED_QUERY_LENGTH) : "";
|
||||
const filePath = toTrimmedString(raw.filePath);
|
||||
const savedQueryId = toTrimmedString(raw.savedQueryId);
|
||||
const rawFormatRestoreSnapshot =
|
||||
raw.formatRestoreSnapshot && typeof raw.formatRestoreSnapshot === "object"
|
||||
? (raw.formatRestoreSnapshot as Record<string, unknown>)
|
||||
: null;
|
||||
const formatRestoreQuery =
|
||||
typeof rawFormatRestoreSnapshot?.query === "string"
|
||||
? rawFormatRestoreSnapshot.query.slice(0, MAX_PERSISTED_QUERY_LENGTH)
|
||||
: "";
|
||||
const formatRestoreCreatedAt = Number(rawFormatRestoreSnapshot?.createdAt);
|
||||
if (!query.trim() && !filePath && !savedQueryId) return;
|
||||
|
||||
let id = toTrimmedString(raw.id, `query-${index + 1}`) || `query-${index + 1}`;
|
||||
@@ -1505,6 +1519,14 @@ const sanitizeQueryTabs = (value: unknown): TabData[] => {
|
||||
filePath: filePath || undefined,
|
||||
savedQueryId: savedQueryId || undefined,
|
||||
readOnly: raw.readOnly === true,
|
||||
formatRestoreSnapshot: formatRestoreQuery
|
||||
? {
|
||||
query: formatRestoreQuery,
|
||||
createdAt: Number.isFinite(formatRestoreCreatedAt)
|
||||
? formatRestoreCreatedAt
|
||||
: Date.now(),
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2560,6 +2582,30 @@ export const useStore = create<AppState>()(
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(draft, "formatRestoreSnapshot")) {
|
||||
const rawSnapshot = draft.formatRestoreSnapshot;
|
||||
const nextSnapshot =
|
||||
rawSnapshot && typeof rawSnapshot.query === "string"
|
||||
? {
|
||||
query: rawSnapshot.query.slice(0, MAX_PERSISTED_QUERY_LENGTH),
|
||||
createdAt: Number.isFinite(Number(rawSnapshot.createdAt))
|
||||
? Number(rawSnapshot.createdAt)
|
||||
: Date.now(),
|
||||
}
|
||||
: undefined;
|
||||
const currentSnapshot = nextTab.formatRestoreSnapshot;
|
||||
if (
|
||||
currentSnapshot?.query !== nextSnapshot?.query ||
|
||||
currentSnapshot?.createdAt !== nextSnapshot?.createdAt
|
||||
) {
|
||||
if (nextSnapshot?.query) {
|
||||
nextTab.formatRestoreSnapshot = nextSnapshot;
|
||||
} else {
|
||||
delete nextTab.formatRestoreSnapshot;
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return nextTab;
|
||||
});
|
||||
|
||||
@@ -436,6 +436,10 @@ export interface TabData {
|
||||
sidebarLocateKey?: string; // Precise sidebar tree key for locating an object node
|
||||
savedQueryId?: string; // Saved query identity for quick-save behavior
|
||||
objectType?: 'table' | 'view' | 'materialized-view'; // Table-like object type for shared viewers
|
||||
formatRestoreSnapshot?: {
|
||||
query: string;
|
||||
createdAt: number;
|
||||
}; // Last SQL content before beautify, for cross-session restore
|
||||
}
|
||||
|
||||
export interface JVMAIPlanContext {
|
||||
|
||||
Reference in New Issue
Block a user