Files
MyGoNavi/frontend/src/utils/sqlEditorApplicationQuit.ts
Syngnat 1423ac0902 🐛 fix(sql-editor): 修复退出保存重复生成查询副本
- 退出保存临时查询时复用原查询页签 ID
- 重启后通过稳定 ID 识别已经保存的同一条查询
- 增加连续退出不重复生成历史副本的回归测试
2026-07-13 17:30:02 +08:00

181 lines
5.5 KiB
TypeScript

import type { SavedQuery, TabData } from '../types';
import { ReadSQLFile, WriteSQLFile } from '../../wailsjs/go/app/App';
import {
getSQLFileTabPath,
hasSQLFileTabUnsavedChanges,
isSQLFileMissingReadResult,
isSQLFileQueryTab,
normalizeSQLFileReadContent,
} from './sqlFileTabDirty';
import { getQueryTabDraft, getSQLFileTabDraft } from './sqlFileTabDrafts';
type QueryResultLike = {
success?: boolean;
message?: string;
data?: unknown;
};
export type ApplicationQuitUnsavedSQLTarget =
| {
kind: 'sql-file';
tabId: string;
title: string;
filePath: string;
draft: string;
}
| {
kind: 'saved-query';
tabId: string;
title: string;
savedQuery: SavedQuery;
draft: string;
connectionId: string;
dbName: string;
}
| {
kind: 'unsaved-query';
tabId: string;
title: string;
draft: string;
connectionId: string;
dbName: string;
};
export type ReadSQLFileForQuit = (filePath: string) => Promise<QueryResultLike>;
export type WriteSQLFileForQuit = (filePath: string, content: string) => Promise<QueryResultLike>;
export type SaveQueryForQuit = (query: SavedQuery) => Promise<SavedQuery>;
const toTrimmedString = (value: unknown): string => String(value ?? '').trim();
const resolveTabTitle = (tab: TabData, fallback: string): string =>
toTrimmedString(tab.title) || fallback;
const resolveSavedQueryForTab = (
tab: TabData,
savedQueries: SavedQuery[],
): SavedQuery | null => {
if (tab.type !== 'query' || isSQLFileQueryTab(tab)) return null;
const savedId = toTrimmedString(tab.savedQueryId) || toTrimmedString(tab.id);
if (!savedId) return null;
return savedQueries.find((query) => query.id === savedId) || null;
};
const hasSavedQueryUnsavedChanges = (
tab: TabData,
savedQuery: SavedQuery,
draft: string,
): boolean => {
const connectionId = toTrimmedString(tab.connectionId || savedQuery.connectionId);
const dbName = toTrimmedString(tab.dbName || savedQuery.dbName);
return draft !== String(savedQuery.sql ?? '')
|| connectionId !== toTrimmedString(savedQuery.connectionId)
|| dbName !== toTrimmedString(savedQuery.dbName);
};
export const buildApplicationQuitUnsavedSQLLabel = (
targets: ApplicationQuitUnsavedSQLTarget[],
): string => {
if (targets.length === 0) return '';
if (targets.length === 1) return targets[0].title;
return String(targets.length);
};
export const collectApplicationQuitUnsavedSQLTargets = async (
tabs: TabData[],
savedQueries: SavedQuery[],
readSQLFile: ReadSQLFileForQuit = ReadSQLFile,
): Promise<ApplicationQuitUnsavedSQLTarget[]> => {
const targets: ApplicationQuitUnsavedSQLTarget[] = [];
for (const tab of tabs) {
if (tab.type !== 'query') continue;
if (isSQLFileQueryTab(tab)) {
const filePath = getSQLFileTabPath(tab);
const draft = getSQLFileTabDraft(tab.id, String(tab.query ?? ''));
const title = resolveTabTitle(tab, filePath);
try {
const res = await readSQLFile(filePath);
if (res?.success) {
if (hasSQLFileTabUnsavedChanges({ ...tab, query: draft }, normalizeSQLFileReadContent(res.data))) {
targets.push({ kind: 'sql-file', tabId: tab.id, title, filePath, draft });
}
continue;
}
if (isSQLFileMissingReadResult(res)) {
targets.push({ kind: 'sql-file', tabId: tab.id, title, filePath, draft });
continue;
}
throw new Error(res?.message || filePath);
} catch {
targets.push({ kind: 'sql-file', tabId: tab.id, title, filePath, draft });
}
continue;
}
const draft = getQueryTabDraft(tab.id, String(tab.query ?? ''));
const savedQuery = resolveSavedQueryForTab(tab, savedQueries);
if (!savedQuery) {
if (!draft.trim()) continue;
targets.push({
kind: 'unsaved-query',
tabId: tab.id,
title: resolveTabTitle(tab, 'SQL Query'),
draft,
connectionId: toTrimmedString(tab.connectionId),
dbName: toTrimmedString(tab.dbName),
});
continue;
}
if (!hasSavedQueryUnsavedChanges(tab, savedQuery, draft)) continue;
targets.push({
kind: 'saved-query',
tabId: tab.id,
title: resolveTabTitle(tab, savedQuery.name),
savedQuery,
draft,
connectionId: toTrimmedString(tab.connectionId || savedQuery.connectionId),
dbName: toTrimmedString(tab.dbName || savedQuery.dbName),
});
}
return targets;
};
export const saveApplicationQuitUnsavedSQLTargets = async (
targets: ApplicationQuitUnsavedSQLTarget[],
saveQuery: SaveQueryForQuit,
writeSQLFile: WriteSQLFileForQuit = WriteSQLFile,
): Promise<void> => {
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);
}
continue;
}
if (target.kind === 'saved-query') {
await saveQuery({
...target.savedQuery,
sql: target.draft,
connectionId: target.connectionId,
dbName: target.dbName,
});
continue;
}
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,
name: target.title,
sql: target.draft,
connectionId: target.connectionId,
dbName: target.dbName,
createdAt: Date.now(),
});
}
};