feat(ui): 优化侧边栏设置中心与数据表交互

- 收敛左上角入口为工具和设置中心,并调整新建连接操作优先级
- 优化表设计器 SQL 预览高亮和刷新前未保存字段变更确认
- 下移数据页次级操作并将编辑行收口到单元格右键菜单
- 补充侧边栏布局、表设计器草稿检测和数据页布局回归测试

Refs #324
This commit is contained in:
Syngnat
2026-04-17 20:09:46 +08:00
parent 39f6fbbe1f
commit 0bccdeed8c
9 changed files with 341 additions and 129 deletions

View File

@@ -869,8 +869,8 @@ function App() {
...sidebarQuickActionBaseStyle,
flex: '1 1 0',
border: 'none',
background: 'linear-gradient(135deg, rgba(255,214,102,0.96) 0%, rgba(240,183,39,0.92) 100%)',
color: '#2a1f00',
background: 'linear-gradient(135deg, rgba(34,197,94,0.96) 0%, rgba(22,163,74,0.92) 100%)',
color: '#f3fff7',
}), [sidebarQuickActionBaseStyle]);
const utilityModalShellStyle = useMemo(() => ({
@@ -1855,6 +1855,7 @@ function App() {
};
const [isToolsModalOpen, setIsToolsModalOpen] = useState(false);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [isThemeModalOpen, setIsThemeModalOpen] = useState(false);
const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme');
const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false);
@@ -1888,26 +1889,11 @@ function App() {
icon: <ToolOutlined />,
onClick: () => setIsToolsModalOpen(true),
},
proxy: {
key: 'proxy',
title: '代理',
icon: <GlobalOutlined />,
onClick: () => {
setSecurityUpdateRepairSource(null);
setIsProxyModalOpen(true);
},
},
theme: {
key: 'theme',
title: '主题',
icon: <SkinOutlined />,
onClick: () => setIsThemeModalOpen(true),
},
about: {
key: 'about',
title: '关于',
icon: <InfoCircleOutlined />,
onClick: () => setIsAboutOpen(true),
settings: {
key: 'settings',
title: '设置',
icon: <SettingOutlined />,
onClick: () => setIsSettingsModalOpen(true),
},
} as const;
@@ -2648,7 +2634,7 @@ function App() {
>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ padding: `12px ${sidebarHorizontalPadding}px 8px`, borderBottom: 'none', display: 'flex', alignItems: 'center', flexShrink: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 8, width: '100%' }}>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${sidebarUtilityItems.length}, minmax(0, 1fr))`, gap: 8, width: '100%' }}>
{sidebarUtilityItems.map((item) => (
<Tooltip key={item.key} title={item.title}>
<Button type="text" icon={item.icon} style={utilityButtonStyle} onClick={item.onClick} />
@@ -2658,12 +2644,12 @@ function App() {
</div>
<div style={{ padding: `0 ${sidebarHorizontalPadding}px 10px`, borderBottom: 'none', display: 'flex', alignItems: 'center', flexShrink: 0 }}>
<div style={{ display: 'grid', gridTemplateColumns: isSidebarCompact ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) minmax(0, 1fr)', gap: 8, width: '100%' }}>
<Button icon={<ConsoleSqlOutlined />} onClick={handleNewQuery} title="新建查询" style={sidebarQueryActionStyle}>
</Button>
<Button icon={<PlusOutlined />} onClick={handleCreateConnection} title="新建连接" style={sidebarCreateConnectionActionStyle}>
</Button>
<Button icon={<ConsoleSqlOutlined />} onClick={handleNewQuery} title="新建查询" style={sidebarQueryActionStyle}>
</Button>
</div>
</div>
@@ -2910,6 +2896,71 @@ function App() {
))}
</div>
</Modal>
<Modal
title={renderUtilityModalTitle(<SettingOutlined />, '设置中心', '集中处理代理、主题、AI 与关于等通用配置入口。')}
open={isSettingsModalOpen}
onCancel={() => setIsSettingsModalOpen(false)}
footer={null}
width={560}
styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }}
>
<div style={{ display: 'grid', gap: 12, padding: '12px 0' }}>
{[
{
key: 'theme',
icon: <SkinOutlined />,
title: '主题与外观',
description: '切换亮暗主题并调整界面观感。',
onClick: () => {
setIsSettingsModalOpen(false);
setThemeModalSection('theme');
setIsThemeModalOpen(true);
},
},
{
key: 'proxy',
icon: <GlobalOutlined />,
title: '全局代理',
description: '统一配置更新检查、驱动管理和公共网络出口。',
onClick: () => {
setIsSettingsModalOpen(false);
setSecurityUpdateRepairSource(null);
setIsProxyModalOpen(true);
},
},
{
key: 'ai',
icon: <RobotOutlined />,
title: 'AI 设置',
description: '管理模型供应商、密钥和默认行为。',
onClick: () => {
setIsSettingsModalOpen(false);
handleOpenAISettings();
},
},
{
key: 'about',
icon: <InfoCircleOutlined />,
title: '关于 GoNavi',
description: '查看版本信息、仓库地址和更新状态。',
onClick: () => {
setIsSettingsModalOpen(false);
setIsAboutOpen(true);
},
},
].map((item) => (
<Button key={item.key} type="text" style={utilityActionCardStyle} onClick={item.onClick}>
<span style={{ width: 36, height: 36, borderRadius: 12, display: 'grid', placeItems: 'center', background: overlayTheme.iconBg, color: overlayTheme.iconColor, flexShrink: 0 }}>
{item.icon}
</span>
<span style={{ display: 'grid', gap: 4, textAlign: 'left', minWidth: 0 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: overlayTheme.titleText }}>{item.title}</span>
<span style={{ fontSize: 12, color: overlayTheme.mutedText, whiteSpace: 'normal' }}>{item.description}</span>
</span>
</Button>
))}
</div>
</Modal>
<Modal
title={renderUtilityModalTitle(<HddOutlined />, '数据存储位置', '统一管理连接、代理、AI 配置与驱动等文件型数据的根目录。')}
open={isDataRootModalOpen}

View File

@@ -0,0 +1,80 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import DataGrid from './DataGrid';
vi.mock('../store', () => ({
useStore: (selector: (state: any) => any) => selector({
connections: [],
addSqlLog: vi.fn(),
theme: 'light',
appearance: {
enabled: true,
opacity: 1,
blur: 0,
showDataTableVerticalBorders: false,
dataTableColumnWidthMode: 'standard',
},
queryOptions: {
showColumnComment: false,
showColumnType: false,
},
setQueryOptions: vi.fn(),
tableColumnOrders: {},
enableColumnOrderMemory: false,
setTableColumnOrder: vi.fn(),
setEnableColumnOrderMemory: vi.fn(),
clearTableColumnOrder: vi.fn(),
tableHiddenColumns: {},
enableHiddenColumnMemory: false,
setTableHiddenColumns: vi.fn(),
setEnableHiddenColumnMemory: vi.fn(),
clearTableHiddenColumns: vi.fn(),
aiPanelVisible: false,
setAIPanelVisible: vi.fn(),
}),
}));
vi.mock('../../wailsjs/go/app/App', () => ({
ImportData: vi.fn(),
ExportTable: vi.fn(),
ExportData: vi.fn(),
ExportQuery: vi.fn(),
ApplyChanges: vi.fn(),
DBGetColumns: vi.fn(),
DBGetIndexes: vi.fn(),
}));
vi.mock('@monaco-editor/react', () => ({
default: () => null,
}));
describe('DataGrid layout', () => {
it('renders a secondary action strip for view switching and auxiliary actions', () => {
const markup = renderToStaticMarkup(
<DataGrid
data={[
{
__gonavi_row_key__: 'row-1',
id: 1,
name: 'alpha',
},
]}
columnNames={['id', 'name']}
loading={false}
tableName="users"
readOnly
pagination={{
current: 1,
pageSize: 100,
total: 1,
}}
onPageChange={() => {}}
/>,
);
expect(markup).toContain('data-grid-secondary-actions="true"');
expect(markup).toContain('data-grid-view-switcher="true"');
});
});

View File

@@ -3247,20 +3247,6 @@ const DataGrid: React.FC<DataGridProps> = ({
setRowEditorOpen(true);
}, [canModifyData, mergedDisplayData, data, addedRows, displayColumnNames, rowEditorForm, rowKeyStr, columnMetaMap, columnMetaMapByLowerName]);
const openRowEditor = useCallback(() => {
if (!canModifyData) return;
if (selectedRowKeys.length > 1) {
void message.info('一次只能编辑一行,请仅选择一行');
return;
}
const keyStr = selectedRowKeys.length === 1 ? rowKeyStr(selectedRowKeys[0]) : undefined;
if (!keyStr) {
void message.info('请先选择一行(勾选复选框)');
return;
}
openRowEditorByKey(keyStr);
}, [canModifyData, selectedRowKeys, rowKeyStr, openRowEditorByKey]);
const openCurrentViewRowEditor = useCallback(() => {
if (!canModifyData) return;
const currentRow = mergedDisplayData[textRecordIndex];
@@ -3278,6 +3264,50 @@ const DataGrid: React.FC<DataGridProps> = ({
setJsonEditorOpen(true);
}, [canModifyData, jsonViewText]);
const handleViewModeChange = useCallback((nextMode: GridViewMode) => {
if (nextMode === 'json' && cellEditMode) {
setCellEditMode(false);
setSelectedCells(new Set());
currentSelectionRef.current = new Set();
selectionStartRef.current = null;
isDraggingRef.current = false;
cellSelectionPointerRef.current = null;
if (cellSelectionRafRef.current !== null) {
cancelAnimationFrame(cellSelectionRafRef.current);
cellSelectionRafRef.current = null;
}
if (cellSelectionScrollRafRef.current !== null) {
cancelAnimationFrame(cellSelectionScrollRafRef.current);
cellSelectionScrollRafRef.current = null;
}
if (cellSelectionAutoScrollRafRef.current !== null) {
cancelAnimationFrame(cellSelectionAutoScrollRafRef.current);
cellSelectionAutoScrollRafRef.current = null;
}
updateCellSelection(new Set());
}
if (nextMode === 'text') {
const selectedKey = selectedRowKeys[0];
if (selectedKey !== undefined) {
const idx = mergedDisplayData.findIndex((row) => rowKeyStr(row?.[GONAVI_ROW_KEY]) === rowKeyStr(selectedKey));
if (idx >= 0) {
setTextRecordIndex(idx);
}
}
}
setViewMode(nextMode);
}, [cellEditMode, mergedDisplayData, selectedRowKeys, rowKeyStr, updateCellSelection]);
const handleOpenContextMenuRowEditor = useCallback(() => {
if (!canModifyData) return;
const rowKey = cellContextMenu.record?.[GONAVI_ROW_KEY];
if (rowKey === undefined || rowKey === null) return;
openRowEditorByKey(rowKeyStr(rowKey));
setCellContextMenu(prev => ({ ...prev, visible: false }));
}, [canModifyData, cellContextMenu.record, openRowEditorByKey, rowKeyStr]);
const handleFormatJsonEditor = useCallback(() => {
try {
const parsed = JSON.parse(jsonEditorValue);
@@ -4868,7 +4898,7 @@ const DataGrid: React.FC<DataGridProps> = ({
<div className={`${gridId}${cellEditMode ? ' cell-edit-mode' : ''} data-grid-root`} style={{ flex: '1 1 auto', height: '100%', overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column', minHeight: 0, minWidth: 0, background: 'transparent' }}>
{/* Toolbar + Filter Panel */}
<div style={{ margin: `${panelOuterGap}px 0 ${panelOuterGap}px 0`, border: `1px solid ${panelFrameColor}`, borderRadius: `${panelRadius}px`, background: bgFilter, overflow: 'hidden', boxSizing: 'border-box' }}>
<div className="data-grid-toolbar-scroll" style={{ padding: showFilter ? `${panelPaddingY}px ${panelPaddingX}px ${toolbarBottomPadding}px ${panelPaddingX}px` : `${panelPaddingY}px ${panelPaddingX}px`, border: 'none', borderRadius: 0, background: 'transparent', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'nowrap', minWidth: 0, overflowX: 'auto', overflowY: 'hidden', scrollbarGutter: 'stable', WebkitOverflowScrolling: 'touch', boxSizing: 'border-box' }}>
<div className="data-grid-toolbar-scroll" data-grid-primary-actions="true" style={{ padding: showFilter ? `${panelPaddingY}px ${panelPaddingX}px ${toolbarBottomPadding}px ${panelPaddingX}px` : `${panelPaddingY}px ${panelPaddingX}px`, border: 'none', borderRadius: 0, background: 'transparent', display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'nowrap', minWidth: 0, overflowX: 'auto', overflowY: 'hidden', scrollbarGutter: 'stable', WebkitOverflowScrolling: 'touch', boxSizing: 'border-box' }}>
{onReload && <Button icon={<ReloadOutlined />} disabled={loading} onClick={() => {
setAddedRows([]);
setModifiedRows({});
@@ -4891,13 +4921,6 @@ const DataGrid: React.FC<DataGridProps> = ({
<>
<div style={{ width: 1, background: toolbarDividerColor, height: 20, margin: '0 8px' }} />
<Button icon={<PlusOutlined />} onClick={handleAddRow}></Button>
<Button
icon={<EditOutlined />}
disabled={selectedRowKeys.length !== 1}
onClick={openRowEditor}
>
</Button>
<Button icon={<DeleteOutlined />} danger disabled={selectedRowKeys.length === 0} onClick={handleDeleteSelected}></Button>
{selectedRowKeys.length > 0 && <span style={{ fontSize: '12px', color: '#888' }}> {selectedRowKeys.length}</span>}
<div style={{ width: 1, background: toolbarDividerColor, height: 20, margin: '0 8px' }} />
@@ -5047,78 +5070,6 @@ const DataGrid: React.FC<DataGridProps> = ({
)}
<div style={{ marginLeft: 'auto' }} />
<div style={{ flexShrink: 0 }}>
<Button
icon={<EditOutlined />}
type={dataPanelOpen ? 'primary' : 'default'}
onClick={() => {
const next = !dataPanelOpen;
setDataPanelOpen(next);
if (!next) {
setFocusedCellInfo(null);
setDataPanelValue('');
setDataPanelIsJson(false);
dataPanelDirtyRef.current = false;
}
}}
>
</Button>
</div>
<div style={{ flexShrink: 0 }}>
<Popover
trigger="click"
placement="bottomRight"
content={columnInfoSettingContent}
>
<Button icon={<FileTextOutlined />}></Button>
</Popover>
</div>
<div style={{ flexShrink: 0 }}>
<Segmented
size="small"
value={viewMode}
options={[
{ label: '表格', value: 'table' },
{ label: 'JSON', value: 'json' },
{ label: '文本', value: 'text' }
]}
onChange={(val) => {
const nextMode = String(val) as GridViewMode;
if (nextMode === 'json' && cellEditMode) {
setCellEditMode(false);
setSelectedCells(new Set());
currentSelectionRef.current = new Set();
selectionStartRef.current = null;
isDraggingRef.current = false;
cellSelectionPointerRef.current = null;
if (cellSelectionRafRef.current !== null) {
cancelAnimationFrame(cellSelectionRafRef.current);
cellSelectionRafRef.current = null;
}
if (cellSelectionScrollRafRef.current !== null) {
cancelAnimationFrame(cellSelectionScrollRafRef.current);
cellSelectionScrollRafRef.current = null;
}
if (cellSelectionAutoScrollRafRef.current !== null) {
cancelAnimationFrame(cellSelectionAutoScrollRafRef.current);
cellSelectionAutoScrollRafRef.current = null;
}
updateCellSelection(new Set());
}
if (nextMode === 'text') {
const selectedKey = selectedRowKeys[0];
if (selectedKey !== undefined) {
const idx = mergedDisplayData.findIndex((row) => rowKeyStr(row?.[GONAVI_ROW_KEY]) === rowKeyStr(selectedKey));
if (idx >= 0) {
setTextRecordIndex(idx);
}
}
}
setViewMode(nextMode);
}}
/>
</div>
</div>
{showFilter && (
@@ -5710,6 +5661,19 @@ const DataGrid: React.FC<DataGridProps> = ({
>
NULL
</div>
<div
style={{
padding: '8px 12px',
cursor: 'pointer',
transition: 'background 0.2s',
}}
onMouseEnter={(e) => e.currentTarget.style.background = darkMode ? '#303030' : '#f5f5f5'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
onClick={handleOpenContextMenuRowEditor}
>
<EditOutlined style={{ marginRight: 8 }} />
</div>
<div
style={{
padding: '8px 12px',
@@ -5919,6 +5883,58 @@ const DataGrid: React.FC<DataGridProps> = ({
document.body
)}
</div>
<div
data-grid-secondary-actions="true"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 10,
flexWrap: 'wrap',
padding: '4px 0 0',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<Button
icon={<EditOutlined />}
type={dataPanelOpen ? 'primary' : 'default'}
disabled={viewMode !== 'table'}
onClick={() => {
const next = !dataPanelOpen;
setDataPanelOpen(next);
if (!next) {
setFocusedCellInfo(null);
setDataPanelValue('');
setDataPanelIsJson(false);
dataPanelDirtyRef.current = false;
}
}}
>
</Button>
<Popover
trigger="click"
placement="bottomRight"
content={columnInfoSettingContent}
>
<Button icon={<FileTextOutlined />}></Button>
</Popover>
</div>
<div data-grid-view-switcher="true" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontSize: 12, color: darkMode ? '#999' : '#666' }}></span>
<Segmented
size="small"
value={viewMode}
options={[
{ label: '表格', value: 'table' },
{ label: 'JSON', value: 'json' },
{ label: '文本', value: 'text' }
]}
onChange={(val) => handleViewModeChange(String(val) as GridViewMode)}
/>
</div>
</div>
{pagination && (
<div className="data-grid-pagination-wrap" style={{ padding: '12px 0 0', borderTop: 'none', display: 'flex', justifyContent: 'flex-end' }}>

View File

@@ -320,6 +320,10 @@ const TabManager: React.FC = () => {
box-shadow: 0 0 0 2px rgba(9, 109, 217, 0.32);
background: rgba(9, 109, 217, 0.08);
}
body[data-theme='light'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: rgba(24, 144, 255, 0.10) !important;
border-color: rgba(24, 144, 255, 0.28) !important;
}
body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: rgba(255, 214, 102, 0.12) !important;
border-color: rgba(255, 214, 102, 0.4) !important;

View File

@@ -9,7 +9,7 @@ import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, Trigg
import { useStore } from '../store';
import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils';
import { buildAlterTablePreviewSql } from './tableDesignerSchemaSql';
import { buildAlterTablePreviewSql, hasAlterTableDraftChanges } from './tableDesignerSchemaSql';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
@@ -1396,6 +1396,19 @@ ${selectedTrigger.statement}`;
};
};
const hasUnsavedDraftChanges = useMemo(() => {
if (isNewTable || readOnly) {
return false;
}
const tableInfo = resolveTableInfo();
return hasAlterTableDraftChanges({
dbType: tableInfo.dbType,
tableName: tableInfo.qualifiedName,
originalColumns,
columns,
});
}, [columns, connections, isNewTable, originalColumns, readOnly, tab.connectionId, tab.dbName, tab.tableName]);
const supportsIndexSchemaOps = (): boolean => {
const dbType = getDbType();
if (!dbType) return false;
@@ -2143,6 +2156,24 @@ END;`;
}
};
const handleRefreshDesigner = () => {
if (!hasUnsavedDraftChanges) {
void fetchData();
return;
}
Modal.confirm({
title: '存在未保存的字段变更',
icon: <ExclamationCircleOutlined />,
content: '刷新后会丢失当前尚未保存的字段调整,是否仍要刷新并覆盖当前草稿?',
okText: '仍然刷新',
cancelText: '取消',
onOk: async () => {
await fetchData();
},
});
};
const handleExecuteSave = async () => {
const result = await executeSchemaStatements(previewSql);
if (!result.ok) {
@@ -2519,7 +2550,7 @@ END;`;
</>
)}
{!readOnly && <Button size="small" icon={<SaveOutlined />} type="primary" onClick={generateDDL}></Button>}
{!isNewTable && <Button size="small" icon={<ReloadOutlined />} onClick={fetchData}></Button>}
{!isNewTable && <Button size="small" icon={<ReloadOutlined />} onClick={handleRefreshDesigner}></Button>}
{!isNewTable && !readOnly && supportsTableCommentOps() && (
<Button size="small" icon={<EditOutlined />} onClick={openTableCommentModal}></Button>
)}
@@ -2983,10 +3014,24 @@ END;`;
okText="执行"
cancelText="取消"
>
<div style={{ maxHeight: '400px', overflow: 'auto' }}>
<pre style={{ background: darkMode ? '#1e1e1e' : '#f5f5f5', color: darkMode ? '#d4d4d4' : 'inherit', padding: '10px', borderRadius: '4px', border: darkMode ? '1px solid #333' : '1px solid #eee', whiteSpace: 'pre-wrap' }}>
{previewSql}
</pre>
<div style={{ maxHeight: '400px', overflow: 'hidden', borderRadius: 8, border: darkMode ? '1px solid #333' : '1px solid #eee' }}>
<Editor
height="360px"
defaultLanguage="sql"
language="sql"
theme={darkMode ? 'transparent-dark' : 'transparent-light'}
value={previewSql}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
lineNumbers: 'on',
scrollBeyondLastLine: false,
wordWrap: 'on',
automaticLayout: true,
padding: { top: 8, bottom: 8 },
}}
/>
</div>
<p style={{ marginTop: 10, color: '#faad14' }}> SQL</p>
</Modal>

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
buildAlterTablePreviewSql,
hasAlterTableDraftChanges,
type BuildAlterTablePreviewInput,
type EditableColumnSnapshot,
} from './tableDesignerSchemaSql';
@@ -29,6 +30,18 @@ const buildInput = (overrides: Partial<BuildAlterTablePreviewInput>): BuildAlter
});
describe('tableDesignerSchemaSql', () => {
it('detects when alter table drafts contain unsaved column changes', () => {
expect(hasAlterTableDraftChanges(buildInput({ dbType: 'mysql' }))).toBe(true);
expect(
hasAlterTableDraftChanges(
buildInput({
dbType: 'mysql',
columns: [baseColumn({ _key: 'id', name: 'id', key: 'PRI', nullable: 'NO' })],
}),
),
).toBe(false);
});
it('keeps mysql alter preview syntax with column position clauses', () => {
const sql = buildAlterTablePreviewSql(buildInput({ dbType: 'mysql' }));

View File

@@ -260,3 +260,6 @@ export const buildAlterTablePreviewSql = (input: BuildAlterTablePreviewInput): s
}
return buildMySqlAlterPreviewSql({ ...input, dbType });
};
export const hasAlterTableDraftChanges = (input: BuildAlterTablePreviewInput): boolean =>
buildAlterTablePreviewSql(input).trim().length > 0;

View File

@@ -8,8 +8,8 @@ import {
} from './aiEntryLayout';
describe('ai entry layout', () => {
it('keeps the sidebar utility group free of the AI entry', () => {
expect(SIDEBAR_UTILITY_ITEM_KEYS).toEqual(['tools', 'proxy', 'theme', 'about']);
it('keeps the sidebar utility group compact and free of the AI entry', () => {
expect(SIDEBAR_UTILITY_ITEM_KEYS).toEqual(['tools', 'settings']);
});
it('anchors the AI entry to the content edge', () => {

View File

@@ -1,6 +1,6 @@
import type { CSSProperties } from 'react';
export const SIDEBAR_UTILITY_ITEM_KEYS = ['tools', 'proxy', 'theme', 'about'] as const;
export const SIDEBAR_UTILITY_ITEM_KEYS = ['tools', 'settings'] as const;
export type AIEntryPlacement = 'content-edge';
export type AIEdgeHandleAttachment = 'content-shell' | 'panel-shell';