@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.
Files changed (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +251 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -6,15 +6,14 @@ import { useTranslation } from 'react-i18next';
6
6
  import { Banner, Button, ContextMenu, Icon, isApplePlatform, Kbd, KEY_SHORTCUTS, matchesShortcut, messageOf, PanelEmpty, PromptDialog, shortcut, Spinner } from '@adecore/ui';
7
7
  import { formatNumber } from '@adecore/ui/format';
8
8
  import { DatabaseRequestError } from '../client/types.js';
9
- import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage } from '../client-context.js';
9
+ import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.js';
10
10
  import { aggregateBlock, blockSize } from '../grid/aggregates.js';
11
11
  import { DataGrid } from '../grid/DataGrid.js';
12
- import { wholeValueOf } from '../grid/focused-value.js';
13
12
  import { RecordView } from '../grid/RecordView.js';
14
13
  import { ValueDock } from '../grid/ValueDock.js';
15
14
  import { valueOfCell } from '../protocol/index.js';
16
15
  import { useSchemaChange } from '../use-schema-change.js';
17
- import { ValuePanel } from '../value/ValuePanel.js';
16
+ import { useStableCallback } from '../use-stable-callback.js';
18
17
  import { addChip, cellChip, chipsFromQuery, chipsToOrderBy, chipsToWhere, shorten, sortsOf, toggleSort, withSorts } from './command-field.js';
19
18
  import { buildGridColumns, buildGridRows, cloneValues, parseRowKey } from './grid-rows.js';
20
19
  import { importableColumns } from './import-mapping.js';
@@ -28,7 +27,32 @@ import { TableToolbar } from './TableToolbar.js';
28
27
  import { useLoaded } from './useLoaded.js';
29
28
  import { useStoredLayout } from './useStoredLayout.js';
30
29
  import { useTableTransfer } from './useTableTransfer.js';
30
+ /* Where the width of the record view is kept, in pixels, for every table. */
31
+ export const RECORD_WIDTH_KEY = 'database:record-view';
32
+ const RECORD_WIDTH = 360;
33
+ const parseRecordWidth = (raw) => {
34
+ try {
35
+ const value = JSON.parse(raw ?? 'null');
36
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ };
31
42
  const NO_KEYS = new Set();
43
+ /* What the strip over the table, or the app's notice, says of an export or an import. */
44
+ const transferText = (found, t) => {
45
+ switch (found.kind) {
46
+ case 'exporting':
47
+ return t('table.export.running', { format: found.format.toUpperCase() });
48
+ case 'exported':
49
+ return t('table.export.done', { count: found.rows, rows: formatNumber(found.rows) });
50
+ case 'imported':
51
+ return t('table.import.done', { count: found.rows, rows: formatNumber(found.rows) });
52
+ case 'failed':
53
+ return found.message;
54
+ }
55
+ };
32
56
  /* How much of a value a menu item spells out before it cuts it. */
33
57
  const MENU_VALUE_LENGTH = 40;
34
58
  const FOCUS_COMMAND = shortcut('Mod+F');
@@ -37,16 +61,18 @@ const DUPLICATE_ROWS = shortcut('Mod+D');
37
61
  * The rows of one table: read a page at a time, filtered and sorted with SQL a person types, and
38
62
  * editable in place. Changes pile up as pending and go to the server together on Submit.
39
63
  */
40
- export function TableView({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }) {
64
+ export function TableView({ connection, schema, table, toolbarStart, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }) {
41
65
  // A new table, or the same one asked for with other filters, starts from nothing: no page, no pending change.
42
- return (_jsx(TableBody, { connection: connection, schema: schema, table: table, defaultWhere: defaultWhere, defaultOrderBy: defaultOrderBy, onDirtyChange: onDirtyChange, className: className, ref: ref }, [connection.id, schema, table, defaultWhere ?? '', defaultOrderBy ?? ''].join('\u0000')));
66
+ return (_jsx(TableBody, { connection: connection, schema: schema, table: table, toolbarStart: toolbarStart, defaultWhere: defaultWhere, defaultOrderBy: defaultOrderBy, onDirtyChange: onDirtyChange, className: className, ref: ref }, [connection.id, schema, table, defaultWhere ?? '', defaultOrderBy ?? ''].join('\u0000')));
43
67
  }
44
- function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }) {
68
+ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }) {
45
69
  const { t } = useTranslation('database');
46
70
  const client = useDatabaseClient();
47
71
  const onAction = useDatabaseAction();
72
+ const onNotice = useDatabaseNotice();
48
73
  const files = useDatabaseFiles();
49
- const { stored, remember } = useStoredLayout(useDatabaseStorage(), layoutStorageKey(connection.id, schema, table));
74
+ const storage = useDatabaseStorage();
75
+ const { stored, remember } = useStoredLayout(storage, layoutStorageKey(connection.id, schema, table));
50
76
  const session = useMemo(() => client.session(connection), [client, connection]);
51
77
  const grid = useRef(null);
52
78
  const command = useRef(null);
@@ -69,14 +95,12 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
69
95
  const [failure, setFailure] = useState(null);
70
96
  const [shapeChanged, setShapeChanged] = useState(false);
71
97
  const [discard, setDiscard] = useState(null);
72
- const [panelOpen, setPanelOpen] = useState(false);
73
98
  const [focus, setFocus] = useState(null);
74
99
  const [recordOpen, setRecordOpen] = useState(false);
75
- const [recordKey, setRecordKey] = useState(null);
100
+ const [recordWidth, setRecordWidth] = useState(() => parseRecordWidth(storage?.get(RECORD_WIDTH_KEY)));
76
101
  const [focusRequest, setFocusRequest] = useState(null);
77
102
  const [columnRequest, setColumnRequest] = useState(null);
78
103
  const [range, setRange] = useState(null);
79
- const [fetched, setFetched] = useState(null);
80
104
  const applied = useMemo(() => ({ where: chipsToWhere(chips), orderBy: chipsToOrderBy(engine, chips) }), [chips, engine]);
81
105
  const target = `${connection.id}|${schema}|${table}`;
82
106
  const structureLoad = useLoaded((signal) => session.structure(schema, table, { signal }), target);
@@ -112,7 +136,16 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
112
136
  onImported: () => {
113
137
  setCounted(null);
114
138
  rowsLoad.reload();
115
- }
139
+ },
140
+ report: onNotice === undefined
141
+ ? undefined
142
+ : (found) => onNotice(found.kind === 'failed'
143
+ ? {
144
+ tone: 'error',
145
+ title: t(found.during === 'export' ? 'table.export.failed' : 'table.import.readFailed'),
146
+ description: found.message
147
+ }
148
+ : { tone: 'success', title: transferText(found, t) })
116
149
  });
117
150
  const changeCount = pendingCount(pending);
118
151
  const dirty = changeCount > 0;
@@ -121,23 +154,16 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
121
154
  const sorts = useMemo(() => sortsOf(chips), [chips]);
122
155
  const commandColumns = useMemo(() => columns.map((column) => ({ name: column.name, type: column.type })), [columns]);
123
156
  const rowByKey = useMemo(() => new Map(rows.map((row) => [row.key, row])), [rows]);
124
- const recordIndex = Math.max(0, rows.findIndex((row) => row.key === recordKey));
125
- const selectionFigures = useMemo(() => recordOpen || range === null || blockSize(range) < 2
157
+ // The record view shows the row of the focused cell, or the first row before a cell has the focus.
158
+ const focusedIndex = focus === null ? -1 : rows.findIndex((row) => row.key === focus.rowKey);
159
+ const recordIndex = focusedIndex >= 0 ? focusedIndex : rows.length > 0 ? 0 : -1;
160
+ const selectionFigures = useMemo(() => range === null || blockSize(range) < 2
126
161
  ? null
127
162
  : {
128
163
  columns: range.columns.map((index) => columns[index]?.name ?? ''),
129
164
  aggregates: aggregateBlock(rows.map((row) => row.cells), columns.map((column) => column.kind), range)
130
- }, [recordOpen, range, rows, columns]);
165
+ }, [range, rows, columns]);
131
166
  const sqlTarget = useMemo(() => ({ engine, schema, table }), [engine, schema, table]);
132
- const focusedRow = panelOpen && focus !== null ? rows.find((row) => row.key === focus.rowKey) : undefined;
133
- const focusedCell = focus === null ? undefined : focusedRow?.cells[focus.column];
134
- const needsFetch = focusedCell !== undefined &&
135
- focus !== null &&
136
- wholeValueOf(focusedCell) === undefined &&
137
- !(focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default');
138
- const focusedRef = focus === null ? null : parseRowKey(focus.rowKey);
139
- const canFetch = focusedRef?.kind === 'loaded' && structure !== null && loaded !== null && rowKeyOf(structure, loaded, focusedRef.index) !== null;
140
- const fetchId = needsFetch && canFetch ? `${focus.rowKey}:${focus.column}` : null;
141
167
  useSchemaChange(connection.id, schema, () => {
142
168
  if (isPendingEmpty(pending)) {
143
169
  setCounted(null);
@@ -172,28 +198,10 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
172
198
  insertedBefore.current = pending.inserts.length;
173
199
  }, [pending.inserts.length]);
174
200
  useEffect(() => {
175
- if (fetchId === null || focus === null || structure === null || loaded === null) {
176
- return;
177
- }
178
- const row = parseRowKey(focus.rowKey);
179
- const key = row.kind === 'loaded' ? rowKeyOf(structure, loaded, row.index) : null;
180
- const name = loaded.columns[focus.column]?.name;
181
- if (key === null || name === undefined) {
182
- return;
201
+ if (recordWidth !== null) {
202
+ storage?.set(RECORD_WIDTH_KEY, JSON.stringify(recordWidth));
183
203
  }
184
- const controller = new AbortController();
185
- session.cell(schema, table, key, name, { signal: controller.signal }).then((value) => {
186
- if (!controller.signal.aborted) {
187
- setFetched({ id: fetchId, rows: loaded, value });
188
- }
189
- }, (error) => {
190
- if (!controller.signal.aborted) {
191
- setFetched({ id: fetchId, rows: loaded, value: undefined });
192
- setFailure(messageOf(error));
193
- }
194
- });
195
- return () => controller.abort();
196
- }, [fetchId, focus, structure, loaded, session, schema, table]);
204
+ }, [storage, recordWidth]);
197
205
  const discardPending = () => {
198
206
  setPending(emptyPending);
199
207
  setSelected(NO_KEYS);
@@ -320,7 +328,7 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
320
328
  setPending((now) => setEdit(now, row.index, name, value, loaded.rows[row.index][column] ?? null));
321
329
  }
322
330
  };
323
- const loadValue = async (rowKey, column) => {
331
+ const loadValue = useStableCallback(async (rowKey, column) => {
324
332
  const row = parseRowKey(rowKey);
325
333
  const name = loaded?.columns[column]?.name;
326
334
  const key = row.kind === 'loaded' && structure !== null && loaded !== null ? rowKeyOf(structure, loaded, row.index) : null;
@@ -334,7 +342,7 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
334
342
  setFailure(messageOf(error));
335
343
  return undefined;
336
344
  }
337
- };
345
+ });
338
346
  const refsOf = (keys) => {
339
347
  const loadedRows = [];
340
348
  const inserted = [];
@@ -389,23 +397,17 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
389
397
  }
390
398
  };
391
399
  const toggleRecordView = () => {
392
- if (recordOpen) {
393
- const row = rows[recordIndex];
394
- setRecordOpen(false);
395
- if (row !== undefined) {
396
- setFocusRequest({ rowKey: row.key, column: focus?.column ?? 0 });
397
- }
398
- return;
399
- }
400
- const focused = focus === null ? undefined : rows.find((row) => row.key === focus.rowKey);
401
- setRecordKey((focused ?? rows[0])?.key ?? null);
402
- setRecordOpen(true);
400
+ setRecordOpen((now) => !now);
401
+ };
402
+ /* The record view moves the grid's focus along with it, without taking the keyboard from its field. */
403
+ const followInGrid = (cell) => {
404
+ setFocus(cell);
405
+ setFocusRequest({ ...cell, quiet: true });
403
406
  };
404
407
  const changeRecordIndex = (index) => {
405
408
  const row = rows[index];
406
409
  if (row !== undefined) {
407
- setRecordKey(row.key);
408
- setFocus((now) => (now === null ? now : { rowKey: row.key, column: now.column }));
410
+ followInGrid({ rowKey: row.key, column: focus?.column ?? 0 });
409
411
  }
410
412
  };
411
413
  const queryMenu = (context) => {
@@ -460,44 +462,14 @@ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, on
460
462
  }
461
463
  };
462
464
  const bounds = pageBounds(loadedOffset, loaded?.rows.length ?? 0, loaded?.hasMore ?? false, counted);
463
- const focusedColumn = focus === null ? null : (columns[focus.column] ?? null);
464
- const panelEditable = editable && focusedRow !== undefined && focusedRow.locked !== true && focusedRow.state !== 'deleted' && focusedColumn?.readOnly !== true;
465
- const fetchedNow = fetched !== null && fetched.id === fetchId && fetched.rows === loaded;
466
- const panelValue = (() => {
467
- if (focusedCell === undefined || focusedRow === undefined || focus === null) {
468
- return undefined;
469
- }
470
- const whole = wholeValueOf(focusedCell);
471
- if (whole !== undefined) {
472
- return whole;
473
- }
474
- if (focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default') {
475
- // A pending DEFAULT holds no value of its own, so the panel keeps showing what the row had.
476
- const row = parseRowKey(focusedRow.key);
477
- return row.kind === 'loaded' && loaded !== null ? wholeValueOf(loaded.rows[row.index]?.[focus.column] ?? null) : null;
478
- }
479
- return fetchedNow ? fetched.value : undefined;
480
- })();
481
465
  const notice = failure ?? rowsLoad.error ?? structureLoad.error;
482
- const noticeText = (found) => {
483
- switch (found.kind) {
484
- case 'exporting':
485
- return t('table.export.running', { format: found.format.toUpperCase() });
486
- case 'exported':
487
- return t('table.export.done', { count: found.rows, rows: formatNumber(found.rows) });
488
- case 'imported':
489
- return t('table.import.done', { count: found.rows, rows: formatNumber(found.rows) });
490
- case 'failed':
491
- return found.message;
492
- }
493
- };
494
- return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), onKeyDown: handleKeyDown, children: [_jsx(TableToolbar, { refreshing: rowsLoad.loading, readOnlyReason: readOnlyReason, hasSelection: selected.size > 0, canRevertSelection: selected.size > 0 && canRevert(selected), valuePanelOpen: panelOpen, recordViewOpen: recordOpen, transfer: files === undefined
466
+ return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), onKeyDown: handleKeyDown, children: [_jsx(TableToolbar, { start: toolbarStart, refreshing: rowsLoad.loading, readOnlyReason: readOnlyReason, hasSelection: selected.size > 0, canRevertSelection: selected.size > 0 && canRevert(selected), recordViewOpen: recordOpen, transfer: files === undefined
495
467
  ? undefined
496
468
  : {
497
469
  busy: transfer.notice?.kind === 'exporting',
498
470
  onExport: transfer.exportAs,
499
471
  onImport: readOnlyReason === null && structure !== null ? () => guard(transfer.chooseImportFile) : undefined
500
- }, pendingCount: changeCount, submitting: submitting, engine: engine, chips: chips, columns: commandColumns, pageSize: pageSize, bounds: bounds, commandRef: command, onChipsChange: applyChips, onJumpToColumn: jumpToColumn, onRefresh: refresh, onAddRow: () => setPending((now) => addInsertWith(now, {})), onDeleteRows: deleteSelected, onCloneRows: () => cloneRows([...selected]), onRevertRows: () => revertRowKeys(selected), onToggleValuePanel: () => setPanelOpen((now) => !now), onToggleRecordView: toggleRecordView, onPageSizeChange: changePageSize, onFirstPage: () => goToPage(0), onLastPage: () => void goToLast(), onSubmit: () => void submit(), onRevert: discardPending }), notice !== null && (_jsx(Banner, { icon: CircleAlert, tone: "error", message: notice, className: "shrink-0 pt-2", children: failure === null ? (_jsx(Button, { size: "xs", onClick: refresh, children: t('table.retry') })) : (_jsx(Button, { size: "xs", onClick: () => setFailure(null), children: t('table.dismiss') })) })), shapeChanged && (_jsx(Banner, { icon: RefreshCw, tone: "neutral", message: t('table.changed'), className: "shrink-0 pt-2", children: _jsx(Button, { size: "xs", onClick: refresh, children: t('table.reload') }) })), transfer.notice !== null && (_jsx(Banner, { icon: transfer.notice.kind === 'failed' ? CircleAlert : transfer.notice.kind === 'exporting' ? Download : Check, tone: transfer.notice.kind === 'failed' ? 'error' : 'neutral', message: noticeText(transfer.notice), className: "shrink-0 pt-2", children: transfer.notice.kind === 'exporting' ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { size: 12, label: t('table.export.running', { format: transfer.notice.format.toUpperCase() }) }), _jsx(Button, { size: "xs", onClick: transfer.cancelExport, children: t('table.export.cancel') })] })) : (_jsx(Button, { size: "xs", onClick: transfer.dismiss, children: t('table.dismiss') })) })), loaded === null ? (_jsx(PanelEmpty, { busy: notice === null, children: notice === null ? t('table.loading') : t('table.notLoaded') })) : (_jsxs(_Fragment, { children: [_jsxs(ValueDock, { open: panelOpen, panel: _jsx(ValuePanel, { column: focusedColumn, value: panelValue, loading: fetchId !== null && !fetchedNow, editable: panelEditable, onCommit: (value) => focus !== null && commit(focus.rowKey, focus.column, value), onClose: () => setPanelOpen(false), className: "min-w-0 flex-1" }), children: [_jsx("div", { "aria-busy": rowsLoad.loading, className: clsx('relative min-h-0 flex-1 flex-col', recordOpen ? 'hidden' : 'flex', rowsLoad.loading && 'pointer-events-none opacity-60'), children: _jsx(DataGrid, { ref: grid, label: t('table.grid', { table }), columns: columns, rows: rows, editable: editable, empty: t('table.empty'), selectedKeys: selected, onSelectedKeysChange: setSelected, onCommit: commit, loadValue: loadValue, sorts: sorts, onSortsChange: changeSorts, onFocusedCellChange: setFocus, onDeleteSelected: editable ? deleteSelected : undefined, sqlTarget: sqlTarget, queryMenu: queryMenu, menu: gridMenu, onDuplicateRows: editable ? cloneRows : undefined, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onRangeChange: setRange, initialLayout: stored ?? undefined, onLayoutChange: remember, focusRequest: focusRequest, columnRequest: columnRequest }) }), recordOpen && (_jsx(RecordView, { label: t('table.recordFields', { table }), columns: columns, rows: rows, index: recordIndex, onIndexChange: changeRecordIndex, editable: editable, onCommit: commit, loadValue: loadValue, onFocusedCellChange: setFocus, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, empty: t('table.empty') }))] }), _jsx(TableStatusBar, { elapsedMs: loaded.elapsedMs, bounds: bounds, counting: counting, selection: selectionFigures, onCount: () => void countRows(), onPrevious: () => goToPage(offset - pageSize), onNext: () => goToPage(offset + pageSize) })] })), transfer.importDraft !== null && structure !== null && (_jsx(ImportDialog, { open: true, path: transfer.importDraft.path, tableColumns: importableColumns(structure.columns), sample: transfer.importDraft.sample, header: transfer.importDraft.header, mapping: transfer.importDraft.mapping, busy: transfer.importDraft.busy, error: transfer.importDraft.error, onHeaderChange: transfer.changeHeader, onMappingChange: transfer.changeMapping, onImport: transfer.runImport, onCancel: transfer.closeImport, onOpenChange: (open) => !open && transfer.closeImport() })), _jsx(PromptDialog, { open: discard !== null, danger: true, title: t('table.discard.title'), description: t('table.discard.description'), confirmLabel: t('table.discard.confirm'), onConfirm: () => {
472
+ }, pendingCount: changeCount, submitting: submitting, engine: engine, chips: chips, columns: commandColumns, pageSize: pageSize, bounds: bounds, commandRef: command, onChipsChange: applyChips, onJumpToColumn: jumpToColumn, onRefresh: refresh, onAddRow: () => setPending((now) => addInsertWith(now, {})), onDeleteRows: deleteSelected, onCloneRows: () => cloneRows([...selected]), onRevertRows: () => revertRowKeys(selected), onToggleRecordView: toggleRecordView, onPageSizeChange: changePageSize, onFirstPage: () => goToPage(0), onLastPage: () => void goToLast(), onSubmit: () => void submit(), onRevert: discardPending }), notice !== null && (_jsx(Banner, { icon: CircleAlert, tone: "error", message: notice, className: "shrink-0 pt-2", children: failure === null ? (_jsx(Button, { size: "xs", onClick: refresh, children: t('table.retry') })) : (_jsx(Button, { size: "xs", onClick: () => setFailure(null), children: t('table.dismiss') })) })), shapeChanged && (_jsx(Banner, { icon: RefreshCw, tone: "neutral", message: t('table.changed'), className: "shrink-0 pt-2", children: _jsx(Button, { size: "xs", onClick: refresh, children: t('table.reload') }) })), transfer.notice !== null && (_jsx(Banner, { icon: transfer.notice.kind === 'failed' ? CircleAlert : transfer.notice.kind === 'exporting' ? Download : Check, tone: transfer.notice.kind === 'failed' ? 'error' : 'neutral', message: transferText(transfer.notice, t), className: "shrink-0 pt-2", children: transfer.notice.kind === 'exporting' ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { size: 12, label: t('table.export.running', { format: transfer.notice.format.toUpperCase() }) }), _jsx(Button, { size: "xs", onClick: transfer.cancelExport, children: t('table.export.cancel') })] })) : (_jsx(Button, { size: "xs", onClick: transfer.dismiss, children: t('table.dismiss') })) })), loaded === null ? (_jsx(PanelEmpty, { busy: notice === null, children: notice === null ? t('table.loading') : t('table.notLoaded') })) : (_jsxs(_Fragment, { children: [_jsx(ValueDock, { open: recordOpen, width: recordWidth ?? RECORD_WIDTH, onWidthChange: setRecordWidth, panel: _jsx(RecordView, { label: t('table.recordFields', { table }), columns: columns, rows: rows, index: recordIndex, onIndexChange: changeRecordIndex, focusedColumn: focus !== null && focusedIndex === recordIndex ? focus.column : null, editable: editable, onCommit: commit, loadValue: loadValue, onFocusedCellChange: followInGrid, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onClose: () => setRecordOpen(false), empty: t('table.empty') }), children: _jsx("div", { "aria-busy": rowsLoad.loading, className: clsx('relative flex min-h-0 flex-1 flex-col', rowsLoad.loading && 'pointer-events-none opacity-60'), children: _jsx(DataGrid, { ref: grid, label: t('table.grid', { table }), columns: columns, rows: rows, editable: editable, empty: t('table.empty'), selectedKeys: selected, onSelectedKeysChange: setSelected, onCommit: commit, loadValue: loadValue, sorts: sorts, onSortsChange: changeSorts, onFocusedCellChange: setFocus, onDeleteSelected: editable ? deleteSelected : undefined, sqlTarget: sqlTarget, queryMenu: queryMenu, menu: gridMenu, onDuplicateRows: editable ? cloneRows : undefined, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onRangeChange: setRange, initialLayout: stored ?? undefined, onLayoutChange: remember, focusRequest: focusRequest, columnRequest: columnRequest }) }) }), _jsx(TableStatusBar, { elapsedMs: loaded.elapsedMs, bounds: bounds, counting: counting, selection: selectionFigures, onCount: () => void countRows(), onPrevious: () => goToPage(offset - pageSize), onNext: () => goToPage(offset + pageSize) })] })), transfer.importDraft !== null && structure !== null && (_jsx(ImportDialog, { open: true, path: transfer.importDraft.path, tableColumns: importableColumns(structure.columns), sample: transfer.importDraft.sample, header: transfer.importDraft.header, mapping: transfer.importDraft.mapping, busy: transfer.importDraft.busy, error: transfer.importDraft.error, onHeaderChange: transfer.changeHeader, onMappingChange: transfer.changeMapping, onImport: transfer.runImport, onCancel: transfer.closeImport, onOpenChange: (open) => !open && transfer.closeImport() })), _jsx(PromptDialog, { open: discard !== null, danger: true, title: t('table.discard.title'), description: t('table.discard.description'), confirmLabel: t('table.discard.confirm'), onConfirm: () => {
501
473
  discard?.run();
502
474
  setDiscard(null);
503
475
  }, onOpenChange: (open) => {
@@ -5,7 +5,10 @@ import { rowKeyOf } from './pending.js';
5
5
  export const loadedRowKey = (index) => `row:${index}`;
6
6
  export const insertedRowKey = (id) => `new:${id}`;
7
7
  export const parseRowKey = (key) => key.startsWith('new:') ? { kind: 'inserted', id: Number(key.slice(4)) } : { kind: 'loaded', index: Number(key.slice(4)) };
8
- export const buildGridColumns = (loaded, structure) => loaded.columns.map((column) => gridColumnOf(column, structure?.columns.find((info) => info.name === column.name), structure?.primaryKey ?? []));
8
+ export const buildGridColumns = (loaded, structure) => {
9
+ const keys = { primaryKey: structure?.primaryKey ?? [], foreignKey: structure?.foreignKeys.flatMap((key) => key.columns) ?? [] };
10
+ return loaded.columns.map((column) => gridColumnOf(column, structure?.columns.find((info) => info.name === column.name), keys));
11
+ };
9
12
  /* The loaded rows with their pending edits laid over them, then the rows waiting to be inserted. */
10
13
  export const buildGridRows = ({ structure, loaded, pending, offset }) => {
11
14
  const rows = loaded.rows.map((cells, index) => {
@@ -14,6 +14,7 @@ export type TransferNotice = {
14
14
  readonly rows: number;
15
15
  } | {
16
16
  readonly kind: 'failed';
17
+ readonly during: 'export' | 'import';
17
18
  readonly message: string;
18
19
  };
19
20
  export interface ImportDraft {
@@ -35,6 +36,7 @@ export interface TableTransferInput {
35
36
  readonly where: string;
36
37
  readonly orderBy: string;
37
38
  readonly onImported: () => void;
39
+ readonly report?: (notice: TransferNotice) => void;
38
40
  }
39
41
  export interface TableTransfer {
40
42
  readonly notice: TransferNotice | null;
@@ -48,4 +50,4 @@ export interface TableTransfer {
48
50
  runImport(): void;
49
51
  closeImport(): void;
50
52
  }
51
- export declare function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported }: TableTransferInput): TableTransfer;
53
+ export declare function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported, report }: TableTransferInput): TableTransfer;
@@ -3,7 +3,7 @@ import { messageOf } from '@adecore/ui';
3
3
  import { DatabaseRequestError } from '../client/types.js';
4
4
  import { formatOfPath, importableColumns, mapColumn, matchColumns } from './import-mapping.js';
5
5
  /* Export of the table as filtered, and import of a file into it, with the state both need between the dialogs and the strip. */
6
- export function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported }) {
6
+ export function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported, report }) {
7
7
  const [notice, setNotice] = useState(null);
8
8
  const [importDraft, setImportDraft] = useState(null);
9
9
  const exporting = useRef(null);
@@ -17,6 +17,15 @@ export function useTableTransfer({ client, session, files, schema, table, struct
17
17
  return () => clearTimeout(timer);
18
18
  }, [notice]);
19
19
  useEffect(() => () => sampling.current?.abort(), []);
20
+ const show = (next) => {
21
+ if (report === undefined) {
22
+ setNotice(next);
23
+ }
24
+ else {
25
+ setNotice(null);
26
+ report(next);
27
+ }
28
+ };
20
29
  const exportAs = (format) => {
21
30
  if (files === undefined || exporting.current !== null) {
22
31
  return;
@@ -31,11 +40,16 @@ export function useTableTransfer({ client, session, files, schema, table, struct
31
40
  setNotice({ kind: 'exporting', format });
32
41
  try {
33
42
  const result = await session.export({ source: { kind: 'table', schema, table, where: where || undefined, orderBy: orderBy || undefined }, format, path }, { signal: controller.signal });
34
- setNotice({ kind: 'exported', rows: result.rows });
43
+ show({ kind: 'exported', rows: result.rows });
35
44
  }
36
45
  catch (error) {
37
46
  const cancelled = controller.signal.aborted || (error instanceof DatabaseRequestError && error.code === 'cancelled');
38
- setNotice(cancelled ? null : { kind: 'failed', message: messageOf(error) });
47
+ if (cancelled) {
48
+ setNotice(null);
49
+ }
50
+ else {
51
+ show({ kind: 'failed', during: 'export', message: messageOf(error) });
52
+ }
39
53
  }
40
54
  finally {
41
55
  exporting.current = null;
@@ -63,7 +77,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
63
77
  setImportDraft({ path, format, header: true, sample: read, mapping: matchColumns(read.columns, tableColumns, true), busy: false, error: null });
64
78
  }
65
79
  catch (error) {
66
- setNotice({ kind: 'failed', message: messageOf(error) });
80
+ show({ kind: 'failed', during: 'import', message: messageOf(error) });
67
81
  }
68
82
  })();
69
83
  };
@@ -98,7 +112,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
98
112
  try {
99
113
  const rows = await session.import(schema, table, { path, format, header, columns: mapping });
100
114
  setImportDraft(null);
101
- setNotice({ kind: 'imported', rows });
115
+ show({ kind: 'imported', rows });
102
116
  onImported();
103
117
  }
104
118
  catch (error) {
@@ -1,4 +1,4 @@
1
- import type { ColumnInfo, DockerContainer, ForeignKeyInfo, IndexInfo, ServerInfo, TableKind, Value } from '../protocol/index.ts';
1
+ import type { CheckInfo, ColumnInfo, DockerContainer, ForeignKeyInfo, IndexInfo, ServerInfo, TableKind, TriggerInfo, Value } from '../protocol/index.ts';
2
2
  import type { DatabaseTransport } from '../client/types.ts';
3
3
  export interface FakeTable {
4
4
  readonly kind?: TableKind;
@@ -7,6 +7,8 @@ export interface FakeTable {
7
7
  readonly rows: readonly (readonly Value[])[];
8
8
  readonly indexes?: readonly IndexInfo[];
9
9
  readonly foreignKeys?: readonly ForeignKeyInfo[];
10
+ readonly checks?: readonly CheckInfo[];
11
+ readonly triggers?: readonly TriggerInfo[];
10
12
  readonly ddl?: string;
11
13
  }
12
14
  export interface FakeDatabase {
@@ -18,12 +18,16 @@ const toTables = (database) => new Map(Object.entries(database.schemas).map(([sc
18
18
  new Map(Object.entries(tables).map(([name, table]) => [
19
19
  name,
20
20
  {
21
+ schema,
22
+ name,
21
23
  kind: table.kind ?? 'table',
22
24
  columns: structuredClone([...table.columns]),
23
25
  primaryKey: [...(table.primaryKey ?? [])],
24
26
  rows: structuredClone(table.rows.map((row) => [...row])),
25
27
  indexes: structuredClone([...(table.indexes ?? [])]),
26
28
  foreignKeys: structuredClone([...(table.foreignKeys ?? [])]),
29
+ checks: table.checks === undefined ? undefined : structuredClone([...table.checks]),
30
+ triggers: table.triggers === undefined ? undefined : structuredClone([...table.triggers]),
27
31
  ddl: table.ddl ?? null
28
32
  }
29
33
  ]))
@@ -71,7 +75,7 @@ const toCell = (value, cellLimit) => {
71
75
  }
72
76
  return value;
73
77
  };
74
- const toResultColumns = (table) => table.columns.map(({ name, type, kind }) => ({ name, type, kind }));
78
+ const toResultColumns = (table) => table.columns.map(({ name, type, kind }) => ({ name, type, kind, source: { schema: table.schema, table: table.name, column: name } }));
75
79
  const toRowsResult = (table, rows, offset, limit, cellLimit) => ({
76
80
  columns: toResultColumns(table),
77
81
  rows: rows.slice(offset, offset + limit).map((row) => row.map((value) => toCell(value, cellLimit))),
@@ -259,6 +263,8 @@ export const fakeDatabaseTransport = (options) => {
259
263
  rowKey: rowKeyOf(table),
260
264
  indexes: table.indexes,
261
265
  foreignKeys: table.foreignKeys,
266
+ ...(table.checks === undefined ? {} : { checks: table.checks }),
267
+ ...(table.triggers === undefined ? {} : { triggers: table.triggers }),
262
268
  ddl: table.ddl
263
269
  };
264
270
  },
@@ -0,0 +1,9 @@
1
+ import type { GridColumn } from '../grid/types.ts';
2
+ import type { EditValue, Value } from '../protocol/index.ts';
3
+ export interface ValueEditorProps {
4
+ column: GridColumn;
5
+ value: Value;
6
+ editable: boolean;
7
+ onCommit?(value: EditValue): void;
8
+ }
9
+ export declare function ValueEditor({ column, value, editable, onCommit }: ValueEditorProps): import("react").JSX.Element;
@@ -0,0 +1,102 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { Copy, Ellipsis, WrapText } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Button, ButtonGroup, copyText, FormError, IconButton, isApplePlatform, KEY_SHORTCUTS, matchesShortcut, Menu, Segmented, TextArea, Tooltip } from '@adecore/ui';
7
+ import { formatBytes, formatNumber } from '@adecore/ui/format';
8
+ import { CODE_TEXT } from '../code-text.js';
9
+ import { editValueOf, sameDraft, sameSource, sourceOf } from './draft.js';
10
+ import { hexDumpLines, parseHex } from './hex.js';
11
+ import { renderView, sizeOf, viewsOf } from './views.js';
12
+ const WRAPPING_VIEWS = new Set(['text', 'formatted', 'utf8']);
13
+ const READING = `min-h-0 flex-1 overflow-auto rounded-lg border border-border bg-surface p-2 ${CODE_TEXT} text-text`;
14
+ /*
15
+ * The whole of one value, to read and change what a cell is too small for: Text, Formatted for JSON, or
16
+ * Hex, Text and UUID for binary, with the checks of JSON and hex before a change is taken.
17
+ */
18
+ export function ValueEditor({ column, value, editable, onCommit }) {
19
+ const { t } = useTranslation('database');
20
+ const { name, type, kind } = column;
21
+ const source = useMemo(() => sourceOf({ name, type, kind }, value), [name, type, kind, value]);
22
+ const [seen, setSeen] = useState(source);
23
+ const [draft, setDraft] = useState(source.original);
24
+ const [choice, setChoice] = useState(null);
25
+ const [problem, setProblem] = useState(null);
26
+ const [wrap, setWrap] = useState(true);
27
+ // A new cell, or the same cell with a new value, starts over from that value.
28
+ if (!sameSource(seen, source)) {
29
+ setSeen(source);
30
+ setDraft(source.original);
31
+ setChoice(null);
32
+ setProblem(null);
33
+ }
34
+ const views = viewsOf(source, draft);
35
+ const view = choice !== null && views.includes(choice) ? choice : views[0];
36
+ const dirty = !sameDraft(draft, source.original);
37
+ const rendered = useMemo(() => (view === 'text' ? null : renderView(view, draft)), [view, draft]);
38
+ const hex = source.binary && draft.mode === 'value' ? parseHex(draft.text) : null;
39
+ const shownProblem = problem ?? (hex !== null && !hex.ok ? { kind: 'hex', problem: hex.problem } : null);
40
+ const size = sizeOf(source, draft);
41
+ const placeholder = draft.mode === 'null' ? 'NULL' : draft.mode === 'default' ? 'DEFAULT' : undefined;
42
+ const change = (next) => {
43
+ setDraft(next);
44
+ setProblem(null);
45
+ };
46
+ const apply = () => {
47
+ if (!editable || onCommit === undefined || !dirty) {
48
+ return;
49
+ }
50
+ const conversion = editValueOf(draft, source);
51
+ if (conversion.ok) {
52
+ onCommit(conversion.value);
53
+ }
54
+ else {
55
+ setProblem(conversion.problem);
56
+ }
57
+ };
58
+ const handleKeyDown = (event) => {
59
+ if (!event.nativeEvent.isComposing && matchesShortcut(KEY_SHORTCUTS.modEnter, event.nativeEvent, isApplePlatform())) {
60
+ event.preventDefault();
61
+ apply();
62
+ }
63
+ };
64
+ const copy = () => {
65
+ if (view === 'hex' && hex !== null && hex.ok) {
66
+ copyText(hexDumpLines(hex.hex).join('\n'));
67
+ }
68
+ else if (rendered !== null && rendered.kind === 'text') {
69
+ copyText(rendered.text);
70
+ }
71
+ else {
72
+ copyText(draft.mode === 'value' ? draft.text : draft.mode === 'null' ? 'NULL' : 'DEFAULT');
73
+ }
74
+ };
75
+ const editor = (extra) => (_jsx(TextArea, { "aria-label": t(source.binary ? 'value.hexEditor' : 'value.editor', { column: column.name }), wrap: wrap || source.binary ? 'soft' : 'off', spellCheck: false, readOnly: !editable, placeholder: placeholder, value: draft.mode === 'value' ? draft.text : '', className: clsx(CODE_TEXT, extra), onChange: (event) => change({ text: event.target.value, mode: 'value' }), onKeyDown: handleKeyDown }));
76
+ const problemText = (found) => found.kind === 'json'
77
+ ? t('value.invalidJson', { reason: found.reason })
78
+ : t(found.problem === 'odd' ? 'value.invalidHexOdd' : 'value.invalidHexCharacters');
79
+ const reading = (content, wrapped) => {
80
+ if (content.kind === 'empty') {
81
+ return _jsx(Faint, { children: placeholder });
82
+ }
83
+ if (content.kind === 'problem') {
84
+ return _jsx(Faint, { children: problemText(content.problem) });
85
+ }
86
+ return (_jsxs(_Fragment, { children: [_jsx("pre", { className: clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre'), children: content.text }), content.truncated && (_jsx("p", { className: "shrink-0 text-xs text-text-faint", children: t('value.dumpTruncated', { shown: formatNumber(content.truncated.shown), total: formatNumber(content.truncated.total) }) }))] }));
87
+ };
88
+ const body = () => {
89
+ if (view === 'text') {
90
+ return editor('min-h-0 flex-1');
91
+ }
92
+ if (view === 'hex' && editable) {
93
+ return (_jsxs(_Fragment, { children: [draft.mode === 'value' && rendered !== null && reading(rendered, false), editor(draft.mode === 'value' ? 'flex-none' : 'min-h-0 flex-1')] }));
94
+ }
95
+ return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
96
+ };
97
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex h-10 shrink-0 items-center gap-2 px-2", children: [views.length > 1 && (_jsx(Segmented, { value: view, onValueChange: (id) => setChoice(id), label: t('value.views'), options: views.map((id) => ({ id, label: t(`value.view.${id}`) })) })), _jsxs(ButtonGroup, { className: "ml-auto", children: [WRAPPING_VIEWS.has(view) && (_jsx(IconButton, { icon: WrapText, size: "sm", label: t('value.wrap'), "aria-pressed": wrap, active: wrap, onClick: () => setWrap(!wrap) })), _jsx(IconButton, { icon: Copy, size: "sm", label: t('value.copy'), onClick: copy })] })] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-2 px-2 pb-2", children: body() }), shownProblem !== null && _jsx(FormError, { className: "shrink-0 px-3 pb-2 text-xs", children: problemText(shownProblem) }), editable && (_jsxs("div", { className: "flex shrink-0 items-center gap-1.5 border-t border-border p-2", children: [_jsxs(Menu.Root, { children: [_jsx(IconButton, { icon: Ellipsis, size: "sm", label: t('value.more'), render: _jsx(Menu.Trigger, {}) }), _jsxs(Menu.Popup, { children: [column.nullable !== false && (_jsx(Menu.Item, { disabled: draft.mode === 'null', onClick: () => change({ text: '', mode: 'null' }), children: t('value.setNull') })), _jsx(Menu.Item, { disabled: draft.mode === 'default', onClick: () => change({ text: '', mode: 'default' }), children: t('value.setDefault') })] })] }), _jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [_jsx(Button, { size: "xs", disabled: !dirty, onClick: () => change(source.original), children: t('value.revert') }), _jsx(Tooltip, { label: t('value.applyHint'), kbd: KEY_SHORTCUTS.modEnter, children: _jsx(Button, { size: "xs", variant: "primary", disabled: !dirty || onCommit === undefined, onClick: apply, children: t('value.apply') }) })] })] })), _jsx("footer", { className: "flex h-8 shrink-0 items-center border-t border-border px-3 text-xs text-text-muted tabular-nums", children: size !== null &&
98
+ (size.unit === 'bytes' ? formatBytes(size.count) : t('value.characters', { count: size.count, formatted: formatNumber(size.count) })) })] }));
99
+ }
100
+ function Faint({ children }) {
101
+ return _jsx("div", { className: clsx(CODE_TEXT, 'grid min-h-0 flex-1 place-items-center text-text-faint'), children: children });
102
+ }
@@ -1,4 +1,4 @@
1
- import { type Ref } from 'react';
1
+ import type { Ref } from 'react';
2
2
  import type { GridColumn } from '../grid/types.ts';
3
3
  import type { EditValue, Value } from '../protocol/index.ts';
4
4
  export interface ValuePanelProps {