@adecore/database 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DatabaseProvider.d.ts +4 -3
- package/dist/DatabaseProvider.js +15 -3
- package/dist/KeyIcon.d.ts +4 -0
- package/dist/KeyIcon.js +14 -0
- package/dist/actions.d.ts +12 -0
- package/dist/client-context.d.ts +3 -1
- package/dist/client-context.js +2 -0
- package/dist/code-text.d.ts +1 -0
- package/dist/code-text.js +2 -0
- package/dist/column-keys.d.ts +5 -0
- package/dist/column-keys.js +2 -0
- package/dist/connections/ConnectionManager.d.ts +3 -2
- package/dist/connections/ConnectionManager.js +27 -16
- package/dist/connections/connection-config.js +2 -0
- package/dist/console/QueryConsole.d.ts +8 -3
- package/dist/console/QueryConsole.js +110 -50
- package/dist/console/SqlEditor.d.ts +2 -12
- package/dist/console/SqlEditor.js +29 -18
- package/dist/console/StatementResultView.d.ts +5 -1
- package/dist/console/StatementResultView.js +18 -5
- package/dist/console/TransactionControls.js +3 -6
- package/dist/console/editor-slot.d.ts +18 -0
- package/dist/console/editor-slot.js +1 -0
- package/dist/console/result-keys.d.ts +15 -0
- package/dist/console/result-keys.js +79 -0
- package/dist/designer/ColumnsEditor.js +2 -2
- package/dist/designer/DesignerView.d.ts +3 -2
- package/dist/designer/DesignerView.js +5 -4
- package/dist/designer/SqlPreview.js +2 -1
- package/dist/designer/TableDesigner.d.ts +3 -2
- package/dist/designer/TableDesigner.js +2 -2
- package/dist/explorer/DatabaseExplorer.d.ts +4 -1
- package/dist/explorer/DatabaseExplorer.js +79 -23
- package/dist/explorer/RowMenu.js +2 -2
- package/dist/explorer/expansion.js +2 -2
- package/dist/explorer/folders.d.ts +16 -0
- package/dist/explorer/folders.js +1 -0
- package/dist/explorer/tree.d.ts +55 -4
- package/dist/explorer/tree.js +137 -23
- package/dist/grid/CellEditor.js +3 -1
- package/dist/grid/DataGrid.d.ts +2 -2
- package/dist/grid/DataGrid.js +61 -59
- package/dist/grid/EnumPicker.js +2 -1
- package/dist/grid/GridHeaderCell.d.ts +1 -2
- package/dist/grid/GridHeaderCell.js +12 -5
- package/dist/grid/GridRowView.d.ts +49 -0
- package/dist/grid/GridRowView.js +51 -0
- package/dist/grid/RecordView.d.ts +4 -2
- package/dist/grid/RecordView.js +71 -22
- package/dist/grid/ValueDock.d.ts +3 -1
- package/dist/grid/ValueDock.js +9 -3
- package/dist/grid/cell-id.d.ts +1 -0
- package/dist/grid/cell-id.js +2 -0
- package/dist/grid/layout.d.ts +3 -2
- package/dist/grid/layout.js +15 -6
- package/dist/grid/types.d.ts +8 -1
- package/dist/grid/types.js +4 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/locales/en.json +32 -15
- package/dist/locales/nl.json +32 -15
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/messages.d.ts +1 -1
- package/dist/protocol/messages.js +1 -1
- package/dist/protocol/schema.d.ts +17 -0
- package/dist/structure/StructureView.d.ts +5 -3
- package/dist/structure/StructureView.js +8 -6
- package/dist/table/CommandField.js +2 -2
- package/dist/table/TableStatusBar.js +2 -2
- package/dist/table/TableToolbar.d.ts +3 -4
- package/dist/table/TableToolbar.js +4 -3
- package/dist/table/TableView.d.ts +4 -2
- package/dist/table/TableView.js +63 -91
- package/dist/table/grid-rows.js +4 -1
- package/dist/table/useTableTransfer.d.ts +3 -1
- package/dist/table/useTableTransfer.js +19 -5
- package/dist/testing/fake.d.ts +3 -1
- package/dist/testing/fake.js +7 -1
- package/dist/value/ValueEditor.d.ts +9 -0
- package/dist/value/ValueEditor.js +102 -0
- package/dist/value/ValuePanel.d.ts +1 -1
- package/dist/value/ValuePanel.js +3 -94
- package/dist/workbench/DatabaseWorkbench.js +4 -7
- package/package.json +6 -6
- package/src/DatabaseProvider.tsx +24 -5
- package/src/KeyIcon.tsx +15 -0
- package/src/actions.ts +28 -2
- package/src/client-context.ts +5 -1
- package/src/code-text.ts +2 -0
- package/src/column-keys.ts +8 -0
- package/src/connections/ConnectionManager.tsx +98 -68
- package/src/connections/connection-config.ts +2 -0
- package/src/console/QueryConsole.tsx +251 -150
- package/src/console/SqlEditor.tsx +33 -37
- package/src/console/StatementResultView.tsx +61 -27
- package/src/console/TransactionControls.tsx +8 -11
- package/src/console/editor-slot.ts +27 -0
- package/src/console/result-keys.ts +109 -0
- package/src/designer/ColumnsEditor.tsx +3 -3
- package/src/designer/DesignerView.tsx +14 -3
- package/src/designer/SqlPreview.tsx +2 -1
- package/src/designer/TableDesigner.tsx +5 -2
- package/src/explorer/DatabaseExplorer.tsx +170 -26
- package/src/explorer/RowMenu.tsx +2 -2
- package/src/explorer/expansion.ts +4 -4
- package/src/explorer/folders.ts +25 -0
- package/src/explorer/tree.ts +219 -28
- package/src/grid/CellEditor.tsx +3 -2
- package/src/grid/DataGrid.tsx +114 -176
- package/src/grid/EnumPicker.tsx +2 -1
- package/src/grid/GridHeaderCell.tsx +14 -9
- package/src/grid/GridRowView.tsx +184 -0
- package/src/grid/RecordView.tsx +188 -75
- package/src/grid/ValueDock.tsx +12 -3
- package/src/grid/cell-id.ts +2 -0
- package/src/grid/layout.ts +16 -6
- package/src/grid/types.ts +15 -3
- package/src/index.ts +5 -1
- package/src/locales/en.json +32 -15
- package/src/locales/nl.json +32 -15
- package/src/protocol/index.ts +13 -1
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/schema.ts +28 -0
- package/src/structure/StructureView.tsx +16 -6
- package/src/table/CommandField.tsx +2 -2
- package/src/table/TableStatusBar.tsx +3 -3
- package/src/table/TableToolbar.tsx +107 -92
- package/src/table/TableView.tsx +95 -132
- package/src/table/grid-rows.ts +5 -3
- package/src/table/useTableTransfer.ts +21 -6
- package/src/testing/fake.ts +18 -1
- package/src/value/ValueEditor.tsx +213 -0
- package/src/value/ValuePanel.tsx +4 -206
- package/src/workbench/DatabaseWorkbench.tsx +9 -10
package/src/grid/DataGrid.tsx
CHANGED
|
@@ -12,15 +12,14 @@ import {
|
|
|
12
12
|
type Ref
|
|
13
13
|
} from 'react';
|
|
14
14
|
import clsx from 'clsx';
|
|
15
|
-
import {
|
|
15
|
+
import { Ban, Copy, RotateCcw } from 'lucide-react';
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
|
-
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon,
|
|
18
|
-
import { formatNumber } from '@adecore/ui/format';
|
|
17
|
+
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, isApplePlatform, isModHeld, Kbd, useContentSize } from '@adecore/ui';
|
|
19
18
|
import type { EditValue, Value } from '../protocol/index.ts';
|
|
20
19
|
import type { SqlTarget } from '../sql.ts';
|
|
21
20
|
import type { RangeBlock } from './aggregates.ts';
|
|
22
21
|
import { fullRect, rectBetween, rectContains } from './cell-range.ts';
|
|
23
|
-
import {
|
|
22
|
+
import type { EditMove } from './CellEditor.tsx';
|
|
24
23
|
import { captureLayout, restoreLayout, sameLayout, type GridLayout } from './column-layout.ts';
|
|
25
24
|
import { NO_COLUMN_SELECTION, selectColumn, shownSelection, type ColumnSelection } from './column-selection.ts';
|
|
26
25
|
import { displayOrder, hideColumn, pinnedCount, showAllColumns, togglePin, type ColumnView } from './column-view.ts';
|
|
@@ -29,8 +28,9 @@ import { CopyAsMenu } from './CopyAsMenu.tsx';
|
|
|
29
28
|
import { cellView, copyTextOf, isPreview, type CellView, type NumberNotation } from './display.ts';
|
|
30
29
|
import { draftOf, parseDraft } from './edit-value.ts';
|
|
31
30
|
import { parseEnumType } from './enum-type.ts';
|
|
32
|
-
import { EnumPicker } from './EnumPicker.tsx';
|
|
33
31
|
import { GridHeaderCell, type HeaderActions } from './GridHeaderCell.tsx';
|
|
32
|
+
import { cellIdOf } from './cell-id.ts';
|
|
33
|
+
import { GridRowView, PINNED_GROUP, type CellEdit, type RowActions, type RowLook } from './GridRowView.tsx';
|
|
34
34
|
import {
|
|
35
35
|
clampColumnWidth,
|
|
36
36
|
columnOffsets,
|
|
@@ -39,18 +39,19 @@ import {
|
|
|
39
39
|
FALLBACK_VIEWPORT_HEIGHT,
|
|
40
40
|
growWidths,
|
|
41
41
|
gutterWidth,
|
|
42
|
+
drawnRange,
|
|
42
43
|
HEADER_HEIGHT,
|
|
43
|
-
OVERSCAN_ROWS,
|
|
44
44
|
ROW_HEIGHT,
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
scrollBlock,
|
|
46
|
+
scrollToReveal
|
|
47
47
|
} from './layout.ts';
|
|
48
48
|
import { moveFocus, type CellPosition, type NavigationKey } from './navigation.ts';
|
|
49
49
|
import { selectRow } from './row-selection.ts';
|
|
50
50
|
import { sortOnly, sortStateOf, type GridSort } from './sort.ts';
|
|
51
|
-
import type { ColumnRequest, FocusedCell, GridColumn, GridMenuContext, GridRow } from './types.ts';
|
|
51
|
+
import type { ColumnRequest, FocusedCell, FocusRequest, GridColumn, GridMenuContext, GridRow } from './types.ts';
|
|
52
52
|
import { useNumberNotation } from '../client-context.ts';
|
|
53
53
|
import { usePopupPress } from '../use-popup-press.ts';
|
|
54
|
+
import { useStableCallback } from '../use-stable-callback.ts';
|
|
54
55
|
|
|
55
56
|
export interface DataGridProps {
|
|
56
57
|
columns: readonly GridColumn[];
|
|
@@ -92,7 +93,7 @@ export interface DataGridProps {
|
|
|
92
93
|
/* A person resized, hid or pinned a column. */
|
|
93
94
|
onLayoutChange?(layout: GridLayout): void;
|
|
94
95
|
/* Puts the focus on a cell, whenever the object changes. */
|
|
95
|
-
focusRequest?:
|
|
96
|
+
focusRequest?: FocusRequest | null;
|
|
96
97
|
/* Picks a column and brings it into view, whenever the object changes. */
|
|
97
98
|
columnRequest?: ColumnRequest | null;
|
|
98
99
|
/* Drawn under the header while there are no rows. */
|
|
@@ -101,12 +102,6 @@ export interface DataGridProps {
|
|
|
101
102
|
ref?: Ref<HTMLDivElement>;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
interface Editing extends CellPosition {
|
|
105
|
-
draft: string;
|
|
106
|
-
initial: string;
|
|
107
|
-
loading: boolean;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
105
|
interface MenuTarget {
|
|
111
106
|
readonly row: number;
|
|
112
107
|
/* `null` for the row number. */
|
|
@@ -139,17 +134,6 @@ const estimateWidths = (columns: readonly GridColumn[], rows: readonly GridRow[]
|
|
|
139
134
|
)
|
|
140
135
|
);
|
|
141
136
|
|
|
142
|
-
const TONE: Record<CellView['tone'], string> = {
|
|
143
|
-
value: '',
|
|
144
|
-
null: 'text-text-faint',
|
|
145
|
-
default: 'text-text-faint italic'
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const ROW_STATE = {
|
|
149
|
-
inserted: 'bg-positive/10',
|
|
150
|
-
deleted: 'bg-status-error/10 text-text-faint'
|
|
151
|
-
} as const;
|
|
152
|
-
|
|
153
137
|
/*
|
|
154
138
|
* The table of the table view and of the console. Only the rows in view are drawn, at a fixed
|
|
155
139
|
* height, inside a box as tall as all of them; the header and the row-number gutter stick to the edges
|
|
@@ -198,12 +182,12 @@ export function DataGrid({
|
|
|
198
182
|
const reportRange = useRef(onRangeChange);
|
|
199
183
|
const reportLayout = useRef(onLayoutChange);
|
|
200
184
|
const [measure, size] = useContentSize();
|
|
201
|
-
const [
|
|
185
|
+
const [scrolledBlock, setScrolledBlock] = useState(0);
|
|
202
186
|
const [focused, setFocused] = useState<CellPosition | null>(null);
|
|
203
187
|
const [rangeAnchor, setRangeAnchor] = useState<CellPosition | null>(null);
|
|
204
188
|
const [columnSelection, setColumnSelection] = useState<ColumnSelection>(NO_COLUMN_SELECTION);
|
|
205
189
|
const [menuTarget, setMenuTarget] = useState<MenuTarget | null>(null);
|
|
206
|
-
const [editing, setEditing] = useState<
|
|
190
|
+
const [editing, setEditing] = useState<CellEdit | null>(null);
|
|
207
191
|
const [ownSelection, setOwnSelection] = useState<ReadonlySet<string>>(NO_KEYS);
|
|
208
192
|
const [layout, setLayout] = useState<Layout>(() => ({
|
|
209
193
|
signature: signatureOf(columns),
|
|
@@ -229,17 +213,21 @@ export function DataGrid({
|
|
|
229
213
|
setLayout(current);
|
|
230
214
|
}
|
|
231
215
|
|
|
232
|
-
|
|
216
|
+
// Kept by identity while nothing about the columns changes, since every drawn row compares them.
|
|
217
|
+
const order = useMemo(() => displayOrder(columns.length, current.view), [columns.length, current.view]);
|
|
233
218
|
const pinnedShown = pinnedCount(columns.length, current.view);
|
|
234
|
-
const displayOf = new Map(order.map((index, position) => [index, position]));
|
|
235
|
-
const widths =
|
|
236
|
-
|
|
219
|
+
const displayOf = useMemo(() => new Map(order.map((index, position) => [index, position])), [order]);
|
|
220
|
+
const widths = useMemo(
|
|
221
|
+
() => columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0)),
|
|
222
|
+
[columns, current.resized, current.estimated]
|
|
223
|
+
);
|
|
224
|
+
const shownWidths = useMemo(() => order.map((index) => widths[index]!), [order, widths]);
|
|
237
225
|
const offsets = columnOffsets(shownWidths);
|
|
238
226
|
const gutter = gutterWidth(rows.length);
|
|
239
227
|
const totalWidth = gutter + shownWidths.reduce((sum, width) => sum + width, 0);
|
|
240
228
|
const pinnedWidth = shownWidths.slice(0, pinnedShown).reduce((sum, width) => sum + width, 0);
|
|
241
229
|
const viewportHeight = size.height > 0 ? size.height : FALLBACK_VIEWPORT_HEIGHT;
|
|
242
|
-
const range =
|
|
230
|
+
const range = drawnRange(scrolledBlock, viewportHeight, rows.length);
|
|
243
231
|
const selection = selectedKeys ?? ownSelection;
|
|
244
232
|
const isShown = (position: CellPosition | null): position is CellPosition =>
|
|
245
233
|
position !== null && position.row < rows.length && displayOf.has(position.column);
|
|
@@ -253,8 +241,8 @@ export function DataGrid({
|
|
|
253
241
|
const sortList = sorts ?? NO_SORTS;
|
|
254
242
|
const focusedRowKey = focusCell === null ? null : (rows[focusCell.row]?.key ?? null);
|
|
255
243
|
const focusedColumn = focusCell?.column ?? -1;
|
|
256
|
-
const pickedColumns = shownSelection(columnSelection, order);
|
|
257
|
-
const pickedSet = new Set(pickedColumns);
|
|
244
|
+
const pickedColumns = useMemo(() => shownSelection(columnSelection, order), [columnSelection, order]);
|
|
245
|
+
const pickedSet = useMemo(() => new Set(pickedColumns), [pickedColumns]);
|
|
258
246
|
const columnBlock: RangeBlock | null = pickedColumns.length > 0 && rows.length > 0 ? { top: 0, bottom: rows.length - 1, columns: pickedColumns } : null;
|
|
259
247
|
const cellBlock: RangeBlock | null = rect === null ? null : { top: rect.top, bottom: rect.bottom, columns: order.slice(rect.left, rect.right + 1) };
|
|
260
248
|
const block = columnBlock ?? cellBlock;
|
|
@@ -296,7 +284,9 @@ export function DataGrid({
|
|
|
296
284
|
const index = focusRequest === null || focusRequest === undefined ? -1 : rows.findIndex((row) => row.key === focusRequest.rowKey);
|
|
297
285
|
if (focusRequest !== null && focusRequest !== undefined && index >= 0) {
|
|
298
286
|
focusAt({ row: index, column: focusRequest.column });
|
|
299
|
-
|
|
287
|
+
if (focusRequest.quiet !== true) {
|
|
288
|
+
scroller.current?.focus();
|
|
289
|
+
}
|
|
300
290
|
}
|
|
301
291
|
// Only a new request moves the focus; the rows changing under a request already handled must not.
|
|
302
292
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -337,7 +327,7 @@ export function DataGrid({
|
|
|
337
327
|
[measure, ref]
|
|
338
328
|
);
|
|
339
329
|
|
|
340
|
-
const cellId = (row: number, column: number): string =>
|
|
330
|
+
const cellId = (row: number, column: number): string => cellIdOf(gridId, row, column);
|
|
341
331
|
|
|
342
332
|
const canEdit = (position: CellPosition): boolean => {
|
|
343
333
|
const row = rows[position.row];
|
|
@@ -649,124 +639,93 @@ export function DataGrid({
|
|
|
649
639
|
onTogglePin: (index) => changeView((view) => togglePin(view, index))
|
|
650
640
|
};
|
|
651
641
|
|
|
642
|
+
const look = useMemo(
|
|
643
|
+
(): RowLook => ({
|
|
644
|
+
gridId,
|
|
645
|
+
columns,
|
|
646
|
+
order,
|
|
647
|
+
widths: shownWidths,
|
|
648
|
+
pinnedShown,
|
|
649
|
+
gutter,
|
|
650
|
+
notation,
|
|
651
|
+
enumTypes,
|
|
652
|
+
picked: pickedSet,
|
|
653
|
+
loadingLabel: t('grid.loadingValue')
|
|
654
|
+
}),
|
|
655
|
+
[gridId, columns, order, shownWidths, pinnedShown, gutter, notation, enumTypes, pickedSet, t]
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const stablePressCell = useStableCallback(pressCell);
|
|
659
|
+
const stableDragTo = useStableCallback(dragTo);
|
|
660
|
+
const stableBeginEdit = useStableCallback((position: CellPosition) => void beginEdit(position));
|
|
661
|
+
const stableOpenCellMenu = useStableCallback(openCellMenu);
|
|
662
|
+
const stablePressRowNumber = useStableCallback((index: number, event: MouseEvent) => {
|
|
663
|
+
if (!pressedInPopup()) {
|
|
664
|
+
selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
const stableOpenRowMenu = useStableCallback(openRowMenu);
|
|
668
|
+
const stableFinishPick = useStableCallback(finishPick);
|
|
669
|
+
const stableFinishEdit = useStableCallback(finishEdit);
|
|
670
|
+
const rowActions = useMemo(
|
|
671
|
+
(): RowActions => ({
|
|
672
|
+
pressCell: stablePressCell,
|
|
673
|
+
dragTo: stableDragTo,
|
|
674
|
+
beginEdit: stableBeginEdit,
|
|
675
|
+
openCellMenu: stableOpenCellMenu,
|
|
676
|
+
pressRowNumber: stablePressRowNumber,
|
|
677
|
+
openRowMenu: stableOpenRowMenu,
|
|
678
|
+
finishPick: stableFinishPick,
|
|
679
|
+
finishEdit: stableFinishEdit,
|
|
680
|
+
changeDraft: (draft) => setEditing((now) => (now === null ? now : { ...now, draft }))
|
|
681
|
+
}),
|
|
682
|
+
[stablePressCell, stableDragTo, stableBeginEdit, stableOpenCellMenu, stablePressRowNumber, stableOpenRowMenu, stableFinishPick, stableFinishEdit]
|
|
683
|
+
);
|
|
684
|
+
|
|
652
685
|
const renderRow = (index: number): ReactNode => {
|
|
653
686
|
const row = rows[index]!;
|
|
654
|
-
const
|
|
655
|
-
const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
|
|
687
|
+
const inRect = columnBlock === null && rect !== null && index >= rect.top && index <= rect.bottom;
|
|
656
688
|
return (
|
|
657
|
-
<
|
|
689
|
+
<GridRowView
|
|
658
690
|
key={row.key}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
'sticky left-0 z-10 flex shrink-0 cursor-default items-center justify-end border-r border-border bg-surface pr-2 pl-1 font-mono text-xs text-text-faint tabular-nums select-none data-[selected]:bg-accent-soft data-[selected]:text-text',
|
|
670
|
-
row.state === 'inserted' && 'text-positive'
|
|
671
|
-
)}
|
|
672
|
-
style={{ width: gutter }}
|
|
673
|
-
onClick={(event: MouseEvent) => {
|
|
674
|
-
if (!pressedInPopup()) {
|
|
675
|
-
selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
|
|
676
|
-
}
|
|
677
|
-
}}
|
|
678
|
-
onContextMenu={() => openRowMenu(index)}
|
|
679
|
-
>
|
|
680
|
-
{row.number === null ? '+' : formatNumber(row.number)}
|
|
681
|
-
</div>
|
|
682
|
-
{order.map((columnIndex, position) => {
|
|
683
|
-
const column = columns[columnIndex]!;
|
|
684
|
-
const cell = row.cells[columnIndex] ?? null;
|
|
685
|
-
const view = cellView(cell, column.kind, notation);
|
|
686
|
-
const cellPosition = { row: index, column: columnIndex };
|
|
687
|
-
const isFocused = focusCell !== null && focusCell.row === index && focusCell.column === columnIndex;
|
|
688
|
-
const ranged = pickedSet.has(columnIndex) || (columnBlock === null && rectContains(rect, index, position));
|
|
689
|
-
const edited = row.edited?.has(columnIndex) === true;
|
|
690
|
-
const isEditing = editing !== null && editing.row === index && editing.column === columnIndex;
|
|
691
|
-
const pinned = position < pinnedShown;
|
|
692
|
-
const enumType = enumTypes[columnIndex] ?? null;
|
|
693
|
-
const followable = !isEditing && followCell({ rowKey: row.key, column: columnIndex });
|
|
694
|
-
return (
|
|
695
|
-
<div
|
|
696
|
-
key={columnIndex}
|
|
697
|
-
id={cellId(index, columnIndex)}
|
|
698
|
-
role="gridcell"
|
|
699
|
-
aria-colindex={position + 2}
|
|
700
|
-
aria-selected={ranged}
|
|
701
|
-
data-focused={isFocused ? '' : undefined}
|
|
702
|
-
data-ranged={ranged ? '' : undefined}
|
|
703
|
-
className={clsx(
|
|
704
|
-
'group/cell flex h-full shrink-0 items-center overflow-hidden px-3 font-mono text-code whitespace-nowrap outline-0 select-none data-[focused]:outline-1 data-[focused]:-outline-offset-1 data-[focused]:outline-border-strong group-focus-within/grid:data-[focused]:outline-accent',
|
|
705
|
-
position === order.length - 1 ? 'border-r-0' : 'border-r',
|
|
706
|
-
view.align === 'end' ? 'justify-end tabular-nums' : 'justify-start',
|
|
707
|
-
pinned
|
|
708
|
-
? [
|
|
709
|
-
'sticky z-5',
|
|
710
|
-
position === pinnedShown - 1 ? 'border-border-strong' : 'border-border-soft',
|
|
711
|
-
ranged ? 'bg-accent-soft' : ['bg-surface', tint === '' && 'group-hover/row:bg-surface-hover']
|
|
712
|
-
]
|
|
713
|
-
: ['relative border-border-soft', ranged ? 'bg-accent-soft' : edited && 'bg-accent/10']
|
|
714
|
-
)}
|
|
715
|
-
style={{ width: shownWidths[position], left: pinned ? gutter + (offsets[position] ?? 0) : undefined }}
|
|
716
|
-
onPointerDown={(event) => pressCell(event, cellPosition, isEditing)}
|
|
717
|
-
onPointerEnter={() => dragTo(cellPosition)}
|
|
718
|
-
onDoubleClick={() => void beginEdit(cellPosition)}
|
|
719
|
-
onContextMenu={() => openCellMenu(cellPosition)}
|
|
720
|
-
>
|
|
721
|
-
{pinned && !ranged && <span aria-hidden className={clsx('pointer-events-none absolute inset-0', tint, edited && 'bg-accent/10')} />}
|
|
722
|
-
{isEditing && editing.loading && <Spinner size={12} label={t('grid.loadingValue')} />}
|
|
723
|
-
{isEditing && !editing.loading && enumType !== null && (
|
|
724
|
-
<EnumPicker
|
|
725
|
-
autoOpen
|
|
726
|
-
type={enumType}
|
|
727
|
-
value={typeof cell === 'string' ? cell : null}
|
|
728
|
-
nullable={column.nullable !== false}
|
|
729
|
-
label={column.name}
|
|
730
|
-
onDone={finishPick}
|
|
731
|
-
/>
|
|
732
|
-
)}
|
|
733
|
-
{isEditing && !editing.loading && enumType === null && (
|
|
734
|
-
<CellEditor
|
|
735
|
-
value={editing.draft}
|
|
736
|
-
label={column.name}
|
|
737
|
-
onValueChange={(draft) => setEditing({ ...editing, draft })}
|
|
738
|
-
onCommit={(move) => finishEdit(true, move)}
|
|
739
|
-
onCancel={() => finishEdit(false)}
|
|
740
|
-
/>
|
|
741
|
-
)}
|
|
742
|
-
{!isEditing && (
|
|
743
|
-
<>
|
|
744
|
-
<span className={clsx('min-w-0 truncate', TONE[view.tone], row.state === 'deleted' && 'line-through')}>{view.text}</span>
|
|
745
|
-
{view.suffix !== undefined && <span className="ml-2 shrink-0 text-text-faint">{view.suffix}</span>}
|
|
746
|
-
</>
|
|
747
|
-
)}
|
|
748
|
-
{followable && (
|
|
749
|
-
<IconButton
|
|
750
|
-
icon={ArrowUpRight}
|
|
751
|
-
size="2xs"
|
|
752
|
-
label={t('grid.followReference')}
|
|
753
|
-
tabIndex={-1}
|
|
754
|
-
className="absolute top-1/2 right-1 -translate-y-1/2 bg-surface-raised opacity-0 group-hover/cell:opacity-100 group-data-[focused]/cell:opacity-100 focus-visible:opacity-100"
|
|
755
|
-
onPointerDown={(event) => event.stopPropagation()}
|
|
756
|
-
onClick={() => {
|
|
757
|
-
if (!pressedInPopup()) {
|
|
758
|
-
onFollow?.({ rowKey: row.key, column: columnIndex });
|
|
759
|
-
}
|
|
760
|
-
}}
|
|
761
|
-
/>
|
|
762
|
-
)}
|
|
763
|
-
</div>
|
|
764
|
-
);
|
|
765
|
-
})}
|
|
766
|
-
</div>
|
|
691
|
+
row={row}
|
|
692
|
+
index={index}
|
|
693
|
+
selected={selection.has(row.key)}
|
|
694
|
+
focusedColumn={focusCell !== null && focusCell.row === index ? focusCell.column : -1}
|
|
695
|
+
rangeStart={inRect ? rect.left : -1}
|
|
696
|
+
rangeEnd={inRect ? rect.right : -1}
|
|
697
|
+
editing={editing !== null && editing.row === index ? editing : null}
|
|
698
|
+
look={look}
|
|
699
|
+
actions={rowActions}
|
|
700
|
+
/>
|
|
767
701
|
);
|
|
768
702
|
};
|
|
769
703
|
|
|
704
|
+
const headerCells = order.map((columnIndex, position) => {
|
|
705
|
+
const column = columns[columnIndex]!;
|
|
706
|
+
return (
|
|
707
|
+
<GridHeaderCell
|
|
708
|
+
key={columnIndex}
|
|
709
|
+
column={column}
|
|
710
|
+
index={columnIndex}
|
|
711
|
+
position={position}
|
|
712
|
+
width={shownWidths[position]!}
|
|
713
|
+
sortable={sortable}
|
|
714
|
+
selected={pickedSet.has(columnIndex)}
|
|
715
|
+
id={headerId(columnIndex)}
|
|
716
|
+
sort={sortable ? sortStateOf(sortList, column.name) : null}
|
|
717
|
+
multipleSorts={sortList.length > 1}
|
|
718
|
+
hasSorts={sortList.length > 0}
|
|
719
|
+
pinned={position < pinnedShown}
|
|
720
|
+
lastPinned={position === pinnedShown - 1}
|
|
721
|
+
last={position === order.length - 1}
|
|
722
|
+
hasHidden={current.view.hidden.size > 0}
|
|
723
|
+
canHide={order.length > 1}
|
|
724
|
+
actions={headerActions}
|
|
725
|
+
/>
|
|
726
|
+
);
|
|
727
|
+
});
|
|
728
|
+
|
|
770
729
|
const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
|
|
771
730
|
const menuCell: CellPosition | null = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
|
|
772
731
|
const settable = menuCell !== null && canEdit(menuCell);
|
|
@@ -794,36 +753,15 @@ export function DataGrid({
|
|
|
794
753
|
tabIndex={0}
|
|
795
754
|
className={clsx('group/grid relative min-h-0 flex-1 overflow-auto bg-surface text-text outline-none', className)}
|
|
796
755
|
onKeyDown={handleKeyDown}
|
|
797
|
-
onScroll={(event) =>
|
|
756
|
+
onScroll={(event) => setScrolledBlock(scrollBlock(event.currentTarget.scrollTop - HEADER_HEIGHT))}
|
|
798
757
|
>
|
|
799
758
|
<div className="relative min-w-full" style={{ width: totalWidth }}>
|
|
800
759
|
<div role="row" aria-rowindex={1} className="sticky top-0 z-20 flex w-full border-b border-border bg-surface" style={{ height: HEADER_HEIGHT }}>
|
|
801
|
-
<div className=
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
key={columnIndex}
|
|
807
|
-
column={column}
|
|
808
|
-
index={columnIndex}
|
|
809
|
-
position={position}
|
|
810
|
-
width={shownWidths[position]!}
|
|
811
|
-
sortable={sortable}
|
|
812
|
-
selected={pickedSet.has(columnIndex)}
|
|
813
|
-
id={headerId(columnIndex)}
|
|
814
|
-
sort={sortable ? sortStateOf(sortList, column.name) : null}
|
|
815
|
-
multipleSorts={sortList.length > 1}
|
|
816
|
-
hasSorts={sortList.length > 0}
|
|
817
|
-
pinned={position < pinnedShown}
|
|
818
|
-
stickyLeft={gutter + (offsets[position] ?? 0)}
|
|
819
|
-
lastPinned={position === pinnedShown - 1}
|
|
820
|
-
last={position === order.length - 1}
|
|
821
|
-
hasHidden={current.view.hidden.size > 0}
|
|
822
|
-
canHide={order.length > 1}
|
|
823
|
-
actions={headerActions}
|
|
824
|
-
/>
|
|
825
|
-
);
|
|
826
|
-
})}
|
|
760
|
+
<div className={PINNED_GROUP}>
|
|
761
|
+
<div className="shrink-0 border-r border-border bg-surface bg-clip-border" style={{ width: gutter }} />
|
|
762
|
+
{headerCells.slice(0, pinnedShown)}
|
|
763
|
+
</div>
|
|
764
|
+
{headerCells.slice(pinnedShown)}
|
|
827
765
|
</div>
|
|
828
766
|
<ContextMenu.Root>
|
|
829
767
|
<ContextMenu.Trigger role="rowgroup" className="relative" style={{ height: rows.length * ROW_HEIGHT }}>
|
package/src/grid/EnumPicker.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { Ban, ChevronDown } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Icon, Menu } from '@adecore/ui';
|
|
6
|
+
import { CODE_TEXT } from '../code-text.ts';
|
|
6
7
|
import { joinMembers, membersOf, type EnumType } from './enum-type.ts';
|
|
7
8
|
|
|
8
9
|
export interface EnumPickerProps {
|
|
@@ -24,7 +25,7 @@ export interface EnumPickerProps {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const LOOK = {
|
|
27
|
-
cell:
|
|
28
|
+
cell: `absolute inset-0 z-10 flex items-center gap-2 border border-accent bg-surface-raised px-3 ${CODE_TEXT} outline-0`,
|
|
28
29
|
field: 'field field-sm flex w-full items-center gap-2 font-mono text-code data-disabled:opacity-50'
|
|
29
30
|
} as const;
|
|
30
31
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { useRef, type MouseEvent, type PointerEvent } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff,
|
|
3
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Pin, PinOff } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { ColumnResizeHandle, ContextMenu, copyText, Icon, IconButton, isApplePlatform, isModHeld, Menu, Tooltip, useColumnResize } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { CODE_TEXT } from '../code-text.ts';
|
|
8
|
+
import { keyLabelOf } from '../column-keys.ts';
|
|
9
|
+
import { KeyIcon } from '../KeyIcon.tsx';
|
|
7
10
|
import type { SortDirection } from '../sql.ts';
|
|
8
11
|
import type { ColumnClick } from './column-selection.ts';
|
|
9
12
|
import { isNumericKind } from './display.ts';
|
|
@@ -41,8 +44,6 @@ export interface GridHeaderCellProps {
|
|
|
41
44
|
multipleSorts: boolean;
|
|
42
45
|
hasSorts: boolean;
|
|
43
46
|
pinned: boolean;
|
|
44
|
-
/* Where a pinned header sticks, counted from the left edge of the scroll box. */
|
|
45
|
-
stickyLeft: number;
|
|
46
47
|
/* The pinned column the others scroll under, which carries the stronger edge. */
|
|
47
48
|
lastPinned: boolean;
|
|
48
49
|
last: boolean;
|
|
@@ -101,7 +102,9 @@ function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden,
|
|
|
101
102
|
/* The facts about a table column that a tooltip can carry: its type, whether it takes NULL, its default. */
|
|
102
103
|
function ColumnFacts({ column }: { column: GridColumn }) {
|
|
103
104
|
const { t } = useTranslation('database');
|
|
105
|
+
const keys = { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true };
|
|
104
106
|
const facts = [
|
|
107
|
+
keys.primaryKey || keys.foreignKey ? t(keyLabelOf(keys)) : null,
|
|
105
108
|
column.type === '' ? null : column.type,
|
|
106
109
|
column.nullable === undefined ? null : t(column.nullable ? 'grid.column.nullable' : 'grid.column.notNull'),
|
|
107
110
|
column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
|
|
@@ -115,7 +118,7 @@ function ColumnFacts({ column }: { column: GridColumn }) {
|
|
|
115
118
|
);
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
/* One column's head: its name, the sort it carries,
|
|
121
|
+
/* One column's head: its name, the sort it carries, the key of a key column, a menu and the strip that resizes it. Numbers sit against the end, like their cells. */
|
|
119
122
|
export function GridHeaderCell({
|
|
120
123
|
column,
|
|
121
124
|
index,
|
|
@@ -128,7 +131,6 @@ export function GridHeaderCell({
|
|
|
128
131
|
multipleSorts,
|
|
129
132
|
hasSorts,
|
|
130
133
|
pinned,
|
|
131
|
-
stickyLeft,
|
|
132
134
|
lastPinned,
|
|
133
135
|
last,
|
|
134
136
|
hasHidden,
|
|
@@ -186,13 +188,16 @@ export function GridHeaderCell({
|
|
|
186
188
|
aria-sort={sortable ? (sort === null ? 'none' : sort.direction === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
187
189
|
data-selected={selected ? '' : undefined}
|
|
188
190
|
className={clsx(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
// The name in the face and size of the cells under it, which is how the column is written in SQL too.
|
|
192
|
+
'group/header flex h-full shrink-0 cursor-default font-medium select-none',
|
|
193
|
+
CODE_TEXT,
|
|
194
|
+
'relative',
|
|
195
|
+
pinned && 'bg-clip-border',
|
|
191
196
|
selected ? 'bg-accent text-accent-text' : [pinned && 'bg-surface', sort === null ? 'text-text-muted' : 'text-text'],
|
|
192
197
|
last ? 'border-r-0' : 'border-r',
|
|
193
198
|
pinned && lastPinned ? 'border-border-strong' : 'border-border-soft'
|
|
194
199
|
)}
|
|
195
|
-
style={{ width
|
|
200
|
+
style={{ width }}
|
|
196
201
|
onPointerDownCapture={() => {
|
|
197
202
|
resized.current = false;
|
|
198
203
|
}}
|
|
@@ -204,7 +209,7 @@ export function GridHeaderCell({
|
|
|
204
209
|
numeric && 'justify-end group-data-[selected]/header:pr-8'
|
|
205
210
|
)}
|
|
206
211
|
>
|
|
207
|
-
{column.primaryKey === true
|
|
212
|
+
<KeyIcon primaryKey={column.primaryKey === true} foreignKey={column.foreignKey === true} size={12} />
|
|
208
213
|
<Tooltip label={<ColumnFacts column={column} />} side="bottom">
|
|
209
214
|
<span className="min-w-0 truncate">{column.name}</span>
|
|
210
215
|
</Tooltip>
|