@adecore/database 0.17.0-beta.2 → 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
@@ -0,0 +1,79 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { foreignKeyOf, referenceOf } from '../table/references.js';
3
+ const tableKey = ({ schema, table }) => `${schema}\u0000${table}`;
4
+ /* The tables the columns of a result come from, each once, in the order they first appear. */
5
+ export const sourceTablesOf = (columns) => {
6
+ const tables = new Map();
7
+ for (const { source } of columns) {
8
+ if (source !== undefined && !tables.has(tableKey(source))) {
9
+ tables.set(tableKey(source), { schema: source.schema, table: source.table });
10
+ }
11
+ }
12
+ return [...tables.values()];
13
+ };
14
+ /*
15
+ * The keys of a result's columns, from the structures of the tables they come from by key (`schema`, NUL,
16
+ * `table`). A column without a source, or of a table with no structure, is marked as nothing.
17
+ */
18
+ export const resultKeysOf = (engine, columns, structures) => {
19
+ const structureOf = (column) => column?.source === undefined ? undefined : structures.get(tableKey(column.source));
20
+ const foreignKeyAt = (column) => {
21
+ const source = columns[column]?.source;
22
+ const structure = structureOf(columns[column]);
23
+ return source === undefined || structure === undefined ? undefined : foreignKeyOf(structure.foreignKeys, source.column);
24
+ };
25
+ return {
26
+ columns: columns.map((column) => {
27
+ const structure = structureOf(column);
28
+ const name = column.source?.column;
29
+ if (structure === undefined || name === undefined) {
30
+ return column;
31
+ }
32
+ return {
33
+ ...column,
34
+ primaryKey: structure.primaryKey.includes(name),
35
+ foreignKey: structure.foreignKeys.some((key) => key.columns.includes(name))
36
+ };
37
+ }),
38
+ isReference: (column) => foreignKeyAt(column) !== undefined,
39
+ referenceAt: (column, cells) => {
40
+ const key = foreignKeyAt(column);
41
+ const source = columns[column]?.source;
42
+ if (key === undefined || source === undefined) {
43
+ return null;
44
+ }
45
+ // The columns of the result as the columns of that table, so a join names each key column of its own table only.
46
+ const asTable = columns.map((entry) => ({
47
+ name: entry.source !== undefined && tableKey(entry.source) === tableKey(source) ? entry.source.column : '',
48
+ kind: entry.kind
49
+ }));
50
+ return referenceOf(engine, key, asTable, cells);
51
+ }
52
+ };
53
+ };
54
+ /*
55
+ * The keys of a result set, once the structures of the tables its columns come from have loaded: each table
56
+ * once per result, through the console's session. `null` while they load and for what is no result set.
57
+ */
58
+ export function useResultKeys(session, engine, result) {
59
+ const [loaded, setLoaded] = useState(null);
60
+ useEffect(() => {
61
+ if (session === null || result?.kind !== 'rows') {
62
+ return undefined;
63
+ }
64
+ const tables = sourceTablesOf(result.columns);
65
+ if (tables.length === 0) {
66
+ return undefined;
67
+ }
68
+ const controller = new AbortController();
69
+ // A table the server named but cannot describe, such as a derived one, marks no column.
70
+ const loads = tables.map((table) => session.structure(table.schema, table.table, { signal: controller.signal }).then((structure) => [tableKey(table), structure], () => null));
71
+ void Promise.all(loads).then((entries) => {
72
+ if (!controller.signal.aborted) {
73
+ setLoaded({ result, structures: new Map(entries.filter((entry) => entry !== null)) });
74
+ }
75
+ });
76
+ return () => controller.abort();
77
+ }, [session, result]);
78
+ return useMemo(() => (engine === undefined || result?.kind !== 'rows' || loaded?.result !== result ? null : resultKeysOf(engine, result.columns, loaded.structures)), [engine, result, loaded]);
79
+ }
@@ -3,7 +3,7 @@ import { useId } from 'react';
3
3
  import clsx from 'clsx';
4
4
  import { ArrowDown, ArrowUp, KeyRound, Plus, Trash2 } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { Button, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
6
+ import { Button, ButtonGroup, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
7
7
  import { formatNumber } from '@adecore/ui/format';
8
8
  import { typeSuggestionsOf } from '../ddl/index.js';
9
9
  import { addColumn, moveColumn, patchColumn, removeColumn, togglePrimaryKey } from './edit.js';
@@ -19,6 +19,6 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
19
19
  const keyAt = draft.primaryKey.indexOf(column.name);
20
20
  const locked = disabled || column.generated;
21
21
  const patch = (change) => onChange(patchColumn(draft, column.key, change));
22
- return (_jsxs("tr", { children: [_jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs("span", { className: "flex", children: [_jsx(IconButton, { icon: ArrowUp, size: "xs", label: t('designer.columns.moveUp'), disabled: disabled || at === 0, onClick: () => onChange(moveColumn(draft, column.key, -1)) }), _jsx(IconButton, { icon: ArrowDown, size: "xs", label: t('designer.columns.moveDown'), disabled: disabled || at === draft.columns.length - 1, onClick: () => onChange(moveColumn(draft, column.key, 1)) })] }) }), _jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.name'), spellCheck: false, value: column.name, disabled: locked, onChange: (event) => patch({ name: event.target.value }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: _jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.type'), list: listId, spellCheck: false, value: column.type, disabled: locked, onChange: (event) => patch({ type: event.target.value }) }) }), _jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.notNullFor', { name: column.name }), checked: !column.nullable, disabled: locked, onCheckedChange: (checked) => patch({ nullable: !checked }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: column.generated ? (_jsx(Pill, { mono: true, children: column.generatedClause ?? t('designer.columns.generated') })) : (_jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.default'), placeholder: t('designer.columns.noDefault'), spellCheck: false, value: column.defaultValue ?? '', disabled: disabled, onChange: (event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value }) })) }), mysql && (_jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.autoIncrementFor', { name: column.name }), checked: column.autoIncrement, disabled: locked, onCheckedChange: (checked) => patch({ autoIncrement: checked }) }) })), _jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: KeyRound, size: "xs", label: keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey'), "aria-pressed": keyAt >= 0, disabled: locked, onClick: () => onChange(togglePrimaryKey(draft, column.key)) }), keyAt >= 0 && draft.primaryKey.length > 1 && (_jsx("span", { className: "text-xs text-text-muted", children: formatNumber(keyAt + 1) }))] }) }), mysql && (_jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.comment'), value: column.comment, disabled: locked, onChange: (event) => patch({ comment: event.target.value }) }) })), _jsx("td", { className: TD, children: _jsx(IconButton, { icon: Trash2, size: "xs", label: t('designer.columns.remove'), disabled: disabled, onClick: () => onChange(removeColumn(draft, column.key)) }) })] }, column.key));
22
+ return (_jsxs("tr", { children: [_jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs(ButtonGroup, { children: [_jsx(IconButton, { icon: ArrowUp, size: "xs", label: t('designer.columns.moveUp'), disabled: disabled || at === 0, onClick: () => onChange(moveColumn(draft, column.key, -1)) }), _jsx(IconButton, { icon: ArrowDown, size: "xs", label: t('designer.columns.moveDown'), disabled: disabled || at === draft.columns.length - 1, onClick: () => onChange(moveColumn(draft, column.key, 1)) })] }) }), _jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.name'), spellCheck: false, value: column.name, disabled: locked, onChange: (event) => patch({ name: event.target.value }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: _jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.type'), list: listId, spellCheck: false, value: column.type, disabled: locked, onChange: (event) => patch({ type: event.target.value }) }) }), _jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.notNullFor', { name: column.name }), checked: !column.nullable, disabled: locked, onCheckedChange: (checked) => patch({ nullable: !checked }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: column.generated ? (_jsx(Pill, { mono: true, children: column.generatedClause ?? t('designer.columns.generated') })) : (_jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.default'), placeholder: t('designer.columns.noDefault'), spellCheck: false, value: column.defaultValue ?? '', disabled: disabled, onChange: (event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value }) })) }), mysql && (_jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.autoIncrementFor', { name: column.name }), checked: column.autoIncrement, disabled: locked, onCheckedChange: (checked) => patch({ autoIncrement: checked }) }) })), _jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: KeyRound, size: "xs", label: keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey'), "aria-pressed": keyAt >= 0, disabled: locked, onClick: () => onChange(togglePrimaryKey(draft, column.key)) }), keyAt >= 0 && draft.primaryKey.length > 1 && (_jsx("span", { className: "text-xs text-text-muted", children: formatNumber(keyAt + 1) }))] }) }), mysql && (_jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.comment'), value: column.comment, disabled: locked, onChange: (event) => patch({ comment: event.target.value }) }) })), _jsx("td", { className: TD, children: _jsx(IconButton, { icon: Trash2, size: "xs", label: t('designer.columns.remove'), disabled: disabled, onClick: () => onChange(removeColumn(draft, column.key)) }) })] }, column.key));
23
23
  }) })] })] })), _jsx("div", { className: "shrink-0 border-t border-border-soft px-3 py-2", children: _jsxs(Button, { variant: "secondary", size: "sm", disabled: disabled, onClick: () => onChange(addColumn(draft, defaultType)), children: [_jsx(Icon, { icon: Plus, size: 12 }), t('designer.columns.add')] }) })] }));
24
24
  }
@@ -1,4 +1,4 @@
1
- import type { Ref } from 'react';
1
+ import type { ReactNode, Ref } from 'react';
2
2
  import type { Dialect, TableDraft } from '../ddl/index.ts';
3
3
  import { type Plan } from './plan.ts';
4
4
  export interface DesignerViewProps {
@@ -19,7 +19,8 @@ export interface DesignerViewProps {
19
19
  onCancelConfirm(): void;
20
20
  onRevert(): void;
21
21
  onDismissFailure(): void;
22
+ toolbarStart?: ReactNode;
22
23
  className?: string;
23
24
  ref?: Ref<HTMLDivElement>;
24
25
  }
25
- export declare function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, className, ref }: DesignerViewProps): import("react").JSX.Element;
26
+ export declare function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, toolbarStart, className, ref }: DesignerViewProps): import("react").JSX.Element;
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import clsx from 'clsx';
3
3
  import { CircleAlert, Lock, Undo2 } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
5
+ import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Separator, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
+ import { CODE_TEXT } from '../code-text.js';
7
8
  import { ColumnsEditor } from './ColumnsEditor.js';
8
9
  import { ForeignKeysEditor } from './ForeignKeysEditor.js';
9
10
  import { IndexesEditor } from './IndexesEditor.js';
@@ -13,7 +14,7 @@ import { scriptOf } from './plan.js';
13
14
  import { SqlPreview } from './SqlPreview.js';
14
15
  const APPLY_SHORTCUT = /* @__PURE__ */ shortcut('Mod+S');
15
16
  /* The designer with everything it needs handed in: the name, the four tabs, what Apply would run and the confirmation before it does. */
16
- export function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, className, ref }) {
17
+ export function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, toolbarStart, className, ref }) {
17
18
  const { t } = useTranslation('database');
18
19
  const disabled = readOnlyReason !== null;
19
20
  const applyReason = readOnlyReason ?? (plan.problems.length > 0 ? t('designer.fixProblems') : plan.statements.length === 0 ? t('designer.noChanges') : null);
@@ -25,9 +26,9 @@ export function DesignerView({ dialect, schema, draft, isNew, changed, tables, p
25
26
  }
26
27
  }
27
28
  }
28
- return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2", children: [_jsx(Field, { label: t('designer.tableName'), className: "w-80", children: _jsx(Input, { mono: true, spellCheck: false, placeholder: t('designer.tableNamePlaceholder'), value: draft.name, disabled: disabled, onChange: (event) => onChange(renameTable(draft, event.target.value)) }) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs(Button, { variant: "secondary", disabled: !changed, onClick: onRevert, children: [_jsx(Icon, { icon: Undo2, size: 12 }), t('designer.revert')] }), _jsx(DisabledReason, { reason: applyReason, children: _jsx("span", { className: "inline-flex", children: _jsx(Button, { variant: "primary", disabled: applyReason !== null, onClick: onRequestApply, children: isNew ? t('designer.create') : t('designer.apply') }) }) })] })] }), readOnlyReason !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: Lock, tone: "neutral", className: "w-full", message: readOnlyReason }) })), failure !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: failure, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: onDismissFailure, children: t('designer.dismiss') }) }) })), _jsxs(Tabs.Root, { defaultValue: "columns", className: "flex min-h-0 grow flex-col", children: [_jsxs(Tabs.List, { className: "shrink-0 px-3", children: [_jsxs(Tabs.Tab, { value: "columns", children: [t('designer.tabs.columns'), _jsx(Tabs.Count, { value: draft.columns.length })] }), _jsxs(Tabs.Tab, { value: "indexes", children: [t('designer.tabs.indexes'), _jsx(Tabs.Count, { value: draft.indexes.length })] }), _jsxs(Tabs.Tab, { value: "foreignKeys", children: [t('designer.tabs.foreignKeys'), _jsx(Tabs.Count, { value: draft.foreignKeys.length })] }), _jsx(Tabs.Tab, { value: "options", children: t('designer.tabs.options') })] }), _jsx(Tabs.Panel, { value: "columns", className: "min-h-0 grow overflow-auto", children: _jsx(ColumnsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "indexes", className: "min-h-0 grow overflow-auto", children: _jsx(IndexesEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "foreignKeys", className: "min-h-0 grow overflow-auto", children: _jsx(ForeignKeysEditor, { draft: draft, dialect: dialect, disabled: disabled, schema: schema, tables: tables, referenceColumns: referenceColumns, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "options", className: "min-h-0 grow overflow-auto", children: _jsx(OptionsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) })] }), _jsx(SqlPreview, { plan: plan, className: "shrink-0" }), _jsxs(PromptDialog, { open: confirming, danger: plan.destructive, title: t('designer.confirm.title', { count: plan.statements.length, formatted: formatNumber(plan.statements.length) }), description: isNew ? t('designer.confirm.create', { table: draft.name }) : t('designer.confirm.alter', { table: draft.originalName }), confirmLabel: isNew ? t('designer.create') : t('designer.apply'), confirmBusyLabel: t('designer.applying'), onConfirm: onConfirm, onOpenChange: (open) => {
29
+ return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2", children: [toolbarStart !== undefined && toolbarStart !== null && toolbarStart !== false && (_jsxs("span", { className: "flex h-8 shrink-0 items-center gap-3", children: [toolbarStart, _jsx(Separator, {})] })), _jsx(Field, { label: t('designer.tableName'), className: "w-80", children: _jsx(Input, { mono: true, spellCheck: false, placeholder: t('designer.tableNamePlaceholder'), value: draft.name, disabled: disabled, onChange: (event) => onChange(renameTable(draft, event.target.value)) }) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs(Button, { variant: "secondary", disabled: !changed, onClick: onRevert, children: [_jsx(Icon, { icon: Undo2, size: 12 }), t('designer.revert')] }), _jsx(DisabledReason, { reason: applyReason, children: _jsx("span", { className: "inline-flex", children: _jsx(Button, { variant: "primary", disabled: applyReason !== null, onClick: onRequestApply, children: isNew ? t('designer.create') : t('designer.apply') }) }) })] })] }), readOnlyReason !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: Lock, tone: "neutral", className: "w-full", message: readOnlyReason }) })), failure !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: failure, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: onDismissFailure, children: t('designer.dismiss') }) }) })), _jsxs(Tabs.Root, { defaultValue: "columns", className: "flex min-h-0 grow flex-col", children: [_jsxs(Tabs.List, { className: "shrink-0 px-3", children: [_jsxs(Tabs.Tab, { value: "columns", children: [t('designer.tabs.columns'), _jsx(Tabs.Count, { value: draft.columns.length })] }), _jsxs(Tabs.Tab, { value: "indexes", children: [t('designer.tabs.indexes'), _jsx(Tabs.Count, { value: draft.indexes.length })] }), _jsxs(Tabs.Tab, { value: "foreignKeys", children: [t('designer.tabs.foreignKeys'), _jsx(Tabs.Count, { value: draft.foreignKeys.length })] }), _jsx(Tabs.Tab, { value: "options", children: t('designer.tabs.options') })] }), _jsx(Tabs.Panel, { value: "columns", className: "min-h-0 grow overflow-auto", children: _jsx(ColumnsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "indexes", className: "min-h-0 grow overflow-auto", children: _jsx(IndexesEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "foreignKeys", className: "min-h-0 grow overflow-auto", children: _jsx(ForeignKeysEditor, { draft: draft, dialect: dialect, disabled: disabled, schema: schema, tables: tables, referenceColumns: referenceColumns, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "options", className: "min-h-0 grow overflow-auto", children: _jsx(OptionsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) })] }), _jsx(SqlPreview, { plan: plan, className: "shrink-0" }), _jsxs(PromptDialog, { open: confirming, danger: plan.destructive, title: t('designer.confirm.title', { count: plan.statements.length, formatted: formatNumber(plan.statements.length) }), description: isNew ? t('designer.confirm.create', { table: draft.name }) : t('designer.confirm.alter', { table: draft.originalName }), confirmLabel: isNew ? t('designer.create') : t('designer.apply'), confirmBusyLabel: t('designer.applying'), onConfirm: onConfirm, onOpenChange: (open) => {
29
30
  if (!open) {
30
31
  onCancelConfirm();
31
32
  }
32
- }, children: [plan.destructive && _jsx("p", { className: "mt-3 text-xs text-status-error", children: t('designer.confirm.destructive') }), _jsx("pre", { className: "mt-3 max-h-64 overflow-auto rounded-md bg-surface-sunken p-2 font-mono text-code whitespace-pre text-text select-text", children: scriptOf(plan.statements) })] })] }));
33
+ }, children: [plan.destructive && _jsx("p", { className: "mt-3 text-xs text-status-error", children: t('designer.confirm.destructive') }), _jsx("pre", { className: clsx(CODE_TEXT, 'mt-3 max-h-64 overflow-auto rounded-md bg-surface-sunken p-2 whitespace-pre text-text select-text'), children: scriptOf(plan.statements) })] })] }));
33
34
  }
@@ -3,6 +3,7 @@ import clsx from 'clsx';
3
3
  import { Copy } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Button, Icon, copyText } from '@adecore/ui';
6
+ import { CODE_TEXT } from '../code-text.js';
6
7
  import { scriptOf } from './plan.js';
7
8
  /* The sentence a problem of the draft is read as. */
8
9
  export function ProblemText({ problem }) {
@@ -13,5 +14,5 @@ export function ProblemText({ problem }) {
13
14
  export function SqlPreview({ plan, className, ref }) {
14
15
  const { t } = useTranslation('database');
15
16
  const script = scriptOf(plan.statements);
16
- return (_jsxs("section", { ref: ref, "aria-label": t('designer.sql'), className: clsx('relative flex min-h-0 flex-col border-t border-border-soft bg-surface-sunken', className), children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center justify-between pr-2 pl-3", children: [_jsx("span", { className: "text-xs font-medium text-text-faint", children: t('designer.sql') }), _jsxs(Button, { variant: "ghost", size: "xs", disabled: script === '', onClick: () => copyText(script), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('designer.copy')] })] }), _jsx("div", { className: "max-h-48 min-h-12 overflow-auto px-3 pb-3 select-text", children: plan.problems.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-xs text-status-error", children: plan.problems.map((problem, i) => (_jsx("li", { children: _jsx(ProblemText, { problem: problem }) }, `${problem.code}:${problem.subject}:${i}`))) })) : script === '' ? (_jsx("p", { className: "text-xs text-text-faint", children: t('designer.noChanges') })) : (_jsx("pre", { className: "font-mono text-code whitespace-pre text-text", children: script })) })] }));
17
+ return (_jsxs("section", { ref: ref, "aria-label": t('designer.sql'), className: clsx('relative flex min-h-0 flex-col border-t border-border-soft bg-surface-sunken', className), children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center justify-between pr-2 pl-3", children: [_jsx("span", { className: "text-xs font-medium text-text-faint", children: t('designer.sql') }), _jsxs(Button, { variant: "ghost", size: "xs", disabled: script === '', onClick: () => copyText(script), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('designer.copy')] })] }), _jsx("div", { className: "max-h-48 min-h-12 overflow-auto px-3 pb-3 select-text", children: plan.problems.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-xs text-status-error", children: plan.problems.map((problem, i) => (_jsx("li", { children: _jsx(ProblemText, { problem: problem }) }, `${problem.code}:${problem.subject}:${i}`))) })) : script === '' ? (_jsx("p", { className: "text-xs text-text-faint", children: t('designer.noChanges') })) : (_jsx("pre", { className: clsx(CODE_TEXT, 'whitespace-pre text-text'), children: script })) })] }));
17
18
  }
@@ -1,10 +1,11 @@
1
- import { type Ref } from 'react';
1
+ import { type ReactNode, type Ref } from 'react';
2
2
  import type { Connection } from '../client/types.ts';
3
3
  export interface TableDesignerProps {
4
4
  connection: Connection;
5
5
  schema: string;
6
6
  table?: string;
7
+ toolbarStart?: ReactNode;
7
8
  className?: string;
8
9
  ref?: Ref<HTMLDivElement>;
9
10
  }
10
- export declare function TableDesigner({ connection, schema, table, className, ref }: TableDesignerProps): import("react").JSX.Element;
11
+ export declare function TableDesigner({ connection, schema, table, toolbarStart, className, ref }: TableDesignerProps): import("react").JSX.Element;
@@ -13,7 +13,7 @@ import { planOf } from './plan.js';
13
13
  import { recoverFrom } from './recover.js';
14
14
  import { useReferenceColumns } from './useReferenceColumns.js';
15
15
  /* Creates a table or modifies one: its columns, indexes, foreign keys and options, with the SQL it will run shown before it runs. */
16
- export function TableDesigner({ connection, schema, table, className, ref }) {
16
+ export function TableDesigner({ connection, schema, table, toolbarStart, className, ref }) {
17
17
  const { t } = useTranslation('database');
18
18
  const client = useDatabaseClient();
19
19
  const onAction = useDatabaseAction();
@@ -99,5 +99,5 @@ export function TableDesigner({ connection, schema, table, className, ref }) {
99
99
  return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), children: [(loaded.loading || snapshot === null) && loaded.error === null && (_jsx("div", { className: "grid grow place-items-center py-8 text-text-muted", children: _jsx(Spinner, { size: 20, label: t('designer.loading') }) })), loaded.error !== null && snapshot === null && (_jsx("div", { className: "p-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: loaded.error, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: loaded.reload, children: t('designer.retry') }) }) })), shapeChanged && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: RefreshCw, tone: "neutral", className: "w-full", message: t('designer.changed'), children: _jsx(Button, { variant: "secondary", size: "sm", onClick: loaded.reload, children: t('designer.reload') }) }) })), !loaded.loading && snapshot !== null && plan !== null && (_jsx(DesignerView, { className: "grow", dialect: snapshot.dialect, schema: schema, draft: draft, isNew: snapshot.structure === null, changed: draft !== editing.baseline, tables: snapshot.tables, plan: plan, readOnlyReason: readOnlyReason, failure: failure, confirming: confirming, referenceColumns: referenceColumns, onChange: (next) => setEditing({ ...editing, draft: next }), onRequestApply: () => setConfirming(true), onConfirm: apply, onCancelConfirm: () => setConfirming(false), onRevert: () => {
100
100
  setEditing({ ...editing, draft: editing.baseline });
101
101
  setFailure(null);
102
- }, onDismissFailure: () => setFailure(null) }))] }));
102
+ }, onDismissFailure: () => setFailure(null), toolbarStart: toolbarStart }))] }));
103
103
  }
@@ -1,13 +1,16 @@
1
1
  import { type Ref } from 'react';
2
2
  import type { ExplorerSelection } from '../actions.ts';
3
3
  import type { Connection } from '../client/types.ts';
4
+ import type { ExplorerFolder } from './folders.ts';
4
5
  export interface DatabaseExplorerProps {
5
6
  connections: readonly Connection[];
6
7
  value?: ExplorerSelection | null;
7
8
  defaultValue?: ExplorerSelection | null;
8
9
  onValueChange?(selection: ExplorerSelection | null): void;
9
10
  showSystemSchemas?: boolean;
11
+ openOnClick?: boolean;
12
+ folders?: Readonly<Record<string, readonly ExplorerFolder[]>>;
10
13
  className?: string;
11
14
  ref?: Ref<HTMLDivElement>;
12
15
  }
13
- export declare function DatabaseExplorer({ connections, value, defaultValue, onValueChange, showSystemSchemas, className, ref }: DatabaseExplorerProps): import("react").JSX.Element;
16
+ export declare function DatabaseExplorer({ connections, value, defaultValue, onValueChange, showSystemSchemas, openOnClick, folders, className, ref }: DatabaseExplorerProps): import("react").JSX.Element;
@@ -1,23 +1,26 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
- import { CircleAlert, Database, Eye, Folder, KeyRound, Link2, Lock, RectangleVertical, Search, Table } from 'lucide-react';
4
+ import { CircleAlert, Columns2, Database, Eye, Folder, KeyRound, ListOrdered, Lock, Search, ShieldCheck, Table, Zap } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip, Tree } from '@adecore/ui';
7
7
  import { formatNumber } from '@adecore/ui/format';
8
8
  import { useDatabaseAction, useDatabaseClient } from '../client-context.js';
9
9
  import { EngineIcon } from '../connections/EngineIcon.js';
10
+ import { keyLabelOf } from '../column-keys.js';
11
+ import { KeyIcon } from '../KeyIcon.js';
10
12
  import { RowMenu } from './RowMenu.js';
11
13
  import { TableDialog } from './TableDialog.js';
12
- import { connectionKey, flattenTree, hasMenu, isExpandable, isFolderKey, isSelectable, navigate, neededLoads, selectionKey, selectionOf, tabStop, tableOf } from './tree.js';
14
+ import { connectionKey, flattenTree, hasMenu, isExpandable, startsOpen, isSelectable, navigate, neededLoads, selectionKey, selectionOf, tabStop, openedTableOf, tableKindIn, tableOf } from './tree.js';
13
15
  import { useExplorerLoads } from './use-explorer-loads.js';
14
16
  import { usePopupPress } from '../use-popup-press.js';
15
17
  import { useStoredExpansion } from './use-stored-expansion.js';
16
18
  /*
17
19
  * The connections, their schemas and their tables as a tree, loaded as each node opens. Every row
18
- * selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`.
20
+ * selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`,
21
+ * and on an item of a folder the app added opens it through the item's own `onOpen`.
19
22
  */
20
- export function DatabaseExplorer({ connections, value, defaultValue = null, onValueChange, showSystemSchemas = false, className, ref }) {
23
+ export function DatabaseExplorer({ connections, value, defaultValue = null, onValueChange, showSystemSchemas = false, openOnClick = false, folders, className, ref }) {
21
24
  const { t } = useTranslation('database');
22
25
  const client = useDatabaseClient();
23
26
  const act = useDatabaseAction();
@@ -40,7 +43,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
40
43
  schemas: loads.schemas,
41
44
  tables: loads.tables,
42
45
  structures: loads.structures,
43
- versions: loads.versions
46
+ versions: loads.versions,
47
+ folders: (connectionId) => folders?.[connectionId] ?? []
44
48
  };
45
49
  const rows = flattenTree({ ...input, filter });
46
50
  const stop = tabStop(rows, activeKey, selectedKey);
@@ -56,8 +60,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
56
60
  }
57
61
  }, [activeKey]);
58
62
  const setOpen = (key, open) => {
59
- // A folder is open until the person closes it, everything else is closed until they open it.
60
- const folder = isFolderKey(key);
63
+ const folder = startsOpen(key);
61
64
  const toggle = (current) => {
62
65
  const next = new Set(current);
63
66
  if (open !== folder) {
@@ -85,22 +88,33 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
85
88
  select(selection);
86
89
  }
87
90
  };
88
- const open = (row) => {
89
- const ref = tableOf(row);
90
- if (ref !== null) {
91
- pick(row);
92
- act?.({ kind: 'open-table', ref, view: 'data' });
91
+ /* `preview` says a click opened it, which only an explorer that opens on a click tells the app. */
92
+ const open = (row, preview = false) => {
93
+ if (row.kind === 'appItem') {
94
+ row.item.onOpen?.({ preview });
95
+ return;
96
+ }
97
+ const ref = openedTableOf(row);
98
+ if (ref === null) {
99
+ return;
93
100
  }
101
+ pick(row);
102
+ const tableKind = row.kind === 'table' && row.ref === ref ? row.table.kind : tableKindIn(loads.tables(ref.connectionId, ref.schema), ref.table);
103
+ act?.({ kind: 'open-table', ref, view: 'data', ...(tableKind === undefined ? {} : { tableKind }), ...(openOnClick ? { preview } : {}) });
94
104
  };
95
105
  const activate = (row) => {
96
106
  switch (row.kind) {
97
107
  case 'table':
98
108
  case 'column':
109
+ case 'entry':
110
+ case 'appItem':
99
111
  open(row);
100
112
  break;
101
113
  case 'connection':
102
114
  case 'schema':
103
115
  case 'folder':
116
+ case 'part':
117
+ case 'appFolder':
104
118
  pick(row);
105
119
  toggle(row);
106
120
  break;
@@ -132,11 +146,18 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
132
146
  setOpen(action.key, action.type === 'expand');
133
147
  }
134
148
  };
135
- const click = (row) => {
149
+ /* `count` is the click's `detail`: the second click of a double click leaves the opening to the double click. */
150
+ const click = (row, count) => {
136
151
  if (pressedInPopup()) {
137
152
  return;
138
153
  }
139
154
  setActiveKey(row.key);
155
+ if (openOnClick && (row.kind === 'table' || row.kind === 'appItem')) {
156
+ if (count <= 1) {
157
+ open(row, true);
158
+ }
159
+ return;
160
+ }
140
161
  pick(row);
141
162
  if (row.kind !== 'table' && isExpandable(row)) {
142
163
  toggle(row);
@@ -167,7 +188,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
167
188
  const isSelected = (row) => row !== undefined && isSelectable(row) && row.key === selectedKey;
168
189
  const renderRow = (row, index) => {
169
190
  const connection = connectionOf(row);
170
- const menu = connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: refresh, onDisconnect: disconnect, onRequest: setRequest })) : null;
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;
171
192
  const selectedRow = isSelected(row);
172
193
  const props = {
173
194
  level: row.level,
@@ -189,8 +210,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
189
210
  rowElements.current.set(row.key, element);
190
211
  }
191
212
  },
192
- onClick: () => click(row),
193
- onDoubleClick: tableOf(row) === null ? undefined : () => open(row),
213
+ onClick: (event) => click(row, event.detail),
214
+ onDoubleClick: tableOf(row) === null && row.kind !== 'appItem' ? undefined : () => open(row),
194
215
  onFocus: () => {
195
216
  if (row.focusable) {
196
217
  setActiveKey(row.key);
@@ -207,21 +228,29 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
207
228
  if (e.key === 'Escape') {
208
229
  setFilter('');
209
230
  }
210
- } }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(Tree.Root, { "aria-label": t('explorer.label'), className: "min-h-0 grow overflow-y-auto py-2", onKeyDown: onKeyDown, children: rows.map(renderRow) })), request !== null && _jsx(TableDialog, { request: request, onClose: () => setRequest(null), onDone: finish })] }));
231
+ } }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(Tree.Root, { "aria-label": t('explorer.label'), overflow: "scroll", className: "min-h-0 grow overflow-y-auto pt-2", onKeyDown: onKeyDown, children: rows.map(renderRow) })), request !== null && _jsx(TableDialog, { request: request, onClose: () => setRequest(null), onDone: finish })] }));
211
232
  }
212
233
  function RowContent({ row, onToggle }) {
213
234
  const { t } = useTranslation('database');
214
235
  switch (row.kind) {
215
236
  case 'connection':
216
- return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(EngineIcon, { engine: row.connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.connection.name || t('connections.untitled') }), row.connection.config.readOnly === true && (_jsx(Tooltip, { label: t('explorer.readOnly'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-faint", children: _jsx(Icon, { icon: Lock, size: 12 }) }) })), row.version !== null && _jsx("span", { className: "min-w-0 shrink-[2] truncate text-text-faint", children: row.version }), row.schemaCount !== null && _jsx(SchemaCount, { shown: row.schemaCount.shown, total: row.schemaCount.total })] }));
237
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(EngineIcon, { engine: row.connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.connection.name || t('connections.untitled') }), row.connection.config.readOnly === true && (_jsx(Tooltip, { label: t('explorer.readOnly'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-faint", children: _jsx(Icon, { icon: Lock, size: 12 }) }) })), row.version !== null && _jsx("span", { className: "text-text-faint", children: row.version }), row.schemaCount !== null && _jsx(SchemaCount, { shown: row.schemaCount.shown, total: row.schemaCount.total })] }));
217
238
  case 'schema':
218
239
  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 })] }));
219
240
  case 'folder':
220
241
  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) })] }));
221
242
  case 'table':
222
243
  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 })] }));
244
+ case 'part':
245
+ 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
+ case 'appFolder':
247
+ 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
+ case 'appItem':
249
+ 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 })] }));
250
+ case 'entry':
251
+ return _jsx(EntryContent, { row: row });
223
252
  case 'column':
224
- return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(Tree.Label, { children: row.column.name }), row.column.type !== '' && _jsx("span", { className: "min-w-0 shrink-[2] truncate text-text-faint", children: row.column.type })] }));
253
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(Tree.Label, { children: row.column.name }), row.column.type !== '' && _jsx("span", { className: "text-text-faint", children: row.column.type })] }));
225
254
  case 'loading':
226
255
  return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Spinner, { size: 12 }), _jsx("span", { className: "text-text-faint", children: t('explorer.loading') })] }));
227
256
  case 'error':
@@ -236,12 +265,39 @@ function SchemaCount({ shown, total }) {
236
265
  const counts = { shown: formatNumber(shown), total: formatNumber(total) };
237
266
  return (_jsx(Tooltip, { label: t('explorer.schemaCountHint', { count: total, ...counts }), children: _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: t('explorer.schemaCount', counts) }) }));
238
267
  }
239
- function ColumnIcon({ primaryKey, foreignKey }) {
268
+ /* A step smaller than the icons of the rows above, in the same 16px slot, so the names stay in line. */
269
+ const ENTRY_ICON_SLOT = 'inline-flex size-4 shrink-0 items-center justify-center';
270
+ function ColumnIcon(keys) {
240
271
  const { t } = useTranslation('database');
241
- if (!primaryKey && !foreignKey) {
242
- return _jsx(Icon, { icon: RectangleVertical, size: 16, className: "shrink-0 text-text-faint" });
272
+ if (!keys.primaryKey && !keys.foreignKey) {
273
+ return (_jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(Icon, { icon: Columns2, size: 14, className: "text-text-faint" }) }));
274
+ }
275
+ return (_jsx(Tooltip, { label: t(keyLabelOf(keys)), children: _jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(KeyIcon, { ...keys, size: 14 }) }) }));
276
+ }
277
+ /* 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. */
278
+ function EntryContent({ row }) {
279
+ const { t } = useTranslation('database');
280
+ const { entry } = row;
281
+ const icon = (glyph, className) => (_jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(Icon, { icon: glyph, size: 14, className: className }) }));
282
+ switch (entry.type) {
283
+ case 'key':
284
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(KeyRound, entry.primary ? 'text-(--file-icon-yellow)' : 'text-text-muted'), _jsx(Tree.Label, { children: entry.name ?? t('explorer.primaryKey') }), _jsx("span", { className: "text-text-faint", children: entry.columns.join(', ') })] }));
285
+ case 'foreignKey': {
286
+ const { foreignKey } = entry;
287
+ const columns = foreignKey.columns.join(', ');
288
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(KeyRound, 'text-(--file-icon-blue)'), _jsx(Tree.Label, { children: foreignKey.name ?? columns }), _jsx("span", { className: "text-text-faint", children: t('explorer.references', {
289
+ columns,
290
+ table: foreignKey.referencedTable,
291
+ referenced: foreignKey.referencedColumns.join(', ')
292
+ }) })] }));
293
+ }
294
+ case 'index':
295
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(ListOrdered, 'text-text-faint'), _jsx(Tree.Label, { children: entry.index.name }), _jsx("span", { className: "text-text-faint", children: entry.index.columns.join(', ') }), entry.index.unique && _jsx("span", { className: "text-text-faint", children: t('explorer.unique') })] }));
296
+ case 'check':
297
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(ShieldCheck, 'text-text-faint'), _jsx(Tree.Label, { children: entry.check.name ?? t('explorer.check') }), _jsx("span", { className: "font-mono text-text-faint", children: entry.check.expression })] }));
298
+ case 'trigger':
299
+ return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(Zap, 'text-text-faint'), _jsx(Tree.Label, { children: entry.trigger.name }), _jsx("span", { className: "text-text-faint", children: `${entry.trigger.timing} ${entry.trigger.event}` })] }));
243
300
  }
244
- return (_jsx(Tooltip, { label: t(primaryKey ? 'explorer.primaryKey' : 'explorer.foreignKey'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-muted", children: _jsx(Icon, { icon: primaryKey ? KeyRound : Link2, size: 16 }) }) }));
245
301
  }
246
302
  function ErrorContent({ row }) {
247
303
  const { t } = useTranslation('database');
@@ -29,8 +29,8 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest })
29
29
  const request = (change) => () => onRequest({ change, connection, ref, kind });
30
30
  return [
31
31
  [
32
- actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data' }),
33
- actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure' }),
32
+ actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data', tableKind: kind }),
33
+ actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure', tableKind: kind }),
34
34
  kind === 'table' && writable ? actionItem('edit-table', PencilRuler, t('explorer.editTable'), { kind: 'edit-table', ref }) : null,
35
35
  actionItem('console', SquareTerminal, t('explorer.newConsoleHere'), consoleAction(ref.schema, selectAllSql(connection.config.engine, ref)))
36
36
  ],
@@ -1,4 +1,4 @@
1
- import { connectionOfKey, isFolderKey } from './tree.js';
1
+ import { connectionOfKey, startsOpen } from './tree.js';
2
2
  const VERSION = 1;
3
3
  export const expansionKey = (connectionId) => `database:explorer:${connectionId}`;
4
4
  /* Reads what `serializeExpansion` wrote. Anything that does not fit gives nothing open and no folder closed. */
@@ -14,7 +14,7 @@ export const parseExpansion = (raw, connectionId) => {
14
14
  }
15
15
  const { expanded, collapsed } = parsed;
16
16
  const own = (keys, folders) => new Set(Array.isArray(keys)
17
- ? keys.filter((key) => typeof key === 'string' && key.length > 2 && isFolderKey(key) === folders && connectionOfKey(key) === connectionId)
17
+ ? keys.filter((key) => typeof key === 'string' && key.length > 2 && startsOpen(key) === folders && connectionOfKey(key) === connectionId)
18
18
  : []);
19
19
  return { expanded: own(expanded, false), collapsed: own(collapsed, true) };
20
20
  }
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ export interface ExplorerItem {
4
+ readonly id: string;
5
+ readonly label: string;
6
+ readonly icon: LucideIcon;
7
+ onOpen?(options: {
8
+ readonly preview: boolean;
9
+ }): void;
10
+ menu?: ReactNode;
11
+ }
12
+ export interface ExplorerFolder {
13
+ readonly id: string;
14
+ readonly label: string;
15
+ readonly items: readonly ExplorerItem[];
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,7 @@
1
1
  import type { ExplorerSelection } from '../actions.ts';
2
2
  import type { Connection, TableRef } from '../client/types.ts';
3
- import type { ColumnInfo, Engine, SchemaInfo, TableInfo, TableKind, TableStructure } from '../protocol/index.ts';
3
+ import type { CheckInfo, ColumnInfo, Engine, ForeignKeyInfo, IndexInfo, SchemaInfo, TableInfo, TableKind, TableStructure, TriggerInfo } from '../protocol/index.ts';
4
+ import type { ExplorerFolder, ExplorerItem } from './folders.ts';
4
5
  export type Load<T> = {
5
6
  readonly status: 'idle';
6
7
  } | {
@@ -22,9 +23,13 @@ export declare const connectionKey: (connectionId: string) => string;
22
23
  export declare const schemaKey: (connectionId: string, schema: string) => string;
23
24
  export declare const folderKey: (connectionId: string, schema: string, group: TableKind) => string;
24
25
  export declare const tableKey: (ref: TableRef) => string;
25
- export declare const isFolderKey: (key: string) => boolean;
26
+ export declare const PARTS: readonly ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'];
27
+ export type TablePart = (typeof PARTS)[number];
28
+ export declare const partKey: (ref: TableRef, part: TablePart) => string;
29
+ export declare const startsOpen: (key: string) => boolean;
26
30
  export declare const connectionOfKey: (key: string) => string;
27
31
  export declare const columnKey: (ref: TableRef, column: string) => string;
32
+ export declare const appFolderKey: (connectionId: string, folder: string) => string;
28
33
  export declare const loadKey: (target: LoadTarget) => string;
29
34
  export declare const GROUPS: readonly TableKind[];
30
35
  interface RowBase {
@@ -72,6 +77,36 @@ export interface ColumnRow extends RowBase {
72
77
  readonly primaryKey: boolean;
73
78
  readonly foreignKey: boolean;
74
79
  }
80
+ export interface PartRow extends RowBase {
81
+ readonly kind: 'part';
82
+ readonly ref: TableRef;
83
+ readonly part: TablePart;
84
+ readonly expanded: boolean;
85
+ readonly count: number;
86
+ }
87
+ export type TableEntry = {
88
+ readonly type: 'key';
89
+ readonly name: string | null;
90
+ readonly columns: readonly string[];
91
+ readonly primary: boolean;
92
+ } | {
93
+ readonly type: 'foreignKey';
94
+ readonly foreignKey: ForeignKeyInfo;
95
+ } | {
96
+ readonly type: 'index';
97
+ readonly index: IndexInfo;
98
+ } | {
99
+ readonly type: 'check';
100
+ readonly check: CheckInfo;
101
+ } | {
102
+ readonly type: 'trigger';
103
+ readonly trigger: TriggerInfo;
104
+ };
105
+ export interface EntryRow extends RowBase {
106
+ readonly kind: 'entry';
107
+ readonly ref: TableRef;
108
+ readonly entry: TableEntry;
109
+ }
75
110
  export interface LoadingRow extends RowBase {
76
111
  readonly kind: 'loading';
77
112
  }
@@ -80,14 +115,28 @@ export interface ErrorRow extends RowBase {
80
115
  readonly message: string;
81
116
  readonly retry: LoadTarget;
82
117
  }
118
+ export interface AppFolderRow extends RowBase {
119
+ readonly kind: 'appFolder';
120
+ readonly connection: Connection;
121
+ readonly folder: ExplorerFolder;
122
+ readonly expanded: boolean;
123
+ readonly count: number;
124
+ }
125
+ export interface AppItemRow extends RowBase {
126
+ readonly kind: 'appItem';
127
+ readonly connection: Connection;
128
+ readonly item: ExplorerItem;
129
+ }
83
130
  export interface EmptyRow extends RowBase {
84
131
  readonly kind: 'empty';
85
132
  readonly of: 'schemas' | 'tables' | 'columns';
86
133
  }
87
- export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | ColumnRow | LoadingRow | ErrorRow | EmptyRow;
88
- export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow;
134
+ export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | PartRow | ColumnRow | EntryRow | AppFolderRow | AppItemRow | LoadingRow | ErrorRow | EmptyRow;
135
+ export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow | PartRow | AppFolderRow;
89
136
  export declare const isExpandable: (row: TreeRow) => row is ExpandableRow;
90
137
  export declare const tableOf: (row: TreeRow) => TableRef | null;
138
+ export declare const openedTableOf: (row: TreeRow) => TableRef | null;
139
+ export declare const tableKindIn: (tables: Load<readonly TableInfo[]>, name: string) => TableKind | undefined;
91
140
  export declare const selectionOf: (row: TreeRow) => ExplorerSelection | null;
92
141
  export declare const selectionKey: (selection: ExplorerSelection) => string;
93
142
  export declare const hasMenu: (row: TreeRow) => boolean;
@@ -103,8 +152,10 @@ export interface TreeInput {
103
152
  tables(connectionId: string, schema: string): Load<readonly TableInfo[]>;
104
153
  structures(connectionId: string, schema: string, table: string): Load<TableStructure>;
105
154
  versions(connectionId: string): string | null;
155
+ folders?(connectionId: string): readonly ExplorerFolder[];
106
156
  }
107
157
  export declare const visibleSchemas: (schemas: readonly SchemaInfo[], showSystemSchemas: boolean) => readonly SchemaInfo[];
158
+ export declare const tableEntries: (structure: TableStructure) => Readonly<Record<Exclude<TablePart, 'columns'>, readonly TableEntry[]>>;
108
159
  export declare const flattenTree: (input: TreeInput) => TreeRow[];
109
160
  export declare const neededLoads: (input: Omit<TreeInput, 'filter'>) => LoadTarget[];
110
161
  export type TreeAction = {