🐛 fix(sql-editor): 修复对象超链接侧栏定位与样式

- 侧栏定位 fallback 统一按限定名解析,兼容 schema.object 与对象名、大小写差异

- 补充视图、函数、触发器超链接定位事件与树节点匹配回归测试

- 将 SQL 编辑器超链接改为蓝色实线下划线,并补充暗色主题样式断言
This commit is contained in:
Syngnat
2026-06-04 11:41:54 +08:00
parent 02faa4586b
commit 9d39440438
4 changed files with 153 additions and 9 deletions

View File

@@ -649,9 +649,11 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check
}
}
.gonavi-query-editor-link-hint {
color: #1677ff !important;
cursor: pointer;
text-decoration: underline;
text-decoration-style: dashed;
text-decoration-style: solid;
text-decoration-color: currentColor;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
@@ -672,6 +674,10 @@ body[data-theme='dark'] .gonavi-query-editor-object-token {
color: #7dd3fc;
}
body[data-theme='dark'] .gonavi-query-editor-link-hint {
color: #69b1ff !important;
}
body[data-theme='dark'] .gonavi-query-editor-column-token {
color: #5eead4;
}

View File

@@ -1079,6 +1079,13 @@ describe('QueryEditor external SQL save', () => {
expect(lastDecorationCall?.[1]?.[0]?.options?.inlineClassName).toBe('gonavi-query-editor-link-hint');
});
it('keeps query editor hyperlink decorations blue with a solid underline', () => {
const css = readFileSync(new URL('../App.css', import.meta.url), 'utf8');
expect(css).toMatch(/\.gonavi-query-editor-link-hint\s*\{[^}]*color:\s*#1677ff\s*!important;[^}]*text-decoration:\s*underline;[^}]*text-decoration-style:\s*solid;[^}]*text-decoration-color:\s*currentColor;/s);
expect(css).toMatch(/body\[data-theme='dark'\]\s+\.gonavi-query-editor-link-hint\s*\{[^}]*color:\s*#69b1ff\s*!important;/s);
});
it('opens a view tab on ctrl left click inside the editor', async () => {
editorState.value = 'select * from reporting.active_users';
autoFetchState.visible = true;
@@ -1209,6 +1216,24 @@ describe('QueryEditor external SQL save', () => {
schemaName: 'reporting',
sidebarLocateKey: 'conn-1-main-routine-reporting.refresh_stats',
});
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
type: 'gonavi:locate-sidebar-object',
detail: expect.objectContaining({
tabId: 'conn-1-main-trigger-audit.users_bi-audit.users',
triggerName: 'audit.users_bi',
schemaName: 'audit',
objectGroup: 'triggers',
}),
}));
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
type: 'gonavi:locate-sidebar-object',
detail: expect.objectContaining({
tabId: 'conn-1-main-routine-reporting.refresh_stats',
routineName: 'reporting.refresh_stats',
schemaName: 'reporting',
objectGroup: 'routines',
}),
}));
});
it('switches current database on cmd left click for database identifiers', async () => {

View File

@@ -309,6 +309,111 @@ describe('sidebarLocate', () => {
]);
});
it('finds schema objects when tree nodes use unqualified names or different case', () => {
const viewTarget = resolveSidebarLocateTarget({
tabId: 'conn-1-main-view-reporting.active_users',
connectionId: 'conn-1',
dbName: 'main',
tableName: 'reporting.active_users',
schemaName: 'reporting',
objectGroup: 'views',
}, { groupBySchema: true });
const routineTarget = resolveSidebarLocateTarget({
tabId: 'conn-1-main-routine-reporting.refresh_stats',
connectionId: 'conn-1',
dbName: 'main',
tableName: 'reporting.refresh_stats',
schemaName: 'reporting',
objectGroup: 'routines',
}, { groupBySchema: true });
const triggerTarget = resolveSidebarLocateTarget({
tabId: 'conn-1-main-trigger-audit.users_bi-audit.users',
connectionId: 'conn-1',
dbName: 'main',
tableName: 'audit.users_bi',
schemaName: 'audit',
objectGroup: 'triggers',
}, { groupBySchema: true });
const tree = [
{
key: 'conn-1',
children: [
{
key: 'conn-1-main',
dataRef: { id: 'conn-1', dbName: 'main' },
children: [
{
key: 'conn-1-main-schema-REPORTING',
children: [
{
key: 'conn-1-main-schema-REPORTING-views',
children: [
{
key: 'conn-1-main-view-ACTIVE_USERS',
type: 'view',
dataRef: { id: 'conn-1', dbName: 'main', viewName: 'ACTIVE_USERS', schemaName: 'REPORTING' },
},
],
},
{
key: 'conn-1-main-schema-REPORTING-routines',
children: [
{
key: 'conn-1-main-routine-REFRESH_STATS',
type: 'routine',
dataRef: { id: 'conn-1', dbName: 'main', routineName: 'REFRESH_STATS', schemaName: 'REPORTING' },
},
],
},
],
},
{
key: 'conn-1-main-schema-AUDIT',
children: [
{
key: 'conn-1-main-schema-AUDIT-triggers',
children: [
{
key: 'conn-1-main-trigger-USERS_BI-AUDIT.USERS',
type: 'db-trigger',
dataRef: { id: 'conn-1', dbName: 'main', triggerName: 'USERS_BI', tableName: 'AUDIT.USERS', schemaName: 'AUDIT' },
},
],
},
],
},
],
},
],
},
];
expect(findSidebarNodePathForLocate(tree, viewTarget)).toEqual([
'conn-1',
'conn-1-main',
'conn-1-main-schema-REPORTING',
'conn-1-main-schema-REPORTING-views',
'conn-1-main-view-ACTIVE_USERS',
]);
expect(findSidebarNodePathForLocate(tree, routineTarget)).toEqual([
'conn-1',
'conn-1-main',
'conn-1-main-schema-REPORTING',
'conn-1-main-schema-REPORTING-routines',
'conn-1-main-routine-REFRESH_STATS',
]);
expect(findSidebarNodePathForLocate(tree, triggerTarget)).toEqual([
'conn-1',
'conn-1-main',
'conn-1-main-schema-AUDIT',
'conn-1-main-schema-AUDIT-triggers',
'conn-1-main-trigger-USERS_BI-AUDIT.USERS',
]);
});
it('finds external SQL file paths from loaded tree data', () => {
const target = resolveSidebarLocateTarget({
filePath: 'C:\\Users\\me\\sql\\report.sql',

View File

@@ -1,3 +1,5 @@
import { splitQualifiedNameLast } from './qualifiedName';
export type SidebarLocateObjectGroup = 'tables' | 'views' | 'materializedViews' | 'triggers' | 'routines' | 'externalSqlFiles';
export type SidebarLocateDatabaseObjectGroup = Exclude<SidebarLocateObjectGroup, 'externalSqlFiles'>;
@@ -66,11 +68,10 @@ const normalizeExternalSQLLocatePath = (value: unknown): string => toTrimmedStri
export const splitSidebarQualifiedName = (qualifiedName: string): { schemaName: string; objectName: string } => {
const raw = toTrimmedString(qualifiedName);
if (!raw) return { schemaName: '', objectName: '' };
const idx = raw.lastIndexOf('.');
if (idx <= 0 || idx >= raw.length - 1) return { schemaName: '', objectName: raw };
const parsed = splitQualifiedNameLast(raw);
return {
schemaName: raw.substring(0, idx).trim(),
objectName: raw.substring(idx + 1).trim(),
schemaName: parsed.parentPath,
objectName: parsed.objectName,
};
};
@@ -251,16 +252,23 @@ export const findSidebarNodePathByKey = (
const matchesLocateObjectName = (target: SidebarLocateTarget, nodeObjectName: string, nodeSchemaName: string): boolean => {
const normalizedNodeName = toTrimmedString(nodeObjectName);
if (!normalizedNodeName) return false;
if (normalizedNodeName === target.tableName) return true;
if (!target.schemaName) return false;
const nodeParsed = splitSidebarQualifiedName(normalizedNodeName);
const targetParsed = splitSidebarQualifiedName(target.tableName);
const nodeObject = nodeParsed.objectName || normalizedNodeName;
const targetObject = targetParsed.objectName || target.tableName;
const resolvedNodeSchema = toTrimmedString(nodeSchemaName) || nodeParsed.schemaName;
return resolvedNodeSchema === target.schemaName && nodeObject === targetObject;
const resolvedTargetSchema = toTrimmedString(target.schemaName) || targetParsed.schemaName;
const normalize = (value: string): string => toTrimmedString(value).toLowerCase();
if (normalize(normalizedNodeName) === normalize(target.tableName)) return true;
if (!resolvedTargetSchema) {
return !resolvedNodeSchema && normalize(nodeObject) === normalize(targetObject);
}
return normalize(resolvedNodeSchema) === normalize(resolvedTargetSchema)
&& normalize(nodeObject) === normalize(targetObject);
};
const matchesLocateObjectNode = (node: SidebarLocateTreeNodeLike, target: SidebarLocateTarget): boolean => {