mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-21 12:51:47 +08:00
- 对象树支持序列和存储包双击及右键查看定义 - Oracle package 同时读取 PACKAGE 与 PACKAGE BODY 并生成可编辑 SQL - SQL 拆分器跳过 SQL*Plus 独立斜杠和注释分隔符 - 补充前后端 PL/SQL 拆分与定义查看回归测试
636 lines
21 KiB
TypeScript
636 lines
21 KiB
TypeScript
import type { ConnectionConfig, SavedConnection, TabData } from '../types';
|
||
import { t as catalogTranslate } from '../i18n/catalog';
|
||
import type { I18nParams } from '../i18n/types';
|
||
import { resolveLocalizedUntitledQueryTitle } from './queryTabTitle';
|
||
|
||
export const TAB_DISPLAY_ELEMENT_KEYS = ['connection', 'kind', 'object', 'database', 'schema', 'host'] as const;
|
||
|
||
export type TabDisplayElementKey = typeof TAB_DISPLAY_ELEMENT_KEYS[number];
|
||
export type TabDisplayLayout = 'single' | 'double';
|
||
|
||
export interface TabDisplayLayoutSnapshot {
|
||
primaryElements: TabDisplayElementKey[];
|
||
secondaryElements: TabDisplayElementKey[];
|
||
}
|
||
|
||
export interface TabDisplaySettings {
|
||
layout: TabDisplayLayout;
|
||
primaryElements: TabDisplayElementKey[];
|
||
secondaryElements: TabDisplayElementKey[];
|
||
single?: TabDisplayLayoutSnapshot;
|
||
double?: TabDisplayLayoutSnapshot;
|
||
}
|
||
|
||
export type TabDisplayTranslate = (key: string, params?: I18nParams) => string;
|
||
|
||
const defaultTranslate: TabDisplayTranslate = (key, params) => catalogTranslate('en-US', key, params);
|
||
|
||
export const TAB_DISPLAY_SECONDARY_DEFAULT_KEYS: TabDisplayElementKey[] = ['connection', 'database', 'schema', 'host'];
|
||
|
||
export const TAB_DISPLAY_ELEMENT_META: Record<TabDisplayElementKey, { labelKey: string; descriptionKey: string }> = {
|
||
connection: {
|
||
labelKey: 'app.theme.tab_display.element.connection.label',
|
||
descriptionKey: 'app.theme.tab_display.element.connection.description',
|
||
},
|
||
kind: {
|
||
labelKey: 'app.theme.tab_display.element.kind.label',
|
||
descriptionKey: 'app.theme.tab_display.element.kind.description',
|
||
},
|
||
object: {
|
||
labelKey: 'app.theme.tab_display.element.object.label',
|
||
descriptionKey: 'app.theme.tab_display.element.object.description',
|
||
},
|
||
database: {
|
||
labelKey: 'app.theme.tab_display.element.database.label',
|
||
descriptionKey: 'app.theme.tab_display.element.database.description',
|
||
},
|
||
schema: {
|
||
labelKey: 'app.theme.tab_display.element.schema.label',
|
||
descriptionKey: 'app.theme.tab_display.element.schema.description',
|
||
},
|
||
host: {
|
||
labelKey: 'app.theme.tab_display.element.host.label',
|
||
descriptionKey: 'app.theme.tab_display.element.host.description',
|
||
},
|
||
};
|
||
|
||
export const DEFAULT_TAB_DISPLAY_SETTINGS: TabDisplaySettings = {
|
||
layout: 'single',
|
||
primaryElements: ['connection', 'kind', 'object'],
|
||
secondaryElements: [],
|
||
};
|
||
|
||
export const getCurrentTabDisplaySnapshot = (settings: TabDisplaySettings): TabDisplayLayoutSnapshot => ({
|
||
primaryElements: [...settings.primaryElements],
|
||
secondaryElements: [...settings.secondaryElements],
|
||
});
|
||
|
||
export const getDefaultTabDisplaySnapshot = (layout: TabDisplayLayout): TabDisplayLayoutSnapshot => {
|
||
if (layout === 'single') {
|
||
return {
|
||
primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements],
|
||
secondaryElements: [],
|
||
};
|
||
}
|
||
|
||
return {
|
||
primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements],
|
||
secondaryElements: TAB_DISPLAY_SECONDARY_DEFAULT_KEYS.filter((key) => !DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements.includes(key)),
|
||
};
|
||
};
|
||
|
||
export const getSavedTabDisplaySnapshot = (
|
||
settings: TabDisplaySettings,
|
||
layout: TabDisplayLayout,
|
||
): TabDisplayLayoutSnapshot => {
|
||
const saved = settings[layout];
|
||
if (saved) {
|
||
return {
|
||
primaryElements: [...saved.primaryElements],
|
||
secondaryElements: [...saved.secondaryElements],
|
||
};
|
||
}
|
||
if (settings.layout === layout) {
|
||
return getCurrentTabDisplaySnapshot(settings);
|
||
}
|
||
return getDefaultTabDisplaySnapshot(layout);
|
||
};
|
||
|
||
export const applyTabDisplaySettingsPatch = (
|
||
currentSettings: TabDisplaySettings,
|
||
patch: Partial<TabDisplaySettings>,
|
||
): TabDisplaySettings => {
|
||
const nextSettings = sanitizeTabDisplaySettings({
|
||
...currentSettings,
|
||
...patch,
|
||
});
|
||
const nextSnapshot = getCurrentTabDisplaySnapshot(nextSettings);
|
||
return sanitizeTabDisplaySettings({
|
||
...nextSettings,
|
||
[nextSettings.layout]: nextSnapshot,
|
||
});
|
||
};
|
||
|
||
export const switchTabDisplayLayout = (
|
||
currentSettings: TabDisplaySettings,
|
||
layout: TabDisplayLayout,
|
||
): TabDisplaySettings => {
|
||
if (layout === currentSettings.layout) {
|
||
return sanitizeTabDisplaySettings(currentSettings);
|
||
}
|
||
const currentSnapshot = getCurrentTabDisplaySnapshot(currentSettings);
|
||
const targetSnapshot = getSavedTabDisplaySnapshot(currentSettings, layout);
|
||
return sanitizeTabDisplaySettings({
|
||
...currentSettings,
|
||
[currentSettings.layout]: currentSnapshot,
|
||
layout,
|
||
primaryElements: targetSnapshot.primaryElements,
|
||
secondaryElements: targetSnapshot.secondaryElements,
|
||
[layout]: targetSnapshot,
|
||
});
|
||
};
|
||
|
||
const isTabDisplayElementKey = (value: unknown): value is TabDisplayElementKey => (
|
||
typeof value === 'string' && (TAB_DISPLAY_ELEMENT_KEYS as readonly string[]).includes(value)
|
||
);
|
||
|
||
const sanitizeTabDisplayElementList = (
|
||
value: unknown,
|
||
used: Set<TabDisplayElementKey>,
|
||
): TabDisplayElementKey[] => {
|
||
if (!Array.isArray(value)) return [];
|
||
const result: TabDisplayElementKey[] = [];
|
||
value.forEach((entry) => {
|
||
if (!isTabDisplayElementKey(entry) || used.has(entry)) return;
|
||
used.add(entry);
|
||
result.push(entry);
|
||
});
|
||
return result;
|
||
};
|
||
|
||
const sanitizeTabDisplayLayoutSnapshot = (value: unknown): TabDisplayLayoutSnapshot | null => {
|
||
if (!value || typeof value !== 'object') {
|
||
return null;
|
||
}
|
||
const raw = value as Partial<TabDisplayLayoutSnapshot>;
|
||
const used = new Set<TabDisplayElementKey>();
|
||
const primaryElements = sanitizeTabDisplayElementList(raw.primaryElements, used);
|
||
const secondaryElements = sanitizeTabDisplayElementList(raw.secondaryElements, used);
|
||
return {
|
||
primaryElements: primaryElements.length > 0 ? primaryElements : [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements],
|
||
secondaryElements,
|
||
};
|
||
};
|
||
|
||
export const sanitizeTabDisplaySettings = (value: unknown): TabDisplaySettings => {
|
||
if (!value || typeof value !== 'object') {
|
||
return { ...DEFAULT_TAB_DISPLAY_SETTINGS, primaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements], secondaryElements: [...DEFAULT_TAB_DISPLAY_SETTINGS.secondaryElements] };
|
||
}
|
||
const raw = value as Partial<TabDisplaySettings>;
|
||
const used = new Set<TabDisplayElementKey>();
|
||
const primaryElements = sanitizeTabDisplayElementList(raw.primaryElements, used);
|
||
const secondaryElements = sanitizeTabDisplayElementList(raw.secondaryElements, used);
|
||
const result: TabDisplaySettings = {
|
||
layout: raw.layout === 'double' ? 'double' : 'single',
|
||
primaryElements: primaryElements.length > 0 ? primaryElements : [...DEFAULT_TAB_DISPLAY_SETTINGS.primaryElements],
|
||
secondaryElements,
|
||
};
|
||
const single = sanitizeTabDisplayLayoutSnapshot(raw.single);
|
||
const double = sanitizeTabDisplayLayoutSnapshot(raw.double);
|
||
if (single) {
|
||
result.single = single;
|
||
}
|
||
if (double) {
|
||
result.double = double;
|
||
}
|
||
return result;
|
||
};
|
||
|
||
export const resolveTabDisplayElementOrder = (settings?: Partial<TabDisplaySettings> | null): TabDisplayElementKey[] => {
|
||
const sanitized = sanitizeTabDisplaySettings(settings);
|
||
const visible = [...sanitized.primaryElements, ...sanitized.secondaryElements];
|
||
return [
|
||
...visible,
|
||
...TAB_DISPLAY_ELEMENT_KEYS.filter((key) => !visible.includes(key)),
|
||
];
|
||
};
|
||
|
||
export const detectConnectionEnvLabel = (connectionName: string): string | null => {
|
||
const tokens = connectionName.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
||
if (tokens.includes('prod') || tokens.includes('production')) return 'PROD';
|
||
if (tokens.includes('uat')) return 'UAT';
|
||
if (tokens.includes('dev') || tokens.includes('development')) return 'DEV';
|
||
if (tokens.includes('sit')) return 'SIT';
|
||
if (tokens.includes('stg') || tokens.includes('stage') || tokens.includes('staging') || tokens.includes('pre')) return 'STG';
|
||
if (tokens.includes('test') || tokens.includes('qa')) return 'TEST';
|
||
return null;
|
||
};
|
||
|
||
const parseHostOnlyToken = (value: unknown): string[] => {
|
||
const raw = String(value || '').trim();
|
||
if (!raw) {
|
||
return [];
|
||
}
|
||
|
||
let text = raw.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '');
|
||
if (text.includes('/')) {
|
||
text = text.split('/')[0];
|
||
}
|
||
if (text.includes('?')) {
|
||
text = text.split('?')[0];
|
||
}
|
||
if (text.includes('@')) {
|
||
text = text.split('@').pop() || '';
|
||
}
|
||
|
||
return text
|
||
.split(',')
|
||
.map((entry) => {
|
||
const token = entry.trim();
|
||
if (!token) return '';
|
||
if (token.startsWith('[')) {
|
||
const rightBracketIndex = token.indexOf(']');
|
||
if (rightBracketIndex > 0) {
|
||
return token.slice(0, rightBracketIndex + 1).toLowerCase();
|
||
}
|
||
}
|
||
const colonIndex = token.lastIndexOf(':');
|
||
if (colonIndex > 0) {
|
||
return token.slice(0, colonIndex).toLowerCase();
|
||
}
|
||
return token.toLowerCase();
|
||
})
|
||
.filter(Boolean);
|
||
};
|
||
|
||
export const resolveConnectionHostTokens = (config?: ConnectionConfig): string[] => {
|
||
if (!config) {
|
||
return [];
|
||
}
|
||
|
||
return Array.from(new Set([
|
||
...parseHostOnlyToken(config.host),
|
||
...(Array.isArray(config.hosts) ? config.hosts.flatMap((entry) => parseHostOnlyToken(entry)) : []),
|
||
...parseHostOnlyToken(config.uri),
|
||
]));
|
||
};
|
||
|
||
export const resolveConnectionHostSummary = (config?: ConnectionConfig): string => {
|
||
const hosts = resolveConnectionHostTokens(config);
|
||
if (hosts.length === 0) return '';
|
||
if (hosts.length === 1) return hosts[0];
|
||
return `${hosts[0]} +${hosts.length - 1}`;
|
||
};
|
||
|
||
const isRedisTab = (tab: TabData): boolean => {
|
||
return tab.type === 'redis-keys' || tab.type === 'redis-command' || tab.type === 'redis-monitor';
|
||
};
|
||
|
||
const buildRedisBaseTitle = (tab: TabData, translate: TabDisplayTranslate = defaultTranslate): string => {
|
||
const dbLabel = `db${tab.redisDB ?? 0}`;
|
||
if (tab.type === 'redis-command') return translate('sidebar.tab.redis_command', { database: dbLabel });
|
||
if (tab.type === 'redis-monitor') return translate('sidebar.tab.redis_monitor', { database: dbLabel });
|
||
return dbLabel;
|
||
};
|
||
|
||
const splitQualifiedIdentifier = (value: unknown): string[] => {
|
||
const raw = String(value || '').trim();
|
||
if (!raw) return [];
|
||
|
||
const parts: string[] = [];
|
||
let current = '';
|
||
let quote: '"' | '`' | '[' | null = null;
|
||
|
||
for (let index = 0; index < raw.length; index += 1) {
|
||
const char = raw[index];
|
||
const next = raw[index + 1];
|
||
|
||
if (char === '\\' && next === '"') {
|
||
current += '"';
|
||
index += 1;
|
||
continue;
|
||
}
|
||
|
||
if (quote === '"') {
|
||
current += char;
|
||
if (char === '"' && next === '"') {
|
||
current += next;
|
||
index += 1;
|
||
} else if (char === '"') {
|
||
quote = null;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (quote === '`') {
|
||
current += char;
|
||
if (char === '`' && next === '`') {
|
||
current += next;
|
||
index += 1;
|
||
} else if (char === '`') {
|
||
quote = null;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (quote === '[') {
|
||
current += char;
|
||
if (char === ']') {
|
||
quote = null;
|
||
}
|
||
continue;
|
||
}
|
||
|
||
if (char === '"' || char === '`' || char === '[') {
|
||
quote = char;
|
||
current += char;
|
||
continue;
|
||
}
|
||
|
||
if (char === '.') {
|
||
parts.push(current.trim());
|
||
current = '';
|
||
continue;
|
||
}
|
||
|
||
current += char;
|
||
}
|
||
|
||
parts.push(current.trim());
|
||
return parts.filter(Boolean);
|
||
};
|
||
|
||
const unwrapIdentifierLabel = (value: string): string => {
|
||
let text = String(value || '').trim().replace(/\\"/g, '"');
|
||
if (!text) return '';
|
||
|
||
const first = text[0];
|
||
const last = text[text.length - 1];
|
||
if ((first === '"' && last === '"') || (first === '`' && last === '`')) {
|
||
text = text.slice(1, -1);
|
||
} else if (first === '[' && last === ']') {
|
||
text = text.slice(1, -1);
|
||
}
|
||
|
||
return text
|
||
.replace(/""/g, '"')
|
||
.replace(/``/g, '`')
|
||
.replace(/\]\]/g, ']')
|
||
.trim();
|
||
};
|
||
|
||
export const stripSchemaFromTabObjectLabel = (value: unknown): string => {
|
||
const raw = String(value || '').trim();
|
||
if (!raw) return '';
|
||
|
||
const parts = splitQualifiedIdentifier(raw);
|
||
const lastPart = parts[parts.length - 1] || raw;
|
||
return unwrapIdentifierLabel(lastPart) || raw;
|
||
};
|
||
|
||
const getSchemaFromTabObjectLabel = (value: unknown): string => {
|
||
const parts = splitQualifiedIdentifier(value);
|
||
if (parts.length <= 1) return '';
|
||
return parts.slice(0, -1).map((part) => unwrapIdentifierLabel(part)).filter(Boolean).join('.');
|
||
};
|
||
|
||
const replaceTitleObjectLabel = (title: string, objectName?: string): string => {
|
||
const rawTitle = String(title || '').trim();
|
||
if (!rawTitle) return rawTitle;
|
||
|
||
const rawObjectName = String(objectName || '').trim();
|
||
const displayObjectName = stripSchemaFromTabObjectLabel(rawObjectName);
|
||
if (rawObjectName && displayObjectName && displayObjectName !== rawObjectName) {
|
||
const lastIndex = rawTitle.lastIndexOf(rawObjectName);
|
||
if (lastIndex >= 0) {
|
||
return `${rawTitle.slice(0, lastIndex)}${displayObjectName}${rawTitle.slice(lastIndex + rawObjectName.length)}`;
|
||
}
|
||
}
|
||
|
||
const parenMatch = rawTitle.match(/^(.*\()([^()]*)\)(\s*)$/);
|
||
if (parenMatch) {
|
||
const objectLabel = stripSchemaFromTabObjectLabel(parenMatch[2]);
|
||
return `${parenMatch[1]}${objectLabel})${parenMatch[3]}`;
|
||
}
|
||
|
||
const colonMatch = rawTitle.match(/^([^::]+[::]\s*)(.+)$/);
|
||
if (colonMatch) {
|
||
return `${colonMatch[1]}${stripSchemaFromTabObjectLabel(colonMatch[2])}`;
|
||
}
|
||
|
||
return stripSchemaFromTabObjectLabel(rawTitle);
|
||
};
|
||
|
||
const stripSchemaFromTableOverviewTitle = (title: string): string => {
|
||
const rawTitle = String(title || '').trim();
|
||
return rawTitle.replace(/\s+\([^()]+\)\s*$/, '').trim() || rawTitle;
|
||
};
|
||
|
||
const QUERY_TAB_TITLE_MAX_LENGTH = 28;
|
||
|
||
const getFileNameFromPath = (value: string): string => (
|
||
value.split(/[\\/]/).filter(Boolean).pop() || value
|
||
);
|
||
|
||
const isLikelyRawSqlTitle = (value: string): boolean => {
|
||
const text = value.trim();
|
||
if (!text) return false;
|
||
if (/[\r\n;]/.test(text)) return true;
|
||
return /^(select|with|insert|update|delete|merge|create|alter|drop|truncate|explain|show|desc|describe)\b/i.test(text);
|
||
};
|
||
|
||
const compactQueryTabTitle = (tab: TabData, translate: TabDisplayTranslate = defaultTranslate): string => {
|
||
const filePath = String(tab.filePath || '').trim();
|
||
if (filePath) {
|
||
return getFileNameFromPath(filePath);
|
||
}
|
||
|
||
const rawTitle = String(tab.title || '').trim();
|
||
const resolvedUntitledTitle = resolveLocalizedUntitledQueryTitle(rawTitle, tab.dbName, translate);
|
||
const displayTitle = resolvedUntitledTitle || rawTitle;
|
||
const title = displayTitle && !isLikelyRawSqlTitle(displayTitle) ? displayTitle : translate('sidebar.tab.new_query');
|
||
if (title.length <= QUERY_TAB_TITLE_MAX_LENGTH) {
|
||
return title;
|
||
}
|
||
return `${title.slice(0, QUERY_TAB_TITLE_MAX_LENGTH - 3)}...`;
|
||
};
|
||
|
||
const buildCompactObjectTabTitle = (tab: TabData, translate: TabDisplayTranslate = defaultTranslate): string => {
|
||
if (tab.type === 'query') {
|
||
return compactQueryTabTitle(tab, translate);
|
||
}
|
||
if (tab.type === 'table') {
|
||
return stripSchemaFromTabObjectLabel(tab.tableName || tab.title) || tab.title;
|
||
}
|
||
if (tab.type === 'design') {
|
||
return replaceTitleObjectLabel(tab.title, tab.tableName);
|
||
}
|
||
if (tab.type === 'table-overview') {
|
||
return stripSchemaFromTableOverviewTitle(tab.title);
|
||
}
|
||
if (tab.type === 'table-export') {
|
||
return replaceTitleObjectLabel(tab.title, tab.tableName);
|
||
}
|
||
if (tab.type === 'view-def') {
|
||
return replaceTitleObjectLabel(tab.title, tab.viewName);
|
||
}
|
||
if (tab.type === 'trigger') {
|
||
return replaceTitleObjectLabel(tab.title, tab.triggerName);
|
||
}
|
||
if (tab.type === 'event-def') {
|
||
return replaceTitleObjectLabel(tab.title, tab.eventName);
|
||
}
|
||
if (tab.type === 'routine-def') {
|
||
return replaceTitleObjectLabel(tab.title, tab.routineName);
|
||
}
|
||
if (tab.type === 'sequence-def') {
|
||
return replaceTitleObjectLabel(tab.title, tab.sequenceName);
|
||
}
|
||
if (tab.type === 'package-def') {
|
||
return replaceTitleObjectLabel(tab.title, tab.packageName);
|
||
}
|
||
return tab.title;
|
||
};
|
||
|
||
export const getTabDisplayKindLabel = (tab: TabData): string => {
|
||
if (tab.type === 'query') return 'SQL';
|
||
if (tab.type === 'table') return 'TABLE';
|
||
if (tab.type === 'design') return 'DESIGN';
|
||
if (tab.type === 'table-overview') return 'DB';
|
||
if (tab.type === 'table-export') return 'EXPORT';
|
||
if (tab.type === 'sql-analysis') return 'ANALYZE';
|
||
if (tab.type.startsWith('redis')) return 'REDIS';
|
||
if (tab.type.startsWith('jvm')) return 'JVM';
|
||
if (tab.type === 'trigger') return 'TRG';
|
||
if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? 'MV' : 'VIEW';
|
||
if (tab.type === 'event-def') return 'EVT';
|
||
if (tab.type === 'routine-def') return 'FUNC';
|
||
if (tab.type === 'sequence-def') return 'SEQ';
|
||
if (tab.type === 'package-def') return 'PKG';
|
||
return 'TAB';
|
||
};
|
||
|
||
const getTabRawObjectLabel = (tab: TabData, translate: TabDisplayTranslate = defaultTranslate): string => {
|
||
if (tab.type === 'query') return compactQueryTabTitle(tab, translate);
|
||
if (tab.tableName) return tab.tableName;
|
||
if (tab.viewName) return tab.viewName;
|
||
if (tab.eventName) return tab.eventName;
|
||
if (tab.routineName) return tab.routineName;
|
||
if (tab.sequenceName) return tab.sequenceName;
|
||
if (tab.packageName) return tab.packageName;
|
||
if (tab.triggerName) return tab.triggerName;
|
||
if (tab.resourcePath) return tab.resourcePath;
|
||
if (tab.filePath) return getFileNameFromPath(tab.filePath);
|
||
if (tab.type.startsWith('redis')) return `db${tab.redisDB ?? 0}`;
|
||
return tab.title;
|
||
};
|
||
|
||
const getTabConnectionLabel = (connection?: SavedConnection): string => {
|
||
const connectionName = String(connection?.name || '').trim();
|
||
return detectConnectionEnvLabel(connectionName) || connectionName;
|
||
};
|
||
|
||
const getTabDisplayElementValue = (
|
||
key: TabDisplayElementKey,
|
||
tab: TabData,
|
||
connection?: SavedConnection,
|
||
translate: TabDisplayTranslate = defaultTranslate,
|
||
): string => {
|
||
const rawObjectLabel = getTabRawObjectLabel(tab, translate);
|
||
switch (key) {
|
||
case 'connection':
|
||
return getTabConnectionLabel(connection);
|
||
case 'kind':
|
||
return getTabDisplayKindLabel(tab);
|
||
case 'object':
|
||
return buildCompactObjectTabTitle({
|
||
...tab,
|
||
title: tab.type === 'table' || tab.type === 'query' ? rawObjectLabel : tab.title,
|
||
}, translate);
|
||
case 'database':
|
||
return String(tab.dbName || '').trim();
|
||
case 'schema':
|
||
return getSchemaFromTabObjectLabel(rawObjectLabel);
|
||
case 'host':
|
||
return resolveConnectionHostSummary(connection?.config);
|
||
default:
|
||
return '';
|
||
}
|
||
};
|
||
|
||
const formatTabDisplayPartValue = (key: TabDisplayElementKey, value: string): string => {
|
||
if (!value) return '';
|
||
if (key === 'connection') return `[${value}]`;
|
||
if (key === 'schema') return `SCHEMA:${value}`;
|
||
return value;
|
||
};
|
||
|
||
export interface TabDisplayPart {
|
||
key: TabDisplayElementKey;
|
||
value: string;
|
||
text: string;
|
||
}
|
||
|
||
export interface TabDisplayModel {
|
||
layout: TabDisplayLayout;
|
||
primaryParts: TabDisplayPart[];
|
||
secondaryParts: TabDisplayPart[];
|
||
primaryText: string;
|
||
secondaryText: string;
|
||
fullTitle: string;
|
||
}
|
||
|
||
const buildTabDisplayParts = (
|
||
keys: TabDisplayElementKey[],
|
||
tab: TabData,
|
||
connection?: SavedConnection,
|
||
translate: TabDisplayTranslate = defaultTranslate,
|
||
): TabDisplayPart[] => keys
|
||
.map((key) => {
|
||
const value = getTabDisplayElementValue(key, tab, connection, translate);
|
||
return {
|
||
key,
|
||
value,
|
||
text: formatTabDisplayPartValue(key, value),
|
||
};
|
||
})
|
||
.filter((part) => part.text);
|
||
|
||
export const buildTabDisplayModel = (
|
||
tab: TabData,
|
||
connection?: SavedConnection,
|
||
settings?: Partial<TabDisplaySettings> | null,
|
||
translate: TabDisplayTranslate = defaultTranslate,
|
||
): TabDisplayModel => {
|
||
const sanitized = sanitizeTabDisplaySettings(settings);
|
||
const primaryParts = buildTabDisplayParts(sanitized.primaryElements, tab, connection, translate);
|
||
const secondaryParts = buildTabDisplayParts(sanitized.secondaryElements, tab, connection, translate);
|
||
const primaryText = primaryParts.map((part) => part.text).join(' ').trim() || buildCompactObjectTabTitle(tab, translate);
|
||
const secondaryText = secondaryParts.map((part) => part.text).join('·').trim();
|
||
const fullTitle = [primaryText, secondaryText].filter(Boolean).join(' · ');
|
||
return {
|
||
layout: sanitized.layout,
|
||
primaryParts,
|
||
secondaryParts,
|
||
primaryText,
|
||
secondaryText,
|
||
fullTitle,
|
||
};
|
||
};
|
||
|
||
export const buildTabDisplayTitle = (
|
||
tab: TabData,
|
||
connection?: SavedConnection,
|
||
settings?: Partial<TabDisplaySettings> | null,
|
||
translate: TabDisplayTranslate = defaultTranslate,
|
||
): string => {
|
||
if (settings) {
|
||
return buildTabDisplayModel(tab, connection, settings, translate).fullTitle;
|
||
}
|
||
|
||
const connectionName = String(connection?.name || '').trim();
|
||
|
||
if (isRedisTab(tab)) {
|
||
const hostSummary = resolveConnectionHostSummary(connection?.config);
|
||
const identity = [connectionName, hostSummary].filter(Boolean).join(' | ');
|
||
return identity ? `[${identity}] ${buildRedisBaseTitle(tab, translate)}` : buildRedisBaseTitle(tab, translate);
|
||
}
|
||
|
||
const baseTitle = buildCompactObjectTabTitle(tab, translate);
|
||
if (
|
||
tab.type !== 'table' &&
|
||
tab.type !== 'design' &&
|
||
tab.type !== 'table-overview' &&
|
||
tab.type !== 'table-export' &&
|
||
tab.type !== 'sql-analysis'
|
||
) {
|
||
return baseTitle;
|
||
}
|
||
if (!connectionName) {
|
||
return baseTitle;
|
||
}
|
||
|
||
const prefix = detectConnectionEnvLabel(connectionName) || connectionName;
|
||
return `[${prefix}] ${baseTitle}`;
|
||
};
|