From 4b23c013d943ec22cbfdc958d14ea904fec98f40 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 3 Jun 2026 15:27:54 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(designer):=20=E5=B0=86?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E8=AE=BE=E8=AE=A1=E6=95=B4=E5=90=88=E8=BF=9B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=A7=86=E5=9B=BE=E5=B9=B6=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E8=A1=A8=E4=BA=A4=E4=BA=92=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/DataGrid.ddl.test.tsx | 111 ++++- .../src/components/DataGrid.layout.test.tsx | 35 ++ frontend/src/components/DataGrid.tsx | 56 ++- .../components/DataGridSecondaryActions.tsx | 6 +- frontend/src/components/DataViewer.tsx | 1 + frontend/src/components/QueryEditor.tsx | 1 + frontend/src/components/Sidebar.tsx | 2 + frontend/src/components/TableDesigner.tsx | 423 +++++++++++++++--- frontend/src/components/TableOverview.tsx | 1 + frontend/src/types.ts | 1 + frontend/src/v2-theme.css | 238 +++++++++- 11 files changed, 766 insertions(+), 109 deletions(-) diff --git a/frontend/src/components/DataGrid.ddl.test.tsx b/frontend/src/components/DataGrid.ddl.test.tsx index 5831d98..967371b 100644 --- a/frontend/src/components/DataGrid.ddl.test.tsx +++ b/frontend/src/components/DataGrid.ddl.test.tsx @@ -65,6 +65,8 @@ const backendApp = vi.hoisted(() => ({ DBGetColumns: vi.fn(), DBGetIndexes: vi.fn(), DBGetForeignKeys: vi.fn(), + DBGetTriggers: vi.fn(), + DBQuery: vi.fn(), DBShowCreateTable: vi.fn(), })); @@ -112,6 +114,16 @@ vi.mock('./ImportPreviewModal', () => ({ default: () => null, })); +vi.mock('./TableDesigner', () => ({ + default: ({ tab, embedded }: { tab: { tableName?: string; initialTab?: string }; embedded?: boolean }) => ( +
+ SCHEMA DESIGNER + {tab.tableName || 'unknown-table'} + {tab.initialTab || 'columns'} +
+ ), +})); + vi.mock('@ant-design/icons', () => { const Icon = () => ; @@ -152,6 +164,7 @@ vi.mock('@ant-design/icons', () => { vi.mock('@dnd-kit/core', () => ({ DndContext: ({ children }: any) => <>{children}, PointerSensor: vi.fn(), + KeyboardSensor: vi.fn(), MouseSensor: vi.fn(), TouchSensor: vi.fn(), useSensor: vi.fn(() => ({})), @@ -170,6 +183,7 @@ vi.mock('@dnd-kit/sortable', () => ({ isDragging: false, })), horizontalListSortingStrategy: vi.fn(), + sortableKeyboardCoordinates: vi.fn(), arrayMove: (items: any[], from: number, to: number) => { const next = [...items]; const [item] = next.splice(from, 1); @@ -223,6 +237,29 @@ vi.mock('antd', () => { Modal.useModal = () => [{ info: vi.fn(() => ({ destroy: vi.fn() })) }, null]; const passthrough = ({ children }: any) => <>{children}; + const Space = ({ children }: any) =>
{children}
; + const Tabs = ({ items = [], activeKey, onChange }: any) => { + const resolvedActiveKey = activeKey ?? items[0]?.key; + const activeItem = items.find((item: any) => item.key === resolvedActiveKey) || items[0]; + return ( +
+
+ {items.map((item: any) => ( + + ))} +
+
{activeItem?.children ?? null}
+
+ ); + }; + const Empty: any = ({ description }: any) =>
{description || 'empty'}
; + Empty.PRESENTED_IMAGE_SIMPLE = 'presented-image-simple'; + const Tag = ({ children }: any) => {children}; + const Radio: any = ({ children }: any) => {children}; + Radio.Group = ({ children }: any) =>
{children}
; + Radio.Button = ({ children }: any) => ; const Segmented = ({ value, options, onChange }: any) => (
{(options || []).map((option: any) => ( @@ -255,7 +292,7 @@ vi.mock('antd', () => { Dropdown: passthrough, Form, Pagination: () => null, - Select: () => null, + Select: ({ children }: any) =>
{children}
, Modal, Checkbox: ({ checked, onChange }: any) => , Segmented, @@ -264,6 +301,11 @@ vi.mock('antd', () => { DatePicker: () => null, TimePicker: () => null, AutoComplete: ({ children }: any) => <>{children}, + Tabs, + Empty, + Space, + Tag, + Radio, }; }); @@ -509,6 +551,8 @@ describe('DataGrid DDL interactions', () => { backendApp.DBGetColumns.mockResolvedValue({ success: true, data: [] }); backendApp.DBGetIndexes.mockResolvedValue({ success: true, data: [] }); backendApp.DBGetForeignKeys.mockResolvedValue({ success: true, data: [] }); + backendApp.DBGetTriggers.mockResolvedValue({ success: true, data: [] }); + backendApp.DBQuery.mockResolvedValue({ success: true, data: [] }); backendApp.DBShowCreateTable.mockResolvedValue({ success: true, data: 'CREATE TABLE users' }); storeState.appearance.uiVersion = 'legacy'; storeState.addTab.mockReset(); @@ -557,6 +601,8 @@ describe('DataGrid DDL interactions', () => { backendApp.DBGetColumns.mockReset(); backendApp.DBGetIndexes.mockReset(); backendApp.DBGetForeignKeys.mockReset(); + backendApp.DBGetTriggers.mockReset(); + backendApp.DBQuery.mockReset(); backendApp.DBShowCreateTable.mockReset(); vi.unstubAllGlobals(); }); @@ -654,6 +700,7 @@ describe('DataGrid DDL interactions', () => { connectionId: 'conn-1', dbName: 'main', tableName: 'customers', + objectType: 'table', }); }, ); @@ -949,8 +996,15 @@ describe('DataGrid DDL interactions', () => { renderer!.unmount(); }); - it('switches the v2 footer field tab into the main fields view', async () => { + it('switches the v2 footer object tab into the embedded designer view', async () => { storeState.appearance.uiVersion = 'v2'; + backendApp.DBGetColumns.mockResolvedValueOnce({ + success: true, + data: [ + { name: 'id', type: 'bigint', key: 'PRI', nullable: 'NO', default: '', comment: '' }, + { name: 'name', type: 'varchar(255)', key: '', nullable: 'YES', default: '', comment: '' }, + ], + }); let renderer: ReactTestRenderer; await act(async () => { @@ -968,12 +1022,12 @@ describe('DataGrid DDL interactions', () => { await waitForEffects(); await act(async () => { - findButton(renderer!, '字段信息').props.onClick(); + findButton(renderer!, '对象设计').props.onClick(); }); const content = textContent(renderer!.root); - expect(content).toContain('FIELDS'); - expect(content).toContain('2 个字段'); + expect(content).toContain('SCHEMA DESIGNER'); + expect(content).toContain('字段'); expect(content).toContain('id'); expect(content).toContain('name'); }); @@ -997,9 +1051,9 @@ describe('DataGrid DDL interactions', () => { await waitForEffects(); await act(async () => { - findButton(renderer!, '字段信息').props.onClick(); + findButton(renderer!, '对象设计').props.onClick(); }); - expect(textContent(renderer!.root)).toContain('FIELDS'); + expect(textContent(renderer!.root)).toContain('SCHEMA DESIGNER'); storeState.appearance.uiVersion = 'legacy'; await act(async () => { @@ -1017,13 +1071,54 @@ describe('DataGrid DDL interactions', () => { await waitForEffects(); const content = textContent(renderer!.root); - expect(content).not.toContain('FIELDS'); + expect(content).not.toContain('SCHEMA DESIGNER'); expect(content).not.toContain('gn-v2-data-grid-fields-view'); expect(content).toContain('数据预览'); expect(content).toContain('结果视图'); expect(content).toContain('字段信息'); }); + it('keeps the v2 fields tab as read-only field info for views', async () => { + storeState.appearance.uiVersion = 'v2'; + backendApp.DBGetColumns.mockResolvedValueOnce({ + success: true, + data: [ + { name: 'id', type: 'bigint', key: '', nullable: 'NO', default: '', comment: '' }, + { name: 'name', type: 'varchar(255)', key: '', nullable: 'YES', default: '', comment: '' }, + ], + }); + + let renderer: ReactTestRenderer; + await act(async () => { + renderer = create( + , + ); + }); + await waitForEffects(); + + expect(textContent(renderer!.root)).toContain('字段信息'); + expect(textContent(renderer!.root)).not.toContain('对象设计'); + + await act(async () => { + findButton(renderer!, '字段信息').props.onClick(); + }); + + const content = textContent(renderer!.root); + expect(content).toContain('FIELDS'); + expect(content).toContain('2 个字段'); + expect(content).toContain('id'); + expect(content).toContain('name'); + expect(content).not.toContain('SCHEMA DESIGNER'); + }); + it('renders the v2 footer DDL view with the Monaco SQL editor', async () => { storeState.appearance.uiVersion = 'v2'; backendApp.DBShowCreateTable.mockResolvedValueOnce({ diff --git a/frontend/src/components/DataGrid.layout.test.tsx b/frontend/src/components/DataGrid.layout.test.tsx index 1103ce0..221522d 100644 --- a/frontend/src/components/DataGrid.layout.test.tsx +++ b/frontend/src/components/DataGrid.layout.test.tsx @@ -79,6 +79,8 @@ describe('DataGrid layout', () => { columnNames={['id', 'name']} loading={false} tableName="users" + dbName="main" + connectionId="conn-1" readOnly pagination={{ current: 1, @@ -95,6 +97,7 @@ describe('DataGrid layout', () => { expect(markup).toContain('data-grid-column-quick-find-action="true"'); expect(markup).toContain('字段显示'); expect(markup).toContain('跳列'); + expect(markup).toContain('对象设计'); expect(markup).toContain('data-grid-page-find="true"'); expect(markup).toContain('data-grid-page-find-prev="true"'); expect(markup).toContain('data-grid-page-find-next="true"'); @@ -112,6 +115,34 @@ describe('DataGrid layout', () => { expect(markup).toContain('当前页查找...'); }); + it('keeps the v2 footer fields action labeled as field info for views', () => { + const markup = renderToStaticMarkup( + {}} + />, + ); + + expect(markup).toContain('字段信息'); + expect(markup).not.toContain('对象设计'); + }); + it('hides current-page find in JSON and text record views', () => { const source = readFileSync(new URL('./DataGrid.tsx', import.meta.url), 'utf8'); @@ -321,6 +352,7 @@ describe('DataGrid layout', () => { expect(tableMarkup).toContain('data-grid-ddl-action="true"'); expect(tableMarkup).toContain('查看 DDL'); + expect(tableMarkup).toContain('对象设计'); expect(tableMarkup).not.toContain('data-grid-locate-sidebar-action="true"'); const schemaTableMarkup = renderToStaticMarkup( @@ -342,6 +374,7 @@ describe('DataGrid layout', () => { expect(schemaTableMarkup).toContain('data-grid-ddl-action="true"'); expect(schemaTableMarkup).toContain('查看 DDL'); + expect(schemaTableMarkup).toContain('对象设计'); expect(schemaTableMarkup).toContain('data-grid-page-find="true"'); const queryMarkup = renderToStaticMarkup( @@ -363,6 +396,8 @@ describe('DataGrid layout', () => { ); expect(queryMarkup).not.toContain('data-grid-ddl-action="true"'); + expect(queryMarkup).toContain('字段信息'); + expect(queryMarkup).not.toContain('对象设计'); }); it('keeps row copy and paste as context menu actions instead of toolbar buttons', () => { diff --git a/frontend/src/components/DataGrid.tsx b/frontend/src/components/DataGrid.tsx index 08c2c05..b860102 100644 --- a/frontend/src/components/DataGrid.tsx +++ b/frontend/src/components/DataGrid.tsx @@ -129,6 +129,7 @@ import DataGridPreviewPanel from './DataGridPreviewPanel'; import { DataGridJsonView, DataGridTextView } from './DataGridRecordViews'; import { DataGridV2DdlSideWorkspace, DataGridV2DdlView } from './DataGridV2DdlWorkspace'; import { DataGridV2ErView, DataGridV2FieldsView } from './DataGridV2MetadataViews'; +import TableDesigner from './TableDesigner'; import { useDataGridFilters } from './useDataGridFilters'; import { useDataGridDdlView } from './useDataGridDdlView'; import { useDataGridModalEditors } from './useDataGridModalEditors'; @@ -1191,6 +1192,7 @@ interface DataGridProps { columnNames: string[]; loading: boolean; tableName?: string; + objectType?: 'table' | 'view' | 'materialized-view'; exportScope?: 'table' | 'queryResult'; resultSql?: string; dbName?: string; @@ -1482,7 +1484,7 @@ const VIRTUAL_EDITING_CELL_STYLE: React.CSSProperties = { }; const DataGrid: React.FC = ({ - data, columnNames, loading, tableName, exportScope = 'table', dbName, connectionId, pkColumns = [], editLocator, readOnly = false, + data, columnNames, loading, tableName, objectType = 'table', exportScope = 'table', dbName, connectionId, pkColumns = [], editLocator, readOnly = false, onReload, onSort, onPageChange, pagination, onRequestTotalCount, onCancelTotalCount, sortInfoExternal, showFilter, onToggleFilter, exportSqlWithFilter, onApplyFilter, appliedFilterConditions, quickWhereCondition, onApplyQuickWhereCondition, scrollSnapshot, onScrollSnapshotChange @@ -1720,6 +1722,7 @@ const DataGrid: React.FC = ({ const canImport = exportScope === 'table' && !!tableName; const canExport = !!connectionId && (isQueryResultExport || !!tableName); const canViewDdl = exportScope === 'table' && !!connectionId && !!tableName; + const canOpenObjectDesigner = exportScope === 'table' && objectType === 'table' && !!connectionId && !!tableName; const filteredExportSql = useMemo(() => String(exportSqlWithFilter || '').trim(), [exportSqlWithFilter]); const hasFilteredExportSql = exportScope === 'table' && filteredExportSql.length > 0; @@ -2357,6 +2360,7 @@ const DataGrid: React.FC = ({ connectionId, dbName: targetDbName, tableName: refTableName, + objectType: 'table', }); }, [addTab, connectionId, dbName, setActiveContext]); @@ -3225,6 +3229,22 @@ const DataGrid: React.FC = ({ }, }); + useEffect(() => { + const handleExternalViewModeChange = (event: Event) => { + const detail = (event as CustomEvent)?.detail || {}; + if (String(detail.connectionId || '') !== String(connectionId || '')) return; + if (String(detail.dbName || '') !== String(dbName || '')) return; + if (String(detail.tableName || '') !== String(tableName || '')) return; + const nextMode = String(detail.viewMode || '').trim(); + if (!nextMode) return; + if (!['table', 'json', 'text', 'fields', 'ddl', 'er'].includes(nextMode)) return; + handleViewModeChange(nextMode as GridViewMode); + }; + + window.addEventListener('gonavi:data-grid:set-view-mode', handleExternalViewModeChange as EventListener); + return () => window.removeEventListener('gonavi:data-grid:set-view-mode', handleExternalViewModeChange as EventListener); + }, [canOpenObjectDesigner, connectionId, dbName, handleViewModeChange, tableName]); + useEffect(() => { if (!isTableSurfaceActive || !isV2Ui || !cellContextMenu.visible) return; const portal = cellContextMenuPortalRef.current; @@ -7542,14 +7562,31 @@ const DataGrid: React.FC = ({ {viewMode === 'table' ? ( renderDataTableView() ) : isV2Ui && viewMode === 'fields' ? ( - + canOpenObjectDesigner ? ( + + ) : ( + + ) ) : isV2Ui && viewMode === 'ddl' && ddlViewLayout === 'side' ? ( = ({ = ({ isV2Ui, canViewDdl, + canOpenObjectDesigner, viewMode, ddlLoading, showColumnComment, @@ -53,9 +55,11 @@ const DataGridSecondaryActions: React.FC = ({ onOpenTableDdl, }) => { if (isV2Ui) { + const fieldsActionLabel = canOpenObjectDesigner ? '对象设计' : '字段信息'; + const fieldsActionIcon = canOpenObjectDesigner ? : ; const viewTabItems: Array<{ key: GridViewMode; label: string; icon: React.ReactNode; disabled?: boolean }> = [ { key: 'table', label: '数据预览', icon: }, - { key: 'fields', label: '字段信息', icon: }, + { key: 'fields', label: fieldsActionLabel, icon: fieldsActionIcon }, { key: 'ddl', label: '查看 DDL', icon: , disabled: !canViewDdl }, { key: 'er', label: 'ER 图', icon: }, ]; diff --git a/frontend/src/components/DataViewer.tsx b/frontend/src/components/DataViewer.tsx index 1a82962..6bbc59a 100644 --- a/frontend/src/components/DataViewer.tsx +++ b/frontend/src/components/DataViewer.tsx @@ -1095,6 +1095,7 @@ const DataViewer: React.FC<{ tab: TabData; isActive?: boolean }> = React.memo(({ columnNames={columnNames} loading={loading} tableName={tab.tableName} + objectType={tab.objectType || 'table'} exportScope="table" dbName={tab.dbName} connectionId={tab.connectionId} diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 663f8d4..b9fa1f3 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -2726,6 +2726,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc connectionId, dbName: targetDbName, tableName: targetTableName, + objectType: 'table', }); dispatchQueryEditorSidebarLocate({ connectionId, diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d2a7f8b..3d90733 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -3415,6 +3415,7 @@ const Sidebar: React.FC<{ connectionId: id, dbName, tableName, + objectType: 'table', }); return; } else if (node.type === 'view' || node.type === 'materialized-view') { @@ -3426,6 +3427,7 @@ const Sidebar: React.FC<{ connectionId: id, dbName, tableName: viewName, + objectType: node.type === 'materialized-view' ? 'materialized-view' : 'view', }); return; } else if (node.type === 'saved-query') { diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx index c15260f..328bf6b 100644 --- a/frontend/src/components/TableDesigner.tsx +++ b/frontend/src/components/TableDesigner.tsx @@ -360,7 +360,23 @@ const SortableRow = ({ children, ...props }: RowProps) => { ); }; -const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { +const renderDesignerCellField = (content: React.ReactNode, className?: string) => ( +
+ {content} +
+); + +const renderDesignerCellCheck = (content: React.ReactNode, className?: string) => ( +
+ {content} +
+); + +const renderDesignerHeaderTitle = (title: string) => ( + {title} +); + +const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, embedded = false }) => { const isNewTable = !tab.tableName; const [columns, setColumns] = useState([]); @@ -431,6 +447,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { const [commentEditorColumnKey, setCommentEditorColumnKey] = useState(''); const [commentEditorColumnName, setCommentEditorColumnName] = useState(''); const [commentEditorValue, setCommentEditorValue] = useState(''); + const [inlineCommentEditingKey, setInlineCommentEditingKey] = useState(''); const connections = useStore(state => state.connections); const theme = useStore(state => state.theme); @@ -439,9 +456,6 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { const isV2Ui = appearance.uiVersion === 'v2'; const resizeGuideColor = darkMode ? '#f6c453' : '#1890ff'; const readOnly = !!tab.readOnly; - const designerTableTitle = tab.tableName || newTableName || '未命名表'; - const designerDbTitle = tab.dbName || '默认库'; - const designerColumnSummary = `${columns.length} 字段`; const panelRadius = 10; const panelFrameColor = darkMode ? 'rgba(0, 0, 0, 0.18)' : 'rgba(0, 0, 0, 0.12)'; const panelToolbarBorder = darkMode ? 'rgba(255, 255, 255, 0.12)' : 'rgba(0, 0, 0, 0.10)'; @@ -458,6 +472,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { const openCommentEditor = useCallback((record: EditableColumn) => { if (!record?._key) return; + setInlineCommentEditingKey(''); setCommentEditorColumnKey(record._key); setCommentEditorColumnName(record.name || ''); setCommentEditorValue(record.comment || ''); @@ -518,6 +533,10 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { setSelectedColumnRowKeys(prev => prev.filter(key => columns.some(c => c._key === key))); }, [columns]); + useEffect(() => { + setInlineCommentEditingKey(prev => (prev && columns.some(c => c._key === prev) ? prev : '')); + }, [columns]); + useEffect(() => { return () => { if (focusHighlightTimerRef.current !== null) { @@ -552,6 +571,15 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { return true; }, [activeKey, readOnly]); + const startInlineCommentEdit = useCallback((record: EditableColumn) => { + if (readOnly || !record?._key) return; + setInlineCommentEditingKey(record._key); + }, [readOnly]); + + const finishInlineCommentEdit = useCallback(() => { + setInlineCommentEditingKey(''); + }, []); + useEffect(() => { const pendingKey = pendingFocusColumnKeyRef.current; if (!pendingKey || activeKey !== 'columns') return; @@ -578,64 +606,80 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => { const columnTypeOptions = resolveColumnTypeOptions(getDbType()); const initialCols = [ { - title: '名', + title: renderDesignerHeaderTitle('名'), dataIndex: 'name', key: 'name', width: 180, render: (text: string, record: EditableColumn) => readOnly ? text : ( - handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" /> + renderDesignerCellField( + handleColumnChange(record._key, 'name', e.target.value)} variant="borderless" /> + ) ) }, { - title: '类型', + title: renderDesignerHeaderTitle('类型'), dataIndex: 'type', key: 'type', width: 150, render: (text: string, record: EditableColumn) => readOnly ? text : ( - handleColumnChange(record._key, 'type', val)} style={{ width: '100%' }} variant="borderless" /> + renderDesignerCellField( + handleColumnChange(record._key, 'type', val)} style={{ width: '100%' }} variant="borderless" />, + 'is-compact' + ) ) }, { - title: '主键', + title: renderDesignerHeaderTitle('主键'), dataIndex: 'key', key: 'key', width: 60, align: 'center', render: (text: string, record: EditableColumn) => ( - handleColumnChange(record._key, 'key', e.target.checked ? 'PRI' : '')} /> + renderDesignerCellCheck( + handleColumnChange(record._key, 'key', e.target.checked ? 'PRI' : '')} />, + 'is-left-aligned' + ) ) }, { - title: '自增', + title: renderDesignerHeaderTitle('自增'), dataIndex: 'isAutoIncrement', key: 'isAutoIncrement', width: 60, align: 'center', render: (val: boolean, record: EditableColumn) => ( - handleColumnChange(record._key, 'isAutoIncrement', e.target.checked)} /> + renderDesignerCellCheck( + handleColumnChange(record._key, 'isAutoIncrement', e.target.checked)} />, + 'is-left-aligned' + ) ) }, { - title: '不是 Null', + title: renderDesignerHeaderTitle('不是 Null'), dataIndex: 'nullable', key: 'nullable', width: 80, align: 'center', render: (text: string, record: EditableColumn) => ( - handleColumnChange(record._key, 'nullable', e.target.checked ? 'NO' : 'YES')} /> + renderDesignerCellCheck( + handleColumnChange(record._key, 'nullable', e.target.checked ? 'NO' : 'YES')} />, + 'is-left-aligned' + ) ) }, { - title: '默认', + title: renderDesignerHeaderTitle('默认'), dataIndex: 'default', key: 'default', width: 180, // Increased default width render: (text: string, record: EditableColumn) => readOnly ? text : ( - handleColumnChange(record._key, 'default', val)} style={{ width: '100%' }} variant="borderless" placeholder="NULL" /> + renderDesignerCellField( + handleColumnChange(record._key, 'default', val)} style={{ width: '100%' }} variant="borderless" placeholder="NULL" /> + ) ) }, { - title: '注释', + title: renderDesignerHeaderTitle('注释'), dataIndex: 'comment', key: 'comment', width: 200, @@ -644,13 +688,26 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
{text || ''}
) : ( -
- handleColumnChange(record._key, 'comment', e.target.value)} - onDoubleClick={() => openCommentEditor(record)} - variant="borderless" - /> +
+ {inlineCommentEditingKey !== record._key ? ( + +
startInlineCommentEdit(record)} + > + {text || '\u00A0'} +
+
+ ) : ( + handleColumnChange(record._key, 'comment', e.target.value)} + onBlur={finishInlineCommentEdit} + onPressEnter={finishInlineCommentEdit} + autoFocus={inlineCommentEditingKey === record._key} + variant="borderless" + /> + )}
) }]) ]; setTableColumns(initialCols); - }, [connections, openCommentEditor, readOnly, tab.connectionId]); // Re-create when datasource dialect or readonly state changes + }, [connections, embedded, finishInlineCommentEdit, inlineCommentEditingKey, openCommentEditor, readOnly, startInlineCommentEdit, tab.connectionId]); // Re-create when datasource dialect or readonly state changes const flushResizeGhost = useCallback(() => { resizeRafRef.current = null; @@ -2211,29 +2277,36 @@ END;`; const columnSelectCol = useMemo(() => ({ title: () => ( - setSelectedColumnRowKeys(e.target.checked ? allColumnKeys : [])} - style={{ margin: 0 }} - /> +
+ setSelectedColumnRowKeys(e.target.checked ? allColumnKeys : [])} + style={{ margin: 0 }} + /> +
), dataIndex: '_select', key: '_select', - width: 48, + width: 44, + className: 'table-designer-select-column', + onHeaderCell: () => ({ className: 'table-designer-select-column' }), + onCell: () => ({ className: 'table-designer-select-column' }), render: (_: any, record: any) => ( - { - e.stopPropagation(); - setSelectedColumnRowKeys((prev: string[]) => - e.target.checked - ? [...prev, record._key] - : prev.filter((k: string) => k !== record._key) - ); - }} - style={{ margin: 0 }} - /> +
+ { + e.stopPropagation(); + setSelectedColumnRowKeys((prev: string[]) => + e.target.checked + ? [...prev, record._key] + : prev.filter((k: string) => k !== record._key) + ); + }} + style={{ margin: 0 }} + /> +
), }), [selectedColumnRowKeys, allColumnKeys, isAllColumnsSelected, isColumnsIndeterminate]); @@ -2331,6 +2404,9 @@ END;`; dataIndex: '_select', key: '_select', width: 48, + className: 'table-designer-select-column', + onHeaderCell: () => ({ className: 'table-designer-select-column' }), + onCell: () => ({ className: 'table-designer-select-column' }), render: (_: any, record: any) => ( { @@ -2548,7 +2624,11 @@ END;`; ); return ( -
+