@adecore/database 0.21.1 → 0.22.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 (109) hide show
  1. package/dist/assign-ref.d.ts +2 -0
  2. package/dist/assign-ref.js +9 -0
  3. package/dist/client/client.js +14 -15
  4. package/dist/client/types.d.ts +1 -0
  5. package/dist/client/types.js +2 -0
  6. package/dist/connections/ConnectionForm.js +7 -2
  7. package/dist/connections/ConnectionManager.js +5 -5
  8. package/dist/connections/EngineIcon.js +0 -1
  9. package/dist/console/QueryConsole.js +12 -4
  10. package/dist/console/editing.js +2 -4
  11. package/dist/console/useConsoleHistory.js +3 -2
  12. package/dist/ddl/alter-mysql.js +2 -8
  13. package/dist/ddl/alter-sqlite.js +1 -1
  14. package/dist/ddl/column.js +5 -8
  15. package/dist/ddl/create.d.ts +1 -0
  16. package/dist/ddl/create.js +1 -1
  17. package/dist/ddl/diff.js +25 -23
  18. package/dist/designer/ColumnsEditor.js +8 -6
  19. package/dist/designer/ForeignKeysEditor.js +5 -1
  20. package/dist/designer/OptionsEditor.js +12 -3
  21. package/dist/designer/SqlPreview.d.ts +1 -5
  22. package/dist/designer/SqlPreview.js +1 -1
  23. package/dist/designer/TableDesigner.js +7 -6
  24. package/dist/explorer/DatabaseExplorer.js +12 -13
  25. package/dist/explorer/TableDialog.js +1 -1
  26. package/dist/explorer/tree.js +6 -16
  27. package/dist/grid/DataGrid.js +11 -14
  28. package/dist/grid/EnumPicker.js +4 -3
  29. package/dist/grid/GridHeaderCell.d.ts +1 -1
  30. package/dist/grid/GridHeaderCell.js +2 -2
  31. package/dist/grid/RecordView.js +2 -1
  32. package/dist/grid/ValueDock.js +2 -6
  33. package/dist/grid/aggregates.js +2 -3
  34. package/dist/grid/column-layout.js +3 -2
  35. package/dist/grid/copy-formats.js +2 -3
  36. package/dist/grid/display.d.ts +0 -1
  37. package/dist/grid/display.js +1 -3
  38. package/dist/grid/layout.js +6 -11
  39. package/dist/host/helper-path.js +8 -0
  40. package/dist/host/host.js +11 -16
  41. package/dist/host/validate.js +44 -43
  42. package/dist/sql.d.ts +2 -0
  43. package/dist/sql.js +5 -2
  44. package/dist/structure/StructureView.js +5 -6
  45. package/dist/table/TableView.js +19 -12
  46. package/dist/table/command-field.js +8 -12
  47. package/dist/table/grid-rows.js +2 -1
  48. package/dist/table/layout-store.js +5 -2
  49. package/dist/table/pending.js +11 -3
  50. package/dist/table/useTableTransfer.js +6 -5
  51. package/dist/testing/fake.js +7 -9
  52. package/dist/value/ValueEditor.js +1 -1
  53. package/dist/value/draft.js +1 -6
  54. package/dist/value/json.js +4 -4
  55. package/dist/workbench/DatabaseWorkbench.js +12 -5
  56. package/dist/workbench/tabs.js +3 -4
  57. package/package.json +6 -7
  58. package/src/assign-ref.ts +10 -0
  59. package/src/client/client.ts +17 -18
  60. package/src/client/types.ts +3 -0
  61. package/src/connections/ConnectionForm.tsx +62 -47
  62. package/src/connections/ConnectionManager.tsx +6 -5
  63. package/src/connections/EngineIcon.tsx +0 -2
  64. package/src/console/QueryConsole.tsx +52 -31
  65. package/src/console/editing.ts +2 -4
  66. package/src/console/useConsoleHistory.ts +5 -2
  67. package/src/ddl/alter-mysql.ts +3 -11
  68. package/src/ddl/alter-sqlite.ts +1 -1
  69. package/src/ddl/column.ts +5 -7
  70. package/src/ddl/create.ts +1 -1
  71. package/src/ddl/diff.ts +30 -25
  72. package/src/designer/ColumnsEditor.tsx +145 -125
  73. package/src/designer/ForeignKeysEditor.tsx +44 -27
  74. package/src/designer/OptionsEditor.tsx +95 -75
  75. package/src/designer/SqlPreview.tsx +2 -3
  76. package/src/designer/TableDesigner.tsx +7 -6
  77. package/src/explorer/DatabaseExplorer.tsx +26 -31
  78. package/src/explorer/TableDialog.tsx +1 -1
  79. package/src/explorer/tree.ts +8 -19
  80. package/src/grid/DataGrid.tsx +11 -14
  81. package/src/grid/EnumPicker.tsx +4 -3
  82. package/src/grid/GridHeaderCell.tsx +2 -3
  83. package/src/grid/RecordView.tsx +2 -1
  84. package/src/grid/ValueDock.tsx +2 -5
  85. package/src/grid/aggregates.ts +2 -4
  86. package/src/grid/column-layout.ts +3 -2
  87. package/src/grid/copy-formats.ts +2 -3
  88. package/src/grid/display.ts +1 -5
  89. package/src/grid/layout.ts +10 -11
  90. package/src/host/helper-path.ts +8 -8
  91. package/src/host/host.ts +11 -16
  92. package/src/host/validate.ts +45 -43
  93. package/src/protocol/messages.ts +0 -1
  94. package/src/sql.ts +7 -2
  95. package/src/structure/StructureView.tsx +7 -8
  96. package/src/table/CommandField.tsx +0 -1
  97. package/src/table/TableView.tsx +48 -34
  98. package/src/table/command-field.ts +9 -12
  99. package/src/table/grid-rows.ts +2 -7
  100. package/src/table/layout-store.ts +6 -3
  101. package/src/table/pending.ts +12 -7
  102. package/src/table/useTableTransfer.ts +7 -5
  103. package/src/testing/fake.ts +8 -12
  104. package/src/value/ValueEditor.tsx +1 -1
  105. package/src/value/draft.ts +1 -6
  106. package/src/value/json.ts +4 -4
  107. package/src/workbench/DatabaseWorkbench.tsx +14 -6
  108. package/src/workbench/DesignerTab.tsx +1 -1
  109. package/src/workbench/tabs.ts +5 -8
@@ -65,14 +65,17 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
65
65
  const session = client.session(connection);
66
66
  const statements = plan.statements;
67
67
  const wasNew = snapshot.structure === null;
68
+ const settle = (message) => {
69
+ setFailure(message);
70
+ setConfirming(false);
71
+ };
68
72
  let outcome;
69
73
  applying.current = true;
70
74
  try {
71
75
  outcome = await session.execute(statements.join(';\n'), { schema });
72
76
  }
73
77
  catch (e) {
74
- setFailure(messageOf(e, t('designer.failed')));
75
- setConfirming(false);
78
+ settle(messageOf(e, t('designer.failed')));
76
79
  return;
77
80
  }
78
81
  finally {
@@ -81,12 +84,10 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
81
84
  const failed = outcome.results.find((result) => result.kind === 'error');
82
85
  if (failed?.kind === 'error') {
83
86
  await recoverFrom(session, statements, failed.sql, outcome.inTransaction);
84
- setFailure(failed.error.message);
85
- setConfirming(false);
87
+ settle(failed.error.message);
86
88
  return;
87
89
  }
88
- setFailure(null);
89
- setConfirming(false);
90
+ settle(null);
90
91
  // The client has told its listeners already: `execute` does for a statement that creates, alters or drops.
91
92
  if (wasNew || draft.name !== draft.originalName) {
92
93
  setSubject({ requested: table, current: draft.name });
@@ -9,11 +9,11 @@ import { useDatabaseAction, useDatabaseClient } from '../client-context.js';
9
9
  import { EngineIcon } from '../connections/EngineIcon.js';
10
10
  import { keyLabelOf } from '../column-keys.js';
11
11
  import { KeyIcon } from '../KeyIcon.js';
12
+ import { usePopupPress } from '../use-popup-press.js';
12
13
  import { RowMenu } from './RowMenu.js';
13
14
  import { TableDialog } from './TableDialog.js';
14
15
  import { connectionKey, flattenTree, hasMenu, isExpandable, startsOpen, isSelectable, navigate, neededLoads, selectionKey, selectionOf, tabStop, openedTableOf, tableKindIn, tableOf } from './tree.js';
15
16
  import { useExplorerLoads } from './use-explorer-loads.js';
16
- import { usePopupPress } from '../use-popup-press.js';
17
17
  import { useStoredExpansion } from './use-stored-expansion.js';
18
18
  /*
19
19
  * The connections, their schemas and their tables as a tree, loaded as each node opens. Every row
@@ -166,9 +166,6 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
166
166
  loads.reset(row.retry);
167
167
  }
168
168
  };
169
- const refresh = (target) => {
170
- loads.reset(target);
171
- };
172
169
  const disconnect = (connection) => {
173
170
  void client.disconnect(connection.id).catch(() => undefined);
174
171
  loads.reset({ connectionId: connection.id });
@@ -188,8 +185,9 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
188
185
  const isSelected = (row) => row !== undefined && isSelectable(row) && row.key === selectedKey;
189
186
  const renderRow = (row, index) => {
190
187
  const connection = connectionOf(row);
191
- const menu = row.kind === 'appItem' ? ((row.item.menu ?? null)) : connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: refresh, onDisconnect: disconnect, onRequest: setRequest })) : null;
188
+ const menu = row.kind === 'appItem' ? ((row.item.menu ?? null)) : connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: loads.reset, onDisconnect: disconnect, onRequest: setRequest })) : null;
192
189
  const selectedRow = isSelected(row);
190
+ const draggable = row.kind === 'table' && onTableDragStart !== undefined;
193
191
  const props = {
194
192
  level: row.level,
195
193
  selected: selectedRow,
@@ -210,11 +208,9 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
210
208
  rowElements.current.set(row.key, element);
211
209
  }
212
210
  },
213
- draggable: row.kind === 'table' && onTableDragStart !== undefined,
214
- onDragStart: row.kind === 'table' && onTableDragStart !== undefined
215
- ? (event) => onTableDragStart(row.ref, event, row.table.kind)
216
- : undefined,
217
- onDragEnd: row.kind === 'table' && onTableDragStart !== undefined ? onTableDragEnd : undefined,
211
+ draggable,
212
+ onDragStart: draggable ? (event) => onTableDragStart(row.ref, event, row.table.kind) : undefined,
213
+ onDragEnd: draggable ? onTableDragEnd : undefined,
218
214
  onClick: (event) => click(row, event.detail),
219
215
  onDoubleClick: tableOf(row) === null && row.kind !== 'appItem' ? undefined : () => open(row),
220
216
  onFocus: () => {
@@ -243,13 +239,13 @@ function RowContent({ row, onToggle }) {
243
239
  case 'schema':
244
240
  return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Database, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.schema })] }));
245
241
  case 'folder':
246
- return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: t(row.group === 'view' ? 'explorer.views' : 'explorer.tables') }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
242
+ return (_jsx(FolderContent, { expanded: row.expanded, count: row.count, onToggle: onToggle, children: t(row.group === 'view' ? 'explorer.views' : 'explorer.tables') }));
247
243
  case 'table':
248
244
  return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: row.table.kind === 'view' ? Eye : Table, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.table.name })] }));
249
245
  case 'part':
250
- return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: t(`explorer.parts.${row.part}`) }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
246
+ return (_jsx(FolderContent, { expanded: row.expanded, count: row.count, onToggle: onToggle, children: t(`explorer.parts.${row.part}`) }));
251
247
  case 'appFolder':
252
- return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.folder.label }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
248
+ return (_jsx(FolderContent, { expanded: row.expanded, count: row.count, onToggle: onToggle, children: row.folder.label }));
253
249
  case 'appItem':
254
250
  return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Icon, { icon: row.item.icon, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.item.label })] }));
255
251
  case 'entry':
@@ -264,6 +260,9 @@ function RowContent({ row, onToggle }) {
264
260
  return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx("span", { className: "text-text-faint", children: t(EMPTY_WORDS[row.of]) })] }));
265
261
  }
266
262
  }
263
+ function FolderContent({ expanded, count, onToggle, children }) {
264
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: children }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(count) })] }));
265
+ }
267
266
  const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables', columns: 'explorer.noColumns' };
268
267
  function SchemaCount({ shown, total }) {
269
268
  const { t } = useTranslation('database');
@@ -15,7 +15,7 @@ export function TableDialog({ request, onClose, onDone }) {
15
15
  const dialect = dialectOf(await session.server());
16
16
  const { results } = await session.execute(statementOf(dialect, change, ref, kind, to));
17
17
  const failed = results.find((result) => result.kind === 'error');
18
- if (failed !== undefined && failed.kind === 'error') {
18
+ if (failed?.kind === 'error') {
19
19
  throw new Error(failed.error.message);
20
20
  }
21
21
  // The client tells its listeners after a `RENAME`, a `DROP` and a `TRUNCATE`, but not after a SQLite emptying, which is a `DELETE FROM`.
@@ -58,14 +58,7 @@ export const selectionOf = (row) => {
58
58
  }
59
59
  };
60
60
  /* The key of the row a selection points at: its table, else its schema, else its connection. */
61
- export const selectionKey = (selection) => {
62
- if (selection.schema === undefined) {
63
- return connectionKey(selection.connectionId);
64
- }
65
- return selection.table === undefined
66
- ? schemaKey(selection.connectionId, selection.schema)
67
- : tableKey({ connectionId: selection.connectionId, schema: selection.schema, table: selection.table });
68
- };
61
+ export const selectionKey = (selection) => loadKey(selection);
69
62
  /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, an item the app gave one, and none of the rows that only report. */
70
63
  export const hasMenu = (row) => row.kind === 'connection' ||
71
64
  row.kind === 'schema' ||
@@ -87,6 +80,8 @@ const statusDraft = (row, level, parent, focusable) => ({
87
80
  make: (place) => ({ ...row, key: `${row.kind}:${parent}`, level, parent, focusable, ...place }),
88
81
  below: []
89
82
  });
83
+ /* The row of a load that is not ready: its error, which can be retried and so takes the focus, or that it is loading. */
84
+ const pendingDraft = (load, retry, level, parent) => statusDraft(load.status === 'error' ? { kind: 'error', message: load.message, retry } : { kind: 'loading' }, level, parent, load.status === 'error');
90
85
  const matches = (name, query) => name.toLowerCase().includes(query);
91
86
  /* What each folder of a table besides its columns holds: the primary key and the unique keys, the foreign keys, the other indexes, the checks and the triggers. */
92
87
  export const tableEntries = (structure) => {
@@ -133,10 +128,7 @@ export const flattenTree = (input) => {
133
128
  const tableChildren = (ref, kind, level, parent) => {
134
129
  const load = input.structures(ref.connectionId, ref.schema, ref.table);
135
130
  if (load.status !== 'ready') {
136
- const row = load.status === 'error'
137
- ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
138
- : { kind: 'loading' };
139
- return [statusDraft(row, level, parent, load.status === 'error')];
131
+ return [pendingDraft(load, { connectionId: ref.connectionId, schema: ref.schema, table: ref.table }, level, parent)];
140
132
  }
141
133
  const structure = load.value;
142
134
  if (structure.columns.length === 0) {
@@ -224,8 +216,7 @@ export const flattenTree = (input) => {
224
216
  if (filtering) {
225
217
  return { drafts: [], tables: 0 };
226
218
  }
227
- const row = load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id, schema } } : { kind: 'loading' };
228
- return { drafts: [statusDraft(row, level, parent, load.status === 'error')], tables: 0 };
219
+ return { drafts: [pendingDraft(load, { connectionId: connection.id, schema }, level, parent)], tables: 0 };
229
220
  };
230
221
  const schemaDraft = (connection, schema, parent) => {
231
222
  const key = schemaKey(connection.id, schema);
@@ -302,8 +293,7 @@ export const flattenTree = (input) => {
302
293
  }
303
294
  }
304
295
  else if (open && !filtering) {
305
- const row = load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id } } : { kind: 'loading' };
306
- children = [statusDraft(row, 2, key, load.status === 'error')];
296
+ children = [pendingDraft(load, { connectionId: connection.id }, 2, key)];
307
297
  }
308
298
  // Behind a status row too: what the app keeps of a connection does not wait on its server.
309
299
  children = [...children, ...(open ? appFolders : [])];
@@ -4,6 +4,10 @@ import clsx from 'clsx';
4
4
  import { Ban, Copy, RotateCcw } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, isApplePlatform, isModHeld, Kbd, useContentSize } from '@adecore/ui';
7
+ import { assignRef } from '../assign-ref.js';
8
+ import { useNumberNotation } from '../client-context.js';
9
+ import { usePopupPress } from '../use-popup-press.js';
10
+ import { useStableCallback } from '../use-stable-callback.js';
7
11
  import { fullRect, rectBetween, rectContains } from './cell-range.js';
8
12
  import { captureLayout, restoreLayout, sameLayout } from './column-layout.js';
9
13
  import { NO_COLUMN_SELECTION, selectColumn, shownSelection } from './column-selection.js';
@@ -20,9 +24,6 @@ import { clampColumnWidth, columnOffsets, estimateColumnWidth, fitColumnWidth, F
20
24
  import { moveFocus } from './navigation.js';
21
25
  import { selectRow } from './row-selection.js';
22
26
  import { sortOnly, sortStateOf } from './sort.js';
23
- import { useNumberNotation } from '../client-context.js';
24
- import { usePopupPress } from '../use-popup-press.js';
25
- import { useStableCallback } from '../use-stable-callback.js';
26
27
  const NO_KEYS = new Set();
27
28
  const NO_SORTS = [];
28
29
  const signatureOf = (columns) => columns.map((column) => column.name).join('\u0000');
@@ -41,7 +42,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
41
42
  const anchor = useRef(-1);
42
43
  const pressedInPopup = usePopupPress();
43
44
  const notation = useNumberNotation();
44
- /* Set once an edit commits or cancels, so the blur that follows the input's removal does not commit it again. */
45
+ // Set once an edit commits or cancels, so the blur that follows the input's removal does not commit it again.
45
46
  const settled = useRef(true);
46
47
  const dragging = useRef(false);
47
48
  const dragOrigin = useRef(null);
@@ -112,8 +113,6 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
112
113
  const enumTypes = useMemo(() => columns.map((column) => parseEnumType(column.type)), [columns]);
113
114
  useEffect(() => {
114
115
  reportFocus.current = onFocusedCellChange;
115
- });
116
- useEffect(() => {
117
116
  reportRange.current = onRangeChange;
118
117
  reportLayout.current = onLayoutChange;
119
118
  });
@@ -136,8 +135,11 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
136
135
  reportRange.current?.({ top: Number(top), bottom: Number(bottom), columns: list === '' ? [] : list.split(',').map(Number) });
137
136
  }, [blockKey]);
138
137
  useEffect(() => {
139
- const index = focusRequest === null || focusRequest === undefined ? -1 : rows.findIndex((row) => row.key === focusRequest.rowKey);
140
- if (focusRequest !== null && focusRequest !== undefined && index >= 0) {
138
+ if (focusRequest === null || focusRequest === undefined) {
139
+ return;
140
+ }
141
+ const index = rows.findIndex((row) => row.key === focusRequest.rowKey);
142
+ if (index >= 0) {
141
143
  focusAt({ row: index, column: focusRequest.column });
142
144
  if (focusRequest.quiet !== true) {
143
145
  scroller.current?.focus();
@@ -169,12 +171,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
169
171
  const attach = useCallback((node) => {
170
172
  scroller.current = node;
171
173
  measure(node);
172
- if (typeof ref === 'function') {
173
- ref(node);
174
- }
175
- else if (ref) {
176
- ref.current = node;
177
- }
174
+ assignRef(ref, node);
178
175
  }, [measure, ref]);
179
176
  const cellId = (row, column) => cellIdOf(gridId, row, column);
180
177
  const canEdit = (position) => {
@@ -18,9 +18,10 @@ const LOOK = {
18
18
  */
19
19
  export function EnumPicker({ type, value, nullable, label, autoOpen = false, disabled = false, onDone, look = 'cell', className, ref }) {
20
20
  const { t } = useTranslation('database');
21
- const settled = useRef(false);
22
- const [members, setMembers] = useState(() => new Set(type.kind === 'set' && value !== null ? membersOf(value) : []));
23
21
  const set = type.kind === 'set';
22
+ const membersOfValue = () => new Set(set && value !== null ? membersOf(value) : []);
23
+ const settled = useRef(false);
24
+ const [members, setMembers] = useState(membersOfValue);
24
25
  const settle = (result) => {
25
26
  if (!settled.current) {
26
27
  settled.current = true;
@@ -30,7 +31,7 @@ export function EnumPicker({ type, value, nullable, label, autoOpen = false, dis
30
31
  const handleOpenChange = (open) => {
31
32
  if (open) {
32
33
  settled.current = false;
33
- setMembers(new Set(set && value !== null ? membersOf(value) : []));
34
+ setMembers(membersOfValue());
34
35
  return;
35
36
  }
36
37
  const joined = joinMembers(type, members);
@@ -1,4 +1,4 @@
1
- import type { SortDirection } from '../sql.ts';
1
+ import { type SortDirection } from '../sql.ts';
2
2
  import type { ColumnClick } from './column-selection.ts';
3
3
  import type { GridColumn } from './types.ts';
4
4
  export interface HeaderActions {
@@ -8,7 +8,7 @@ import { formatNumber } from '@adecore/ui/format';
8
8
  import { CODE_TEXT } from '../code-text.js';
9
9
  import { keyLabelOf } from '../column-keys.js';
10
10
  import { KeyIcon } from '../KeyIcon.js';
11
- import { isNumericKind } from './display.js';
11
+ import { isNumericKind } from '../sql.js';
12
12
  import { isDoubleClick, MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
13
13
  /* The items of the menu a header opens, from its button and from a right click. */
14
14
  function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions }) {
@@ -33,7 +33,7 @@ export function GridHeaderCell({ column, index, position, width, sortable, selec
33
33
  const { t } = useTranslation('database');
34
34
  const cell = useRef(null);
35
35
  const lastPress = useRef(null);
36
- /* Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column. */
36
+ // Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column.
37
37
  const resized = useRef(false);
38
38
  const { startResize } = useColumnResize(cell, {
39
39
  size: width,
@@ -8,7 +8,8 @@ import { formatNumber } from '@adecore/ui/format';
8
8
  import { useNumberNotation } from '../client-context.js';
9
9
  import { CODE_TEXT } from '../code-text.js';
10
10
  import { ValueEditor } from '../value/ValueEditor.js';
11
- import { cellView, copyTextOf, isNumericKind, isPreview, numeralText } from './display.js';
11
+ import { isNumericKind } from '../sql.js';
12
+ import { cellView, copyTextOf, isPreview, numeralText } from './display.js';
12
13
  import { draftOf, parseDraft } from './edit-value.js';
13
14
  import { parseEnumType } from './enum-type.js';
14
15
  import { EnumPicker } from './EnumPicker.js';
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { ColumnResizeHandle, useColumnResize } from '@adecore/ui';
5
+ import { assignRef } from '../assign-ref.js';
5
6
  export const VALUE_PANEL_WIDTH = 320;
6
7
  export const VALUE_PANEL_MIN_WIDTH = 240;
7
8
  /* A grid with a panel along its right edge that a person can widen by its left edge. */
@@ -24,11 +25,6 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
24
25
  });
25
26
  return (_jsxs("div", { ref: (node) => {
26
27
  root.current = node;
27
- if (typeof ref === 'function') {
28
- ref(node);
29
- }
30
- else if (ref) {
31
- ref.current = node;
32
- }
28
+ assignRef(ref, node);
33
29
  }, className: clsx('flex min-h-0 min-w-0 flex-1', className), children: [_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: children }), open && (_jsxs("div", { ref: dock, className: "relative flex min-h-0 shrink-0 border-l border-border", style: { width }, children: [_jsx(ColumnResizeHandle, { from: "right", onPointerDown: startResize, className: "hover:bg-border-strong" }), panel] }))] }));
34
30
  }
@@ -1,5 +1,4 @@
1
- const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
2
- const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
1
+ import { isNumericKind, NUMERIC_TEXT } from '../sql.js';
3
2
  const numberOf = (cell) => {
4
3
  if (typeof cell === 'number') {
5
4
  return Number.isFinite(cell) ? { value: cell, text: String(cell) } : null;
@@ -30,7 +29,7 @@ export const aggregateBlock = (rows, kinds, block) => {
30
29
  for (const column of block.columns) {
31
30
  count++;
32
31
  const kind = kinds[column];
33
- const found = kind !== undefined && NUMERIC_KINDS.has(kind) ? numberOf(cells[column] ?? null) : null;
32
+ const found = kind !== undefined && isNumericKind(kind) ? numberOf(cells[column] ?? null) : null;
34
33
  if (found === null) {
35
34
  continue;
36
35
  }
@@ -33,10 +33,11 @@ export const captureLayout = (columns, state) => {
33
33
  widths[column.name] = width;
34
34
  }
35
35
  }
36
+ const namesOf = (indexes) => indexes.flatMap((index) => columns[index]?.name ?? []);
36
37
  return {
37
38
  widths,
38
- hidden: [...state.view.hidden].sort((a, b) => a - b).flatMap((index) => (columns[index] === undefined ? [] : [columns[index].name])),
39
- pinned: state.view.pinned.flatMap((index) => (columns[index] === undefined ? [] : [columns[index].name]))
39
+ hidden: namesOf([...state.view.hidden].sort((a, b) => a - b)),
40
+ pinned: namesOf(state.view.pinned)
40
41
  };
41
42
  };
42
43
  const sameList = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
@@ -1,7 +1,6 @@
1
- import { qualifiedName, quoteIdentifier, sqlLiteral } from '../sql.js';
1
+ import { isNumericKind, qualifiedName, quoteIdentifier, sqlLiteral } from '../sql.js';
2
2
  import { copyTextOf } from './display.js';
3
3
  export const COPY_FORMATS = ['tsv', 'csv', 'json', 'sql'];
4
- const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
5
4
  const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
6
5
  const quoteField = (text) => `"${text.replaceAll('"', '""')}"`;
7
6
  /* Tabs and line breaks would split a cell on paste, so a cell that holds one is quoted the way a spreadsheet does. */
@@ -22,7 +21,7 @@ const jsonOf = (cell, kind) => {
22
21
  return Number.isFinite(cell) ? String(cell) : 'null';
23
22
  }
24
23
  if (typeof cell === 'string') {
25
- if (NUMERIC_KINDS.has(kind) && JSON_NUMBER.test(cell)) {
24
+ if (isNumericKind(kind) && JSON_NUMBER.test(cell)) {
26
25
  return cell;
27
26
  }
28
27
  if (kind === 'json') {
@@ -12,7 +12,6 @@ export interface CellView {
12
12
  export type NumberNotation = 'database' | 'region';
13
13
  export declare const DISPLAY_LIMIT = 200;
14
14
  export declare const BINARY_PREVIEW_BYTES = 8;
15
- export declare const isNumericKind: (kind: ValueKind) => boolean;
16
15
  export declare const isPreview: (cell: Shown) => boolean;
17
16
  export declare const numeralText: (numeral: string | number, notation: NumberNotation) => string;
18
17
  export declare const cellView: (cell: Shown, kind: ValueKind, notation?: NumberNotation) => CellView;
@@ -1,11 +1,9 @@
1
1
  import { formatBytes, formatNumeral } from '@adecore/ui/format';
2
+ import { isNumericKind } from '../sql.js';
2
3
  /* A cell never draws more than this, however long the text: the DOM stays small and the cell is one line anyway. */
3
4
  export const DISPLAY_LIMIT = 200;
4
5
  /* How many bytes of a binary value the cell spells out. */
5
6
  export const BINARY_PREVIEW_BYTES = 8;
6
- const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
7
- /* Whether the cells of a column of this kind sit against the end of their cell, as numbers do. */
8
- export const isNumericKind = (kind) => NUMERIC_KINDS.has(kind);
9
7
  const ELLIPSIS = '…';
10
8
  const isObject = (cell) => cell !== null && typeof cell === 'object';
11
9
  /* Whether a cell is only the start of its value, so editing it needs the whole value first. */
@@ -63,18 +63,13 @@ export const columnOffsets = (widths) => {
63
63
  /* Wide enough for the largest row number, never narrower than `GUTTER_MIN_WIDTH`. */
64
64
  export const gutterWidth = (rowCount) => Math.max(GUTTER_MIN_WIDTH, String(Math.max(rowCount, 1)).length * CHARACTER_WIDTH + CELL_PADDING);
65
65
  export const clampColumnWidth = (width) => Math.max(MIN_COLUMN_WIDTH, Math.min(Math.ceil(width), MAX_COLUMN_WIDTH));
66
- /* From the longest text among the header and the first rows. `keyed` makes room for the key icon in front of the name. */
67
- export const estimateColumnWidth = (header, samples, keyed) => {
68
- const body = samples.slice(0, SAMPLE_ROWS).reduce((longest, text) => Math.max(longest, text.length), 0);
69
- const named = header.length * CHARACTER_WIDTH + (keyed ? KEY_ICON_ROOM : 0) + SORT_ROOM;
70
- return clampColumnWidth(Math.max(body * CHARACTER_WIDTH, named) + CELL_PADDING);
71
- };
66
+ const bodyWidth = (texts) => texts.reduce((longest, text) => Math.max(longest, text.length), 0) * CHARACTER_WIDTH;
67
+ /* `keyed` makes room for the key icon in front of the name. */
68
+ const headerWidth = (header, keyed) => header.length * CHARACTER_WIDTH + (keyed ? KEY_ICON_ROOM : 0) + SORT_ROOM;
69
+ /* From the longest text among the header and the first rows. */
70
+ export const estimateColumnWidth = (header, samples, keyed) => clampColumnWidth(Math.max(bodyWidth(samples.slice(0, SAMPLE_ROWS)), headerWidth(header, keyed)) + CELL_PADDING);
72
71
  /* Wide enough for the header and every text of the column, up to `MAX_FIT_WIDTH`. */
73
- export const fitColumnWidth = (header, texts, keyed) => {
74
- const body = texts.reduce((longest, text) => Math.max(longest, text.length), 0) * CHARACTER_WIDTH;
75
- const named = header.length * CHARACTER_WIDTH + (keyed ? KEY_ICON_ROOM : 0) + SORT_ROOM;
76
- return Math.max(MIN_COLUMN_WIDTH, Math.min(Math.ceil(Math.max(body, named) + CELL_PADDING), MAX_FIT_WIDTH));
77
- };
72
+ export const fitColumnWidth = (header, texts, keyed) => Math.max(MIN_COLUMN_WIDTH, Math.min(Math.ceil(Math.max(bodyWidth(texts), headerWidth(header, keyed)) + CELL_PADDING), MAX_FIT_WIDTH));
78
73
  export const isDoubleClick = (previous, now) => previous !== null && now - previous <= DOUBLE_CLICK_MS;
79
74
  /*
80
75
  * The estimated widths of a new page, never narrower than they were. A page with longer content widens
@@ -1,6 +1,14 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import { dirname, join, sep } from 'node:path';
4
+ /*
5
+ * Finds the prebuilt helper that npm installed for this machine, in the `@adecore/database-<platform>-<arch>`
6
+ * package, or null when there is none: an unsupported platform, or an install with optional dependencies off.
7
+ *
8
+ * A binary cannot run from inside an Electron archive. The path is mapped from `app.asar` to
9
+ * `app.asar.unpacked`, so the app must unpack the packages (electron-builder: `asarUnpack` with
10
+ * `node_modules/@adecore/database-*` followed by `/**`).
11
+ */
4
12
  export const helperPath = ({ platform = process.platform, arch = process.arch, resolve = (specifier) => createRequire(import.meta.url).resolve(specifier), exists = existsSync } = {}) => {
5
13
  let manifest;
6
14
  try {
package/dist/host/host.js CHANGED
@@ -3,7 +3,6 @@ import { parseRequest } from './validate.js';
3
3
  const DEFAULT_READY_TIMEOUT_MS = 10000;
4
4
  /* How long a release or a dispose waits for a helper to answer its `close` requests. */
5
5
  const CLOSE_GRACE_MS = 2000;
6
- /* A failure with the code the caller gets. */
7
6
  class Failure extends Error {
8
7
  code;
9
8
  constructor(code, message) {
@@ -13,6 +12,15 @@ class Failure extends Error {
13
12
  }
14
13
  const failure = (id, code, message) => ({ id, ok: false, error: { code, message } });
15
14
  const messageOf = (error) => (error instanceof Error ? error.message : String(error));
15
+ /* `undefined` for a line that is not JSON. */
16
+ const parseLine = (line) => {
17
+ try {
18
+ return JSON.parse(line);
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
16
24
  const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
17
25
  /* Settles when every promise has, or after the grace period; a helper that hangs must not hold up an app that quits. */
18
26
  const settleWithin = async (promises) => {
@@ -72,14 +80,7 @@ export const createDatabaseHost = (options) => {
72
80
  retire(run, message);
73
81
  };
74
82
  const onHandshake = (run, line) => {
75
- let message;
76
- try {
77
- message = JSON.parse(line);
78
- }
79
- catch {
80
- abort(run, 'The helper did not start with a ready line.');
81
- return;
82
- }
83
+ const message = parseLine(line);
83
84
  if (!isDict(message) || message.event !== 'ready') {
84
85
  abort(run, 'The helper did not start with a ready line.');
85
86
  return;
@@ -93,13 +94,7 @@ export const createDatabaseHost = (options) => {
93
94
  run.resolveReady();
94
95
  };
95
96
  const onResponse = (run, line) => {
96
- let message;
97
- try {
98
- message = JSON.parse(line);
99
- }
100
- catch {
101
- return;
102
- }
97
+ const message = parseLine(line);
103
98
  if (!isDict(message) || typeof message.id !== 'string') {
104
99
  return;
105
100
  }