mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-09 14:31:35 +08:00
🐛 fix(definition-viewer): 复用对象修改原标签页
- 查看页对象修改直接复用当前标签切换到 object-edit 编辑态 - 侧边栏视图和函数编辑入口统一补齐 queryMode 标记 - 补充 DefinitionViewer 与 store 回归测试锁定标签复用行为
This commit is contained in:
@@ -124,6 +124,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
|
||||
expect(storeState.setActiveContext).toHaveBeenCalledWith({ connectionId: 'conn-1', dbName: 'main' });
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'view-def-conn-1-main-reporting.active_users',
|
||||
title: 'Edit View: reporting.active_users',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
@@ -187,6 +188,47 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
});
|
||||
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: 'Edit Function/procedure: reporting.refresh_stats',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('CREATE OR REPLACE FUNCTION reporting.refresh_stats()'),
|
||||
}));
|
||||
});
|
||||
|
||||
it('reuses the displayed routine definition when opening object edit without refetching', async () => {
|
||||
backendApp.DBQuery.mockResolvedValue({
|
||||
success: true,
|
||||
data: [{ routine_definition: 'CREATE OR REPLACE FUNCTION reporting.refresh_stats() RETURNS void AS $$ BEGIN END; $$ LANGUAGE plpgsql;' }],
|
||||
});
|
||||
|
||||
let renderer: any;
|
||||
await act(async () => {
|
||||
renderer = create(renderWithI18n(createTab({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: '函数: reporting.refresh_stats',
|
||||
type: 'routine-def',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'FUNCTION',
|
||||
viewName: undefined,
|
||||
viewKind: undefined,
|
||||
})));
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
backendApp.DBQuery.mockClear();
|
||||
backendApp.DBQuery.mockImplementation(() => new Promise(() => undefined));
|
||||
|
||||
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
|
||||
|
||||
await act(async () => {
|
||||
button.props.onClick();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQuery).not.toHaveBeenCalled();
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: 'Edit Function/procedure: reporting.refresh_stats',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
@@ -225,6 +267,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
|
||||
const query = storeState.addTab.mock.calls[0][0].query;
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'event-def-conn-1-main-daily_cleanup',
|
||||
title: 'Edit Event: daily_cleanup',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
@@ -377,6 +420,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
});
|
||||
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'sequence-def-conn-1-H2-H2.HWMS_PACK_SEQNO',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('CREATE SEQUENCE H2.HWMS_PACK_SEQNO'),
|
||||
@@ -427,27 +471,6 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
expect(editorText).toContain('PACKAGE BODY pkg_order AS');
|
||||
expect(editorText).toContain('NULL;');
|
||||
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ TEXT: 'PACKAGE pkg_order AS\n' },
|
||||
{ TEXT: ' PROCEDURE sync_order(p_id IN NUMBER);\n' },
|
||||
{ TEXT: 'END pkg_order;\n' },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ TEXT: 'PACKAGE BODY pkg_order AS\n' },
|
||||
{ TEXT: ' PROCEDURE sync_order(p_id IN NUMBER) IS\n' },
|
||||
{ TEXT: ' BEGIN\n' },
|
||||
{ TEXT: ' NULL;\n' },
|
||||
{ TEXT: ' END sync_order;\n' },
|
||||
{ TEXT: 'END pkg_order;\n' },
|
||||
],
|
||||
});
|
||||
|
||||
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
|
||||
await act(async () => {
|
||||
await button.props.onClick();
|
||||
@@ -457,7 +480,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
const editQuery = String(storeState.addTab.mock.calls[0][0].query || '');
|
||||
expect(editQuery).toContain('CREATE OR REPLACE PACKAGE pkg_order AS');
|
||||
expect(editQuery).toContain('/\nCREATE OR REPLACE PACKAGE BODY pkg_order AS');
|
||||
expect(editQuery).toContain('END sync_order;');
|
||||
expect(editQuery).toContain('END pkg_order;');
|
||||
});
|
||||
|
||||
it('keeps Oracle routine SQLPlus slash delimiters executable when opening object edit', async () => {
|
||||
@@ -509,7 +532,7 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
expect(editQuery).not.toContain('/;');
|
||||
});
|
||||
|
||||
it('reloads the latest object definition before opening object edit', async () => {
|
||||
it('uses the currently displayed object definition before opening object edit', async () => {
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
@@ -533,16 +556,16 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQuery).toHaveBeenCalledTimes(2);
|
||||
expect(backendApp.DBQuery).toHaveBeenCalledTimes(1);
|
||||
const query = storeState.addTab.mock.calls[0][0].query;
|
||||
expect(query).toContain('SELECT id, name, updated_at FROM users;');
|
||||
expect(query).not.toContain('SELECT id FROM users;');
|
||||
expect(query).toContain('SELECT id FROM users;');
|
||||
expect(query).not.toContain('SELECT id, name, updated_at FROM users;');
|
||||
|
||||
const editor = renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0];
|
||||
expect(String(editor.children.join(''))).toContain('SELECT id, name, updated_at FROM users');
|
||||
expect(String(editor.children.join(''))).toContain('SELECT id FROM users');
|
||||
});
|
||||
|
||||
it('keeps the current definition visible when refresh for object edit fails', async () => {
|
||||
it('opens object edit from the current definition even if a later refresh would fail', async () => {
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
@@ -567,10 +590,15 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
expect(storeState.addTab).not.toHaveBeenCalled();
|
||||
expect(storeState.addTab).toHaveBeenCalledWith(expect.objectContaining({
|
||||
id: 'view-def-conn-1-main-reporting.active_users',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
query: expect.stringContaining('SELECT id, name FROM users;'),
|
||||
}));
|
||||
expect(String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''))).toContain('SELECT id, name FROM users');
|
||||
expect(findButtonText(renderer.root)).toContain('Failed to refresh the latest definition');
|
||||
expect(findButtonText(renderer.root)).toContain('network down');
|
||||
expect(findButtonText(renderer.root)).not.toContain('Failed to refresh the latest definition');
|
||||
expect(findButtonText(renderer.root)).not.toContain('network down');
|
||||
});
|
||||
|
||||
it('does not keep the previous object definition when switching objects and the new load fails', async () => {
|
||||
|
||||
@@ -898,6 +898,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
}),
|
||||
};
|
||||
const editorModelPath = `gonavi-definition://${encodeURIComponent(objectIdentityKey)}`;
|
||||
const currentDefinition = loadedDefinitionKeyRef.current === objectIdentityKey ? String(definition || '') : '';
|
||||
|
||||
const refreshEditorLayout = useCallback(() => {
|
||||
const editor = editorRef.current;
|
||||
@@ -936,9 +937,52 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
editorRef.current = null;
|
||||
}, []);
|
||||
|
||||
const openObjectEditTab = useCallback((sourceDefinition: string) => {
|
||||
const dbName = String(tab.dbName || '').trim();
|
||||
const latestDefinition = String(sourceDefinition || '');
|
||||
loadedDefinitionKeyRef.current = objectIdentityKey;
|
||||
setDefinition(latestDefinition);
|
||||
const query = buildEditableDefinitionSql(tab, latestDefinition, normalizedObjectName, editableDefinitionCopy);
|
||||
setActiveContext({ connectionId: tab.connectionId, dbName });
|
||||
addTab({
|
||||
id: tab.id,
|
||||
title: editTabTitle,
|
||||
type: 'query',
|
||||
connectionId: tab.connectionId,
|
||||
dbName,
|
||||
query,
|
||||
queryMode: 'object-edit',
|
||||
returnToTabId: undefined,
|
||||
viewName: undefined,
|
||||
viewKind: undefined,
|
||||
eventName: undefined,
|
||||
routineName: undefined,
|
||||
routineType: undefined,
|
||||
sequenceName: undefined,
|
||||
packageName: undefined,
|
||||
triggerName: undefined,
|
||||
triggerTableName: undefined,
|
||||
schemaName: undefined,
|
||||
objectType: undefined,
|
||||
tableName: undefined,
|
||||
sidebarLocateKey: undefined,
|
||||
});
|
||||
}, [
|
||||
addTab,
|
||||
editTabTitle,
|
||||
editableDefinitionCopy,
|
||||
normalizedObjectName,
|
||||
objectIdentityKey,
|
||||
setActiveContext,
|
||||
tab,
|
||||
]);
|
||||
|
||||
const openObjectEditQuery = async () => {
|
||||
if (!normalizedObjectName || openingObjectEdit) return;
|
||||
const dbName = String(tab.dbName || '').trim();
|
||||
if (String(currentDefinition || '').trim()) {
|
||||
openObjectEditTab(currentDefinition);
|
||||
return;
|
||||
}
|
||||
setOpeningObjectEdit(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -950,20 +994,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
|
||||
setError(result.error || t('definition_viewer.error.query_failed'));
|
||||
return;
|
||||
}
|
||||
const latestDefinition = String(result.definition || '');
|
||||
loadedDefinitionKeyRef.current = objectIdentityKey;
|
||||
setDefinition(latestDefinition);
|
||||
const query = buildEditableDefinitionSql(tab, latestDefinition, normalizedObjectName, editableDefinitionCopy);
|
||||
setActiveContext({ connectionId: tab.connectionId, dbName });
|
||||
addTab({
|
||||
id: `query-edit-object-${tab.connectionId}-${dbName}-${Date.now()}`,
|
||||
title: editTabTitle,
|
||||
type: 'query',
|
||||
connectionId: tab.connectionId,
|
||||
dbName,
|
||||
query,
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
openObjectEditTab(String(result.definition || ''));
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setOpeningObjectEdit(false);
|
||||
|
||||
@@ -753,6 +753,23 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(actionsSource).toContain('SHOW CREATE EVENT ${eventRef}');
|
||||
});
|
||||
|
||||
it('marks sidebar view and routine edits as object-edit query tabs', () => {
|
||||
const actionsSource = readSourceFile('./sidebar/useSidebarObjectActions.tsx');
|
||||
const viewEditSource = actionsSource.slice(
|
||||
actionsSource.indexOf('const openEditView = async (node: any) => {'),
|
||||
actionsSource.indexOf('const openCreateView = (node: any) => {'),
|
||||
);
|
||||
const routineEditSource = actionsSource.slice(
|
||||
actionsSource.indexOf('const openEditRoutine = async (node: any) => {'),
|
||||
actionsSource.indexOf('const openCreateRoutine = (node: any, type: \'FUNCTION\' | \'PROCEDURE\') => {'),
|
||||
);
|
||||
|
||||
expect(viewEditSource).toContain("id: `query-edit-view-${Date.now()}`");
|
||||
expect(viewEditSource).toContain("queryMode: 'object-edit'");
|
||||
expect(routineEditSource).toContain("id: `query-edit-routine-${Date.now()}`");
|
||||
expect(routineEditSource).toContain("queryMode: 'object-edit'");
|
||||
});
|
||||
|
||||
it('wires external SQL directory file actions to dedicated Wails APIs', () => {
|
||||
const source = readSidebarSource();
|
||||
const loadTablesSource = source.slice(
|
||||
|
||||
@@ -759,6 +759,7 @@ export const useSidebarObjectActions = ({
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: template,
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1177,6 +1178,7 @@ export const useSidebarObjectActions = ({
|
||||
connectionId: id,
|
||||
dbName,
|
||||
query: template,
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1518,6 +1518,41 @@ describe('store appearance persistence', () => {
|
||||
expect(useStore.getState().activeTabId).toBe('query-other');
|
||||
});
|
||||
|
||||
it('reuses the current tab when the same id is reopened as an object-edit query', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
useStore.getState().addTab({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: '函数: reporting.refresh_stats',
|
||||
type: 'routine-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'FUNCTION',
|
||||
});
|
||||
|
||||
useStore.getState().addTab({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
title: '修改函数/存储过程: reporting.refresh_stats',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
query: 'CREATE OR REPLACE FUNCTION reporting.refresh_stats() RETURNS void AS $$ BEGIN END; $$ LANGUAGE plpgsql;',
|
||||
queryMode: 'object-edit',
|
||||
});
|
||||
|
||||
const { tabs, activeTabId } = useStore.getState();
|
||||
expect(tabs).toHaveLength(1);
|
||||
expect(activeTabId).toBe('routine-def-conn-1-main-reporting.refresh_stats');
|
||||
expect(tabs[0]).toEqual(expect.objectContaining({
|
||||
id: 'routine-def-conn-1-main-reporting.refresh_stats',
|
||||
type: 'query',
|
||||
queryMode: 'object-edit',
|
||||
title: '修改函数/存储过程: reporting.refresh_stats',
|
||||
query: expect.stringContaining('CREATE OR REPLACE FUNCTION reporting.refresh_stats()'),
|
||||
}));
|
||||
});
|
||||
|
||||
it('reuses the same table-export tab for the same connection and table identity', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user