@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.
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 +246 -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
@@ -1,6 +1,6 @@
1
- import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from 'react';
1
+ import { useEffect, useRef, useState, type KeyboardEvent, type MouseEvent, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { CircleAlert, Database, Eye, Folder, KeyRound, Link2, Lock, RectangleVertical, Search, Table } from 'lucide-react';
3
+ import { CircleAlert, Columns2, Database, Eye, Folder, KeyRound, ListOrdered, Lock, Search, ShieldCheck, Table, Zap, type LucideIcon } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip, Tree } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
@@ -8,6 +8,9 @@ import type { ExplorerSelection } from '../actions.ts';
8
8
  import { useDatabaseAction, useDatabaseClient } from '../client-context.ts';
9
9
  import type { Connection } from '../client/types.ts';
10
10
  import { EngineIcon } from '../connections/EngineIcon.tsx';
11
+ import { keyLabelOf, type ColumnKeys } from '../column-keys.ts';
12
+ import { KeyIcon } from '../KeyIcon.tsx';
13
+ import type { ExplorerFolder } from './folders.ts';
11
14
  import { RowMenu } from './RowMenu.tsx';
12
15
  import { TableDialog, type TableRequest } from './TableDialog.tsx';
13
16
  import {
@@ -15,14 +18,17 @@ import {
15
18
  flattenTree,
16
19
  hasMenu,
17
20
  isExpandable,
18
- isFolderKey,
21
+ startsOpen,
19
22
  isSelectable,
20
23
  navigate,
21
24
  neededLoads,
22
25
  selectionKey,
23
26
  selectionOf,
24
27
  tabStop,
28
+ openedTableOf,
29
+ tableKindIn,
25
30
  tableOf,
31
+ type EntryRow,
26
32
  type ErrorRow,
27
33
  type LoadTarget,
28
34
  type ExpandableRow,
@@ -39,15 +45,30 @@ export interface DatabaseExplorerProps {
39
45
  defaultValue?: ExplorerSelection | null;
40
46
  onValueChange?(selection: ExplorerSelection | null): void;
41
47
  showSystemSchemas?: boolean;
48
+ /* Opens a table on a click too, with `preview: true`; a double click or Enter then sends `preview: false`. */
49
+ openOnClick?: boolean;
50
+ /* The app's own folders under each connection, by connection id, after its schemas. */
51
+ folders?: Readonly<Record<string, readonly ExplorerFolder[]>>;
42
52
  className?: string;
43
53
  ref?: Ref<HTMLDivElement>;
44
54
  }
45
55
 
46
56
  /*
47
57
  * The connections, their schemas and their tables as a tree, loaded as each node opens. Every row
48
- * selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`.
58
+ * selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`,
59
+ * and on an item of a folder the app added opens it through the item's own `onOpen`.
49
60
  */
50
- export function DatabaseExplorer({ connections, value, defaultValue = null, onValueChange, showSystemSchemas = false, className, ref }: DatabaseExplorerProps) {
61
+ export function DatabaseExplorer({
62
+ connections,
63
+ value,
64
+ defaultValue = null,
65
+ onValueChange,
66
+ showSystemSchemas = false,
67
+ openOnClick = false,
68
+ folders,
69
+ className,
70
+ ref
71
+ }: DatabaseExplorerProps) {
51
72
  const { t } = useTranslation('database');
52
73
  const client = useDatabaseClient();
53
74
  const act = useDatabaseAction();
@@ -71,7 +92,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
71
92
  schemas: loads.schemas,
72
93
  tables: loads.tables,
73
94
  structures: loads.structures,
74
- versions: loads.versions
95
+ versions: loads.versions,
96
+ folders: (connectionId: string) => folders?.[connectionId] ?? []
75
97
  };
76
98
  const rows = flattenTree({ ...input, filter });
77
99
  const stop = tabStop(rows, activeKey, selectedKey);
@@ -90,8 +112,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
90
112
  }, [activeKey]);
91
113
 
92
114
  const setOpen = (key: string, open: boolean): void => {
93
- // A folder is open until the person closes it, everything else is closed until they open it.
94
- const folder = isFolderKey(key);
115
+ const folder = startsOpen(key);
95
116
  const toggle = (current: ReadonlySet<string>): ReadonlySet<string> => {
96
117
  const next = new Set(current);
97
118
  if (open !== folder) {
@@ -122,23 +143,34 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
122
143
  }
123
144
  };
124
145
 
125
- const open = (row: TreeRow): void => {
126
- const ref = tableOf(row);
127
- if (ref !== null) {
128
- pick(row);
129
- act?.({ kind: 'open-table', ref, view: 'data' });
146
+ /* `preview` says a click opened it, which only an explorer that opens on a click tells the app. */
147
+ const open = (row: TreeRow, preview = false): void => {
148
+ if (row.kind === 'appItem') {
149
+ row.item.onOpen?.({ preview });
150
+ return;
151
+ }
152
+ const ref = openedTableOf(row);
153
+ if (ref === null) {
154
+ return;
130
155
  }
156
+ pick(row);
157
+ const tableKind = row.kind === 'table' && row.ref === ref ? row.table.kind : tableKindIn(loads.tables(ref.connectionId, ref.schema), ref.table);
158
+ act?.({ kind: 'open-table', ref, view: 'data', ...(tableKind === undefined ? {} : { tableKind }), ...(openOnClick ? { preview } : {}) });
131
159
  };
132
160
 
133
161
  const activate = (row: TreeRow): void => {
134
162
  switch (row.kind) {
135
163
  case 'table':
136
164
  case 'column':
165
+ case 'entry':
166
+ case 'appItem':
137
167
  open(row);
138
168
  break;
139
169
  case 'connection':
140
170
  case 'schema':
141
171
  case 'folder':
172
+ case 'part':
173
+ case 'appFolder':
142
174
  pick(row);
143
175
  toggle(row);
144
176
  break;
@@ -170,11 +202,18 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
170
202
  }
171
203
  };
172
204
 
173
- const click = (row: TreeRow): void => {
205
+ /* `count` is the click's `detail`: the second click of a double click leaves the opening to the double click. */
206
+ const click = (row: TreeRow, count: number): void => {
174
207
  if (pressedInPopup()) {
175
208
  return;
176
209
  }
177
210
  setActiveKey(row.key);
211
+ if (openOnClick && (row.kind === 'table' || row.kind === 'appItem')) {
212
+ if (count <= 1) {
213
+ open(row, true);
214
+ }
215
+ return;
216
+ }
178
217
  pick(row);
179
218
  if (row.kind !== 'table' && isExpandable(row)) {
180
219
  toggle(row);
@@ -211,7 +250,9 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
211
250
  const renderRow = (row: TreeRow, index: number): ReactNode => {
212
251
  const connection = connectionOf(row);
213
252
  const menu =
214
- connection !== undefined && hasMenu(row) ? (
253
+ row.kind === 'appItem' ? (
254
+ (row.item.menu ?? null)
255
+ ) : connection !== undefined && hasMenu(row) ? (
215
256
  <RowMenu row={row} connection={connection} onRefresh={refresh} onDisconnect={disconnect} onRequest={setRequest} />
216
257
  ) : null;
217
258
  const selectedRow = isSelected(row);
@@ -234,8 +275,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
234
275
  rowElements.current.set(row.key, element);
235
276
  }
236
277
  },
237
- onClick: () => click(row),
238
- onDoubleClick: tableOf(row) === null ? undefined : () => open(row),
278
+ onClick: (event: MouseEvent) => click(row, event.detail),
279
+ onDoubleClick: tableOf(row) === null && row.kind !== 'appItem' ? undefined : () => open(row),
239
280
  onFocus: () => {
240
281
  if (row.focusable) {
241
282
  setActiveKey(row.key);
@@ -282,7 +323,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
282
323
  ) : rows.length === 0 ? (
283
324
  <EmptyState icon={Search}>{t('explorer.noMatches', { query: filter.trim() })}</EmptyState>
284
325
  ) : (
285
- <Tree.Root aria-label={t('explorer.label')} className="min-h-0 grow overflow-y-auto py-2" onKeyDown={onKeyDown}>
326
+ <Tree.Root aria-label={t('explorer.label')} overflow="scroll" className="min-h-0 grow overflow-y-auto pt-2" onKeyDown={onKeyDown}>
286
327
  {rows.map(renderRow)}
287
328
  </Tree.Root>
288
329
  )}
@@ -307,7 +348,7 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
307
348
  </span>
308
349
  </Tooltip>
309
350
  )}
310
- {row.version !== null && <span className="min-w-0 shrink-[2] truncate text-text-faint">{row.version}</span>}
351
+ {row.version !== null && <span className="text-text-faint">{row.version}</span>}
311
352
  {row.schemaCount !== null && <SchemaCount shown={row.schemaCount.shown} total={row.schemaCount.total} />}
312
353
  </>
313
354
  );
@@ -336,13 +377,41 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
336
377
  <Tree.Label>{row.table.name}</Tree.Label>
337
378
  </>
338
379
  );
380
+ case 'part':
381
+ return (
382
+ <>
383
+ <Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
384
+ <Icon icon={Folder} size={16} className="shrink-0 text-text-muted" />
385
+ <Tree.Label>{t(`explorer.parts.${row.part}`)}</Tree.Label>
386
+ <span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
387
+ </>
388
+ );
389
+ case 'appFolder':
390
+ return (
391
+ <>
392
+ <Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
393
+ <Icon icon={Folder} size={16} className="shrink-0 text-text-muted" />
394
+ <Tree.Label>{row.folder.label}</Tree.Label>
395
+ <span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
396
+ </>
397
+ );
398
+ case 'appItem':
399
+ return (
400
+ <>
401
+ <Tree.ChevronSlot />
402
+ <Icon icon={row.item.icon} size={16} className="shrink-0 text-text-muted" />
403
+ <Tree.Label>{row.item.label}</Tree.Label>
404
+ </>
405
+ );
406
+ case 'entry':
407
+ return <EntryContent row={row} />;
339
408
  case 'column':
340
409
  return (
341
410
  <>
342
411
  <Tree.ChevronSlot />
343
412
  <ColumnIcon primaryKey={row.primaryKey} foreignKey={row.foreignKey} />
344
413
  <Tree.Label>{row.column.name}</Tree.Label>
345
- {row.column.type !== '' && <span className="min-w-0 shrink-[2] truncate text-text-faint">{row.column.type}</span>}
414
+ {row.column.type !== '' && <span className="text-text-faint">{row.column.type}</span>}
346
415
  </>
347
416
  );
348
417
  case 'loading':
@@ -377,20 +446,95 @@ function SchemaCount({ shown, total }: { shown: number; total: number }) {
377
446
  );
378
447
  }
379
448
 
380
- function ColumnIcon({ primaryKey, foreignKey }: { primaryKey: boolean; foreignKey: boolean }) {
449
+ /* A step smaller than the icons of the rows above, in the same 16px slot, so the names stay in line. */
450
+ const ENTRY_ICON_SLOT = 'inline-flex size-4 shrink-0 items-center justify-center';
451
+
452
+ function ColumnIcon(keys: ColumnKeys) {
381
453
  const { t } = useTranslation('database');
382
- if (!primaryKey && !foreignKey) {
383
- return <Icon icon={RectangleVertical} size={16} className="shrink-0 text-text-faint" />;
454
+ if (!keys.primaryKey && !keys.foreignKey) {
455
+ return (
456
+ <span className={ENTRY_ICON_SLOT}>
457
+ <Icon icon={Columns2} size={14} className="text-text-faint" />
458
+ </span>
459
+ );
384
460
  }
385
461
  return (
386
- <Tooltip label={t(primaryKey ? 'explorer.primaryKey' : 'explorer.foreignKey')}>
387
- <span className="inline-flex shrink-0 text-text-muted">
388
- <Icon icon={primaryKey ? KeyRound : Link2} size={16} />
462
+ <Tooltip label={t(keyLabelOf(keys))}>
463
+ <span className={ENTRY_ICON_SLOT}>
464
+ <KeyIcon {...keys} size={14} />
389
465
  </span>
390
466
  </Tooltip>
391
467
  );
392
468
  }
393
469
 
470
+ /* A key, a foreign key, an index, a check or a trigger of a table: its name, and what it covers in the quiet color of a column's type. */
471
+ function EntryContent({ row }: { row: EntryRow }) {
472
+ const { t } = useTranslation('database');
473
+ const { entry } = row;
474
+ const icon = (glyph: LucideIcon, className: string): ReactNode => (
475
+ <span className={ENTRY_ICON_SLOT}>
476
+ <Icon icon={glyph} size={14} className={className} />
477
+ </span>
478
+ );
479
+ switch (entry.type) {
480
+ case 'key':
481
+ return (
482
+ <>
483
+ <Tree.ChevronSlot />
484
+ {icon(KeyRound, entry.primary ? 'text-(--file-icon-yellow)' : 'text-text-muted')}
485
+ <Tree.Label>{entry.name ?? t('explorer.primaryKey')}</Tree.Label>
486
+ <span className="text-text-faint">{entry.columns.join(', ')}</span>
487
+ </>
488
+ );
489
+ case 'foreignKey': {
490
+ const { foreignKey } = entry;
491
+ const columns = foreignKey.columns.join(', ');
492
+ return (
493
+ <>
494
+ <Tree.ChevronSlot />
495
+ {icon(KeyRound, 'text-(--file-icon-blue)')}
496
+ <Tree.Label>{foreignKey.name ?? columns}</Tree.Label>
497
+ <span className="text-text-faint">
498
+ {t('explorer.references', {
499
+ columns,
500
+ table: foreignKey.referencedTable,
501
+ referenced: foreignKey.referencedColumns.join(', ')
502
+ })}
503
+ </span>
504
+ </>
505
+ );
506
+ }
507
+ case 'index':
508
+ return (
509
+ <>
510
+ <Tree.ChevronSlot />
511
+ {icon(ListOrdered, 'text-text-faint')}
512
+ <Tree.Label>{entry.index.name}</Tree.Label>
513
+ <span className="text-text-faint">{entry.index.columns.join(', ')}</span>
514
+ {entry.index.unique && <span className="text-text-faint">{t('explorer.unique')}</span>}
515
+ </>
516
+ );
517
+ case 'check':
518
+ return (
519
+ <>
520
+ <Tree.ChevronSlot />
521
+ {icon(ShieldCheck, 'text-text-faint')}
522
+ <Tree.Label>{entry.check.name ?? t('explorer.check')}</Tree.Label>
523
+ <span className="font-mono text-text-faint">{entry.check.expression}</span>
524
+ </>
525
+ );
526
+ case 'trigger':
527
+ return (
528
+ <>
529
+ <Tree.ChevronSlot />
530
+ {icon(Zap, 'text-text-faint')}
531
+ <Tree.Label>{entry.trigger.name}</Tree.Label>
532
+ <span className="text-text-faint">{`${entry.trigger.timing} ${entry.trigger.event}`}</span>
533
+ </>
534
+ );
535
+ }
536
+ }
537
+
394
538
  function ErrorContent({ row }: { row: ErrorRow }) {
395
539
  const { t } = useTranslation('database');
396
540
  return (
@@ -66,8 +66,8 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest }:
66
66
  const request = (change: TableRequest['change']) => (): void => onRequest({ change, connection, ref, kind });
67
67
  return [
68
68
  [
69
- actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data' }),
70
- actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure' }),
69
+ actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data', tableKind: kind }),
70
+ actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure', tableKind: kind }),
71
71
  kind === 'table' && writable ? actionItem('edit-table', PencilRuler, t('explorer.editTable'), { kind: 'edit-table', ref }) : null,
72
72
  actionItem('console', SquareTerminal, t('explorer.newConsoleHere'), consoleAction(ref.schema, selectAllSql(connection.config.engine, ref)))
73
73
  ],
@@ -1,11 +1,11 @@
1
- import { connectionOfKey, isFolderKey } from './tree.ts';
1
+ import { connectionOfKey, startsOpen } from './tree.ts';
2
2
 
3
3
  const VERSION = 1;
4
4
 
5
5
  export interface Expansion {
6
- /* Connections, schemas and tables the person opened. */
6
+ /* Connections, schemas, tables and the folders of a table the person opened. */
7
7
  readonly expanded: ReadonlySet<string>;
8
- /* Folders the person closed, since a folder is open until then. */
8
+ /* What starts open and the person closed: the folders of a schema and the columns of a table. */
9
9
  readonly collapsed: ReadonlySet<string>;
10
10
  }
11
11
 
@@ -28,7 +28,7 @@ export const parseExpansion = (raw: string | null, connectionId: string): Expans
28
28
  Array.isArray(keys)
29
29
  ? keys.filter(
30
30
  (key): key is string =>
31
- typeof key === 'string' && key.length > 2 && isFolderKey(key) === folders && connectionOfKey(key) === connectionId
31
+ typeof key === 'string' && key.length > 2 && startsOpen(key) === folders && connectionOfKey(key) === connectionId
32
32
  )
33
33
  : []
34
34
  );
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+
4
+ /* Something of the app's own under a connection, such as a file that belongs to it. */
5
+ export interface ExplorerItem {
6
+ /* Unique in its folder; it names the row. */
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly icon: LucideIcon;
10
+ /*
11
+ * A double click or Enter, with `preview: false`. With `openOnClick` a click opens it too, with
12
+ * `preview: true`, the way a table opens.
13
+ */
14
+ onOpen?(options: { readonly preview: boolean }): void;
15
+ /* The items of its context menu, as `ContextMenu.Item`s. Without them the row has no menu. */
16
+ menu?: ReactNode;
17
+ }
18
+
19
+ /* A folder of the app's own under a connection, after its schemas. It shows only while it holds items. */
20
+ export interface ExplorerFolder {
21
+ /* Unique among the folders of a connection; it keeps the folder open or closed across a remount. */
22
+ readonly id: string;
23
+ readonly label: string;
24
+ readonly items: readonly ExplorerItem[];
25
+ }