mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-13 18:14:24 +08:00
🐛 fix(query-editor): 修复保存关闭后 SQL 恢复旧内容
- 保存关闭时重新读取当前编辑器中的最新 SQL - 保存成功后同步标签页快照并清理已落盘草稿 - 保存期间 SQL 再次变化时取消退出并保留新内容 - 补充已保存查询、新建查询及外部 SQL 文件回归测试
This commit is contained in:
@@ -226,7 +226,7 @@ import {
|
||||
import {
|
||||
buildApplicationQuitUnsavedSQLLabel,
|
||||
collectApplicationQuitUnsavedSQLTargets,
|
||||
saveApplicationQuitUnsavedSQLTargets,
|
||||
saveLatestApplicationQuitUnsavedSQLState,
|
||||
} from './utils/sqlEditorApplicationQuit';
|
||||
import { prepareApplicationQuitPersistence } from './utils/applicationQuitPersistence';
|
||||
import { flushQueryTabDraftSnapshots } from './utils/sqlFileTabDrafts';
|
||||
@@ -2894,7 +2894,19 @@ function App() {
|
||||
},
|
||||
onOk: async () => {
|
||||
try {
|
||||
await saveApplicationQuitUnsavedSQLTargets(targets, saveQuery);
|
||||
await saveLatestApplicationQuitUnsavedSQLState({
|
||||
getState: () => {
|
||||
const latestState = useStore.getState();
|
||||
return {
|
||||
tabs: latestState.tabs,
|
||||
savedQueries: latestState.savedQueries,
|
||||
};
|
||||
},
|
||||
updateTabs: (update) => {
|
||||
useStore.setState((state) => ({ tabs: update(state.tabs) }));
|
||||
},
|
||||
saveQuery,
|
||||
});
|
||||
message.success(t('app.quit.unsaved_sql.saved'));
|
||||
} catch (error) {
|
||||
cancelRequest();
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { SavedQuery, TabData } from '../types';
|
||||
import { clearQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from './sqlFileTabDrafts';
|
||||
import {
|
||||
clearQueryTabDraft,
|
||||
getQueryTabDraft,
|
||||
hasQueryTabDraft,
|
||||
setQueryTabDraft,
|
||||
setSQLFileTabDraft,
|
||||
} from './sqlFileTabDrafts';
|
||||
import {
|
||||
collectApplicationQuitUnsavedSQLTargets,
|
||||
saveLatestApplicationQuitUnsavedSQLState,
|
||||
saveApplicationQuitUnsavedSQLTargets,
|
||||
} from './sqlEditorApplicationQuit';
|
||||
|
||||
@@ -184,4 +191,149 @@ describe('sqlEditorApplicationQuit', () => {
|
||||
expect(savedQuery.id).toBe(tab.id);
|
||||
expect(targetsAfterRestart).toEqual([]);
|
||||
});
|
||||
|
||||
it('persists the latest saved-query draft into the restored tab before application quit', async () => {
|
||||
const savedQuery = createSavedQuery({ sql: 'select stale_snapshot;' });
|
||||
let tabs = [createQueryTab({
|
||||
id: 'tab-3',
|
||||
title: savedQuery.name,
|
||||
query: 'select stale_snapshot;',
|
||||
savedQueryId: savedQuery.id,
|
||||
})];
|
||||
const savedQueries: SavedQuery[] = [savedQuery];
|
||||
setQueryTabDraft('tab-3', 'select prompt_snapshot;');
|
||||
|
||||
const promptTargets = await collectApplicationQuitUnsavedSQLTargets(
|
||||
tabs,
|
||||
savedQueries,
|
||||
vi.fn(),
|
||||
);
|
||||
expect(promptTargets[0]).toMatchObject({ draft: 'select prompt_snapshot;' });
|
||||
|
||||
setQueryTabDraft('tab-3', 'select latest_before_click;');
|
||||
const saveQuery = vi.fn(async (query: SavedQuery) => query);
|
||||
|
||||
await saveLatestApplicationQuitUnsavedSQLState({
|
||||
getState: () => ({ tabs, savedQueries }),
|
||||
updateTabs: (update) => {
|
||||
tabs = update(tabs);
|
||||
},
|
||||
saveQuery,
|
||||
readSQLFile: vi.fn(),
|
||||
});
|
||||
|
||||
expect(saveQuery).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'saved-1',
|
||||
sql: 'select latest_before_click;',
|
||||
}));
|
||||
expect(tabs[0]).toMatchObject({
|
||||
id: 'tab-3',
|
||||
query: 'select latest_before_click;',
|
||||
savedQueryId: 'saved-1',
|
||||
});
|
||||
expect(hasQueryTabDraft('tab-3')).toBe(false);
|
||||
});
|
||||
|
||||
it('turns the latest unnamed query draft into the saved query restored after application quit', async () => {
|
||||
let tabs = [createQueryTab({
|
||||
id: 'tab-3',
|
||||
title: 'New query',
|
||||
query: 'select stale_snapshot;',
|
||||
})];
|
||||
setQueryTabDraft('tab-3', 'select latest_before_click;');
|
||||
const saveQuery = vi.fn(async (query: SavedQuery) => query);
|
||||
|
||||
await saveLatestApplicationQuitUnsavedSQLState({
|
||||
getState: () => ({ tabs, savedQueries: [] }),
|
||||
updateTabs: (update) => {
|
||||
tabs = update(tabs);
|
||||
},
|
||||
saveQuery,
|
||||
readSQLFile: vi.fn(),
|
||||
});
|
||||
|
||||
expect(saveQuery).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'tab-3',
|
||||
name: 'New query',
|
||||
sql: 'select latest_before_click;',
|
||||
}));
|
||||
expect(tabs[0]).toMatchObject({
|
||||
id: 'tab-3',
|
||||
query: 'select latest_before_click;',
|
||||
savedQueryId: 'tab-3',
|
||||
});
|
||||
expect(hasQueryTabDraft('tab-3')).toBe(false);
|
||||
});
|
||||
|
||||
it('persists the latest external SQL file draft into the restored tab after writing it', async () => {
|
||||
let tabs = [createQueryTab({
|
||||
id: 'tab-3',
|
||||
title: 'report.sql',
|
||||
filePath: '/tmp/report.sql',
|
||||
query: 'select stale_snapshot;',
|
||||
})];
|
||||
setSQLFileTabDraft('tab-3', 'select latest_before_click;');
|
||||
const saveQuery = vi.fn(async (query: SavedQuery) => query);
|
||||
const readSQLFile = vi.fn(async () => ({
|
||||
success: true,
|
||||
data: { content: 'select stale_snapshot;' },
|
||||
}));
|
||||
const writeSQLFile = vi.fn(async () => ({ success: true }));
|
||||
|
||||
await saveLatestApplicationQuitUnsavedSQLState({
|
||||
getState: () => ({ tabs, savedQueries: [] }),
|
||||
updateTabs: (update) => {
|
||||
tabs = update(tabs);
|
||||
},
|
||||
saveQuery,
|
||||
readSQLFile,
|
||||
writeSQLFile,
|
||||
});
|
||||
|
||||
expect(writeSQLFile).toHaveBeenCalledWith(
|
||||
'/tmp/report.sql',
|
||||
'select latest_before_click;',
|
||||
);
|
||||
expect(saveQuery).not.toHaveBeenCalled();
|
||||
expect(tabs[0]?.query).toBe('select latest_before_click;');
|
||||
expect(hasQueryTabDraft('tab-3')).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the newer draft and cancels quit when SQL changes while saving', async () => {
|
||||
const savedQuery = createSavedQuery({ sql: 'select stale_snapshot;' });
|
||||
let tabs = [createQueryTab({
|
||||
id: 'tab-3',
|
||||
title: savedQuery.name,
|
||||
query: 'select stale_snapshot;',
|
||||
savedQueryId: savedQuery.id,
|
||||
})];
|
||||
setQueryTabDraft('tab-3', 'select before_save;');
|
||||
|
||||
let releaseSave!: () => void;
|
||||
let markSaveStarted!: () => void;
|
||||
const saveStarted = new Promise<void>((resolve) => {
|
||||
markSaveStarted = resolve;
|
||||
});
|
||||
const saveQuery = vi.fn((query: SavedQuery) => new Promise<SavedQuery>((resolve) => {
|
||||
releaseSave = () => resolve(query);
|
||||
markSaveStarted();
|
||||
}));
|
||||
|
||||
const savePromise = saveLatestApplicationQuitUnsavedSQLState({
|
||||
getState: () => ({ tabs, savedQueries: [savedQuery] }),
|
||||
updateTabs: (update) => {
|
||||
tabs = update(tabs);
|
||||
},
|
||||
saveQuery,
|
||||
readSQLFile: vi.fn(),
|
||||
});
|
||||
await saveStarted;
|
||||
setQueryTabDraft('tab-3', 'select changed_during_save;');
|
||||
releaseSave();
|
||||
|
||||
await expect(savePromise).rejects.toThrow('SQL changed while saving: Saved query');
|
||||
expect(tabs[0]?.query).toBe('select stale_snapshot;');
|
||||
expect(getQueryTabDraft('tab-3')).toBe('select changed_during_save;');
|
||||
expect(hasQueryTabDraft('tab-3')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
isSQLFileQueryTab,
|
||||
normalizeSQLFileReadContent,
|
||||
} from './sqlFileTabDirty';
|
||||
import { getQueryTabDraft, getSQLFileTabDraft } from './sqlFileTabDrafts';
|
||||
import {
|
||||
clearQueryTabDraft,
|
||||
getQueryTabDraft,
|
||||
getSQLFileTabDraft,
|
||||
} from './sqlFileTabDrafts';
|
||||
|
||||
type QueryResultLike = {
|
||||
success?: boolean;
|
||||
@@ -45,6 +49,24 @@ export type ReadSQLFileForQuit = (filePath: string) => Promise<QueryResultLike>;
|
||||
export type WriteSQLFileForQuit = (filePath: string, content: string) => Promise<QueryResultLike>;
|
||||
export type SaveQueryForQuit = (query: SavedQuery) => Promise<SavedQuery>;
|
||||
|
||||
export type ApplicationQuitSavedSQLTarget = {
|
||||
target: ApplicationQuitUnsavedSQLTarget;
|
||||
savedQuery?: SavedQuery;
|
||||
};
|
||||
|
||||
type ApplicationQuitSQLStateSnapshot = {
|
||||
tabs: TabData[];
|
||||
savedQueries: SavedQuery[];
|
||||
};
|
||||
|
||||
type SaveLatestApplicationQuitUnsavedSQLStateArgs = {
|
||||
getState: () => ApplicationQuitSQLStateSnapshot;
|
||||
updateTabs: (update: (tabs: TabData[]) => TabData[]) => void;
|
||||
saveQuery: SaveQueryForQuit;
|
||||
readSQLFile?: ReadSQLFileForQuit;
|
||||
writeSQLFile?: WriteSQLFileForQuit;
|
||||
};
|
||||
|
||||
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
|
||||
|
||||
const resolveTabTitle = (tab: TabData, fallback: string): string =>
|
||||
@@ -146,27 +168,30 @@ export const saveApplicationQuitUnsavedSQLTargets = async (
|
||||
targets: ApplicationQuitUnsavedSQLTarget[],
|
||||
saveQuery: SaveQueryForQuit,
|
||||
writeSQLFile: WriteSQLFileForQuit = WriteSQLFile,
|
||||
): Promise<void> => {
|
||||
): Promise<ApplicationQuitSavedSQLTarget[]> => {
|
||||
const savedTargets: ApplicationQuitSavedSQLTarget[] = [];
|
||||
for (const target of targets) {
|
||||
if (target.kind === 'sql-file') {
|
||||
const res = await writeSQLFile(target.filePath, target.draft);
|
||||
if (!res?.success) {
|
||||
throw new Error(res?.message || target.filePath);
|
||||
}
|
||||
savedTargets.push({ target });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target.kind === 'saved-query') {
|
||||
await saveQuery({
|
||||
const savedQuery = await saveQuery({
|
||||
...target.savedQuery,
|
||||
sql: target.draft,
|
||||
connectionId: target.connectionId,
|
||||
dbName: target.dbName,
|
||||
});
|
||||
savedTargets.push({ target, savedQuery });
|
||||
continue;
|
||||
}
|
||||
|
||||
await saveQuery({
|
||||
const savedQuery = await saveQuery({
|
||||
// Keep the tab identity so a restored tab resolves this saved query on
|
||||
// later exits instead of creating a new history copy every time.
|
||||
id: target.tabId,
|
||||
@@ -176,5 +201,69 @@ export const saveApplicationQuitUnsavedSQLTargets = async (
|
||||
dbName: target.dbName,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
savedTargets.push({ target, savedQuery });
|
||||
}
|
||||
return savedTargets;
|
||||
};
|
||||
|
||||
export const reconcileApplicationQuitSavedSQLTargets = (
|
||||
tabs: TabData[],
|
||||
savedTargets: ApplicationQuitSavedSQLTarget[],
|
||||
): TabData[] => {
|
||||
const savedByTabId = new Map(
|
||||
savedTargets.map((savedTarget) => [savedTarget.target.tabId, savedTarget]),
|
||||
);
|
||||
return tabs.map((tab) => {
|
||||
const savedTarget = savedByTabId.get(tab.id);
|
||||
if (!savedTarget) return tab;
|
||||
|
||||
if (savedTarget.target.kind === 'sql-file') {
|
||||
return {
|
||||
...tab,
|
||||
query: savedTarget.target.draft,
|
||||
};
|
||||
}
|
||||
|
||||
const savedQuery = savedTarget.savedQuery;
|
||||
if (!savedQuery) return tab;
|
||||
return {
|
||||
...tab,
|
||||
title: savedQuery.name,
|
||||
query: savedQuery.sql,
|
||||
connectionId: savedQuery.connectionId,
|
||||
dbName: savedQuery.dbName,
|
||||
savedQueryId: savedQuery.id,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const saveLatestApplicationQuitUnsavedSQLState = async ({
|
||||
getState,
|
||||
updateTabs,
|
||||
saveQuery,
|
||||
readSQLFile = ReadSQLFile,
|
||||
writeSQLFile = WriteSQLFile,
|
||||
}: SaveLatestApplicationQuitUnsavedSQLStateArgs): Promise<ApplicationQuitSavedSQLTarget[]> => {
|
||||
const latestState = getState();
|
||||
const targets = await collectApplicationQuitUnsavedSQLTargets(
|
||||
latestState.tabs,
|
||||
latestState.savedQueries,
|
||||
readSQLFile,
|
||||
);
|
||||
const savedTargets = await saveApplicationQuitUnsavedSQLTargets(
|
||||
targets,
|
||||
saveQuery,
|
||||
writeSQLFile,
|
||||
);
|
||||
|
||||
const changedWhileSaving = savedTargets.find(({ target }) => (
|
||||
getQueryTabDraft(target.tabId, target.draft) !== target.draft
|
||||
));
|
||||
if (changedWhileSaving) {
|
||||
throw new Error(`SQL changed while saving: ${changedWhileSaving.target.title}`);
|
||||
}
|
||||
|
||||
updateTabs((tabs) => reconcileApplicationQuitSavedSQLTargets(tabs, savedTargets));
|
||||
savedTargets.forEach(({ target }) => clearQueryTabDraft(target.tabId));
|
||||
return savedTargets;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user