@adecore/database 0.0.1 → 0.16.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +152 -1
- package/dist/DatabaseProvider.d.ts +13 -0
- package/dist/DatabaseProvider.js +17 -0
- package/dist/actions.d.ts +40 -0
- package/dist/actions.js +1 -0
- package/dist/client/client.d.ts +5 -0
- package/dist/client/client.js +245 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/types.d.ts +97 -0
- package/dist/client/types.js +13 -0
- package/dist/client-context.d.ts +16 -0
- package/dist/client-context.js +17 -0
- package/dist/connections/ConnectionForm.d.ts +10 -0
- package/dist/connections/ConnectionForm.js +145 -0
- package/dist/connections/ConnectionManager.d.ts +12 -0
- package/dist/connections/ConnectionManager.js +93 -0
- package/dist/connections/EngineIcon.d.ts +9 -0
- package/dist/connections/EngineIcon.js +7 -0
- package/dist/connections/connection-config.d.ts +16 -0
- package/dist/connections/connection-config.js +107 -0
- package/dist/connections/docker.d.ts +9 -0
- package/dist/connections/docker.js +38 -0
- package/dist/connections/engine-icons.d.ts +3 -0
- package/dist/connections/engine-icons.js +3 -0
- package/dist/connections/use-docker-containers.d.ts +17 -0
- package/dist/connections/use-docker-containers.js +26 -0
- package/dist/console/DestructiveDialog.d.ts +9 -0
- package/dist/console/DestructiveDialog.js +9 -0
- package/dist/console/HistoryPanel.d.ts +10 -0
- package/dist/console/HistoryPanel.js +15 -0
- package/dist/console/QueryConsole.d.ts +14 -0
- package/dist/console/QueryConsole.js +261 -0
- package/dist/console/SchemaPicker.d.ts +11 -0
- package/dist/console/SchemaPicker.js +8 -0
- package/dist/console/SqlEditor.d.ts +12 -0
- package/dist/console/SqlEditor.js +61 -0
- package/dist/console/StatementResultView.d.ts +18 -0
- package/dist/console/StatementResultView.js +32 -0
- package/dist/console/TransactionControls.d.ts +13 -0
- package/dist/console/TransactionControls.js +12 -0
- package/dist/console/destructive.d.ts +7 -0
- package/dist/console/destructive.js +43 -0
- package/dist/console/editing.d.ts +12 -0
- package/dist/console/editing.js +60 -0
- package/dist/console/history.d.ts +13 -0
- package/dist/console/history.js +43 -0
- package/dist/console/labels.d.ts +1 -0
- package/dist/console/labels.js +6 -0
- package/dist/console/outcome.d.ts +6 -0
- package/dist/console/outcome.js +11 -0
- package/dist/console/schemas.d.ts +2 -0
- package/dist/console/schemas.js +5 -0
- package/dist/console/shortcuts.d.ts +2 -0
- package/dist/console/shortcuts.js +3 -0
- package/dist/console/useConsoleHistory.d.ts +7 -0
- package/dist/console/useConsoleHistory.js +22 -0
- package/dist/ddl/alter-mysql.d.ts +4 -0
- package/dist/ddl/alter-mysql.js +79 -0
- package/dist/ddl/alter-sqlite.d.ts +4 -0
- package/dist/ddl/alter-sqlite.js +80 -0
- package/dist/ddl/alter.d.ts +4 -0
- package/dist/ddl/alter.js +24 -0
- package/dist/ddl/column.d.ts +9 -0
- package/dist/ddl/column.js +62 -0
- package/dist/ddl/create.d.ts +12 -0
- package/dist/ddl/create.js +74 -0
- package/dist/ddl/dialect.d.ts +12 -0
- package/dist/ddl/dialect.js +34 -0
- package/dist/ddl/diff.d.ts +34 -0
- package/dist/ddl/diff.js +111 -0
- package/dist/ddl/draft.d.ts +46 -0
- package/dist/ddl/draft.js +70 -0
- package/dist/ddl/index.d.ts +10 -0
- package/dist/ddl/index.js +10 -0
- package/dist/ddl/parse.d.ts +17 -0
- package/dist/ddl/parse.js +131 -0
- package/dist/ddl/statements.d.ts +6 -0
- package/dist/ddl/statements.js +12 -0
- package/dist/ddl/types.d.ts +6 -0
- package/dist/ddl/types.js +52 -0
- package/dist/ddl/validate.d.ts +7 -0
- package/dist/ddl/validate.js +38 -0
- package/dist/designer/ColumnsEditor.d.ts +2 -0
- package/dist/designer/ColumnsEditor.js +24 -0
- package/dist/designer/DesignerView.d.ts +25 -0
- package/dist/designer/DesignerView.js +33 -0
- package/dist/designer/ForeignKeysEditor.d.ts +7 -0
- package/dist/designer/ForeignKeysEditor.js +24 -0
- package/dist/designer/IndexesEditor.d.ts +2 -0
- package/dist/designer/IndexesEditor.js +13 -0
- package/dist/designer/NamePicker.d.ts +11 -0
- package/dist/designer/NamePicker.js +13 -0
- package/dist/designer/OptionsEditor.d.ts +2 -0
- package/dist/designer/OptionsEditor.js +17 -0
- package/dist/designer/SqlPreview.d.ts +12 -0
- package/dist/designer/SqlPreview.js +17 -0
- package/dist/designer/TableDesigner.d.ts +10 -0
- package/dist/designer/TableDesigner.js +103 -0
- package/dist/designer/edit.d.ts +17 -0
- package/dist/designer/edit.js +115 -0
- package/dist/designer/editor-props.d.ts +10 -0
- package/dist/designer/editor-props.js +1 -0
- package/dist/designer/plan.d.ts +9 -0
- package/dist/designer/plan.js +13 -0
- package/dist/designer/recover.d.ts +2 -0
- package/dist/designer/recover.js +18 -0
- package/dist/designer/useReferenceColumns.d.ts +3 -0
- package/dist/designer/useReferenceColumns.js +36 -0
- package/dist/explorer/DatabaseExplorer.d.ts +13 -0
- package/dist/explorer/DatabaseExplorer.js +264 -0
- package/dist/explorer/RowMenu.d.ts +11 -0
- package/dist/explorer/RowMenu.js +89 -0
- package/dist/explorer/TableDialog.d.ts +15 -0
- package/dist/explorer/TableDialog.js +50 -0
- package/dist/explorer/expansion.d.ts +7 -0
- package/dist/explorer/expansion.js +31 -0
- package/dist/explorer/table-statements.d.ts +5 -0
- package/dist/explorer/table-statements.js +12 -0
- package/dist/explorer/tree.d.ts +125 -0
- package/dist/explorer/tree.js +295 -0
- package/dist/explorer/use-explorer-loads.d.ts +12 -0
- package/dist/explorer/use-explorer-loads.js +109 -0
- package/dist/explorer/use-stored-expansion.d.ts +9 -0
- package/dist/explorer/use-stored-expansion.js +47 -0
- package/dist/grid/CellEditor.d.ts +9 -0
- package/dist/grid/CellEditor.js +29 -0
- package/dist/grid/CopyAsMenu.d.ts +7 -0
- package/dist/grid/CopyAsMenu.js +10 -0
- package/dist/grid/DataGrid.d.ts +36 -0
- package/dist/grid/DataGrid.js +512 -0
- package/dist/grid/EnumPicker.d.ts +17 -0
- package/dist/grid/EnumPicker.js +53 -0
- package/dist/grid/GridHeaderCell.d.ts +36 -0
- package/dist/grid/GridHeaderCell.js +69 -0
- package/dist/grid/RecordView.d.ts +20 -0
- package/dist/grid/RecordView.js +126 -0
- package/dist/grid/ValueDock.d.ts +11 -0
- package/dist/grid/ValueDock.js +28 -0
- package/dist/grid/aggregates.d.ts +22 -0
- package/dist/grid/aggregates.js +62 -0
- package/dist/grid/cell-range.d.ts +14 -0
- package/dist/grid/cell-range.js +11 -0
- package/dist/grid/column-layout.d.ts +18 -0
- package/dist/grid/column-layout.js +49 -0
- package/dist/grid/column-selection.d.ts +11 -0
- package/dist/grid/column-selection.js +28 -0
- package/dist/grid/column-view.d.ts +10 -0
- package/dist/grid/column-view.js +23 -0
- package/dist/grid/copy-formats.d.ts +15 -0
- package/dist/grid/copy-formats.js +80 -0
- package/dist/grid/display.d.ts +20 -0
- package/dist/grid/display.js +81 -0
- package/dist/grid/edit-value.d.ts +5 -0
- package/dist/grid/edit-value.js +55 -0
- package/dist/grid/enum-type.d.ts +7 -0
- package/dist/grid/enum-type.js +52 -0
- package/dist/grid/focused-value.d.ts +4 -0
- package/dist/grid/focused-value.js +19 -0
- package/dist/grid/layout.d.ts +22 -0
- package/dist/grid/layout.js +74 -0
- package/dist/grid/navigation.d.ts +15 -0
- package/dist/grid/navigation.js +52 -0
- package/dist/grid/row-selection.d.ts +9 -0
- package/dist/grid/row-selection.js +24 -0
- package/dist/grid/sort.d.ts +7 -0
- package/dist/grid/sort.js +7 -0
- package/dist/grid/types.d.ts +30 -0
- package/dist/grid/types.js +9 -0
- package/dist/host/helper-path.d.ts +7 -0
- package/dist/host/helper-path.js +15 -0
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +343 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +4 -0
- package/dist/host/spawn.d.ts +7 -0
- package/dist/host/spawn.js +98 -0
- package/dist/host/types.d.ts +19 -0
- package/dist/host/types.js +1 -0
- package/dist/host/validate.d.ts +10 -0
- package/dist/host/validate.js +339 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/locales/en.json +577 -0
- package/dist/locales/nl.json +577 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +14 -0
- package/dist/protocol/connection.d.ts +56 -0
- package/dist/protocol/connection.js +1 -0
- package/dist/protocol/errors.d.ts +7 -0
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/messages.d.ts +250 -0
- package/dist/protocol/messages.js +2 -0
- package/dist/protocol/schema.d.ts +53 -0
- package/dist/protocol/schema.js +1 -0
- package/dist/protocol/values.d.ts +21 -0
- package/dist/protocol/values.js +15 -0
- package/dist/sql-split.d.ts +15 -0
- package/dist/sql-split.js +201 -0
- package/dist/sql.d.ts +29 -0
- package/dist/sql.js +104 -0
- package/dist/structure/StructureView.d.ts +14 -0
- package/dist/structure/StructureView.js +99 -0
- package/dist/structure/structure-text.d.ts +2 -0
- package/dist/structure/structure-text.js +2 -0
- package/dist/table/CommandField.d.ts +14 -0
- package/dist/table/CommandField.js +157 -0
- package/dist/table/ImportDialog.d.ts +27 -0
- package/dist/table/ImportDialog.js +28 -0
- package/dist/table/TableStatusBar.d.ts +19 -0
- package/dist/table/TableStatusBar.js +22 -0
- package/dist/table/TableToolbar.d.ts +43 -0
- package/dist/table/TableToolbar.js +16 -0
- package/dist/table/TableView.d.ts +13 -0
- package/dist/table/TableView.js +508 -0
- package/dist/table/command-field.d.ts +57 -0
- package/dist/table/command-field.js +203 -0
- package/dist/table/grid-rows.d.ts +23 -0
- package/dist/table/grid-rows.js +55 -0
- package/dist/table/import-mapping.d.ts +7 -0
- package/dist/table/import-mapping.js +28 -0
- package/dist/table/layout-store.d.ts +13 -0
- package/dist/table/layout-store.js +43 -0
- package/dist/table/paging.d.ts +12 -0
- package/dist/table/paging.js +20 -0
- package/dist/table/pending.d.ts +32 -0
- package/dist/table/pending.js +94 -0
- package/dist/table/references.d.ts +12 -0
- package/dist/table/references.js +21 -0
- package/dist/table/useLoaded.d.ts +7 -0
- package/dist/table/useLoaded.js +36 -0
- package/dist/table/useStoredLayout.d.ts +8 -0
- package/dist/table/useStoredLayout.js +39 -0
- package/dist/table/useTableTransfer.d.ts +51 -0
- package/dist/table/useTableTransfer.js +125 -0
- package/dist/testing/fake.d.ts +21 -0
- package/dist/testing/fake.js +372 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/use-popup-press.d.ts +1 -0
- package/dist/use-popup-press.js +24 -0
- package/dist/use-schema-change.d.ts +1 -0
- package/dist/use-schema-change.js +13 -0
- package/dist/use-stable-callback.d.ts +1 -0
- package/dist/use-stable-callback.js +9 -0
- package/dist/value/ValuePanel.d.ts +14 -0
- package/dist/value/ValuePanel.js +103 -0
- package/dist/value/draft.d.ts +33 -0
- package/dist/value/draft.js +44 -0
- package/dist/value/hex.d.ts +14 -0
- package/dist/value/hex.js +46 -0
- package/dist/value/json.d.ts +9 -0
- package/dist/value/json.js +71 -0
- package/dist/value/views.d.ts +22 -0
- package/dist/value/views.js +54 -0
- package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
- package/dist/workbench/DatabaseWorkbench.js +179 -0
- package/dist/workbench/DesignerTab.d.ts +10 -0
- package/dist/workbench/DesignerTab.js +22 -0
- package/dist/workbench/TableTab.d.ts +10 -0
- package/dist/workbench/TableTab.js +8 -0
- package/dist/workbench/tabs.d.ts +42 -0
- package/dist/workbench/tabs.js +139 -0
- package/package.json +79 -3
- package/src/DatabaseProvider.tsx +37 -0
- package/src/actions.ts +34 -0
- package/src/client/client.ts +324 -0
- package/src/client/index.ts +18 -0
- package/src/client/types.ts +161 -0
- package/src/client-context.ts +35 -0
- package/src/connections/ConnectionForm.tsx +474 -0
- package/src/connections/ConnectionManager.tsx +224 -0
- package/src/connections/EngineIcon.tsx +18 -0
- package/src/connections/connection-config.ts +143 -0
- package/src/connections/docker.ts +50 -0
- package/src/connections/engine-icons.ts +5 -0
- package/src/connections/use-docker-containers.ts +41 -0
- package/src/console/DestructiveDialog.tsx +40 -0
- package/src/console/HistoryPanel.tsx +67 -0
- package/src/console/QueryConsole.tsx +439 -0
- package/src/console/SchemaPicker.tsx +32 -0
- package/src/console/SqlEditor.tsx +95 -0
- package/src/console/StatementResultView.tsx +176 -0
- package/src/console/TransactionControls.tsx +53 -0
- package/src/console/destructive.ts +60 -0
- package/src/console/editing.ts +77 -0
- package/src/console/history.ts +62 -0
- package/src/console/labels.ts +7 -0
- package/src/console/outcome.ts +14 -0
- package/src/console/schemas.ts +7 -0
- package/src/console/shortcuts.ts +4 -0
- package/src/console/useConsoleHistory.ts +39 -0
- package/src/ddl/alter-mysql.ts +90 -0
- package/src/ddl/alter-sqlite.ts +93 -0
- package/src/ddl/alter.ts +29 -0
- package/src/ddl/column.ts +74 -0
- package/src/ddl/create.ts +92 -0
- package/src/ddl/dialect.ts +53 -0
- package/src/ddl/diff.ts +164 -0
- package/src/ddl/draft.ts +136 -0
- package/src/ddl/index.ts +10 -0
- package/src/ddl/parse.ts +161 -0
- package/src/ddl/statements.ts +20 -0
- package/src/ddl/types.ts +60 -0
- package/src/ddl/validate.ts +47 -0
- package/src/designer/ColumnsEditor.tsx +204 -0
- package/src/designer/DesignerView.tsx +184 -0
- package/src/designer/ForeignKeysEditor.tsx +132 -0
- package/src/designer/IndexesEditor.tsx +63 -0
- package/src/designer/NamePicker.tsx +44 -0
- package/src/designer/OptionsEditor.tsx +101 -0
- package/src/designer/SqlPreview.tsx +56 -0
- package/src/designer/TableDesigner.tsx +186 -0
- package/src/designer/edit.ts +137 -0
- package/src/designer/editor-props.ts +13 -0
- package/src/designer/plan.ts +25 -0
- package/src/designer/recover.ts +20 -0
- package/src/designer/useReferenceColumns.ts +54 -0
- package/src/explorer/DatabaseExplorer.tsx +450 -0
- package/src/explorer/RowMenu.tsx +134 -0
- package/src/explorer/TableDialog.tsx +80 -0
- package/src/explorer/expansion.ts +47 -0
- package/src/explorer/table-statements.ts +18 -0
- package/src/explorer/tree.ts +453 -0
- package/src/explorer/use-explorer-loads.ts +157 -0
- package/src/explorer/use-stored-expansion.ts +66 -0
- package/src/grid/CellEditor.tsx +53 -0
- package/src/grid/CopyAsMenu.tsx +33 -0
- package/src/grid/DataGrid.tsx +871 -0
- package/src/grid/EnumPicker.tsx +108 -0
- package/src/grid/GridHeaderCell.tsx +243 -0
- package/src/grid/RecordView.tsx +293 -0
- package/src/grid/ValueDock.tsx +52 -0
- package/src/grid/aggregates.ts +94 -0
- package/src/grid/cell-range.ts +26 -0
- package/src/grid/column-layout.ts +71 -0
- package/src/grid/column-selection.ts +42 -0
- package/src/grid/column-view.ts +35 -0
- package/src/grid/copy-formats.ts +108 -0
- package/src/grid/display.ts +109 -0
- package/src/grid/edit-value.ts +60 -0
- package/src/grid/enum-type.ts +62 -0
- package/src/grid/focused-value.ts +23 -0
- package/src/grid/layout.ts +91 -0
- package/src/grid/navigation.ts +74 -0
- package/src/grid/row-selection.ts +35 -0
- package/src/grid/sort.ts +12 -0
- package/src/grid/types.ts +59 -0
- package/src/host/helper-path.ts +41 -0
- package/src/host/host.ts +456 -0
- package/src/host/index.ts +5 -0
- package/src/host/spawn.ts +124 -0
- package/src/host/types.ts +46 -0
- package/src/host/validate.ts +402 -0
- package/src/index.ts +37 -0
- package/src/locales/en.json +577 -0
- package/src/locales/nl.json +577 -0
- package/src/locales.ts +18 -0
- package/src/protocol/connection.ts +86 -0
- package/src/protocol/errors.ts +38 -0
- package/src/protocol/index.ts +33 -0
- package/src/protocol/messages.ts +168 -0
- package/src/protocol/schema.ts +76 -0
- package/src/protocol/values.ts +52 -0
- package/src/sql-split.ts +220 -0
- package/src/sql.ts +152 -0
- package/src/structure/StructureView.tsx +269 -0
- package/src/structure/structure-text.ts +4 -0
- package/src/table/CommandField.tsx +318 -0
- package/src/table/ImportDialog.tsx +168 -0
- package/src/table/TableStatusBar.tsx +91 -0
- package/src/table/TableToolbar.tsx +217 -0
- package/src/table/TableView.tsx +880 -0
- package/src/table/command-field.ts +285 -0
- package/src/table/grid-rows.ts +80 -0
- package/src/table/import-mapping.ts +37 -0
- package/src/table/layout-store.ts +62 -0
- package/src/table/paging.ts +33 -0
- package/src/table/pending.ts +143 -0
- package/src/table/references.ts +38 -0
- package/src/table/useLoaded.ts +58 -0
- package/src/table/useStoredLayout.ts +53 -0
- package/src/table/useTableTransfer.ts +187 -0
- package/src/testing/fake.ts +533 -0
- package/src/testing/index.ts +1 -0
- package/src/use-popup-press.ts +27 -0
- package/src/use-schema-change.ts +19 -0
- package/src/use-stable-callback.ts +12 -0
- package/src/value/ValuePanel.tsx +254 -0
- package/src/value/draft.ts +72 -0
- package/src/value/hex.ts +62 -0
- package/src/value/json.ts +74 -0
- package/src/value/views.ts +68 -0
- package/src/workbench/DatabaseWorkbench.tsx +348 -0
- package/src/workbench/DesignerTab.tsx +37 -0
- package/src/workbench/TableTab.tsx +27 -0
- package/src/workbench/tabs.ts +184 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useRef, useState, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Ban, ChevronDown } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Icon, Menu } from '@adecore/ui';
|
|
6
|
+
import { joinMembers, membersOf, type EnumType } from './enum-type.ts';
|
|
7
|
+
|
|
8
|
+
export interface EnumPickerProps {
|
|
9
|
+
type: EnumType;
|
|
10
|
+
/* The column's value; `null` is NULL. A set is its members joined by commas. */
|
|
11
|
+
value: string | null;
|
|
12
|
+
nullable: boolean;
|
|
13
|
+
/* The accessible name of the trigger. */
|
|
14
|
+
label: string;
|
|
15
|
+
/* Opens the menu as it mounts, for a cell that was just put into edit mode. */
|
|
16
|
+
autoOpen?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
/* The value picked, or `undefined` when the menu closed with nothing changed. */
|
|
19
|
+
onDone(result: { readonly value: string | null } | undefined): void;
|
|
20
|
+
/* `cell` fills the box it sits in; `field` is a boxed field of a form. */
|
|
21
|
+
look?: 'cell' | 'field';
|
|
22
|
+
className?: string;
|
|
23
|
+
ref?: Ref<HTMLButtonElement>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LOOK = {
|
|
27
|
+
cell: 'absolute inset-0 z-10 flex items-center gap-2 border border-accent bg-surface-raised px-3 font-mono text-code outline-0',
|
|
28
|
+
field: 'field field-sm flex w-full items-center gap-2 font-mono text-code data-disabled:opacity-50'
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* The editor of a MySQL ENUM or SET column: a menu of the values the type declares, plus NULL where
|
|
33
|
+
* the column allows it. An enum picks one value and is done; a set toggles its members and is done
|
|
34
|
+
* when the menu closes. It is a menu rather than a `Select` since a set takes several values and a
|
|
35
|
+
* cell has to open it by itself as the edit starts.
|
|
36
|
+
*/
|
|
37
|
+
export function EnumPicker({ type, value, nullable, label, autoOpen = false, disabled = false, onDone, look = 'cell', className, ref }: EnumPickerProps) {
|
|
38
|
+
const { t } = useTranslation('database');
|
|
39
|
+
const settled = useRef(false);
|
|
40
|
+
const [members, setMembers] = useState<ReadonlySet<string>>(() => new Set(type.kind === 'set' && value !== null ? membersOf(value) : []));
|
|
41
|
+
const set = type.kind === 'set';
|
|
42
|
+
|
|
43
|
+
const settle = (result: { readonly value: string | null } | undefined): void => {
|
|
44
|
+
if (!settled.current) {
|
|
45
|
+
settled.current = true;
|
|
46
|
+
onDone(result);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleOpenChange = (open: boolean): void => {
|
|
51
|
+
if (open) {
|
|
52
|
+
settled.current = false;
|
|
53
|
+
setMembers(new Set(set && value !== null ? membersOf(value) : []));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const joined = joinMembers(type, members);
|
|
57
|
+
settle(set && joined !== value ? { value: joined } : undefined);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const toggle = (member: string, checked: boolean): void => {
|
|
61
|
+
setMembers((now) => {
|
|
62
|
+
const next = new Set(now);
|
|
63
|
+
if (checked) {
|
|
64
|
+
next.add(member);
|
|
65
|
+
} else {
|
|
66
|
+
next.delete(member);
|
|
67
|
+
}
|
|
68
|
+
return next;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Menu.Root defaultOpen={autoOpen} onOpenChange={handleOpenChange}>
|
|
74
|
+
<Menu.Trigger ref={ref} disabled={disabled} aria-label={label} className={clsx(LOOK[look], className)}>
|
|
75
|
+
<span className={clsx('min-w-0 flex-1 truncate text-left', value === null && 'text-text-faint')}>{value ?? 'NULL'}</span>
|
|
76
|
+
<Icon icon={ChevronDown} size={14} className="shrink-0 text-text-faint" />
|
|
77
|
+
</Menu.Trigger>
|
|
78
|
+
<Menu.Popup className="max-h-80 overflow-auto">
|
|
79
|
+
{nullable && (
|
|
80
|
+
<>
|
|
81
|
+
<Menu.Item onClick={() => settle({ value: null })}>
|
|
82
|
+
<Icon icon={Ban} size={14} />
|
|
83
|
+
{t('grid.setNull')}
|
|
84
|
+
</Menu.Item>
|
|
85
|
+
<Menu.Separator />
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
{set
|
|
89
|
+
? type.values.map((member) => (
|
|
90
|
+
<Menu.CheckboxItem
|
|
91
|
+
key={member}
|
|
92
|
+
checked={members.has(member)}
|
|
93
|
+
closeOnClick={false}
|
|
94
|
+
onCheckedChange={(checked) => toggle(member, checked)}
|
|
95
|
+
>
|
|
96
|
+
{member === '' ? <span className="text-text-faint">{t('grid.emptyValue')}</span> : member}
|
|
97
|
+
</Menu.CheckboxItem>
|
|
98
|
+
))
|
|
99
|
+
: type.values.map((option) => (
|
|
100
|
+
<Menu.Item key={option} onClick={() => settle({ value: option })}>
|
|
101
|
+
<Menu.Check kind="radio" checked={option === value} />
|
|
102
|
+
{option === '' ? <span className="text-text-faint">{t('grid.emptyValue')}</span> : option}
|
|
103
|
+
</Menu.Item>
|
|
104
|
+
))}
|
|
105
|
+
</Menu.Popup>
|
|
106
|
+
</Menu.Root>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { useRef, type MouseEvent, type PointerEvent } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Key, Pin, PinOff } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { ColumnResizeHandle, ContextMenu, copyText, Icon, IconButton, isApplePlatform, isModHeld, Menu, Tooltip, useColumnResize } from '@adecore/ui';
|
|
6
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import type { SortDirection } from '../sql.ts';
|
|
8
|
+
import type { ColumnClick } from './column-selection.ts';
|
|
9
|
+
import { isNumericKind } from './display.ts';
|
|
10
|
+
import { isDoubleClick, MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.ts';
|
|
11
|
+
import type { GridColumn } from './types.ts';
|
|
12
|
+
|
|
13
|
+
/* What a header can ask the grid for. Every call names the column by its index in the columns the grid was given. */
|
|
14
|
+
export interface HeaderActions {
|
|
15
|
+
/* A click on the head picks its column; Shift extends the pick to a range and Mod toggles the column. */
|
|
16
|
+
onSelect(index: number, click: ColumnClick): void;
|
|
17
|
+
onSortDirection(index: number, direction: SortDirection): void;
|
|
18
|
+
onClearSort(): void;
|
|
19
|
+
onResize(index: number, width: number): void;
|
|
20
|
+
onFit(index: number): void;
|
|
21
|
+
onHide(index: number): void;
|
|
22
|
+
onShowAll(): void;
|
|
23
|
+
onTogglePin(index: number): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GridHeaderCellProps {
|
|
27
|
+
column: GridColumn;
|
|
28
|
+
/* Position among the columns the grid was given, counted from zero. */
|
|
29
|
+
index: number;
|
|
30
|
+
/* Position among the columns drawn, counted from zero. */
|
|
31
|
+
position: number;
|
|
32
|
+
width: number;
|
|
33
|
+
/* Whether the grid offers sorting at all, which its menu then holds. */
|
|
34
|
+
sortable: boolean;
|
|
35
|
+
/* Whether the column is one of those picked; the head then wears the accent. */
|
|
36
|
+
selected: boolean;
|
|
37
|
+
/* The DOM id the grid points `aria-activedescendant` at while columns are picked. */
|
|
38
|
+
id: string;
|
|
39
|
+
sort: { direction: SortDirection; position: number } | null;
|
|
40
|
+
/* Whether more than one column is sorted, which numbers the arrows. */
|
|
41
|
+
multipleSorts: boolean;
|
|
42
|
+
hasSorts: boolean;
|
|
43
|
+
pinned: boolean;
|
|
44
|
+
/* Where a pinned header sticks, counted from the left edge of the scroll box. */
|
|
45
|
+
stickyLeft: number;
|
|
46
|
+
/* The pinned column the others scroll under, which carries the stronger edge. */
|
|
47
|
+
lastPinned: boolean;
|
|
48
|
+
last: boolean;
|
|
49
|
+
hasHidden: boolean;
|
|
50
|
+
canHide: boolean;
|
|
51
|
+
actions: HeaderActions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type MenuProps = Pick<GridHeaderCellProps, 'column' | 'index' | 'sortable' | 'hasSorts' | 'pinned' | 'hasHidden' | 'canHide' | 'actions'>;
|
|
55
|
+
|
|
56
|
+
/* The items of the menu a header opens, from its button and from a right click. */
|
|
57
|
+
function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions }: MenuProps) {
|
|
58
|
+
const { t } = useTranslation('database');
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{sortable && (
|
|
62
|
+
<>
|
|
63
|
+
<Menu.Item onClick={() => actions.onSortDirection(index, 'asc')}>
|
|
64
|
+
<Icon icon={ArrowUp} size={14} />
|
|
65
|
+
{t('grid.column.sortAscending')}
|
|
66
|
+
</Menu.Item>
|
|
67
|
+
<Menu.Item onClick={() => actions.onSortDirection(index, 'desc')}>
|
|
68
|
+
<Icon icon={ArrowDown} size={14} />
|
|
69
|
+
{t('grid.column.sortDescending')}
|
|
70
|
+
</Menu.Item>
|
|
71
|
+
<Menu.Item disabled={!hasSorts} onClick={actions.onClearSort}>
|
|
72
|
+
<Icon icon={ArrowUpDown} size={14} />
|
|
73
|
+
{t('grid.column.clearSorting')}
|
|
74
|
+
</Menu.Item>
|
|
75
|
+
<Menu.Separator />
|
|
76
|
+
</>
|
|
77
|
+
)}
|
|
78
|
+
<Menu.Item disabled={!canHide} onClick={() => actions.onHide(index)}>
|
|
79
|
+
<Icon icon={EyeOff} size={14} />
|
|
80
|
+
{t('grid.column.hide')}
|
|
81
|
+
</Menu.Item>
|
|
82
|
+
{hasHidden && (
|
|
83
|
+
<Menu.Item onClick={actions.onShowAll}>
|
|
84
|
+
<Icon icon={Eye} size={14} />
|
|
85
|
+
{t('grid.column.showAll')}
|
|
86
|
+
</Menu.Item>
|
|
87
|
+
)}
|
|
88
|
+
<Menu.Item onClick={() => actions.onTogglePin(index)}>
|
|
89
|
+
<Icon icon={pinned ? PinOff : Pin} size={14} />
|
|
90
|
+
{t(pinned ? 'grid.column.unpin' : 'grid.column.pin')}
|
|
91
|
+
</Menu.Item>
|
|
92
|
+
<Menu.Separator />
|
|
93
|
+
<Menu.Item onClick={() => copyText(column.name)}>
|
|
94
|
+
<Icon icon={Copy} size={14} />
|
|
95
|
+
{t('grid.column.copyName')}
|
|
96
|
+
</Menu.Item>
|
|
97
|
+
</>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* The facts about a table column that a tooltip can carry: its type, whether it takes NULL, its default. */
|
|
102
|
+
function ColumnFacts({ column }: { column: GridColumn }) {
|
|
103
|
+
const { t } = useTranslation('database');
|
|
104
|
+
const facts = [
|
|
105
|
+
column.type === '' ? null : column.type,
|
|
106
|
+
column.nullable === undefined ? null : t(column.nullable ? 'grid.column.nullable' : 'grid.column.notNull'),
|
|
107
|
+
column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
|
|
108
|
+
column.autoIncrement === true ? t('grid.column.autoIncrement') : null
|
|
109
|
+
].filter((fact) => fact !== null);
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<span className="block">{column.name}</span>
|
|
113
|
+
{facts.length > 0 && <span className="block">{facts.join(', ')}</span>}
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* One column's head: its name, the sort it carries, a key on a primary key column, a menu and the strip that resizes it. Numbers sit against the end, like their cells. */
|
|
119
|
+
export function GridHeaderCell({
|
|
120
|
+
column,
|
|
121
|
+
index,
|
|
122
|
+
position,
|
|
123
|
+
width,
|
|
124
|
+
sortable,
|
|
125
|
+
selected,
|
|
126
|
+
id,
|
|
127
|
+
sort,
|
|
128
|
+
multipleSorts,
|
|
129
|
+
hasSorts,
|
|
130
|
+
pinned,
|
|
131
|
+
stickyLeft,
|
|
132
|
+
lastPinned,
|
|
133
|
+
last,
|
|
134
|
+
hasHidden,
|
|
135
|
+
canHide,
|
|
136
|
+
actions
|
|
137
|
+
}: GridHeaderCellProps) {
|
|
138
|
+
const { t } = useTranslation('database');
|
|
139
|
+
const cell = useRef<HTMLDivElement>(null);
|
|
140
|
+
const lastPress = useRef<number | null>(null);
|
|
141
|
+
/* Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column. */
|
|
142
|
+
const resized = useRef(false);
|
|
143
|
+
const { startResize } = useColumnResize(cell, {
|
|
144
|
+
size: width,
|
|
145
|
+
min: MIN_COLUMN_WIDTH,
|
|
146
|
+
from: 'left',
|
|
147
|
+
max: () => MAX_RESIZED_WIDTH,
|
|
148
|
+
onSize: (size) => actions.onResize(index, size)
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const pressHandle = (event: PointerEvent<HTMLElement>): void => {
|
|
152
|
+
resized.current = true;
|
|
153
|
+
if (isDoubleClick(lastPress.current, event.timeStamp)) {
|
|
154
|
+
lastPress.current = null;
|
|
155
|
+
actions.onFit(index);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
lastPress.current = event.timeStamp;
|
|
159
|
+
startResize(event);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleClick = (event: MouseEvent<HTMLDivElement>): void => {
|
|
163
|
+
const target = event.target as Element;
|
|
164
|
+
// A click inside a menu popup reaches here through the React tree, not the DOM.
|
|
165
|
+
if (!event.currentTarget.contains(target) || target.closest('button') !== null) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (resized.current) {
|
|
169
|
+
resized.current = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
actions.onSelect(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const menuProps = { column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions };
|
|
176
|
+
const numeric = isNumericKind(column.kind);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<ContextMenu.Root>
|
|
180
|
+
<div
|
|
181
|
+
ref={cell}
|
|
182
|
+
id={id}
|
|
183
|
+
role="columnheader"
|
|
184
|
+
aria-colindex={position + 2}
|
|
185
|
+
aria-selected={selected}
|
|
186
|
+
aria-sort={sortable ? (sort === null ? 'none' : sort.direction === 'asc' ? 'ascending' : 'descending') : undefined}
|
|
187
|
+
data-selected={selected ? '' : undefined}
|
|
188
|
+
className={clsx(
|
|
189
|
+
'group/header flex h-full shrink-0 cursor-default text-xs font-medium select-none',
|
|
190
|
+
pinned ? 'sticky z-10' : 'relative',
|
|
191
|
+
selected ? 'bg-accent text-accent-text' : [pinned && 'bg-surface', sort === null ? 'text-text-muted' : 'text-text'],
|
|
192
|
+
last ? 'border-r-0' : 'border-r',
|
|
193
|
+
pinned && lastPinned ? 'border-border-strong' : 'border-border-soft'
|
|
194
|
+
)}
|
|
195
|
+
style={{ width, left: pinned ? stickyLeft : undefined }}
|
|
196
|
+
onPointerDownCapture={() => {
|
|
197
|
+
resized.current = false;
|
|
198
|
+
}}
|
|
199
|
+
onClick={handleClick}
|
|
200
|
+
>
|
|
201
|
+
<ContextMenu.Trigger
|
|
202
|
+
className={clsx(
|
|
203
|
+
'flex h-full min-w-0 flex-1 items-center gap-1.5 overflow-hidden px-3',
|
|
204
|
+
numeric && 'justify-end group-data-[selected]/header:pr-8'
|
|
205
|
+
)}
|
|
206
|
+
>
|
|
207
|
+
{column.primaryKey === true && <Icon icon={Key} size={12} className="shrink-0 text-(--file-icon-yellow)" />}
|
|
208
|
+
<Tooltip label={<ColumnFacts column={column} />} side="bottom">
|
|
209
|
+
<span className="min-w-0 truncate">{column.name}</span>
|
|
210
|
+
</Tooltip>
|
|
211
|
+
{sort !== null && (
|
|
212
|
+
<span className={clsx('flex shrink-0 items-center gap-0.5', selected ? 'text-accent-text' : 'text-text-muted')}>
|
|
213
|
+
<Icon icon={sort.direction === 'asc' ? ArrowUp : ArrowDown} size={12} />
|
|
214
|
+
{multipleSorts && <span className="tabular-nums">{formatNumber(sort.position)}</span>}
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
</ContextMenu.Trigger>
|
|
218
|
+
<Menu.Root>
|
|
219
|
+
<IconButton
|
|
220
|
+
render={<Menu.Trigger />}
|
|
221
|
+
icon={ChevronDown}
|
|
222
|
+
size="xs"
|
|
223
|
+
label={t('grid.column.menu')}
|
|
224
|
+
tooltip={false}
|
|
225
|
+
className={clsx(
|
|
226
|
+
'absolute top-1/2 right-2 -translate-y-1/2',
|
|
227
|
+
selected
|
|
228
|
+
? 'bg-transparent text-accent-text'
|
|
229
|
+
: 'bg-surface opacity-0 group-hover/header:opacity-100 focus-visible:opacity-100 data-[popup-open]:opacity-100'
|
|
230
|
+
)}
|
|
231
|
+
/>
|
|
232
|
+
<Menu.Popup>
|
|
233
|
+
<HeaderMenuItems {...menuProps} />
|
|
234
|
+
</Menu.Popup>
|
|
235
|
+
</Menu.Root>
|
|
236
|
+
<ColumnResizeHandle from="left" onPointerDown={pressHandle} className="hover:bg-border-strong" />
|
|
237
|
+
</div>
|
|
238
|
+
<ContextMenu.Popup>
|
|
239
|
+
<HeaderMenuItems {...menuProps} />
|
|
240
|
+
</ContextMenu.Popup>
|
|
241
|
+
</ContextMenu.Root>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { useMemo, useRef, useState, type KeyboardEvent, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { ArrowUpRight, Ban, ChevronDown, ChevronUp, Copy, RotateCcw } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, IconButton, Input, isApplePlatform, isModHeld, Kbd, shortcut, Spinner } from '@adecore/ui';
|
|
6
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { useNumberNotation } from '../client-context.ts';
|
|
8
|
+
import type { EditValue, Value } from '../protocol/index.ts';
|
|
9
|
+
import { cellView, copyTextOf, isNumericKind, isPreview, numeralText, type NumberNotation } from './display.ts';
|
|
10
|
+
import { draftOf, parseDraft } from './edit-value.ts';
|
|
11
|
+
import { parseEnumType } from './enum-type.ts';
|
|
12
|
+
import { EnumPicker } from './EnumPicker.tsx';
|
|
13
|
+
import type { FocusedCell, GridColumn, GridRow } from './types.ts';
|
|
14
|
+
|
|
15
|
+
export interface RecordViewProps {
|
|
16
|
+
columns: readonly GridColumn[];
|
|
17
|
+
rows: readonly GridRow[];
|
|
18
|
+
/* The row shown, an index into `rows`. */
|
|
19
|
+
index: number;
|
|
20
|
+
onIndexChange(index: number): void;
|
|
21
|
+
/* Lets a field be edited; the rows that are `locked` stay as they are. */
|
|
22
|
+
editable?: boolean;
|
|
23
|
+
onCommit?(rowKey: string, column: number, value: EditValue): void;
|
|
24
|
+
/* The whole value of a field that is only a preview. Resolves `undefined` when it cannot be had. */
|
|
25
|
+
loadValue?(rowKey: string, column: number): Promise<Value | undefined>;
|
|
26
|
+
/* The field that holds the focus changed, or lost it. */
|
|
27
|
+
onFocusedCellChange?(cell: FocusedCell | null): void;
|
|
28
|
+
onFollow?(cell: FocusedCell): void;
|
|
29
|
+
canFollow?(cell: FocusedCell): boolean;
|
|
30
|
+
/* The accessible name of the list of fields. */
|
|
31
|
+
label: string;
|
|
32
|
+
/* Drawn while there are no rows. */
|
|
33
|
+
empty?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
ref?: Ref<HTMLDivElement>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Editing {
|
|
39
|
+
column: number;
|
|
40
|
+
draft: string;
|
|
41
|
+
initial: string;
|
|
42
|
+
loading: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PREVIOUS_ROW = shortcut('Mod+ArrowUp');
|
|
46
|
+
const NEXT_ROW = shortcut('Mod+ArrowDown');
|
|
47
|
+
|
|
48
|
+
const textOf = (cell: GridRow['cells'][number], kind: GridColumn['kind'], notation: NumberNotation, readOnly: boolean): string => {
|
|
49
|
+
if (isPreview(cell)) {
|
|
50
|
+
return cellView(cell, kind, notation).text;
|
|
51
|
+
}
|
|
52
|
+
if (cell !== null && typeof cell === 'object' && cell.kind === 'default') {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
// A field that can be edited holds the server's text, which is what an edit starts from.
|
|
56
|
+
if (readOnly && isNumericKind(kind) && (typeof cell === 'number' || typeof cell === 'string')) {
|
|
57
|
+
return numeralText(cell, notation);
|
|
58
|
+
}
|
|
59
|
+
return draftOf(cell);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* One row as a list of fields, the column's name and type beside an editor for its value. Edits go
|
|
64
|
+
* to the same `onCommit` the grid uses, so they pile up as pending changes there. Up and Down move
|
|
65
|
+
* between the fields; with Mod they move between the rows.
|
|
66
|
+
*/
|
|
67
|
+
export function RecordView({
|
|
68
|
+
columns,
|
|
69
|
+
rows,
|
|
70
|
+
index,
|
|
71
|
+
onIndexChange,
|
|
72
|
+
editable = false,
|
|
73
|
+
onCommit,
|
|
74
|
+
loadValue,
|
|
75
|
+
onFocusedCellChange,
|
|
76
|
+
onFollow,
|
|
77
|
+
canFollow,
|
|
78
|
+
label,
|
|
79
|
+
empty,
|
|
80
|
+
className,
|
|
81
|
+
ref
|
|
82
|
+
}: RecordViewProps) {
|
|
83
|
+
const { t } = useTranslation('database');
|
|
84
|
+
const notation = useNumberNotation();
|
|
85
|
+
const fields = useRef<(HTMLElement | null)[]>([]);
|
|
86
|
+
const [editing, setEditing] = useState<Editing | null>(null);
|
|
87
|
+
const row = rows[index];
|
|
88
|
+
const enumTypes = useMemo(() => columns.map((column) => parseEnumType(column.type)), [columns]);
|
|
89
|
+
const apple = isApplePlatform();
|
|
90
|
+
|
|
91
|
+
const canEdit = (column: number): boolean =>
|
|
92
|
+
editable && row !== undefined && row.locked !== true && row.state !== 'deleted' && columns[column]?.readOnly !== true;
|
|
93
|
+
|
|
94
|
+
const commitEdit = (): void => {
|
|
95
|
+
if (editing !== null && !editing.loading && row !== undefined && editing.draft !== editing.initial) {
|
|
96
|
+
onCommit?.(row.key, editing.column, parseDraft(editing.draft, columns[editing.column]!.kind));
|
|
97
|
+
}
|
|
98
|
+
setEditing(null);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const commitValue = (column: number, value: EditValue): void => {
|
|
102
|
+
if (row !== undefined && canEdit(column)) {
|
|
103
|
+
onCommit?.(row.key, column, value);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const beginEdit = async (column: number): Promise<void> => {
|
|
108
|
+
const cell = row?.cells[column] ?? null;
|
|
109
|
+
if (row === undefined || !canEdit(column) || !isPreview(cell) || loadValue === undefined) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setEditing({ column, draft: '', initial: '', loading: true });
|
|
113
|
+
const value = await loadValue(row.key, column);
|
|
114
|
+
setEditing((now) => {
|
|
115
|
+
if (now === null || !now.loading || now.column !== column) {
|
|
116
|
+
return now;
|
|
117
|
+
}
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const text = draftOf(value);
|
|
122
|
+
return { column, draft: text, initial: text, loading: false };
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const changeDraft = (column: number, draft: string): void => {
|
|
127
|
+
setEditing((now) =>
|
|
128
|
+
now !== null && now.column === column
|
|
129
|
+
? { ...now, draft }
|
|
130
|
+
: { column, draft, initial: textOf(row?.cells[column] ?? null, columns[column]!.kind, 'database', false), loading: false }
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const focusField = (column: number): void => {
|
|
135
|
+
fields.current[column]?.focus();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLElement>, column: number): void => {
|
|
139
|
+
if (event.nativeEvent.isComposing) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const mod = isModHeld(event, apple);
|
|
143
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
144
|
+
event.preventDefault();
|
|
145
|
+
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
146
|
+
if (mod) {
|
|
147
|
+
commitEdit();
|
|
148
|
+
onIndexChange(Math.max(0, Math.min(index + direction, rows.length - 1)));
|
|
149
|
+
} else {
|
|
150
|
+
focusField(Math.max(0, Math.min(column + direction, columns.length - 1)));
|
|
151
|
+
}
|
|
152
|
+
} else if (event.key === 'Enter') {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
commitEdit();
|
|
155
|
+
} else if (event.key === 'Escape' && editing !== null) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
event.stopPropagation();
|
|
158
|
+
setEditing(null);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const cellOf = (column: number): FocusedCell | null => (row === undefined ? null : { rowKey: row.key, column });
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div ref={ref} className={clsx('flex min-h-0 flex-1 flex-col bg-surface', className)}>
|
|
166
|
+
<div className="flex h-10 shrink-0 items-center gap-2 border-b border-border px-3">
|
|
167
|
+
<IconButton
|
|
168
|
+
icon={ChevronUp}
|
|
169
|
+
size="sm"
|
|
170
|
+
label={t('table.record.previous')}
|
|
171
|
+
kbd={PREVIOUS_ROW}
|
|
172
|
+
disabled={index <= 0}
|
|
173
|
+
onClick={() => onIndexChange(index - 1)}
|
|
174
|
+
/>
|
|
175
|
+
<IconButton
|
|
176
|
+
icon={ChevronDown}
|
|
177
|
+
size="sm"
|
|
178
|
+
label={t('table.record.next')}
|
|
179
|
+
kbd={NEXT_ROW}
|
|
180
|
+
disabled={index >= rows.length - 1}
|
|
181
|
+
onClick={() => onIndexChange(index + 1)}
|
|
182
|
+
/>
|
|
183
|
+
{row !== undefined && (
|
|
184
|
+
<>
|
|
185
|
+
<span className="text-sm font-medium text-text tabular-nums">
|
|
186
|
+
{row.number === null ? t('table.record.newRow') : t('table.record.row', { number: formatNumber(row.number) })}
|
|
187
|
+
</span>
|
|
188
|
+
<span className="text-xs text-text-faint tabular-nums">
|
|
189
|
+
{t('table.record.position', { index: formatNumber(index + 1), count: formatNumber(rows.length) })}
|
|
190
|
+
</span>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
{row === undefined ? (
|
|
195
|
+
<p className="px-4 py-6 text-xs text-text-muted">{empty}</p>
|
|
196
|
+
) : (
|
|
197
|
+
<div role="group" aria-label={label} className="min-h-0 flex-1 overflow-auto py-1">
|
|
198
|
+
{columns.map((column, position) => {
|
|
199
|
+
const cell = row.cells[position] ?? null;
|
|
200
|
+
const enumType = enumTypes[position] ?? null;
|
|
201
|
+
const active = editing !== null && editing.column === position;
|
|
202
|
+
const editableField = canEdit(position);
|
|
203
|
+
const preview = isPreview(cell);
|
|
204
|
+
const cellKey = cellOf(position);
|
|
205
|
+
const followable = cellKey !== null && onFollow !== undefined && (canFollow?.(cellKey) ?? true);
|
|
206
|
+
const nullable = column.nullable !== false;
|
|
207
|
+
const placeholder = cell === null ? 'NULL' : typeof cell === 'object' && cell.kind === 'default' ? 'DEFAULT' : undefined;
|
|
208
|
+
return (
|
|
209
|
+
<ContextMenu.Root key={position}>
|
|
210
|
+
<ContextMenu.Trigger
|
|
211
|
+
className={clsx('flex items-center gap-3 px-4 py-1', row.edited?.has(position) && 'bg-accent/10')}
|
|
212
|
+
onFocusCapture={() => onFocusedCellChange?.(cellKey)}
|
|
213
|
+
>
|
|
214
|
+
<span className="flex w-56 shrink-0 items-baseline gap-2 truncate">
|
|
215
|
+
<span className="truncate font-mono text-code text-text">{column.name}</span>
|
|
216
|
+
{column.type !== '' && <span className="truncate text-xs text-text-faint">{column.type}</span>}
|
|
217
|
+
</span>
|
|
218
|
+
<span className="relative flex min-w-0 flex-1 items-center gap-1">
|
|
219
|
+
{enumType !== null ? (
|
|
220
|
+
<EnumPicker
|
|
221
|
+
look="field"
|
|
222
|
+
type={enumType}
|
|
223
|
+
value={typeof cell === 'string' ? cell : null}
|
|
224
|
+
nullable={nullable}
|
|
225
|
+
label={column.name}
|
|
226
|
+
disabled={!editableField}
|
|
227
|
+
ref={(node) => {
|
|
228
|
+
fields.current[position] = node;
|
|
229
|
+
}}
|
|
230
|
+
onDone={(result) => {
|
|
231
|
+
if (result !== undefined && result.value !== (typeof cell === 'string' ? cell : null)) {
|
|
232
|
+
commitValue(position, result.value);
|
|
233
|
+
}
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
) : (
|
|
237
|
+
<Input
|
|
238
|
+
size="sm"
|
|
239
|
+
mono
|
|
240
|
+
spellCheck={false}
|
|
241
|
+
aria-label={column.name}
|
|
242
|
+
ref={(node) => {
|
|
243
|
+
fields.current[position] = node;
|
|
244
|
+
}}
|
|
245
|
+
value={active ? editing.draft : textOf(cell, column.kind, notation, !editableField)}
|
|
246
|
+
placeholder={placeholder}
|
|
247
|
+
readOnly={!editableField || (preview && (!active || editing.loading))}
|
|
248
|
+
className={clsx('min-w-0 flex-1', row.state === 'deleted' && 'line-through')}
|
|
249
|
+
onFocus={() => void beginEdit(position)}
|
|
250
|
+
onBlur={commitEdit}
|
|
251
|
+
onChange={(event) => changeDraft(position, event.target.value)}
|
|
252
|
+
onKeyDown={(event) => handleKeyDown(event, position)}
|
|
253
|
+
/>
|
|
254
|
+
)}
|
|
255
|
+
{active && editing.loading && <Spinner size={12} label={t('grid.loadingValue')} className="absolute right-2" />}
|
|
256
|
+
{followable && (
|
|
257
|
+
<IconButton
|
|
258
|
+
icon={ArrowUpRight}
|
|
259
|
+
size="xs"
|
|
260
|
+
label={t('grid.followReference')}
|
|
261
|
+
onClick={() => cellKey !== null && onFollow?.(cellKey)}
|
|
262
|
+
/>
|
|
263
|
+
)}
|
|
264
|
+
</span>
|
|
265
|
+
</ContextMenu.Trigger>
|
|
266
|
+
<ContextMenu.Popup>
|
|
267
|
+
<ContextMenu.Item onClick={() => copyText(copyTextOf(cell))}>
|
|
268
|
+
<Icon icon={Copy} size={14} />
|
|
269
|
+
{t('grid.copyValue')}
|
|
270
|
+
<Kbd shortcut={EDIT_SHORTCUTS.copy} />
|
|
271
|
+
</ContextMenu.Item>
|
|
272
|
+
{editable && (
|
|
273
|
+
<>
|
|
274
|
+
<ContextMenu.Separator />
|
|
275
|
+
<ContextMenu.Item disabled={!editableField || !nullable} onClick={() => commitValue(position, null)}>
|
|
276
|
+
<Icon icon={Ban} size={14} />
|
|
277
|
+
{t('grid.setNull')}
|
|
278
|
+
</ContextMenu.Item>
|
|
279
|
+
<ContextMenu.Item disabled={!editableField} onClick={() => commitValue(position, { kind: 'default' })}>
|
|
280
|
+
<Icon icon={RotateCcw} size={14} />
|
|
281
|
+
{t('grid.setDefault')}
|
|
282
|
+
</ContextMenu.Item>
|
|
283
|
+
</>
|
|
284
|
+
)}
|
|
285
|
+
</ContextMenu.Popup>
|
|
286
|
+
</ContextMenu.Root>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|