@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,69 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Key, Pin, PinOff } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { ColumnResizeHandle, ContextMenu, copyText, Icon, IconButton, isApplePlatform, isModHeld, Menu, Tooltip, useColumnResize } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { isNumericKind } from './display.js';
|
|
9
|
+
import { isDoubleClick, MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
|
|
10
|
+
/* The items of the menu a header opens, from its button and from a right click. */
|
|
11
|
+
function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions }) {
|
|
12
|
+
const { t } = useTranslation('database');
|
|
13
|
+
return (_jsxs(_Fragment, { children: [sortable && (_jsxs(_Fragment, { children: [_jsxs(Menu.Item, { onClick: () => actions.onSortDirection(index, 'asc'), children: [_jsx(Icon, { icon: ArrowUp, size: 14 }), t('grid.column.sortAscending')] }), _jsxs(Menu.Item, { onClick: () => actions.onSortDirection(index, 'desc'), children: [_jsx(Icon, { icon: ArrowDown, size: 14 }), t('grid.column.sortDescending')] }), _jsxs(Menu.Item, { disabled: !hasSorts, onClick: actions.onClearSort, children: [_jsx(Icon, { icon: ArrowUpDown, size: 14 }), t('grid.column.clearSorting')] }), _jsx(Menu.Separator, {})] })), _jsxs(Menu.Item, { disabled: !canHide, onClick: () => actions.onHide(index), children: [_jsx(Icon, { icon: EyeOff, size: 14 }), t('grid.column.hide')] }), hasHidden && (_jsxs(Menu.Item, { onClick: actions.onShowAll, children: [_jsx(Icon, { icon: Eye, size: 14 }), t('grid.column.showAll')] })), _jsxs(Menu.Item, { onClick: () => actions.onTogglePin(index), children: [_jsx(Icon, { icon: pinned ? PinOff : Pin, size: 14 }), t(pinned ? 'grid.column.unpin' : 'grid.column.pin')] }), _jsx(Menu.Separator, {}), _jsxs(Menu.Item, { onClick: () => copyText(column.name), children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.column.copyName')] })] }));
|
|
14
|
+
}
|
|
15
|
+
/* The facts about a table column that a tooltip can carry: its type, whether it takes NULL, its default. */
|
|
16
|
+
function ColumnFacts({ column }) {
|
|
17
|
+
const { t } = useTranslation('database');
|
|
18
|
+
const facts = [
|
|
19
|
+
column.type === '' ? null : column.type,
|
|
20
|
+
column.nullable === undefined ? null : t(column.nullable ? 'grid.column.nullable' : 'grid.column.notNull'),
|
|
21
|
+
column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
|
|
22
|
+
column.autoIncrement === true ? t('grid.column.autoIncrement') : null
|
|
23
|
+
].filter((fact) => fact !== null);
|
|
24
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: column.name }), facts.length > 0 && _jsx("span", { className: "block", children: facts.join(', ') })] }));
|
|
25
|
+
}
|
|
26
|
+
/* 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. */
|
|
27
|
+
export function GridHeaderCell({ column, index, position, width, sortable, selected, id, sort, multipleSorts, hasSorts, pinned, stickyLeft, lastPinned, last, hasHidden, canHide, actions }) {
|
|
28
|
+
const { t } = useTranslation('database');
|
|
29
|
+
const cell = useRef(null);
|
|
30
|
+
const lastPress = useRef(null);
|
|
31
|
+
/* Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column. */
|
|
32
|
+
const resized = useRef(false);
|
|
33
|
+
const { startResize } = useColumnResize(cell, {
|
|
34
|
+
size: width,
|
|
35
|
+
min: MIN_COLUMN_WIDTH,
|
|
36
|
+
from: 'left',
|
|
37
|
+
max: () => MAX_RESIZED_WIDTH,
|
|
38
|
+
onSize: (size) => actions.onResize(index, size)
|
|
39
|
+
});
|
|
40
|
+
const pressHandle = (event) => {
|
|
41
|
+
resized.current = true;
|
|
42
|
+
if (isDoubleClick(lastPress.current, event.timeStamp)) {
|
|
43
|
+
lastPress.current = null;
|
|
44
|
+
actions.onFit(index);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
lastPress.current = event.timeStamp;
|
|
48
|
+
startResize(event);
|
|
49
|
+
};
|
|
50
|
+
const handleClick = (event) => {
|
|
51
|
+
const target = event.target;
|
|
52
|
+
// A click inside a menu popup reaches here through the React tree, not the DOM.
|
|
53
|
+
if (!event.currentTarget.contains(target) || target.closest('button') !== null) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (resized.current) {
|
|
57
|
+
resized.current = false;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
actions.onSelect(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
|
|
61
|
+
};
|
|
62
|
+
const menuProps = { column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions };
|
|
63
|
+
const numeric = isNumericKind(column.kind);
|
|
64
|
+
return (_jsxs(ContextMenu.Root, { children: [_jsxs("div", { ref: cell, id: id, role: "columnheader", "aria-colindex": position + 2, "aria-selected": selected, "aria-sort": sortable ? (sort === null ? 'none' : sort.direction === 'asc' ? 'ascending' : 'descending') : undefined, "data-selected": selected ? '' : undefined, className: clsx('group/header flex h-full shrink-0 cursor-default text-xs font-medium select-none', pinned ? 'sticky z-10' : 'relative', selected ? 'bg-accent text-accent-text' : [pinned && 'bg-surface', sort === null ? 'text-text-muted' : 'text-text'], last ? 'border-r-0' : 'border-r', pinned && lastPinned ? 'border-border-strong' : 'border-border-soft'), style: { width, left: pinned ? stickyLeft : undefined }, onPointerDownCapture: () => {
|
|
65
|
+
resized.current = false;
|
|
66
|
+
}, onClick: handleClick, children: [_jsxs(ContextMenu.Trigger, { className: clsx('flex h-full min-w-0 flex-1 items-center gap-1.5 overflow-hidden px-3', numeric && 'justify-end group-data-[selected]/header:pr-8'), children: [column.primaryKey === true && _jsx(Icon, { icon: Key, size: 12, className: "shrink-0 text-(--file-icon-yellow)" }), _jsx(Tooltip, { label: _jsx(ColumnFacts, { column: column }), side: "bottom", children: _jsx("span", { className: "min-w-0 truncate", children: column.name }) }), sort !== null && (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-0.5', selected ? 'text-accent-text' : 'text-text-muted'), children: [_jsx(Icon, { icon: sort.direction === 'asc' ? ArrowUp : ArrowDown, size: 12 }), multipleSorts && _jsx("span", { className: "tabular-nums", children: formatNumber(sort.position) })] }))] }), _jsxs(Menu.Root, { children: [_jsx(IconButton, { render: _jsx(Menu.Trigger, {}), icon: ChevronDown, size: "xs", label: t('grid.column.menu'), tooltip: false, className: clsx('absolute top-1/2 right-2 -translate-y-1/2', selected
|
|
67
|
+
? 'bg-transparent text-accent-text'
|
|
68
|
+
: 'bg-surface opacity-0 group-hover/header:opacity-100 focus-visible:opacity-100 data-[popup-open]:opacity-100') }), _jsx(Menu.Popup, { children: _jsx(HeaderMenuItems, { ...menuProps }) })] }), _jsx(ColumnResizeHandle, { from: "left", onPointerDown: pressHandle, className: "hover:bg-border-strong" })] }), _jsx(ContextMenu.Popup, { children: _jsx(HeaderMenuItems, { ...menuProps }) })] }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Ref } from 'react';
|
|
2
|
+
import type { EditValue, Value } from '../protocol/index.ts';
|
|
3
|
+
import type { FocusedCell, GridColumn, GridRow } from './types.ts';
|
|
4
|
+
export interface RecordViewProps {
|
|
5
|
+
columns: readonly GridColumn[];
|
|
6
|
+
rows: readonly GridRow[];
|
|
7
|
+
index: number;
|
|
8
|
+
onIndexChange(index: number): void;
|
|
9
|
+
editable?: boolean;
|
|
10
|
+
onCommit?(rowKey: string, column: number, value: EditValue): void;
|
|
11
|
+
loadValue?(rowKey: string, column: number): Promise<Value | undefined>;
|
|
12
|
+
onFocusedCellChange?(cell: FocusedCell | null): void;
|
|
13
|
+
onFollow?(cell: FocusedCell): void;
|
|
14
|
+
canFollow?(cell: FocusedCell): boolean;
|
|
15
|
+
label: string;
|
|
16
|
+
empty?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
ref?: Ref<HTMLDivElement>;
|
|
19
|
+
}
|
|
20
|
+
export declare function RecordView({ columns, rows, index, onIndexChange, editable, onCommit, loadValue, onFocusedCellChange, onFollow, canFollow, label, empty, className, ref }: RecordViewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useRef, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ArrowUpRight, Ban, ChevronDown, ChevronUp, Copy, RotateCcw } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, IconButton, Input, isApplePlatform, isModHeld, Kbd, shortcut, Spinner } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { useNumberNotation } from '../client-context.js';
|
|
9
|
+
import { cellView, copyTextOf, isNumericKind, isPreview, numeralText } from './display.js';
|
|
10
|
+
import { draftOf, parseDraft } from './edit-value.js';
|
|
11
|
+
import { parseEnumType } from './enum-type.js';
|
|
12
|
+
import { EnumPicker } from './EnumPicker.js';
|
|
13
|
+
const PREVIOUS_ROW = shortcut('Mod+ArrowUp');
|
|
14
|
+
const NEXT_ROW = shortcut('Mod+ArrowDown');
|
|
15
|
+
const textOf = (cell, kind, notation, readOnly) => {
|
|
16
|
+
if (isPreview(cell)) {
|
|
17
|
+
return cellView(cell, kind, notation).text;
|
|
18
|
+
}
|
|
19
|
+
if (cell !== null && typeof cell === 'object' && cell.kind === 'default') {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
// A field that can be edited holds the server's text, which is what an edit starts from.
|
|
23
|
+
if (readOnly && isNumericKind(kind) && (typeof cell === 'number' || typeof cell === 'string')) {
|
|
24
|
+
return numeralText(cell, notation);
|
|
25
|
+
}
|
|
26
|
+
return draftOf(cell);
|
|
27
|
+
};
|
|
28
|
+
/*
|
|
29
|
+
* One row as a list of fields, the column's name and type beside an editor for its value. Edits go
|
|
30
|
+
* to the same `onCommit` the grid uses, so they pile up as pending changes there. Up and Down move
|
|
31
|
+
* between the fields; with Mod they move between the rows.
|
|
32
|
+
*/
|
|
33
|
+
export function RecordView({ columns, rows, index, onIndexChange, editable = false, onCommit, loadValue, onFocusedCellChange, onFollow, canFollow, label, empty, className, ref }) {
|
|
34
|
+
const { t } = useTranslation('database');
|
|
35
|
+
const notation = useNumberNotation();
|
|
36
|
+
const fields = useRef([]);
|
|
37
|
+
const [editing, setEditing] = useState(null);
|
|
38
|
+
const row = rows[index];
|
|
39
|
+
const enumTypes = useMemo(() => columns.map((column) => parseEnumType(column.type)), [columns]);
|
|
40
|
+
const apple = isApplePlatform();
|
|
41
|
+
const canEdit = (column) => editable && row !== undefined && row.locked !== true && row.state !== 'deleted' && columns[column]?.readOnly !== true;
|
|
42
|
+
const commitEdit = () => {
|
|
43
|
+
if (editing !== null && !editing.loading && row !== undefined && editing.draft !== editing.initial) {
|
|
44
|
+
onCommit?.(row.key, editing.column, parseDraft(editing.draft, columns[editing.column].kind));
|
|
45
|
+
}
|
|
46
|
+
setEditing(null);
|
|
47
|
+
};
|
|
48
|
+
const commitValue = (column, value) => {
|
|
49
|
+
if (row !== undefined && canEdit(column)) {
|
|
50
|
+
onCommit?.(row.key, column, value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const beginEdit = async (column) => {
|
|
54
|
+
const cell = row?.cells[column] ?? null;
|
|
55
|
+
if (row === undefined || !canEdit(column) || !isPreview(cell) || loadValue === undefined) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
setEditing({ column, draft: '', initial: '', loading: true });
|
|
59
|
+
const value = await loadValue(row.key, column);
|
|
60
|
+
setEditing((now) => {
|
|
61
|
+
if (now === null || !now.loading || now.column !== column) {
|
|
62
|
+
return now;
|
|
63
|
+
}
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const text = draftOf(value);
|
|
68
|
+
return { column, draft: text, initial: text, loading: false };
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const changeDraft = (column, draft) => {
|
|
72
|
+
setEditing((now) => now !== null && now.column === column
|
|
73
|
+
? { ...now, draft }
|
|
74
|
+
: { column, draft, initial: textOf(row?.cells[column] ?? null, columns[column].kind, 'database', false), loading: false });
|
|
75
|
+
};
|
|
76
|
+
const focusField = (column) => {
|
|
77
|
+
fields.current[column]?.focus();
|
|
78
|
+
};
|
|
79
|
+
const handleKeyDown = (event, column) => {
|
|
80
|
+
if (event.nativeEvent.isComposing) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const mod = isModHeld(event, apple);
|
|
84
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
87
|
+
if (mod) {
|
|
88
|
+
commitEdit();
|
|
89
|
+
onIndexChange(Math.max(0, Math.min(index + direction, rows.length - 1)));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
focusField(Math.max(0, Math.min(column + direction, columns.length - 1)));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (event.key === 'Enter') {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
commitEdit();
|
|
98
|
+
}
|
|
99
|
+
else if (event.key === 'Escape' && editing !== null) {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
setEditing(null);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const cellOf = (column) => (row === undefined ? null : { rowKey: row.key, column });
|
|
106
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-1 flex-col bg-surface', className), children: [_jsxs("div", { className: "flex h-10 shrink-0 items-center gap-2 border-b border-border px-3", children: [_jsx(IconButton, { icon: ChevronUp, size: "sm", label: t('table.record.previous'), kbd: PREVIOUS_ROW, disabled: index <= 0, onClick: () => onIndexChange(index - 1) }), _jsx(IconButton, { icon: ChevronDown, size: "sm", label: t('table.record.next'), kbd: NEXT_ROW, disabled: index >= rows.length - 1, onClick: () => onIndexChange(index + 1) }), row !== undefined && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-sm font-medium text-text tabular-nums", children: row.number === null ? t('table.record.newRow') : t('table.record.row', { number: formatNumber(row.number) }) }), _jsx("span", { className: "text-xs text-text-faint tabular-nums", children: t('table.record.position', { index: formatNumber(index + 1), count: formatNumber(rows.length) }) })] }))] }), row === undefined ? (_jsx("p", { className: "px-4 py-6 text-xs text-text-muted", children: empty })) : (_jsx("div", { role: "group", "aria-label": label, className: "min-h-0 flex-1 overflow-auto py-1", children: columns.map((column, position) => {
|
|
107
|
+
const cell = row.cells[position] ?? null;
|
|
108
|
+
const enumType = enumTypes[position] ?? null;
|
|
109
|
+
const active = editing !== null && editing.column === position;
|
|
110
|
+
const editableField = canEdit(position);
|
|
111
|
+
const preview = isPreview(cell);
|
|
112
|
+
const cellKey = cellOf(position);
|
|
113
|
+
const followable = cellKey !== null && onFollow !== undefined && (canFollow?.(cellKey) ?? true);
|
|
114
|
+
const nullable = column.nullable !== false;
|
|
115
|
+
const placeholder = cell === null ? 'NULL' : typeof cell === 'object' && cell.kind === 'default' ? 'DEFAULT' : undefined;
|
|
116
|
+
return (_jsxs(ContextMenu.Root, { children: [_jsxs(ContextMenu.Trigger, { className: clsx('flex items-center gap-3 px-4 py-1', row.edited?.has(position) && 'bg-accent/10'), onFocusCapture: () => onFocusedCellChange?.(cellKey), children: [_jsxs("span", { className: "flex w-56 shrink-0 items-baseline gap-2 truncate", children: [_jsx("span", { className: "truncate font-mono text-code text-text", children: column.name }), column.type !== '' && _jsx("span", { className: "truncate text-xs text-text-faint", children: column.type })] }), _jsxs("span", { className: "relative flex min-w-0 flex-1 items-center gap-1", children: [enumType !== null ? (_jsx(EnumPicker, { look: "field", type: enumType, value: typeof cell === 'string' ? cell : null, nullable: nullable, label: column.name, disabled: !editableField, ref: (node) => {
|
|
117
|
+
fields.current[position] = node;
|
|
118
|
+
}, onDone: (result) => {
|
|
119
|
+
if (result !== undefined && result.value !== (typeof cell === 'string' ? cell : null)) {
|
|
120
|
+
commitValue(position, result.value);
|
|
121
|
+
}
|
|
122
|
+
} })) : (_jsx(Input, { size: "sm", mono: true, spellCheck: false, "aria-label": column.name, ref: (node) => {
|
|
123
|
+
fields.current[position] = node;
|
|
124
|
+
}, value: active ? editing.draft : textOf(cell, column.kind, notation, !editableField), placeholder: placeholder, readOnly: !editableField || (preview && (!active || editing.loading)), className: clsx('min-w-0 flex-1', row.state === 'deleted' && 'line-through'), onFocus: () => void beginEdit(position), onBlur: commitEdit, onChange: (event) => changeDraft(position, event.target.value), onKeyDown: (event) => handleKeyDown(event, position) })), active && editing.loading && _jsx(Spinner, { size: 12, label: t('grid.loadingValue'), className: "absolute right-2" }), followable && (_jsx(IconButton, { icon: ArrowUpRight, size: "xs", label: t('grid.followReference'), onClick: () => cellKey !== null && onFollow?.(cellKey) }))] })] }), _jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { onClick: () => copyText(copyTextOf(cell)), children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.copyValue'), _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), editable && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), _jsxs(ContextMenu.Item, { disabled: !editableField || !nullable, onClick: () => commitValue(position, null), children: [_jsx(Icon, { icon: Ban, size: 14 }), t('grid.setNull')] }), _jsxs(ContextMenu.Item, { disabled: !editableField, onClick: () => commitValue(position, { kind: 'default' }), children: [_jsx(Icon, { icon: RotateCcw, size: 14 }), t('grid.setDefault')] })] }))] })] }, position));
|
|
125
|
+
}) }))] }));
|
|
126
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
|
+
export declare const VALUE_PANEL_WIDTH = 320;
|
|
3
|
+
export declare const VALUE_PANEL_MIN_WIDTH = 240;
|
|
4
|
+
export interface ValueDockProps {
|
|
5
|
+
open: boolean;
|
|
6
|
+
panel: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
ref?: Ref<HTMLDivElement>;
|
|
10
|
+
}
|
|
11
|
+
export declare function ValueDock({ open, panel, children, className, ref }: ValueDockProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { ColumnResizeHandle, useColumnResize } from '@adecore/ui';
|
|
5
|
+
export const VALUE_PANEL_WIDTH = 320;
|
|
6
|
+
export const VALUE_PANEL_MIN_WIDTH = 240;
|
|
7
|
+
/* A grid with a panel along its right edge that a person can widen by its left edge. */
|
|
8
|
+
export function ValueDock({ open, panel, children, className, ref }) {
|
|
9
|
+
const root = useRef(null);
|
|
10
|
+
const dock = useRef(null);
|
|
11
|
+
const [width, setWidth] = useState(VALUE_PANEL_WIDTH);
|
|
12
|
+
const { startResize } = useColumnResize(dock, {
|
|
13
|
+
size: width,
|
|
14
|
+
min: VALUE_PANEL_MIN_WIDTH,
|
|
15
|
+
from: 'right',
|
|
16
|
+
max: () => (root.current?.clientWidth ?? 960) - VALUE_PANEL_MIN_WIDTH,
|
|
17
|
+
onSize: setWidth
|
|
18
|
+
});
|
|
19
|
+
return (_jsxs("div", { ref: (node) => {
|
|
20
|
+
root.current = node;
|
|
21
|
+
if (typeof ref === 'function') {
|
|
22
|
+
ref(node);
|
|
23
|
+
}
|
|
24
|
+
else if (ref) {
|
|
25
|
+
ref.current = node;
|
|
26
|
+
}
|
|
27
|
+
}, className: clsx('flex min-h-0 min-w-0 flex-1', className), children: [_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: children }), open && (_jsxs("div", { ref: dock, className: "relative flex min-h-0 shrink-0 border-l border-border", style: { width }, children: [_jsx(ColumnResizeHandle, { from: "right", onPointerDown: startResize, className: "hover:bg-border-strong" }), panel] }))] }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ValueKind } from '../protocol/index.ts';
|
|
2
|
+
import type { Shown } from './display.ts';
|
|
3
|
+
export interface RangeBlock {
|
|
4
|
+
readonly top: number;
|
|
5
|
+
readonly bottom: number;
|
|
6
|
+
readonly columns: readonly number[];
|
|
7
|
+
}
|
|
8
|
+
export interface NumericAggregates {
|
|
9
|
+
readonly count: number;
|
|
10
|
+
readonly sum: number;
|
|
11
|
+
readonly average: number;
|
|
12
|
+
readonly minimum: number;
|
|
13
|
+
readonly maximum: number;
|
|
14
|
+
readonly minimumText: string;
|
|
15
|
+
readonly maximumText: string;
|
|
16
|
+
}
|
|
17
|
+
export interface Aggregates {
|
|
18
|
+
readonly count: number;
|
|
19
|
+
readonly numeric: NumericAggregates | null;
|
|
20
|
+
}
|
|
21
|
+
export declare const blockSize: (block: RangeBlock) => number;
|
|
22
|
+
export declare const aggregateBlock: (rows: readonly (readonly Shown[])[], kinds: readonly ValueKind[], block: RangeBlock) => Aggregates;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
|
|
2
|
+
const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
3
|
+
const numberOf = (cell) => {
|
|
4
|
+
if (typeof cell === 'number') {
|
|
5
|
+
return Number.isFinite(cell) ? { value: cell, text: String(cell) } : null;
|
|
6
|
+
}
|
|
7
|
+
if (typeof cell === 'string') {
|
|
8
|
+
const text = cell.trim();
|
|
9
|
+
return NUMERIC_TEXT.test(text) ? { value: Number(text), text } : null;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
/* The size of a block in cells, which a footer shows aggregates for once it passes one. */
|
|
14
|
+
export const blockSize = (block) => Math.max(0, block.bottom - block.top + 1) * block.columns.length;
|
|
15
|
+
/*
|
|
16
|
+
* The count of a block, and for the cells in numeric columns their sum, average, minimum and maximum.
|
|
17
|
+
* Only the rows the grid has are counted, which is the loaded page.
|
|
18
|
+
*/
|
|
19
|
+
export const aggregateBlock = (rows, kinds, block) => {
|
|
20
|
+
let count = 0;
|
|
21
|
+
let numbers = 0;
|
|
22
|
+
let sum = 0;
|
|
23
|
+
let minimum = null;
|
|
24
|
+
let maximum = null;
|
|
25
|
+
for (let row = block.top; row <= block.bottom; row++) {
|
|
26
|
+
const cells = rows[row];
|
|
27
|
+
if (cells === undefined) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const column of block.columns) {
|
|
31
|
+
count++;
|
|
32
|
+
const kind = kinds[column];
|
|
33
|
+
const found = kind !== undefined && NUMERIC_KINDS.has(kind) ? numberOf(cells[column] ?? null) : null;
|
|
34
|
+
if (found === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
numbers++;
|
|
38
|
+
sum += found.value;
|
|
39
|
+
if (minimum === null || found.value < minimum.value) {
|
|
40
|
+
minimum = found;
|
|
41
|
+
}
|
|
42
|
+
if (maximum === null || found.value > maximum.value) {
|
|
43
|
+
maximum = found;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (numbers === 0 || minimum === null || maximum === null) {
|
|
48
|
+
return { count, numeric: null };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
count,
|
|
52
|
+
numeric: {
|
|
53
|
+
count: numbers,
|
|
54
|
+
sum,
|
|
55
|
+
average: sum / numbers,
|
|
56
|
+
minimum: minimum.value,
|
|
57
|
+
maximum: maximum.value,
|
|
58
|
+
minimumText: minimum.text,
|
|
59
|
+
maximumText: maximum.text
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CellPosition } from './navigation.ts';
|
|
2
|
+
export interface CellRect {
|
|
3
|
+
readonly top: number;
|
|
4
|
+
readonly bottom: number;
|
|
5
|
+
readonly left: number;
|
|
6
|
+
readonly right: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const rectBetween: (from: CellPosition, to: CellPosition) => CellRect;
|
|
9
|
+
export declare const rectContains: (rect: CellRect | null, row: number, column: number) => boolean;
|
|
10
|
+
export declare const fullRect: (rows: number, columns: number) => CellRect | null;
|
|
11
|
+
export declare const rectSize: (rect: CellRect) => {
|
|
12
|
+
rows: number;
|
|
13
|
+
columns: number;
|
|
14
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* The block between two cells, whichever corner each one is. */
|
|
2
|
+
export const rectBetween = (from, to) => ({
|
|
3
|
+
top: Math.min(from.row, to.row),
|
|
4
|
+
bottom: Math.max(from.row, to.row),
|
|
5
|
+
left: Math.min(from.column, to.column),
|
|
6
|
+
right: Math.max(from.column, to.column)
|
|
7
|
+
});
|
|
8
|
+
export const rectContains = (rect, row, column) => rect !== null && row >= rect.top && row <= rect.bottom && column >= rect.left && column <= rect.right;
|
|
9
|
+
/* Every cell of a grid, or `null` when it has none. */
|
|
10
|
+
export const fullRect = (rows, columns) => rows === 0 || columns === 0 ? null : { top: 0, bottom: rows - 1, left: 0, right: columns - 1 };
|
|
11
|
+
export const rectSize = (rect) => ({ rows: rect.bottom - rect.top + 1, columns: rect.right - rect.left + 1 });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ColumnView } from './column-view.ts';
|
|
2
|
+
export interface GridLayout {
|
|
3
|
+
readonly widths: Readonly<Record<string, number>>;
|
|
4
|
+
readonly hidden: readonly string[];
|
|
5
|
+
readonly pinned: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const NO_GRID_LAYOUT: GridLayout;
|
|
8
|
+
export interface ColumnState {
|
|
9
|
+
readonly resized: Readonly<Record<number, number>>;
|
|
10
|
+
readonly view: ColumnView;
|
|
11
|
+
}
|
|
12
|
+
export declare const restoreLayout: (columns: readonly {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
}[], layout: GridLayout | undefined) => ColumnState;
|
|
15
|
+
export declare const captureLayout: (columns: readonly {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
}[], state: ColumnState) => GridLayout;
|
|
18
|
+
export declare const sameLayout: (a: GridLayout, b: GridLayout) => boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
|
|
2
|
+
export const NO_GRID_LAYOUT = { widths: {}, hidden: [], pinned: [] };
|
|
3
|
+
/* The state of a grid for columns it has, from a layout. Names it does not have are skipped; a layout that would hide every column hides none. */
|
|
4
|
+
export const restoreLayout = (columns, layout) => {
|
|
5
|
+
const resized = {};
|
|
6
|
+
const hidden = new Set();
|
|
7
|
+
const pinned = [];
|
|
8
|
+
if (layout !== undefined) {
|
|
9
|
+
columns.forEach((column, index) => {
|
|
10
|
+
const width = layout.widths[column.name];
|
|
11
|
+
if (width !== undefined) {
|
|
12
|
+
resized[index] = Math.max(MIN_COLUMN_WIDTH, Math.min(Math.round(width), MAX_RESIZED_WIDTH));
|
|
13
|
+
}
|
|
14
|
+
if (layout.hidden.includes(column.name)) {
|
|
15
|
+
hidden.add(index);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
for (const name of layout.pinned) {
|
|
19
|
+
const index = columns.findIndex((column) => column.name === name);
|
|
20
|
+
if (index >= 0 && !hidden.has(index) && !pinned.includes(index)) {
|
|
21
|
+
pinned.push(index);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { resized, view: { hidden: hidden.size >= columns.length ? new Set() : hidden, pinned } };
|
|
26
|
+
};
|
|
27
|
+
/* The layout of a grid's state, with the columns by name. */
|
|
28
|
+
export const captureLayout = (columns, state) => {
|
|
29
|
+
const widths = {};
|
|
30
|
+
for (const [index, width] of Object.entries(state.resized)) {
|
|
31
|
+
const column = columns[Number(index)];
|
|
32
|
+
if (column !== undefined) {
|
|
33
|
+
widths[column.name] = width;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
widths,
|
|
38
|
+
hidden: [...state.view.hidden].sort((a, b) => a - b).flatMap((index) => (columns[index] === undefined ? [] : [columns[index].name])),
|
|
39
|
+
pinned: state.view.pinned.flatMap((index) => (columns[index] === undefined ? [] : [columns[index].name]))
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const sameList = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
|
|
43
|
+
export const sameLayout = (a, b) => {
|
|
44
|
+
const names = Object.keys(a.widths);
|
|
45
|
+
return (names.length === Object.keys(b.widths).length &&
|
|
46
|
+
names.every((name) => a.widths[name] === b.widths[name]) &&
|
|
47
|
+
sameList(a.hidden, b.hidden) &&
|
|
48
|
+
sameList(a.pinned, b.pinned));
|
|
49
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ColumnSelection {
|
|
2
|
+
readonly columns: ReadonlySet<number>;
|
|
3
|
+
readonly anchor: number | null;
|
|
4
|
+
}
|
|
5
|
+
export interface ColumnClick {
|
|
6
|
+
readonly shiftKey: boolean;
|
|
7
|
+
readonly mod: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const NO_COLUMN_SELECTION: ColumnSelection;
|
|
10
|
+
export declare const selectColumn: (current: ColumnSelection, order: readonly number[], index: number, click: ColumnClick) => ColumnSelection;
|
|
11
|
+
export declare const shownSelection: (current: ColumnSelection, order: readonly number[]) => number[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const NO_COLUMN_SELECTION = { columns: new Set(), anchor: null };
|
|
2
|
+
/*
|
|
3
|
+
* The selection after a click on the header of `index`. A plain click picks that column alone, Mod
|
|
4
|
+
* toggles it, and Shift picks every column drawn between the anchor and it (added to the others with Mod).
|
|
5
|
+
* `order` is the columns as drawn, so a range follows what the person sees.
|
|
6
|
+
*/
|
|
7
|
+
export const selectColumn = (current, order, index, click) => {
|
|
8
|
+
const at = order.indexOf(index);
|
|
9
|
+
if (at < 0) {
|
|
10
|
+
return current;
|
|
11
|
+
}
|
|
12
|
+
const anchorAt = current.anchor === null ? -1 : order.indexOf(current.anchor);
|
|
13
|
+
if (click.shiftKey && anchorAt >= 0) {
|
|
14
|
+
const range = order.slice(Math.min(anchorAt, at), Math.max(anchorAt, at) + 1);
|
|
15
|
+
return { columns: new Set(click.mod ? [...current.columns, ...range] : range), anchor: current.anchor };
|
|
16
|
+
}
|
|
17
|
+
if (click.mod) {
|
|
18
|
+
const next = new Set(current.columns);
|
|
19
|
+
if (next.delete(index)) {
|
|
20
|
+
return { columns: next, anchor: next.size === 0 ? null : current.anchor };
|
|
21
|
+
}
|
|
22
|
+
next.add(index);
|
|
23
|
+
return { columns: next, anchor: index };
|
|
24
|
+
}
|
|
25
|
+
return { columns: new Set([index]), anchor: index };
|
|
26
|
+
};
|
|
27
|
+
/* The picked columns that are drawn, in the order they are drawn. */
|
|
28
|
+
export const shownSelection = (current, order) => order.filter((index) => current.columns.has(index));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ColumnView {
|
|
2
|
+
readonly hidden: ReadonlySet<number>;
|
|
3
|
+
readonly pinned: readonly number[];
|
|
4
|
+
}
|
|
5
|
+
export declare const NO_COLUMN_VIEW: ColumnView;
|
|
6
|
+
export declare const displayOrder: (count: number, view: ColumnView) => number[];
|
|
7
|
+
export declare const hideColumn: (view: ColumnView, index: number) => ColumnView;
|
|
8
|
+
export declare const showAllColumns: (view: ColumnView) => ColumnView;
|
|
9
|
+
export declare const togglePin: (view: ColumnView, index: number) => ColumnView;
|
|
10
|
+
export declare const pinnedCount: (count: number, view: ColumnView) => number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const NO_COLUMN_VIEW = { hidden: new Set(), pinned: [] };
|
|
2
|
+
/* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
|
|
3
|
+
export const displayOrder = (count, view) => {
|
|
4
|
+
const pinned = view.pinned.filter((index) => index < count && !view.hidden.has(index));
|
|
5
|
+
const rest = [];
|
|
6
|
+
for (let i = 0; i < count; i++) {
|
|
7
|
+
if (!view.hidden.has(i) && !pinned.includes(i)) {
|
|
8
|
+
rest.push(i);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return [...pinned, ...rest];
|
|
12
|
+
};
|
|
13
|
+
export const hideColumn = (view, index) => ({
|
|
14
|
+
hidden: new Set([...view.hidden, index]),
|
|
15
|
+
pinned: view.pinned.filter((pinned) => pinned !== index)
|
|
16
|
+
});
|
|
17
|
+
export const showAllColumns = (view) => ({ ...view, hidden: new Set() });
|
|
18
|
+
export const togglePin = (view, index) => ({
|
|
19
|
+
...view,
|
|
20
|
+
pinned: view.pinned.includes(index) ? view.pinned.filter((pinned) => pinned !== index) : [...view.pinned, index]
|
|
21
|
+
});
|
|
22
|
+
/* How many of the first columns of `displayOrder` are pinned. */
|
|
23
|
+
export const pinnedCount = (count, view) => view.pinned.filter((index) => index < count && !view.hidden.has(index)).length;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValueKind } from '../protocol/index.ts';
|
|
2
|
+
import { type SqlTarget } from '../sql.ts';
|
|
3
|
+
import { type Shown } from './display.ts';
|
|
4
|
+
export type CopyFormat = 'tsv' | 'csv' | 'json' | 'sql';
|
|
5
|
+
export declare const COPY_FORMATS: readonly CopyFormat[];
|
|
6
|
+
export interface CopyColumn {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly kind: ValueKind;
|
|
9
|
+
}
|
|
10
|
+
export interface CopyInput {
|
|
11
|
+
readonly columns: readonly CopyColumn[];
|
|
12
|
+
readonly rows: readonly (readonly Shown[])[];
|
|
13
|
+
readonly target?: SqlTarget;
|
|
14
|
+
}
|
|
15
|
+
export declare const formatCopy: (format: CopyFormat, input: CopyInput) => string;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { qualifiedName, quoteIdentifier, sqlLiteral } from '../sql.js';
|
|
2
|
+
import { copyTextOf } from './display.js';
|
|
3
|
+
export const COPY_FORMATS = ['tsv', 'csv', 'json', 'sql'];
|
|
4
|
+
const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
|
|
5
|
+
const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
6
|
+
const quoteField = (text) => `"${text.replaceAll('"', '""')}"`;
|
|
7
|
+
/* Tabs and line breaks would split a cell on paste, so a cell that holds one is quoted the way a spreadsheet does. */
|
|
8
|
+
const tsvField = (text) => (/[\t\r\n"]/.test(text) ? quoteField(text) : text);
|
|
9
|
+
const csvField = (text) => (/[",\r\n]/.test(text) ? quoteField(text) : text);
|
|
10
|
+
const csvText = (cell) => (cell === null ? '' : copyTextOf(cell));
|
|
11
|
+
const jsonOf = (cell, kind) => {
|
|
12
|
+
if (cell === null) {
|
|
13
|
+
return 'null';
|
|
14
|
+
}
|
|
15
|
+
if (typeof cell === 'boolean') {
|
|
16
|
+
return String(cell);
|
|
17
|
+
}
|
|
18
|
+
if (typeof cell === 'number') {
|
|
19
|
+
if (kind === 'boolean' && (cell === 0 || cell === 1)) {
|
|
20
|
+
return String(cell === 1);
|
|
21
|
+
}
|
|
22
|
+
return Number.isFinite(cell) ? String(cell) : 'null';
|
|
23
|
+
}
|
|
24
|
+
if (typeof cell === 'string') {
|
|
25
|
+
if (NUMERIC_KINDS.has(kind) && JSON_NUMBER.test(cell)) {
|
|
26
|
+
return cell;
|
|
27
|
+
}
|
|
28
|
+
if (kind === 'json') {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(JSON.parse(cell));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return JSON.stringify(cell);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(cell);
|
|
37
|
+
}
|
|
38
|
+
return cell.kind === 'default' ? 'null' : JSON.stringify(copyTextOf(cell));
|
|
39
|
+
};
|
|
40
|
+
const toTsv = ({ rows }) => rows.map((row) => row.map((cell) => tsvField(copyTextOf(cell))).join('\t')).join('\n');
|
|
41
|
+
const toCsv = ({ columns, rows }) => [columns.map((column) => csvField(column.name)), ...rows.map((row) => row.map((cell) => csvField(csvText(cell))))]
|
|
42
|
+
.map((line) => line.join(','))
|
|
43
|
+
.join('\r\n');
|
|
44
|
+
const toJson = ({ columns, rows }) => {
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
return '[]';
|
|
47
|
+
}
|
|
48
|
+
const objects = rows.map((row) => {
|
|
49
|
+
if (columns.length === 0) {
|
|
50
|
+
return ' {}';
|
|
51
|
+
}
|
|
52
|
+
const fields = columns.map((column, index) => ` ${JSON.stringify(column.name)}: ${jsonOf(row[index] ?? null, column.kind)}`);
|
|
53
|
+
return ` {\n${fields.join(',\n')}\n }`;
|
|
54
|
+
});
|
|
55
|
+
return `[\n${objects.join(',\n')}\n]`;
|
|
56
|
+
};
|
|
57
|
+
const toSql = ({ columns, rows, target }) => {
|
|
58
|
+
if (target === undefined) {
|
|
59
|
+
throw new Error('Copying as SQL needs the table the rows belong to.');
|
|
60
|
+
}
|
|
61
|
+
const head = `INSERT INTO ${qualifiedName(target)} (${columns.map((column) => quoteIdentifier(target.engine, column.name)).join(', ')}) VALUES`;
|
|
62
|
+
return rows.map((row) => `${head} (${columns.map((column, index) => sqlLiteral(target.engine, row[index] ?? null, column.kind)).join(', ')});`).join('\n');
|
|
63
|
+
};
|
|
64
|
+
/*
|
|
65
|
+
* A block of cells as text. TSV is what a spreadsheet pastes and has no header; CSV follows RFC 4180
|
|
66
|
+
* with a header and leaves NULL empty; JSON is an array of objects that keeps numbers, booleans and
|
|
67
|
+
* NULL as what they are; SQL is one INSERT per row.
|
|
68
|
+
*/
|
|
69
|
+
export const formatCopy = (format, input) => {
|
|
70
|
+
switch (format) {
|
|
71
|
+
case 'tsv':
|
|
72
|
+
return toTsv(input);
|
|
73
|
+
case 'csv':
|
|
74
|
+
return toCsv(input);
|
|
75
|
+
case 'json':
|
|
76
|
+
return toJson(input);
|
|
77
|
+
case 'sql':
|
|
78
|
+
return toSql(input);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Cell, EditValue, ValueKind } from '../protocol/index.ts';
|
|
2
|
+
export type Shown = Cell | {
|
|
3
|
+
readonly kind: 'default';
|
|
4
|
+
};
|
|
5
|
+
export type CellTone = 'value' | 'null' | 'default';
|
|
6
|
+
export interface CellView {
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly tone: CellTone;
|
|
9
|
+
readonly align: 'start' | 'end';
|
|
10
|
+
readonly suffix?: string;
|
|
11
|
+
}
|
|
12
|
+
export type NumberNotation = 'database' | 'region';
|
|
13
|
+
export declare const DISPLAY_LIMIT = 200;
|
|
14
|
+
export declare const BINARY_PREVIEW_BYTES = 8;
|
|
15
|
+
export declare const isNumericKind: (kind: ValueKind) => boolean;
|
|
16
|
+
export declare const isPreview: (cell: Shown) => boolean;
|
|
17
|
+
export declare const numeralText: (numeral: string | number, notation: NumberNotation) => string;
|
|
18
|
+
export declare const cellView: (cell: Shown, kind: ValueKind, notation?: NumberNotation) => CellView;
|
|
19
|
+
export declare const copyTextOf: (cell: Shown) => string;
|
|
20
|
+
export declare const shownOfEdit: (value: EditValue) => Shown;
|