mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
✨ feat(sidebar): 优化窄宽度连接头部展示
- 移除连接头部状态徽章,释放连接名与库名显示空间 - 窄宽度时收起新建查询文字并保持操作按钮可用 - 连接名或库名截断时悬浮同时展示完整信息 - 补充响应式布局、主题对比度与悬浮提示回归测试
This commit is contained in:
@@ -1325,6 +1325,10 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(css).toMatch(/\.gn-v2-rail-tool \{[^}]*width: calc\(24px \* var\(--gn-v2-rail-scale\)\);/s);
|
||||
expect(css).toMatch(/\.gn-v2-active-connection-trigger \{[^}]*height: 34px;[^}]*border: 0;[^}]*background: transparent;/s);
|
||||
expect(css).toMatch(/\.gn-v2-active-connection-query-action \{[^}]*max-width: 96px;[^}]*font-size: 12px;/s);
|
||||
expect(css).toMatch(/\.gn-v2-active-connection-tooltip \{[^}]*display: flex;[^}]*flex-direction: column;[^}]*gap: 2px;[^}]*max-width: min\(320px, calc\(100vw - 24px\)\);[^}]*overflow-wrap: anywhere;/s);
|
||||
expect(css).toMatch(/\.gn-v2-object-explorer \{[^}]*container-type: inline-size;[^}]*container-name: gn-v2-object-explorer;/s);
|
||||
expect(css).toMatch(/@container gn-v2-object-explorer \(max-width: 440px\) \{[\s\S]*\.gn-v2-active-connection-query-action \{[^}]*width: 28px;[^}]*max-width: 28px;[^}]*padding: 0;/s);
|
||||
expect(css).toMatch(/@container gn-v2-object-explorer \(max-width: 440px\) \{[\s\S]*\.gn-v2-active-connection-query-action > span:not\(\.anticon\):not\(\.ant-btn-icon\) \{[^}]*display: none;/s);
|
||||
expect(css).not.toContain('.gn-v2-active-connection-trigger:hover');
|
||||
});
|
||||
|
||||
@@ -1384,7 +1388,6 @@ describe('Sidebar locate toolbar', () => {
|
||||
const source = readSidebarSource();
|
||||
const treeLoaderSource = readSourceFile('./sidebar/useSidebarTreeLoaders.tsx');
|
||||
const titleRenderSource = readSourceFile('./sidebar/useSidebarTitleRender.tsx');
|
||||
const v2ContextMenuSource = readSourceFile('./sidebar/useSidebarV2ContextMenu.tsx');
|
||||
|
||||
expect(source).toContain("export type SidebarConnectionState = 'loading' | 'success' | 'error';");
|
||||
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [conn.id]: 'loading' }));");
|
||||
@@ -1398,8 +1401,6 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(treeLoaderSource).toContain("setConnectionStates(prev => ({ ...prev, [key as string]: 'success' }));");
|
||||
expect(titleRenderSource).toContain("let status: 'loading' | 'success' | 'error' | 'default' = 'default';");
|
||||
expect(titleRenderSource).toContain("if (connectionStates[node.key] === 'loading') status = 'loading';");
|
||||
expect(v2ContextMenuSource).toContain("const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();");
|
||||
expect(v2ContextMenuSource).toContain("value === 'loading' ? 'loading'");
|
||||
expect(css).toMatch(/\.gn-v2-tree-status\.is-loading::before \{[^}]*border: 2px solid rgba\(37, 99, 235, 0\.24\);[^}]*animation: gn-v2-tree-status-spin 0\.8s linear infinite;/s);
|
||||
expect(css).toMatch(/@keyframes gn-v2-tree-status-spin \{[^}]*to \{ transform: rotate\(360deg\); \}/s);
|
||||
});
|
||||
@@ -1555,8 +1556,11 @@ describe('Sidebar locate toolbar', () => {
|
||||
|
||||
expect(markup).toContain('gn-v2-connection-rail');
|
||||
expect(markup).toContain('gn-v2-active-connection-copy');
|
||||
expect(markup).toContain('<strong>本地</strong>');
|
||||
expect(markup).toContain('<span>app_db</span>');
|
||||
expect(markup).toMatch(/<strong aria-describedby="[^"]+">本地<\/strong>/);
|
||||
expect(markup).toMatch(/<span aria-describedby="[^"]+">app_db<\/span>/);
|
||||
expect(markup).not.toContain('title="本地"');
|
||||
expect(markup).not.toContain('title="app_db"');
|
||||
expect(markup).not.toContain('gn-v2-live-dot');
|
||||
expect(markup).not.toContain('<span>localhost</span>');
|
||||
expect(markup).not.toContain('gn-v2-db-icon-label');
|
||||
});
|
||||
@@ -1586,8 +1590,10 @@ describe('Sidebar locate toolbar', () => {
|
||||
|
||||
const markup = renderSidebarMarkup({ uiVersion: 'v2' });
|
||||
|
||||
expect(markup).toContain(`<strong>${t('sidebar.active_connection.no_host_selected')}</strong>`);
|
||||
expect(markup).toContain(`<span>${t('sidebar.active_connection.no_database_selected')}</span>`);
|
||||
expect(markup).toMatch(new RegExp(`<strong aria-describedby="[^"]+">${t('sidebar.active_connection.no_host_selected')}</strong>`));
|
||||
expect(markup).toMatch(new RegExp(`<span aria-describedby="[^"]+">${t('sidebar.active_connection.no_database_selected')}</span>`));
|
||||
expect(markup).not.toContain(`title="${t('sidebar.active_connection.no_host_selected')}"`);
|
||||
expect(markup).not.toContain(`title="${t('sidebar.active_connection.no_database_selected')}"`);
|
||||
expect(markup).not.toContain('<strong>本地</strong>');
|
||||
});
|
||||
|
||||
@@ -2803,6 +2809,13 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(source).toContain("title: String(node.title || dataRef.dbName || t('database.unnamed'))");
|
||||
expect(source).toContain("meta: conn?.name || dataRef.id || t('database.label')");
|
||||
expect(source).toContain("const activeConnectionDisplayName = String(activeConnection?.name || '').trim() || t('sidebar.active_connection.no_host_selected');");
|
||||
expect(source).toContain('const v2ActiveConnectionTooltipContent = (');
|
||||
expect(source).toContain('<div className="gn-v2-active-connection-tooltip">');
|
||||
expect(source).toContain('<strong>{activeConnectionDisplayName}</strong>');
|
||||
expect(source).toContain('<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>');
|
||||
expect(source.match(/<Tooltip title=\{v2ActiveConnectionTooltipContent\} placement="bottomLeft" mouseEnterDelay=\{0\.35\}>/g)).toHaveLength(2);
|
||||
expect(source).not.toContain('<strong title={activeConnectionDisplayName}>');
|
||||
expect(source).not.toContain('<span title={activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}>');
|
||||
expect(utilsSource).toContain("name: item.tag.name || t('connection.sidebar.group.untitled'),");
|
||||
expect(source).toContain('groupName={group.name}');
|
||||
expect(source).toContain('count={group.connections.length}');
|
||||
|
||||
@@ -2537,7 +2537,6 @@ const Sidebar: React.FC<{
|
||||
contextMenu,
|
||||
setContextMenu,
|
||||
contextMenuPortalRef,
|
||||
buildRailConnectionStatus,
|
||||
openV2ConnectionContextMenu,
|
||||
getV2TreeMetaText,
|
||||
renderV2SidebarContextMenuContent,
|
||||
@@ -2545,7 +2544,6 @@ const Sidebar: React.FC<{
|
||||
refreshV2TableContextMenuStats,
|
||||
} = useSidebarV2ContextMenu({
|
||||
connections,
|
||||
connectionStates,
|
||||
connectionTags,
|
||||
activeShortcutPlatform,
|
||||
flattenConnectionNodes,
|
||||
@@ -2923,6 +2921,12 @@ const Sidebar: React.FC<{
|
||||
const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database');
|
||||
const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected');
|
||||
const v2ConnectionActionsLabel = t('sidebar.active_connection.actions');
|
||||
const v2ActiveConnectionTooltipContent = (
|
||||
<div className="gn-v2-active-connection-tooltip">
|
||||
<strong>{activeConnectionDisplayName}</strong>
|
||||
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
|
||||
</div>
|
||||
);
|
||||
const v2CommandSearchLabel = t('sidebar.command_search.label');
|
||||
const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder');
|
||||
|
||||
@@ -3046,10 +3050,13 @@ const Sidebar: React.FC<{
|
||||
{isV2Ui && (
|
||||
<div className="gn-v2-active-connection-header" data-object-count={activeConnectionObjectCount}>
|
||||
<div className="gn-v2-active-connection-trigger" aria-label={v2ActiveConnectionHeaderLabel}>
|
||||
<span className={`gn-v2-live-dot is-${activeConnection ? buildRailConnectionStatus(activeConnection.id) : 'idle'}`} />
|
||||
<div className="gn-v2-active-connection-copy">
|
||||
<strong>{activeConnectionDisplayName}</strong>
|
||||
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
|
||||
<Tooltip title={v2ActiveConnectionTooltipContent} placement="bottomLeft" mouseEnterDelay={0.35}>
|
||||
<strong>{activeConnectionDisplayName}</strong>
|
||||
</Tooltip>
|
||||
<Tooltip title={v2ActiveConnectionTooltipContent} placement="bottomLeft" mouseEnterDelay={0.35}>
|
||||
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="gn-v2-active-connection-actions">
|
||||
|
||||
@@ -22,7 +22,7 @@ import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
|
||||
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
|
||||
import { resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { formatSidebarRowCount } from './sidebarHelpers';
|
||||
import { isSidebarTablePinned, type SidebarConnectionState, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
|
||||
import { isSidebarTablePinned, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction } from '../tableDataDangerActions';
|
||||
import {
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
@@ -45,7 +45,6 @@ export type SidebarContextMenuState = {
|
||||
|
||||
type SidebarV2ContextMenuOptions = {
|
||||
connections: SavedConnection[];
|
||||
connectionStates: Record<string, SidebarConnectionState>;
|
||||
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
|
||||
activeShortcutPlatform: any;
|
||||
flattenConnectionNodes: (nodes: TreeNode[]) => TreeNode[];
|
||||
@@ -76,7 +75,6 @@ type SidebarV2ContextMenuOptions = {
|
||||
|
||||
export const useSidebarV2ContextMenu = ({
|
||||
connections,
|
||||
connectionStates,
|
||||
connectionTags,
|
||||
activeShortcutPlatform,
|
||||
flattenConnectionNodes,
|
||||
@@ -105,33 +103,6 @@ export const useSidebarV2ContextMenu = ({
|
||||
const contextMenuPortalRef = useRef<HTMLDivElement | null>(null);
|
||||
const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState<Record<string, V2TableContextMenuStats>>({});
|
||||
|
||||
const connectionStatusMap = useMemo(() => {
|
||||
const statusMap = new Map<string, 'loading' | 'live' | 'error' | 'idle'>();
|
||||
const sortedConnectionIds = connections
|
||||
.map((conn) => conn.id)
|
||||
.sort((a, b) => b.length - a.length);
|
||||
connections.forEach((conn) => {
|
||||
statusMap.set(conn.id, 'idle');
|
||||
});
|
||||
Object.entries(connectionStates).forEach(([key, value]) => {
|
||||
const ownState = statusMap.get(key);
|
||||
if (ownState !== undefined) {
|
||||
statusMap.set(key, value === 'loading' ? 'loading' : value === 'success' ? 'live' : 'error');
|
||||
return;
|
||||
}
|
||||
if (value !== 'success') return;
|
||||
const ownerId = sortedConnectionIds.find((id) => key.startsWith(`${id}-`));
|
||||
if (ownerId && statusMap.get(ownerId) === 'idle') {
|
||||
statusMap.set(ownerId, 'live');
|
||||
}
|
||||
});
|
||||
return statusMap;
|
||||
}, [connectionStates, connections]);
|
||||
|
||||
const buildRailConnectionStatus = useCallback((connectionId: string): 'loading' | 'live' | 'error' | 'idle' => {
|
||||
return connectionStatusMap.get(connectionId) || 'idle';
|
||||
}, [connectionStatusMap]);
|
||||
|
||||
const openV2ConnectionContextMenu = (
|
||||
event: React.MouseEvent,
|
||||
connOrNode: SavedConnection | TreeNode,
|
||||
@@ -529,7 +500,6 @@ export const useSidebarV2ContextMenu = ({
|
||||
contextMenu,
|
||||
setContextMenu,
|
||||
contextMenuPortalRef,
|
||||
buildRailConnectionStatus,
|
||||
openV2ConnectionContextMenu,
|
||||
getV2TreeMetaText,
|
||||
renderV2SidebarContextMenuContent,
|
||||
|
||||
@@ -37,15 +37,13 @@ describe('v2 theme connection identity contrast', () => {
|
||||
const dark = readRuleBlock('body[data-ui-version="v2"][data-theme="dark"]');
|
||||
const host = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-host');
|
||||
const database = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-database');
|
||||
const liveDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-live-dot');
|
||||
const treeDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tree-status::before');
|
||||
|
||||
expect(host).toContain('color: var(--gn-info);');
|
||||
expect(database).toContain('color: var(--gn-accent);');
|
||||
expect(liveDot).toContain('width: 10px;');
|
||||
expect(liveDot).toContain('height: 10px;');
|
||||
expect(treeDot).toContain('width: 9px;');
|
||||
expect(treeDot).toContain('height: 9px;');
|
||||
expect(source).not.toContain('.gn-v2-live-dot');
|
||||
expect(source).toContain('background: var(--gn-status-connected);');
|
||||
|
||||
for (const [mode, block] of [['light', light], ['dark', dark]] as const) {
|
||||
|
||||
@@ -2144,6 +2144,8 @@ body[data-ui-version="v2"] .gn-v2-object-explorer {
|
||||
flex: 1 1 calc(100% - calc(38px * var(--gn-v2-rail-scale)));
|
||||
background: var(--gn-bg-panel-2);
|
||||
overflow: hidden;
|
||||
container-type: inline-size;
|
||||
container-name: gn-v2-object-explorer;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-header {
|
||||
@@ -2152,7 +2154,7 @@ body[data-ui-version="v2"] .gn-v2-active-connection-header {
|
||||
border-bottom: 0.5px solid var(--gn-br-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
|
||||
@@ -2161,8 +2163,7 @@ body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 9px;
|
||||
padding: 0 4px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
@@ -2170,37 +2171,6 @@ body[data-ui-version="v2"] .gn-v2-active-connection-trigger {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1.5px solid var(--gn-fg-4);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot.is-live {
|
||||
border: 0;
|
||||
background: var(--gn-status-connected);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-status-connected) 22%, transparent);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot.is-loading {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 2px solid rgba(37, 99, 235, 0.24);
|
||||
border-top-color: #2563eb;
|
||||
background: transparent;
|
||||
animation: gn-v2-tree-status-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-live-dot.is-error {
|
||||
border: 0;
|
||||
background: var(--gn-danger);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-danger) 20%, transparent);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-copy {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
@@ -2232,6 +2202,29 @@ body[data-ui-version="v2"] .gn-v2-active-connection-copy span {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-width: min(320px, calc(100vw - 24px));
|
||||
overflow-wrap: anywhere;
|
||||
font-family: var(--gn-font-sans);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip strong {
|
||||
color: var(--gn-fg-1);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-tooltip span {
|
||||
color: var(--gn-fg-3);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -2246,13 +2239,36 @@ body[data-ui-version="v2"] .gn-v2-active-connection-query-action {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon) {
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon):not(.ant-btn-icon) {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@container gn-v2-object-explorer (max-width: 440px) {
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-header {
|
||||
padding-right: 8px;
|
||||
padding-left: 10px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-actions {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-query-action {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
max-width: 28px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-active-connection-query-action > span:not(.anticon):not(.ant-btn-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-explorer-search {
|
||||
padding: 10px 10px 6px !important;
|
||||
border-bottom: none !important;
|
||||
|
||||
Reference in New Issue
Block a user