@adecore/database 0.17.0 → 0.18.1
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 +246 -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/table/TableView.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { ArrowDownWideNarrow, ArrowUpRight, Check, CircleAlert, CopyPlus, Download, Filter, FilterX, RefreshCw, Trash2, Undo2 } from 'lucide-react';
|
|
4
|
+
import type { TFunction } from 'i18next';
|
|
4
5
|
import { useTranslation } from 'react-i18next';
|
|
5
6
|
import {
|
|
6
7
|
Banner,
|
|
@@ -19,18 +20,17 @@ import {
|
|
|
19
20
|
} from '@adecore/ui';
|
|
20
21
|
import { formatNumber } from '@adecore/ui/format';
|
|
21
22
|
import { DatabaseRequestError, type Connection } from '../client/types.ts';
|
|
22
|
-
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage } from '../client-context.ts';
|
|
23
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.ts';
|
|
23
24
|
import { aggregateBlock, blockSize, type RangeBlock } from '../grid/aggregates.ts';
|
|
24
25
|
import { DataGrid } from '../grid/DataGrid.tsx';
|
|
25
|
-
import { wholeValueOf } from '../grid/focused-value.ts';
|
|
26
26
|
import type { GridSort } from '../grid/sort.ts';
|
|
27
|
-
import type { ColumnRequest, FocusedCell, GridMenuContext } from '../grid/types.ts';
|
|
27
|
+
import type { ColumnRequest, FocusedCell, FocusRequest, GridMenuContext } from '../grid/types.ts';
|
|
28
28
|
import { RecordView } from '../grid/RecordView.tsx';
|
|
29
29
|
import { ValueDock } from '../grid/ValueDock.tsx';
|
|
30
30
|
import { valueOfCell, type EditValue, type RowChange, type Value } from '../protocol/index.ts';
|
|
31
31
|
import type { SqlTarget } from '../sql.ts';
|
|
32
32
|
import { useSchemaChange } from '../use-schema-change.ts';
|
|
33
|
-
import {
|
|
33
|
+
import { useStableCallback } from '../use-stable-callback.ts';
|
|
34
34
|
import {
|
|
35
35
|
addChip,
|
|
36
36
|
cellChip,
|
|
@@ -72,12 +72,27 @@ import { TableStatusBar } from './TableStatusBar.tsx';
|
|
|
72
72
|
import { TableToolbar } from './TableToolbar.tsx';
|
|
73
73
|
import { useLoaded } from './useLoaded.ts';
|
|
74
74
|
import { useStoredLayout } from './useStoredLayout.ts';
|
|
75
|
-
import { useTableTransfer } from './useTableTransfer.ts';
|
|
75
|
+
import { useTableTransfer, type TransferNotice } from './useTableTransfer.ts';
|
|
76
|
+
|
|
77
|
+
/* Where the width of the record view is kept, in pixels, for every table. */
|
|
78
|
+
export const RECORD_WIDTH_KEY = 'database:record-view';
|
|
79
|
+
const RECORD_WIDTH = 360;
|
|
80
|
+
|
|
81
|
+
const parseRecordWidth = (raw: string | null | undefined): number | null => {
|
|
82
|
+
try {
|
|
83
|
+
const value: unknown = JSON.parse(raw ?? 'null');
|
|
84
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
76
89
|
|
|
77
90
|
export interface TableViewProps {
|
|
78
91
|
connection: Connection;
|
|
79
92
|
schema: string;
|
|
80
93
|
table: string;
|
|
94
|
+
/* The app's own content at the start of the toolbar, such as where the table is; the view draws the separators around the command field. */
|
|
95
|
+
toolbarStart?: ReactNode;
|
|
81
96
|
/* The filters the view starts with, such as the condition of a jump along a foreign key. They win over the ones remembered. */
|
|
82
97
|
defaultWhere?: string;
|
|
83
98
|
defaultOrderBy?: string;
|
|
@@ -94,6 +109,20 @@ interface Filters {
|
|
|
94
109
|
|
|
95
110
|
const NO_KEYS: ReadonlySet<string> = new Set();
|
|
96
111
|
|
|
112
|
+
/* What the strip over the table, or the app's notice, says of an export or an import. */
|
|
113
|
+
const transferText = (found: TransferNotice, t: TFunction<'database'>): string => {
|
|
114
|
+
switch (found.kind) {
|
|
115
|
+
case 'exporting':
|
|
116
|
+
return t('table.export.running', { format: found.format.toUpperCase() });
|
|
117
|
+
case 'exported':
|
|
118
|
+
return t('table.export.done', { count: found.rows, rows: formatNumber(found.rows) });
|
|
119
|
+
case 'imported':
|
|
120
|
+
return t('table.import.done', { count: found.rows, rows: formatNumber(found.rows) });
|
|
121
|
+
case 'failed':
|
|
122
|
+
return found.message;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
97
126
|
/* How much of a value a menu item spells out before it cuts it. */
|
|
98
127
|
const MENU_VALUE_LENGTH = 40;
|
|
99
128
|
|
|
@@ -104,7 +133,7 @@ const DUPLICATE_ROWS = shortcut('Mod+D');
|
|
|
104
133
|
* The rows of one table: read a page at a time, filtered and sorted with SQL a person types, and
|
|
105
134
|
* editable in place. Changes pile up as pending and go to the server together on Submit.
|
|
106
135
|
*/
|
|
107
|
-
export function TableView({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
|
|
136
|
+
export function TableView({ connection, schema, table, toolbarStart, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
|
|
108
137
|
// A new table, or the same one asked for with other filters, starts from nothing: no page, no pending change.
|
|
109
138
|
return (
|
|
110
139
|
<TableBody
|
|
@@ -112,6 +141,7 @@ export function TableView({ connection, schema, table, defaultWhere, defaultOrde
|
|
|
112
141
|
connection={connection}
|
|
113
142
|
schema={schema}
|
|
114
143
|
table={table}
|
|
144
|
+
toolbarStart={toolbarStart}
|
|
115
145
|
defaultWhere={defaultWhere}
|
|
116
146
|
defaultOrderBy={defaultOrderBy}
|
|
117
147
|
onDirtyChange={onDirtyChange}
|
|
@@ -121,12 +151,14 @@ export function TableView({ connection, schema, table, defaultWhere, defaultOrde
|
|
|
121
151
|
);
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
|
|
154
|
+
function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
|
|
125
155
|
const { t } = useTranslation('database');
|
|
126
156
|
const client = useDatabaseClient();
|
|
127
157
|
const onAction = useDatabaseAction();
|
|
158
|
+
const onNotice = useDatabaseNotice();
|
|
128
159
|
const files = useDatabaseFiles();
|
|
129
|
-
const
|
|
160
|
+
const storage = useDatabaseStorage();
|
|
161
|
+
const { stored, remember } = useStoredLayout(storage, layoutStorageKey(connection.id, schema, table));
|
|
130
162
|
const session = useMemo(() => client.session(connection), [client, connection]);
|
|
131
163
|
const grid = useRef<HTMLDivElement>(null);
|
|
132
164
|
const command = useRef<HTMLInputElement>(null);
|
|
@@ -151,14 +183,12 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
151
183
|
const [failure, setFailure] = useState<string | null>(null);
|
|
152
184
|
const [shapeChanged, setShapeChanged] = useState(false);
|
|
153
185
|
const [discard, setDiscard] = useState<{ run(): void } | null>(null);
|
|
154
|
-
const [panelOpen, setPanelOpen] = useState(false);
|
|
155
186
|
const [focus, setFocus] = useState<FocusedCell | null>(null);
|
|
156
187
|
const [recordOpen, setRecordOpen] = useState(false);
|
|
157
|
-
const [
|
|
158
|
-
const [focusRequest, setFocusRequest] = useState<
|
|
188
|
+
const [recordWidth, setRecordWidth] = useState(() => parseRecordWidth(storage?.get(RECORD_WIDTH_KEY)));
|
|
189
|
+
const [focusRequest, setFocusRequest] = useState<FocusRequest | null>(null);
|
|
159
190
|
const [columnRequest, setColumnRequest] = useState<ColumnRequest | null>(null);
|
|
160
191
|
const [range, setRange] = useState<RangeBlock | null>(null);
|
|
161
|
-
const [fetched, setFetched] = useState<{ id: string; rows: unknown; value: Value | undefined } | null>(null);
|
|
162
192
|
|
|
163
193
|
const applied = useMemo((): Filters => ({ where: chipsToWhere(chips), orderBy: chipsToOrderBy(engine, chips) }), [chips, engine]);
|
|
164
194
|
const target = `${connection.id}|${schema}|${table}`;
|
|
@@ -209,7 +239,20 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
209
239
|
onImported: () => {
|
|
210
240
|
setCounted(null);
|
|
211
241
|
rowsLoad.reload();
|
|
212
|
-
}
|
|
242
|
+
},
|
|
243
|
+
report:
|
|
244
|
+
onNotice === undefined
|
|
245
|
+
? undefined
|
|
246
|
+
: (found) =>
|
|
247
|
+
onNotice(
|
|
248
|
+
found.kind === 'failed'
|
|
249
|
+
? {
|
|
250
|
+
tone: 'error',
|
|
251
|
+
title: t(found.during === 'export' ? 'table.export.failed' : 'table.import.readFailed'),
|
|
252
|
+
description: found.message
|
|
253
|
+
}
|
|
254
|
+
: { tone: 'success', title: transferText(found, t) }
|
|
255
|
+
)
|
|
213
256
|
});
|
|
214
257
|
const changeCount = pendingCount(pending);
|
|
215
258
|
const dirty = changeCount > 0;
|
|
@@ -218,13 +261,12 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
218
261
|
const sorts = useMemo(() => sortsOf(chips), [chips]);
|
|
219
262
|
const commandColumns = useMemo((): CommandColumn[] => columns.map((column) => ({ name: column.name, type: column.type })), [columns]);
|
|
220
263
|
const rowByKey = useMemo(() => new Map(rows.map((row) => [row.key, row])), [rows]);
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
264
|
+
// The record view shows the row of the focused cell, or the first row before a cell has the focus.
|
|
265
|
+
const focusedIndex = focus === null ? -1 : rows.findIndex((row) => row.key === focus.rowKey);
|
|
266
|
+
const recordIndex = focusedIndex >= 0 ? focusedIndex : rows.length > 0 ? 0 : -1;
|
|
225
267
|
const selectionFigures = useMemo(
|
|
226
268
|
() =>
|
|
227
|
-
|
|
269
|
+
range === null || blockSize(range) < 2
|
|
228
270
|
? null
|
|
229
271
|
: {
|
|
230
272
|
columns: range.columns.map((index) => columns[index]?.name ?? ''),
|
|
@@ -234,19 +276,9 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
234
276
|
range
|
|
235
277
|
)
|
|
236
278
|
},
|
|
237
|
-
[
|
|
279
|
+
[range, rows, columns]
|
|
238
280
|
);
|
|
239
281
|
const sqlTarget = useMemo((): SqlTarget => ({ engine, schema, table }), [engine, schema, table]);
|
|
240
|
-
const focusedRow = panelOpen && focus !== null ? rows.find((row) => row.key === focus.rowKey) : undefined;
|
|
241
|
-
const focusedCell = focus === null ? undefined : focusedRow?.cells[focus.column];
|
|
242
|
-
const needsFetch =
|
|
243
|
-
focusedCell !== undefined &&
|
|
244
|
-
focus !== null &&
|
|
245
|
-
wholeValueOf(focusedCell) === undefined &&
|
|
246
|
-
!(focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default');
|
|
247
|
-
const focusedRef = focus === null ? null : parseRowKey(focus.rowKey);
|
|
248
|
-
const canFetch = focusedRef?.kind === 'loaded' && structure !== null && loaded !== null && rowKeyOf(structure, loaded, focusedRef.index) !== null;
|
|
249
|
-
const fetchId = needsFetch && canFetch ? `${focus.rowKey}:${focus.column}` : null;
|
|
250
282
|
|
|
251
283
|
useSchemaChange(connection.id, schema, () => {
|
|
252
284
|
if (isPendingEmpty(pending)) {
|
|
@@ -286,31 +318,10 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
286
318
|
}, [pending.inserts.length]);
|
|
287
319
|
|
|
288
320
|
useEffect(() => {
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
const row = parseRowKey(focus.rowKey);
|
|
293
|
-
const key = row.kind === 'loaded' ? rowKeyOf(structure, loaded, row.index) : null;
|
|
294
|
-
const name = loaded.columns[focus.column]?.name;
|
|
295
|
-
if (key === null || name === undefined) {
|
|
296
|
-
return;
|
|
321
|
+
if (recordWidth !== null) {
|
|
322
|
+
storage?.set(RECORD_WIDTH_KEY, JSON.stringify(recordWidth));
|
|
297
323
|
}
|
|
298
|
-
|
|
299
|
-
session.cell(schema, table, key, name, { signal: controller.signal }).then(
|
|
300
|
-
(value) => {
|
|
301
|
-
if (!controller.signal.aborted) {
|
|
302
|
-
setFetched({ id: fetchId, rows: loaded, value });
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
(error: unknown) => {
|
|
306
|
-
if (!controller.signal.aborted) {
|
|
307
|
-
setFetched({ id: fetchId, rows: loaded, value: undefined });
|
|
308
|
-
setFailure(messageOf(error));
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
);
|
|
312
|
-
return () => controller.abort();
|
|
313
|
-
}, [fetchId, focus, structure, loaded, session, schema, table]);
|
|
324
|
+
}, [storage, recordWidth]);
|
|
314
325
|
|
|
315
326
|
const discardPending = (): void => {
|
|
316
327
|
setPending(emptyPending);
|
|
@@ -447,7 +458,7 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
447
458
|
}
|
|
448
459
|
};
|
|
449
460
|
|
|
450
|
-
const loadValue = async (rowKey: string, column: number): Promise<Value | undefined> => {
|
|
461
|
+
const loadValue = useStableCallback(async (rowKey: string, column: number): Promise<Value | undefined> => {
|
|
451
462
|
const row = parseRowKey(rowKey);
|
|
452
463
|
const name = loaded?.columns[column]?.name;
|
|
453
464
|
const key = row.kind === 'loaded' && structure !== null && loaded !== null ? rowKeyOf(structure, loaded, row.index) : null;
|
|
@@ -460,7 +471,7 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
460
471
|
setFailure(messageOf(error));
|
|
461
472
|
return undefined;
|
|
462
473
|
}
|
|
463
|
-
};
|
|
474
|
+
});
|
|
464
475
|
|
|
465
476
|
const refsOf = (keys: Iterable<string>): RowSelectionRefs => {
|
|
466
477
|
const loadedRows: number[] = [];
|
|
@@ -527,24 +538,19 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
527
538
|
};
|
|
528
539
|
|
|
529
540
|
const toggleRecordView = (): void => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
const focused = focus === null ? undefined : rows.find((row) => row.key === focus.rowKey);
|
|
539
|
-
setRecordKey((focused ?? rows[0])?.key ?? null);
|
|
540
|
-
setRecordOpen(true);
|
|
541
|
+
setRecordOpen((now) => !now);
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
/* The record view moves the grid's focus along with it, without taking the keyboard from its field. */
|
|
545
|
+
const followInGrid = (cell: FocusedCell): void => {
|
|
546
|
+
setFocus(cell);
|
|
547
|
+
setFocusRequest({ ...cell, quiet: true });
|
|
541
548
|
};
|
|
542
549
|
|
|
543
550
|
const changeRecordIndex = (index: number): void => {
|
|
544
551
|
const row = rows[index];
|
|
545
552
|
if (row !== undefined) {
|
|
546
|
-
|
|
547
|
-
setFocus((now) => (now === null ? now : { rowKey: row.key, column: now.column }));
|
|
553
|
+
followInGrid({ rowKey: row.key, column: focus?.column ?? 0 });
|
|
548
554
|
}
|
|
549
555
|
};
|
|
550
556
|
|
|
@@ -645,47 +651,16 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
645
651
|
};
|
|
646
652
|
|
|
647
653
|
const bounds = pageBounds(loadedOffset, loaded?.rows.length ?? 0, loaded?.hasMore ?? false, counted);
|
|
648
|
-
const focusedColumn = focus === null ? null : (columns[focus.column] ?? null);
|
|
649
|
-
const panelEditable =
|
|
650
|
-
editable && focusedRow !== undefined && focusedRow.locked !== true && focusedRow.state !== 'deleted' && focusedColumn?.readOnly !== true;
|
|
651
|
-
const fetchedNow = fetched !== null && fetched.id === fetchId && fetched.rows === loaded;
|
|
652
|
-
const panelValue = ((): Value | undefined => {
|
|
653
|
-
if (focusedCell === undefined || focusedRow === undefined || focus === null) {
|
|
654
|
-
return undefined;
|
|
655
|
-
}
|
|
656
|
-
const whole = wholeValueOf(focusedCell);
|
|
657
|
-
if (whole !== undefined) {
|
|
658
|
-
return whole;
|
|
659
|
-
}
|
|
660
|
-
if (focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default') {
|
|
661
|
-
// A pending DEFAULT holds no value of its own, so the panel keeps showing what the row had.
|
|
662
|
-
const row = parseRowKey(focusedRow.key);
|
|
663
|
-
return row.kind === 'loaded' && loaded !== null ? wholeValueOf(loaded.rows[row.index]?.[focus.column] ?? null) : null;
|
|
664
|
-
}
|
|
665
|
-
return fetchedNow ? fetched.value : undefined;
|
|
666
|
-
})();
|
|
667
654
|
const notice = failure ?? rowsLoad.error ?? structureLoad.error;
|
|
668
|
-
const noticeText = (found: NonNullable<typeof transfer.notice>): string => {
|
|
669
|
-
switch (found.kind) {
|
|
670
|
-
case 'exporting':
|
|
671
|
-
return t('table.export.running', { format: found.format.toUpperCase() });
|
|
672
|
-
case 'exported':
|
|
673
|
-
return t('table.export.done', { count: found.rows, rows: formatNumber(found.rows) });
|
|
674
|
-
case 'imported':
|
|
675
|
-
return t('table.import.done', { count: found.rows, rows: formatNumber(found.rows) });
|
|
676
|
-
case 'failed':
|
|
677
|
-
return found.message;
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
655
|
|
|
681
656
|
return (
|
|
682
657
|
<div ref={ref} className={clsx('flex min-h-0 flex-col bg-surface text-text', className)} onKeyDown={handleKeyDown}>
|
|
683
658
|
<TableToolbar
|
|
659
|
+
start={toolbarStart}
|
|
684
660
|
refreshing={rowsLoad.loading}
|
|
685
661
|
readOnlyReason={readOnlyReason}
|
|
686
662
|
hasSelection={selected.size > 0}
|
|
687
663
|
canRevertSelection={selected.size > 0 && canRevert(selected)}
|
|
688
|
-
valuePanelOpen={panelOpen}
|
|
689
664
|
recordViewOpen={recordOpen}
|
|
690
665
|
transfer={
|
|
691
666
|
files === undefined
|
|
@@ -711,7 +686,6 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
711
686
|
onDeleteRows={deleteSelected}
|
|
712
687
|
onCloneRows={() => cloneRows([...selected])}
|
|
713
688
|
onRevertRows={() => revertRowKeys(selected)}
|
|
714
|
-
onToggleValuePanel={() => setPanelOpen((now) => !now)}
|
|
715
689
|
onToggleRecordView={toggleRecordView}
|
|
716
690
|
onPageSizeChange={changePageSize}
|
|
717
691
|
onFirstPage={() => goToPage(0)}
|
|
@@ -743,7 +717,7 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
743
717
|
<Banner
|
|
744
718
|
icon={transfer.notice.kind === 'failed' ? CircleAlert : transfer.notice.kind === 'exporting' ? Download : Check}
|
|
745
719
|
tone={transfer.notice.kind === 'failed' ? 'error' : 'neutral'}
|
|
746
|
-
message={
|
|
720
|
+
message={transferText(transfer.notice, t)}
|
|
747
721
|
className="shrink-0 pt-2"
|
|
748
722
|
>
|
|
749
723
|
{transfer.notice.kind === 'exporting' ? (
|
|
@@ -765,26 +739,31 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
765
739
|
) : (
|
|
766
740
|
<>
|
|
767
741
|
<ValueDock
|
|
768
|
-
open={
|
|
742
|
+
open={recordOpen}
|
|
743
|
+
width={recordWidth ?? RECORD_WIDTH}
|
|
744
|
+
onWidthChange={setRecordWidth}
|
|
769
745
|
panel={
|
|
770
|
-
<
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
746
|
+
<RecordView
|
|
747
|
+
label={t('table.recordFields', { table })}
|
|
748
|
+
columns={columns}
|
|
749
|
+
rows={rows}
|
|
750
|
+
index={recordIndex}
|
|
751
|
+
onIndexChange={changeRecordIndex}
|
|
752
|
+
focusedColumn={focus !== null && focusedIndex === recordIndex ? focus.column : null}
|
|
753
|
+
editable={editable}
|
|
754
|
+
onCommit={commit}
|
|
755
|
+
loadValue={loadValue}
|
|
756
|
+
onFocusedCellChange={followInGrid}
|
|
757
|
+
onFollow={onAction === undefined ? undefined : follow}
|
|
758
|
+
canFollow={(cell) => referenceAt(cell) !== null}
|
|
759
|
+
onClose={() => setRecordOpen(false)}
|
|
760
|
+
empty={t('table.empty')}
|
|
778
761
|
/>
|
|
779
762
|
}
|
|
780
763
|
>
|
|
781
764
|
<div
|
|
782
765
|
aria-busy={rowsLoad.loading}
|
|
783
|
-
className={clsx(
|
|
784
|
-
'relative min-h-0 flex-1 flex-col',
|
|
785
|
-
recordOpen ? 'hidden' : 'flex',
|
|
786
|
-
rowsLoad.loading && 'pointer-events-none opacity-60'
|
|
787
|
-
)}
|
|
766
|
+
className={clsx('relative flex min-h-0 flex-1 flex-col', rowsLoad.loading && 'pointer-events-none opacity-60')}
|
|
788
767
|
>
|
|
789
768
|
<DataGrid
|
|
790
769
|
ref={grid}
|
|
@@ -814,22 +793,6 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
|
|
|
814
793
|
columnRequest={columnRequest}
|
|
815
794
|
/>
|
|
816
795
|
</div>
|
|
817
|
-
{recordOpen && (
|
|
818
|
-
<RecordView
|
|
819
|
-
label={t('table.recordFields', { table })}
|
|
820
|
-
columns={columns}
|
|
821
|
-
rows={rows}
|
|
822
|
-
index={recordIndex}
|
|
823
|
-
onIndexChange={changeRecordIndex}
|
|
824
|
-
editable={editable}
|
|
825
|
-
onCommit={commit}
|
|
826
|
-
loadValue={loadValue}
|
|
827
|
-
onFocusedCellChange={setFocus}
|
|
828
|
-
onFollow={onAction === undefined ? undefined : follow}
|
|
829
|
-
canFollow={(cell) => referenceAt(cell) !== null}
|
|
830
|
-
empty={t('table.empty')}
|
|
831
|
-
/>
|
|
832
|
-
)}
|
|
833
796
|
</ValueDock>
|
|
834
797
|
<TableStatusBar
|
|
835
798
|
elapsedMs={loaded.elapsedMs}
|
package/src/table/grid-rows.ts
CHANGED
|
@@ -13,14 +13,16 @@ export const insertedRowKey = (id: number): string => `new:${id}`;
|
|
|
13
13
|
export const parseRowKey = (key: string): RowRef =>
|
|
14
14
|
key.startsWith('new:') ? { kind: 'inserted', id: Number(key.slice(4)) } : { kind: 'loaded', index: Number(key.slice(4)) };
|
|
15
15
|
|
|
16
|
-
export const buildGridColumns = (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null): GridColumn[] =>
|
|
17
|
-
|
|
16
|
+
export const buildGridColumns = (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null): GridColumn[] => {
|
|
17
|
+
const keys = { primaryKey: structure?.primaryKey ?? [], foreignKey: structure?.foreignKeys.flatMap((key) => key.columns) ?? [] };
|
|
18
|
+
return loaded.columns.map((column) =>
|
|
18
19
|
gridColumnOf(
|
|
19
20
|
column,
|
|
20
21
|
structure?.columns.find((info) => info.name === column.name),
|
|
21
|
-
|
|
22
|
+
keys
|
|
22
23
|
)
|
|
23
24
|
);
|
|
25
|
+
};
|
|
24
26
|
|
|
25
27
|
export interface GridRowsInput {
|
|
26
28
|
readonly structure: TableStructure | null;
|
|
@@ -11,7 +11,7 @@ export type TransferNotice =
|
|
|
11
11
|
| { readonly kind: 'exporting'; readonly format: FileFormat }
|
|
12
12
|
| { readonly kind: 'exported'; readonly rows: number }
|
|
13
13
|
| { readonly kind: 'imported'; readonly rows: number }
|
|
14
|
-
| { readonly kind: 'failed'; readonly message: string };
|
|
14
|
+
| { readonly kind: 'failed'; readonly during: 'export' | 'import'; readonly message: string };
|
|
15
15
|
|
|
16
16
|
/* The import a person is setting up. */
|
|
17
17
|
export interface ImportDraft {
|
|
@@ -38,6 +38,8 @@ export interface TableTransferInput {
|
|
|
38
38
|
readonly orderBy: string;
|
|
39
39
|
/* Rows were added; the table reloads. */
|
|
40
40
|
readonly onImported: () => void;
|
|
41
|
+
/* Takes what finished or failed off the strip, for the app to show; the strip keeps only a running export. */
|
|
42
|
+
readonly report?: (notice: TransferNotice) => void;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
export interface TableTransfer {
|
|
@@ -55,7 +57,7 @@ export interface TableTransfer {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/* Export of the table as filtered, and import of a file into it, with the state both need between the dialogs and the strip. */
|
|
58
|
-
export function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported }: TableTransferInput): TableTransfer {
|
|
60
|
+
export function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported, report }: TableTransferInput): TableTransfer {
|
|
59
61
|
const [notice, setNotice] = useState<TransferNotice | null>(null);
|
|
60
62
|
const [importDraft, setImportDraft] = useState<ImportDraft | null>(null);
|
|
61
63
|
const exporting = useRef<AbortController | null>(null);
|
|
@@ -72,6 +74,15 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
72
74
|
|
|
73
75
|
useEffect(() => () => sampling.current?.abort(), []);
|
|
74
76
|
|
|
77
|
+
const show = (next: TransferNotice): void => {
|
|
78
|
+
if (report === undefined) {
|
|
79
|
+
setNotice(next);
|
|
80
|
+
} else {
|
|
81
|
+
setNotice(null);
|
|
82
|
+
report(next);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
75
86
|
const exportAs = (format: FileFormat): void => {
|
|
76
87
|
if (files === undefined || exporting.current !== null) {
|
|
77
88
|
return;
|
|
@@ -89,10 +100,14 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
89
100
|
{ source: { kind: 'table', schema, table, where: where || undefined, orderBy: orderBy || undefined }, format, path },
|
|
90
101
|
{ signal: controller.signal }
|
|
91
102
|
);
|
|
92
|
-
|
|
103
|
+
show({ kind: 'exported', rows: result.rows });
|
|
93
104
|
} catch (error) {
|
|
94
105
|
const cancelled = controller.signal.aborted || (error instanceof DatabaseRequestError && error.code === 'cancelled');
|
|
95
|
-
|
|
106
|
+
if (cancelled) {
|
|
107
|
+
setNotice(null);
|
|
108
|
+
} else {
|
|
109
|
+
show({ kind: 'failed', during: 'export', message: messageOf(error) });
|
|
110
|
+
}
|
|
96
111
|
} finally {
|
|
97
112
|
exporting.current = null;
|
|
98
113
|
}
|
|
@@ -120,7 +135,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
120
135
|
const read = await sample(path, format, true);
|
|
121
136
|
setImportDraft({ path, format, header: true, sample: read, mapping: matchColumns(read.columns, tableColumns, true), busy: false, error: null });
|
|
122
137
|
} catch (error) {
|
|
123
|
-
|
|
138
|
+
show({ kind: 'failed', during: 'import', message: messageOf(error) });
|
|
124
139
|
}
|
|
125
140
|
})();
|
|
126
141
|
};
|
|
@@ -159,7 +174,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
159
174
|
try {
|
|
160
175
|
const rows = await session.import(schema, table, { path, format, header, columns: mapping });
|
|
161
176
|
setImportDraft(null);
|
|
162
|
-
|
|
177
|
+
show({ kind: 'imported', rows });
|
|
163
178
|
onImported();
|
|
164
179
|
} catch (error) {
|
|
165
180
|
setImportDraft((now) => (now === null ? now : { ...now, busy: false, error: messageOf(error) }));
|
package/src/testing/fake.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Cell,
|
|
3
|
+
CheckInfo,
|
|
3
4
|
ColumnInfo,
|
|
4
5
|
DatabaseErrorCode,
|
|
5
6
|
DatabaseMethod,
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
ServerInfo,
|
|
17
18
|
StatementResult,
|
|
18
19
|
TableKind,
|
|
20
|
+
TriggerInfo,
|
|
19
21
|
Value
|
|
20
22
|
} from '../protocol/index.ts';
|
|
21
23
|
import type { DatabaseTransport } from '../client/types.ts';
|
|
@@ -30,6 +32,9 @@ export interface FakeTable {
|
|
|
30
32
|
readonly rows: readonly (readonly Value[])[];
|
|
31
33
|
readonly indexes?: readonly IndexInfo[];
|
|
32
34
|
readonly foreignKeys?: readonly ForeignKeyInfo[];
|
|
35
|
+
/* Left out, `structure` leaves them out too, as SQLite does for its checks. */
|
|
36
|
+
readonly checks?: readonly CheckInfo[];
|
|
37
|
+
readonly triggers?: readonly TriggerInfo[];
|
|
33
38
|
readonly ddl?: string;
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -50,12 +55,17 @@ export interface FakeDatabaseTransportOptions {
|
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
interface Table {
|
|
58
|
+
/* Where the table is, which the columns of a result read from it name as their source. */
|
|
59
|
+
schema: string;
|
|
60
|
+
name: string;
|
|
53
61
|
kind: TableKind;
|
|
54
62
|
columns: ColumnInfo[];
|
|
55
63
|
primaryKey: string[];
|
|
56
64
|
rows: Value[][];
|
|
57
65
|
indexes: IndexInfo[];
|
|
58
66
|
foreignKeys: ForeignKeyInfo[];
|
|
67
|
+
checks: CheckInfo[] | undefined;
|
|
68
|
+
triggers: TriggerInfo[] | undefined;
|
|
59
69
|
ddl: string | null;
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -92,12 +102,16 @@ const toTables = (database: FakeDatabase): Map<string, Map<string, Table>> =>
|
|
|
92
102
|
Object.entries(tables).map(([name, table]) => [
|
|
93
103
|
name,
|
|
94
104
|
{
|
|
105
|
+
schema,
|
|
106
|
+
name,
|
|
95
107
|
kind: table.kind ?? 'table',
|
|
96
108
|
columns: structuredClone([...table.columns]),
|
|
97
109
|
primaryKey: [...(table.primaryKey ?? [])],
|
|
98
110
|
rows: structuredClone(table.rows.map((row) => [...row])),
|
|
99
111
|
indexes: structuredClone([...(table.indexes ?? [])]),
|
|
100
112
|
foreignKeys: structuredClone([...(table.foreignKeys ?? [])]),
|
|
113
|
+
checks: table.checks === undefined ? undefined : structuredClone([...table.checks]),
|
|
114
|
+
triggers: table.triggers === undefined ? undefined : structuredClone([...table.triggers]),
|
|
101
115
|
ddl: table.ddl ?? null
|
|
102
116
|
} satisfies Table
|
|
103
117
|
])
|
|
@@ -167,7 +181,8 @@ const toCell = (value: Value, cellLimit: number): Cell => {
|
|
|
167
181
|
return value;
|
|
168
182
|
};
|
|
169
183
|
|
|
170
|
-
const toResultColumns = (table: Table): ResultColumn[] =>
|
|
184
|
+
const toResultColumns = (table: Table): ResultColumn[] =>
|
|
185
|
+
table.columns.map(({ name, type, kind }) => ({ name, type, kind, source: { schema: table.schema, table: table.name, column: name } }));
|
|
171
186
|
|
|
172
187
|
const toRowsResult = (table: Table, rows: readonly (readonly Value[])[], offset: number, limit: number, cellLimit: number): RowsResult => ({
|
|
173
188
|
columns: toResultColumns(table),
|
|
@@ -406,6 +421,8 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
406
421
|
rowKey: rowKeyOf(table),
|
|
407
422
|
indexes: table.indexes,
|
|
408
423
|
foreignKeys: table.foreignKeys,
|
|
424
|
+
...(table.checks === undefined ? {} : { checks: table.checks }),
|
|
425
|
+
...(table.triggers === undefined ? {} : { triggers: table.triggers }),
|
|
409
426
|
ddl: table.ddl
|
|
410
427
|
};
|
|
411
428
|
},
|