@adecore/database 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +246 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -5,7 +5,11 @@ export const connectionKey = (connectionId) => `c:${connectionId}`;
5
5
  export const schemaKey = (connectionId, schema) => `s:${connectionId}${SEPARATOR}${schema}`;
6
6
  export const folderKey = (connectionId, schema, group) => `f:${connectionId}${SEPARATOR}${schema}${SEPARATOR}${group}`;
7
7
  export const tableKey = (ref) => `t:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}`;
8
- export const isFolderKey = (key) => key.startsWith('f:');
8
+ /* The folders of a table, in the order they come under it. */
9
+ export const PARTS = ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'];
10
+ export const partKey = (ref, part) => `p:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}${SEPARATOR}${part}`;
11
+ /* A folder of a schema, and the columns of a table, are open until the person closes them; everything else is closed until they open it. */
12
+ export const startsOpen = (key) => key.startsWith('f:') || (key.startsWith('p:') && key.endsWith(`${SEPARATOR}columns`));
9
13
  /* The connection a key of a connection, schema, folder or table belongs to. */
10
14
  export const connectionOfKey = (key) => {
11
15
  const rest = key.slice(2);
@@ -13,6 +17,10 @@ export const connectionOfKey = (key) => {
13
17
  return end === -1 ? rest : rest.slice(0, end);
14
18
  };
15
19
  export const columnKey = (ref, column) => `${tableKey(ref)}${SEPARATOR}${column}`;
20
+ /* A folder of the app's own under a connection, and an item in it. */
21
+ export const appFolderKey = (connectionId, folder) => `x:${connectionId}${SEPARATOR}${folder}`;
22
+ const appItemKey = (connectionId, folder, item) => `${appFolderKey(connectionId, folder)}${SEPARATOR}${item}`;
23
+ const entryKey = (ref, part, index) => `${partKey(ref, part)}${SEPARATOR}${index}`;
16
24
  export const loadKey = (target) => {
17
25
  if (target.schema === undefined) {
18
26
  return connectionKey(target.connectionId);
@@ -23,9 +31,15 @@ export const loadKey = (target) => {
23
31
  };
24
32
  /* The kinds of table a schema lists, in the order of its folders. */
25
33
  export const GROUPS = ['table', 'view'];
26
- export const isExpandable = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table';
34
+ export const isExpandable = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table' || row.kind === 'part' || row.kind === 'appFolder';
27
35
  /* The table a row stands for, when it stands for one. */
28
- export const tableOf = (row) => (row.kind === 'table' || row.kind === 'column' ? row.ref : null);
36
+ export const tableOf = (row) => (row.kind === 'table' || row.kind === 'column' || row.kind === 'entry' ? row.ref : null);
37
+ /* The table activating a row opens: its own, or for a foreign key the table it references. */
38
+ export const openedTableOf = (row) => row.kind === 'entry' && row.entry.type === 'foreignKey'
39
+ ? { connectionId: row.ref.connectionId, schema: row.entry.foreignKey.referencedSchema, table: row.entry.foreignKey.referencedTable }
40
+ : tableOf(row);
41
+ /* Whether a list of tables holds `name` as a table or a view; `undefined` while the list has not loaded or lacks it. */
42
+ export const tableKindIn = (tables, name) => tables.status === 'ready' ? tables.value.find((table) => table.name === name)?.kind : undefined;
29
43
  /* What picking a row selects: its connection, its schema, or its table (a column stands for its table). Status rows select nothing. */
30
44
  export const selectionOf = (row) => {
31
45
  switch (row.kind) {
@@ -35,7 +49,9 @@ export const selectionOf = (row) => {
35
49
  case 'folder':
36
50
  return { connectionId: row.connection.id, schema: row.schema };
37
51
  case 'table':
52
+ case 'part':
38
53
  case 'column':
54
+ case 'entry':
39
55
  return { connectionId: row.ref.connectionId, schema: row.ref.schema, table: row.ref.table };
40
56
  default:
41
57
  return null;
@@ -50,8 +66,13 @@ export const selectionKey = (selection) => {
50
66
  ? schemaKey(selection.connectionId, selection.schema)
51
67
  : tableKey({ connectionId: selection.connectionId, schema: selection.schema, table: selection.table });
52
68
  };
53
- /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, and none of the rows that only report. */
54
- export const hasMenu = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table' || row.kind === 'column';
69
+ /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, an item the app gave one, and none of the rows that only report. */
70
+ export const hasMenu = (row) => row.kind === 'connection' ||
71
+ row.kind === 'schema' ||
72
+ row.kind === 'folder' ||
73
+ row.kind === 'table' ||
74
+ row.kind === 'column' ||
75
+ (row.kind === 'appItem' && row.item.menu !== undefined);
55
76
  /* Only the rows a selection can point at are drawn selected; a folder or a column leaves that to the row above it. */
56
77
  export const isSelectable = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'table';
57
78
  /* The statement behind New console here on a table: every row, as the engine quotes it. */
@@ -67,6 +88,21 @@ const statusDraft = (row, level, parent, focusable) => ({
67
88
  below: []
68
89
  });
69
90
  const matches = (name, query) => name.toLowerCase().includes(query);
91
+ /* What each folder of a table besides its columns holds: the primary key and the unique keys, the foreign keys, the other indexes, the checks and the triggers. */
92
+ export const tableEntries = (structure) => {
93
+ const primaryIndex = structure.indexes.find((index) => index.primary);
94
+ const primary = structure.primaryKey.length === 0 ? [] : [{ type: 'key', name: primaryIndex?.name ?? null, columns: structure.primaryKey, primary: true }];
95
+ const unique = structure.indexes
96
+ .filter((index) => index.unique && !index.primary)
97
+ .map((index) => ({ type: 'key', name: index.name, columns: index.columns, primary: false }));
98
+ return {
99
+ keys: [...primary, ...unique],
100
+ foreignKeys: structure.foreignKeys.map((foreignKey) => ({ type: 'foreignKey', foreignKey })),
101
+ indexes: structure.indexes.filter((index) => !index.primary).map((index) => ({ type: 'index', index })),
102
+ checks: (structure.checks ?? []).map((check) => ({ type: 'check', check })),
103
+ triggers: (structure.triggers ?? []).map((trigger) => ({ type: 'trigger', trigger }))
104
+ };
105
+ };
70
106
  /*
71
107
  * The rows a tree shows, top to bottom. With a filter only the loaded tables that match stay, under
72
108
  * their folder, schema and connection, which then count as open whatever the person left them at.
@@ -74,21 +110,10 @@ const matches = (name, query) => name.toLowerCase().includes(query);
74
110
  export const flattenTree = (input) => {
75
111
  const query = input.filter.trim().toLowerCase();
76
112
  const filtering = query !== '';
77
- const columnDrafts = (ref, level, parent) => {
78
- const load = input.structures(ref.connectionId, ref.schema, ref.table);
79
- if (load.status !== 'ready') {
80
- const row = load.status === 'error'
81
- ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
82
- : { kind: 'loading' };
83
- return [statusDraft(row, level, parent, load.status === 'error')];
84
- }
85
- const { columns, primaryKey, foreignKeys } = load.value;
86
- if (columns.length === 0) {
87
- return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
88
- }
89
- const primary = new Set(primaryKey);
90
- const foreign = new Set(foreignKeys.flatMap((foreignKey) => foreignKey.columns));
91
- return columns.map((column) => ({
113
+ const columnRows = (ref, structure, level, parent) => {
114
+ const primary = new Set(structure.primaryKey);
115
+ const foreign = new Set(structure.foreignKeys.flatMap((foreignKey) => foreignKey.columns));
116
+ return structure.columns.map((column) => ({
92
117
  make: (place) => ({
93
118
  kind: 'column',
94
119
  key: columnKey(ref, column.name),
@@ -104,12 +129,59 @@ export const flattenTree = (input) => {
104
129
  below: []
105
130
  }));
106
131
  };
132
+ /* The folders a table opens into, each only when it holds something. A view has only its columns, straight under it. */
133
+ const tableChildren = (ref, kind, level, parent) => {
134
+ const load = input.structures(ref.connectionId, ref.schema, ref.table);
135
+ if (load.status !== 'ready') {
136
+ const row = load.status === 'error'
137
+ ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
138
+ : { kind: 'loading' };
139
+ return [statusDraft(row, level, parent, load.status === 'error')];
140
+ }
141
+ const structure = load.value;
142
+ if (structure.columns.length === 0) {
143
+ return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
144
+ }
145
+ if (kind === 'view') {
146
+ return columnRows(ref, structure, level, parent);
147
+ }
148
+ const entries = tableEntries(structure);
149
+ return PARTS.flatMap((part) => {
150
+ const count = part === 'columns' ? structure.columns.length : entries[part].length;
151
+ if (count === 0) {
152
+ return [];
153
+ }
154
+ const key = partKey(ref, part);
155
+ const open = startsOpen(key) ? !input.collapsed.has(key) : input.expanded.has(key);
156
+ const below = () => part === 'columns'
157
+ ? columnRows(ref, structure, level + 1, key)
158
+ : entries[part].map((entry, index) => ({
159
+ make: (place) => ({
160
+ kind: 'entry',
161
+ key: entryKey(ref, part, index),
162
+ level: level + 1,
163
+ parent: key,
164
+ focusable: true,
165
+ ref,
166
+ entry,
167
+ ...place
168
+ }),
169
+ below: []
170
+ }));
171
+ return [
172
+ {
173
+ make: (place) => ({ kind: 'part', key, level, parent, focusable: true, ref, part, expanded: open, count, ...place }),
174
+ below: open ? settle(below()) : []
175
+ }
176
+ ];
177
+ });
178
+ };
107
179
  const tableDraft = (ref, table, level, parent) => {
108
180
  const key = tableKey(ref);
109
181
  const open = input.expanded.has(key);
110
182
  return {
111
183
  make: (place) => ({ kind: 'table', key, level, parent, focusable: true, ref, table, expanded: open, ...place }),
112
- below: open ? settle(columnDrafts(ref, level + 1, key)) : []
184
+ below: open ? settle(tableChildren(ref, table.kind, level + 1, key)) : []
113
185
  };
114
186
  };
115
187
  /* The folders under a schema (or under a connection with only that schema), and how many tables they hold. */
@@ -167,10 +239,50 @@ export const flattenTree = (input) => {
167
239
  below: open ? settle(drafts) : []
168
240
  };
169
241
  };
242
+ /* The app's folders under a connection, each while it holds items; with a filter only the items that match, in folders that count as open. */
243
+ const appFolderDrafts = (connection, level, parent) => (input.folders?.(connection.id) ?? []).flatMap((folder) => {
244
+ const items = filtering ? folder.items.filter((item) => matches(item.label, query)) : folder.items;
245
+ if (items.length === 0) {
246
+ return [];
247
+ }
248
+ const key = appFolderKey(connection.id, folder.id);
249
+ const open = filtering || input.expanded.has(key);
250
+ const rows = items.map((item) => ({
251
+ make: (place) => ({
252
+ kind: 'appItem',
253
+ key: appItemKey(connection.id, folder.id, item.id),
254
+ level: level + 1,
255
+ parent: key,
256
+ focusable: true,
257
+ connection,
258
+ item,
259
+ ...place
260
+ }),
261
+ below: []
262
+ }));
263
+ return [
264
+ {
265
+ make: (place) => ({
266
+ kind: 'appFolder',
267
+ key,
268
+ level,
269
+ parent,
270
+ focusable: true,
271
+ connection,
272
+ folder,
273
+ expanded: open,
274
+ count: items.length,
275
+ ...place
276
+ }),
277
+ below: open ? settle(rows) : []
278
+ }
279
+ ];
280
+ });
170
281
  const connectionDraft = (connection) => {
171
282
  const key = connectionKey(connection.id);
172
283
  const load = input.schemas(connection.id);
173
- const open = filtering ? load.status === 'ready' : input.expanded.has(key);
284
+ const appFolders = appFolderDrafts(connection, 2, key);
285
+ const open = filtering ? load.status === 'ready' || appFolders.length > 0 : input.expanded.has(key);
174
286
  let children = [];
175
287
  let schemaCount = null;
176
288
  if (load.status === 'ready') {
@@ -189,10 +301,12 @@ export const flattenTree = (input) => {
189
301
  children = visible.flatMap((entry) => schemaDraft(connection, entry.name, key) ?? []);
190
302
  }
191
303
  }
192
- else if (open) {
304
+ else if (open && !filtering) {
193
305
  const row = load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id } } : { kind: 'loading' };
194
306
  children = [statusDraft(row, 2, key, load.status === 'error')];
195
307
  }
308
+ // Behind a status row too: what the app keeps of a connection does not wait on its server.
309
+ children = [...children, ...(open ? appFolders : [])];
196
310
  if (filtering && children.length === 0) {
197
311
  return null;
198
312
  }
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from 'react';
3
+ import clsx from 'clsx';
3
4
  import { Input } from '@adecore/ui';
5
+ import { CODE_TEXT } from '../code-text.js';
4
6
  /* The input a cell turns into while it is edited. It takes the focus and selects its text when it appears. */
5
7
  export function CellEditor({ value, label, onValueChange, onCommit, onCancel }) {
6
8
  const input = useRef(null);
@@ -25,5 +27,5 @@ export function CellEditor({ value, label, onValueChange, onCommit, onCancel })
25
27
  onCommit(event.shiftKey ? 'previous' : 'next');
26
28
  }
27
29
  };
28
- return (_jsx(Input, { ref: input, mono: true, size: "sm", "aria-label": label, value: value, className: "absolute inset-0 z-10 h-full min-w-0 rounded-none px-3", onChange: (event) => onValueChange(event.target.value), onKeyDown: handleKeyDown, onBlur: () => onCommit() }));
30
+ return (_jsx(Input, { ref: input, size: "sm", "aria-label": label, value: value, className: clsx(CODE_TEXT, 'absolute inset-0 z-10 h-full min-w-0 rounded-none px-3'), onChange: (event) => onValueChange(event.target.value), onKeyDown: handleKeyDown, onBlur: () => onCommit() }));
29
31
  }
@@ -4,7 +4,7 @@ import type { SqlTarget } from '../sql.ts';
4
4
  import type { RangeBlock } from './aggregates.ts';
5
5
  import { type GridLayout } from './column-layout.ts';
6
6
  import { type GridSort } from './sort.ts';
7
- import type { ColumnRequest, FocusedCell, GridColumn, GridMenuContext, GridRow } from './types.ts';
7
+ import type { ColumnRequest, FocusedCell, FocusRequest, GridColumn, GridMenuContext, GridRow } from './types.ts';
8
8
  export interface DataGridProps {
9
9
  columns: readonly GridColumn[];
10
10
  rows: readonly GridRow[];
@@ -27,7 +27,7 @@ export interface DataGridProps {
27
27
  onRangeChange?(block: RangeBlock | null): void;
28
28
  initialLayout?: GridLayout;
29
29
  onLayoutChange?(layout: GridLayout): void;
30
- focusRequest?: FocusedCell | null;
30
+ focusRequest?: FocusRequest | null;
31
31
  columnRequest?: ColumnRequest | null;
32
32
  empty?: ReactNode;
33
33
  className?: string;
@@ -1,12 +1,10 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
- import { ArrowUpRight, Ban, Copy, RotateCcw } from 'lucide-react';
4
+ import { Ban, Copy, RotateCcw } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, IconButton, isApplePlatform, isModHeld, Kbd, Spinner, useContentSize } from '@adecore/ui';
7
- import { formatNumber } from '@adecore/ui/format';
6
+ import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, isApplePlatform, isModHeld, Kbd, useContentSize } from '@adecore/ui';
8
7
  import { fullRect, rectBetween, rectContains } from './cell-range.js';
9
- import { CellEditor } from './CellEditor.js';
10
8
  import { captureLayout, restoreLayout, sameLayout } from './column-layout.js';
11
9
  import { NO_COLUMN_SELECTION, selectColumn, shownSelection } from './column-selection.js';
12
10
  import { displayOrder, hideColumn, pinnedCount, showAllColumns, togglePin } from './column-view.js';
@@ -15,28 +13,21 @@ import { CopyAsMenu } from './CopyAsMenu.js';
15
13
  import { cellView, copyTextOf, isPreview } from './display.js';
16
14
  import { draftOf, parseDraft } from './edit-value.js';
17
15
  import { parseEnumType } from './enum-type.js';
18
- import { EnumPicker } from './EnumPicker.js';
19
16
  import { GridHeaderCell } from './GridHeaderCell.js';
20
- import { clampColumnWidth, columnOffsets, estimateColumnWidth, fitColumnWidth, FALLBACK_VIEWPORT_HEIGHT, growWidths, gutterWidth, HEADER_HEIGHT, OVERSCAN_ROWS, ROW_HEIGHT, scrollToReveal, visibleRange } from './layout.js';
17
+ import { cellIdOf } from './cell-id.js';
18
+ import { GridRowView, PINNED_GROUP } from './GridRowView.js';
19
+ import { clampColumnWidth, columnOffsets, estimateColumnWidth, fitColumnWidth, FALLBACK_VIEWPORT_HEIGHT, growWidths, gutterWidth, drawnRange, HEADER_HEIGHT, ROW_HEIGHT, scrollBlock, scrollToReveal } from './layout.js';
21
20
  import { moveFocus } from './navigation.js';
22
21
  import { selectRow } from './row-selection.js';
23
22
  import { sortOnly, sortStateOf } from './sort.js';
24
23
  import { useNumberNotation } from '../client-context.js';
25
24
  import { usePopupPress } from '../use-popup-press.js';
25
+ import { useStableCallback } from '../use-stable-callback.js';
26
26
  const NO_KEYS = new Set();
27
27
  const NO_SORTS = [];
28
28
  const signatureOf = (columns) => columns.map((column) => column.name).join('\u0000');
29
29
  const textOf = (view) => (view.suffix === undefined ? view.text : `${view.text} ${view.suffix}`);
30
30
  const estimateWidths = (columns, rows, notation) => columns.map((column, index) => estimateColumnWidth(column.name, rows.slice(0, 50).map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, notation))), column.primaryKey === true));
31
- const TONE = {
32
- value: '',
33
- null: 'text-text-faint',
34
- default: 'text-text-faint italic'
35
- };
36
- const ROW_STATE = {
37
- inserted: 'bg-positive/10',
38
- deleted: 'bg-status-error/10 text-text-faint'
39
- };
40
31
  /*
41
32
  * The table of the table view and of the console. Only the rows in view are drawn, at a fixed
42
33
  * height, inside a box as tall as all of them; the header and the row-number gutter stick to the edges
@@ -58,7 +49,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
58
49
  const reportRange = useRef(onRangeChange);
59
50
  const reportLayout = useRef(onLayoutChange);
60
51
  const [measure, size] = useContentSize();
61
- const [scrollTop, setScrollTop] = useState(0);
52
+ const [scrolledBlock, setScrolledBlock] = useState(0);
62
53
  const [focused, setFocused] = useState(null);
63
54
  const [rangeAnchor, setRangeAnchor] = useState(null);
64
55
  const [columnSelection, setColumnSelection] = useState(NO_COLUMN_SELECTION);
@@ -87,17 +78,18 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
87
78
  };
88
79
  setLayout(current);
89
80
  }
90
- const order = displayOrder(columns.length, current.view);
81
+ // Kept by identity while nothing about the columns changes, since every drawn row compares them.
82
+ const order = useMemo(() => displayOrder(columns.length, current.view), [columns.length, current.view]);
91
83
  const pinnedShown = pinnedCount(columns.length, current.view);
92
- const displayOf = new Map(order.map((index, position) => [index, position]));
93
- const widths = columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0));
94
- const shownWidths = order.map((index) => widths[index]);
84
+ const displayOf = useMemo(() => new Map(order.map((index, position) => [index, position])), [order]);
85
+ const widths = useMemo(() => columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0)), [columns, current.resized, current.estimated]);
86
+ const shownWidths = useMemo(() => order.map((index) => widths[index]), [order, widths]);
95
87
  const offsets = columnOffsets(shownWidths);
96
88
  const gutter = gutterWidth(rows.length);
97
89
  const totalWidth = gutter + shownWidths.reduce((sum, width) => sum + width, 0);
98
90
  const pinnedWidth = shownWidths.slice(0, pinnedShown).reduce((sum, width) => sum + width, 0);
99
91
  const viewportHeight = size.height > 0 ? size.height : FALLBACK_VIEWPORT_HEIGHT;
100
- const range = visibleRange(scrollTop - HEADER_HEIGHT, viewportHeight, ROW_HEIGHT, rows.length, OVERSCAN_ROWS);
92
+ const range = drawnRange(scrolledBlock, viewportHeight, rows.length);
101
93
  const selection = selectedKeys ?? ownSelection;
102
94
  const isShown = (position) => position !== null && position.row < rows.length && displayOf.has(position.column);
103
95
  const focusCell = isShown(focused) ? focused : null;
@@ -110,8 +102,8 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
110
102
  const sortList = sorts ?? NO_SORTS;
111
103
  const focusedRowKey = focusCell === null ? null : (rows[focusCell.row]?.key ?? null);
112
104
  const focusedColumn = focusCell?.column ?? -1;
113
- const pickedColumns = shownSelection(columnSelection, order);
114
- const pickedSet = new Set(pickedColumns);
105
+ const pickedColumns = useMemo(() => shownSelection(columnSelection, order), [columnSelection, order]);
106
+ const pickedSet = useMemo(() => new Set(pickedColumns), [pickedColumns]);
115
107
  const columnBlock = pickedColumns.length > 0 && rows.length > 0 ? { top: 0, bottom: rows.length - 1, columns: pickedColumns } : null;
116
108
  const cellBlock = rect === null ? null : { top: rect.top, bottom: rect.bottom, columns: order.slice(rect.left, rect.right + 1) };
117
109
  const block = columnBlock ?? cellBlock;
@@ -147,7 +139,9 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
147
139
  const index = focusRequest === null || focusRequest === undefined ? -1 : rows.findIndex((row) => row.key === focusRequest.rowKey);
148
140
  if (focusRequest !== null && focusRequest !== undefined && index >= 0) {
149
141
  focusAt({ row: index, column: focusRequest.column });
150
- scroller.current?.focus();
142
+ if (focusRequest.quiet !== true) {
143
+ scroller.current?.focus();
144
+ }
151
145
  }
152
146
  // Only a new request moves the focus; the rows changing under a request already handled must not.
153
147
  // oxlint-disable-next-line react-hooks/exhaustive-deps
@@ -182,7 +176,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
182
176
  ref.current = node;
183
177
  }
184
178
  }, [measure, ref]);
185
- const cellId = (row, column) => `${gridId}-${row}-${column}`;
179
+ const cellId = (row, column) => cellIdOf(gridId, row, column);
186
180
  const canEdit = (position) => {
187
181
  const row = rows[position.row];
188
182
  const column = columns[position.column];
@@ -460,39 +454,50 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
460
454
  onShowAll: () => changeView(showAllColumns),
461
455
  onTogglePin: (index) => changeView((view) => togglePin(view, index))
462
456
  };
457
+ const look = useMemo(() => ({
458
+ gridId,
459
+ columns,
460
+ order,
461
+ widths: shownWidths,
462
+ pinnedShown,
463
+ gutter,
464
+ notation,
465
+ enumTypes,
466
+ picked: pickedSet,
467
+ loadingLabel: t('grid.loadingValue')
468
+ }), [gridId, columns, order, shownWidths, pinnedShown, gutter, notation, enumTypes, pickedSet, t]);
469
+ const stablePressCell = useStableCallback(pressCell);
470
+ const stableDragTo = useStableCallback(dragTo);
471
+ const stableBeginEdit = useStableCallback((position) => void beginEdit(position));
472
+ const stableOpenCellMenu = useStableCallback(openCellMenu);
473
+ const stablePressRowNumber = useStableCallback((index, event) => {
474
+ if (!pressedInPopup()) {
475
+ selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
476
+ }
477
+ });
478
+ const stableOpenRowMenu = useStableCallback(openRowMenu);
479
+ const stableFinishPick = useStableCallback(finishPick);
480
+ const stableFinishEdit = useStableCallback(finishEdit);
481
+ const rowActions = useMemo(() => ({
482
+ pressCell: stablePressCell,
483
+ dragTo: stableDragTo,
484
+ beginEdit: stableBeginEdit,
485
+ openCellMenu: stableOpenCellMenu,
486
+ pressRowNumber: stablePressRowNumber,
487
+ openRowMenu: stableOpenRowMenu,
488
+ finishPick: stableFinishPick,
489
+ finishEdit: stableFinishEdit,
490
+ changeDraft: (draft) => setEditing((now) => (now === null ? now : { ...now, draft }))
491
+ }), [stablePressCell, stableDragTo, stableBeginEdit, stableOpenCellMenu, stablePressRowNumber, stableOpenRowMenu, stableFinishPick, stableFinishEdit]);
463
492
  const renderRow = (index) => {
464
493
  const row = rows[index];
465
- const selected = selection.has(row.key);
466
- const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
467
- return (_jsxs("div", { role: "row", "aria-rowindex": index + 2, "aria-selected": selected, className: clsx('group/row absolute inset-x-0 flex border-b border-border-soft', tint, tint === '' && 'hover:bg-surface-hover'), style: { top: index * ROW_HEIGHT, height: ROW_HEIGHT }, children: [_jsx("div", { role: "rowheader", "data-selected": selected ? '' : undefined, className: clsx('sticky left-0 z-10 flex shrink-0 cursor-default items-center justify-end border-r border-border bg-surface pr-2 pl-1 font-mono text-xs text-text-faint tabular-nums select-none data-[selected]:bg-accent-soft data-[selected]:text-text', row.state === 'inserted' && 'text-positive'), style: { width: gutter }, onClick: (event) => {
468
- if (!pressedInPopup()) {
469
- selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
470
- }
471
- }, onContextMenu: () => openRowMenu(index), children: row.number === null ? '+' : formatNumber(row.number) }), order.map((columnIndex, position) => {
472
- const column = columns[columnIndex];
473
- const cell = row.cells[columnIndex] ?? null;
474
- const view = cellView(cell, column.kind, notation);
475
- const cellPosition = { row: index, column: columnIndex };
476
- const isFocused = focusCell !== null && focusCell.row === index && focusCell.column === columnIndex;
477
- const ranged = pickedSet.has(columnIndex) || (columnBlock === null && rectContains(rect, index, position));
478
- const edited = row.edited?.has(columnIndex) === true;
479
- const isEditing = editing !== null && editing.row === index && editing.column === columnIndex;
480
- const pinned = position < pinnedShown;
481
- const enumType = enumTypes[columnIndex] ?? null;
482
- const followable = !isEditing && followCell({ rowKey: row.key, column: columnIndex });
483
- return (_jsxs("div", { id: cellId(index, columnIndex), role: "gridcell", "aria-colindex": position + 2, "aria-selected": ranged, "data-focused": isFocused ? '' : undefined, "data-ranged": ranged ? '' : undefined, className: clsx('group/cell flex h-full shrink-0 items-center overflow-hidden px-3 font-mono text-code whitespace-nowrap outline-0 select-none data-[focused]:outline-1 data-[focused]:-outline-offset-1 data-[focused]:outline-border-strong group-focus-within/grid:data-[focused]:outline-accent', position === order.length - 1 ? 'border-r-0' : 'border-r', view.align === 'end' ? 'justify-end tabular-nums' : 'justify-start', pinned
484
- ? [
485
- 'sticky z-5',
486
- position === pinnedShown - 1 ? 'border-border-strong' : 'border-border-soft',
487
- ranged ? 'bg-accent-soft' : ['bg-surface', tint === '' && 'group-hover/row:bg-surface-hover']
488
- ]
489
- : ['relative border-border-soft', ranged ? 'bg-accent-soft' : edited && 'bg-accent/10']), style: { width: shownWidths[position], left: pinned ? gutter + (offsets[position] ?? 0) : undefined }, onPointerDown: (event) => pressCell(event, cellPosition, isEditing), onPointerEnter: () => dragTo(cellPosition), onDoubleClick: () => void beginEdit(cellPosition), onContextMenu: () => openCellMenu(cellPosition), children: [pinned && !ranged && _jsx("span", { "aria-hidden": true, className: clsx('pointer-events-none absolute inset-0', tint, edited && 'bg-accent/10') }), isEditing && editing.loading && _jsx(Spinner, { size: 12, label: t('grid.loadingValue') }), isEditing && !editing.loading && enumType !== null && (_jsx(EnumPicker, { autoOpen: true, type: enumType, value: typeof cell === 'string' ? cell : null, nullable: column.nullable !== false, label: column.name, onDone: finishPick })), isEditing && !editing.loading && enumType === null && (_jsx(CellEditor, { value: editing.draft, label: column.name, onValueChange: (draft) => setEditing({ ...editing, draft }), onCommit: (move) => finishEdit(true, move), onCancel: () => finishEdit(false) })), !isEditing && (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx('min-w-0 truncate', TONE[view.tone], row.state === 'deleted' && 'line-through'), children: view.text }), view.suffix !== undefined && _jsx("span", { className: "ml-2 shrink-0 text-text-faint", children: view.suffix })] })), followable && (_jsx(IconButton, { icon: ArrowUpRight, size: "2xs", label: t('grid.followReference'), tabIndex: -1, className: "absolute top-1/2 right-1 -translate-y-1/2 bg-surface-raised opacity-0 group-hover/cell:opacity-100 group-data-[focused]/cell:opacity-100 focus-visible:opacity-100", onPointerDown: (event) => event.stopPropagation(), onClick: () => {
490
- if (!pressedInPopup()) {
491
- onFollow?.({ rowKey: row.key, column: columnIndex });
492
- }
493
- } }))] }, columnIndex));
494
- })] }, row.key));
494
+ const inRect = columnBlock === null && rect !== null && index >= rect.top && index <= rect.bottom;
495
+ return (_jsx(GridRowView, { row: row, index: index, selected: selection.has(row.key), focusedColumn: focusCell !== null && focusCell.row === index ? focusCell.column : -1, rangeStart: inRect ? rect.left : -1, rangeEnd: inRect ? rect.right : -1, editing: editing !== null && editing.row === index ? editing : null, look: look, actions: rowActions }, row.key));
495
496
  };
497
+ const headerCells = order.map((columnIndex, position) => {
498
+ const column = columns[columnIndex];
499
+ return (_jsx(GridHeaderCell, { column: column, index: columnIndex, position: position, width: shownWidths[position], sortable: sortable, selected: pickedSet.has(columnIndex), id: headerId(columnIndex), sort: sortable ? sortStateOf(sortList, column.name) : null, multipleSorts: sortList.length > 1, hasSorts: sortList.length > 0, pinned: position < pinnedShown, lastPinned: position === pinnedShown - 1, last: position === order.length - 1, hasHidden: current.view.hidden.size > 0, canHide: order.length > 1, actions: headerActions }, columnIndex));
500
+ });
496
501
  const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
497
502
  const menuCell = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
498
503
  const settable = menuCell !== null && canEdit(menuCell);
@@ -505,8 +510,5 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
505
510
  rowKeys: selection.has(menuRow.key) ? rows.filter((row) => selection.has(row.key)).map((row) => row.key) : [menuRow.key],
506
511
  cell: menuTarget.column === null ? null : { rowKey: menuRow.key, column: menuTarget.column }
507
512
  };
508
- return (_jsx("div", { ref: attach, role: "grid", "aria-label": label, "aria-rowcount": rows.length + 1, "aria-colcount": order.length + 1, "aria-activedescendant": pickedColumns.length > 0 ? headerId(pickedColumns[0]) : focusCell === null ? undefined : cellId(focusCell.row, focusCell.column), tabIndex: 0, className: clsx('group/grid relative min-h-0 flex-1 overflow-auto bg-surface text-text outline-none', className), onKeyDown: handleKeyDown, onScroll: (event) => setScrollTop(Math.floor(event.currentTarget.scrollTop / ROW_HEIGHT) * ROW_HEIGHT), children: _jsxs("div", { className: "relative min-w-full", style: { width: totalWidth }, children: [_jsxs("div", { role: "row", "aria-rowindex": 1, className: "sticky top-0 z-20 flex w-full border-b border-border bg-surface", style: { height: HEADER_HEIGHT }, children: [_jsx("div", { className: "sticky left-0 z-10 shrink-0 border-r border-border bg-surface", style: { width: gutter } }), order.map((columnIndex, position) => {
509
- const column = columns[columnIndex];
510
- return (_jsx(GridHeaderCell, { column: column, index: columnIndex, position: position, width: shownWidths[position], sortable: sortable, selected: pickedSet.has(columnIndex), id: headerId(columnIndex), sort: sortable ? sortStateOf(sortList, column.name) : null, multipleSorts: sortList.length > 1, hasSorts: sortList.length > 0, pinned: position < pinnedShown, stickyLeft: gutter + (offsets[position] ?? 0), lastPinned: position === pinnedShown - 1, last: position === order.length - 1, hasHidden: current.view.hidden.size > 0, canHide: order.length > 1, actions: headerActions }, columnIndex));
511
- })] }), _jsxs(ContextMenu.Root, { children: [_jsx(ContextMenu.Trigger, { role: "rowgroup", className: "relative", style: { height: rows.length * ROW_HEIGHT }, children: Array.from({ length: range.end - range.start }, (_, offset) => renderRow(range.start + offset)) }), _jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: menuCell === null, onClick: copyCell, children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.copyValue'), _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), _jsx(CopyAsMenu, { disabled: block === null, formats: sqlTarget === undefined ? COPY_FORMATS.filter((format) => format !== 'sql') : COPY_FORMATS, onCopy: copyAs }), menuContext !== null && menuCell !== null && queryMenu !== undefined && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), queryMenu(menuContext)] })), _jsx(ContextMenu.Separator, {}), editable && menuCell !== null && (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Item, { disabled: !settable || !nullable, onClick: () => commitValue(null), children: [_jsx(Icon, { icon: Ban, size: 14 }), t('grid.setNull')] }), hasDefault && (_jsxs(ContextMenu.Item, { disabled: !settable, onClick: () => commitValue({ kind: 'default' }), children: [_jsx(Icon, { icon: RotateCcw, size: 14 }), t('grid.setDefault')] }))] })), menuContext !== null && menu?.(menuContext)] })] }), rows.length === 0 && empty !== undefined && _jsx("div", { className: "sticky left-0 w-fit px-4 py-6 text-xs text-text-muted", children: empty })] }) }));
513
+ return (_jsx("div", { ref: attach, role: "grid", "aria-label": label, "aria-rowcount": rows.length + 1, "aria-colcount": order.length + 1, "aria-activedescendant": pickedColumns.length > 0 ? headerId(pickedColumns[0]) : focusCell === null ? undefined : cellId(focusCell.row, focusCell.column), tabIndex: 0, className: clsx('group/grid relative min-h-0 flex-1 overflow-auto bg-surface text-text outline-none', className), onKeyDown: handleKeyDown, onScroll: (event) => setScrolledBlock(scrollBlock(event.currentTarget.scrollTop - HEADER_HEIGHT)), children: _jsxs("div", { className: "relative min-w-full", style: { width: totalWidth }, children: [_jsxs("div", { role: "row", "aria-rowindex": 1, className: "sticky top-0 z-20 flex w-full border-b border-border bg-surface", style: { height: HEADER_HEIGHT }, children: [_jsxs("div", { className: PINNED_GROUP, children: [_jsx("div", { className: "shrink-0 border-r border-border bg-surface bg-clip-border", style: { width: gutter } }), headerCells.slice(0, pinnedShown)] }), headerCells.slice(pinnedShown)] }), _jsxs(ContextMenu.Root, { children: [_jsx(ContextMenu.Trigger, { role: "rowgroup", className: "relative", style: { height: rows.length * ROW_HEIGHT }, children: Array.from({ length: range.end - range.start }, (_, offset) => renderRow(range.start + offset)) }), _jsxs(ContextMenu.Popup, { children: [_jsxs(ContextMenu.Item, { disabled: menuCell === null, onClick: copyCell, children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.copyValue'), _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), _jsx(CopyAsMenu, { disabled: block === null, formats: sqlTarget === undefined ? COPY_FORMATS.filter((format) => format !== 'sql') : COPY_FORMATS, onCopy: copyAs }), menuContext !== null && menuCell !== null && queryMenu !== undefined && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, {}), queryMenu(menuContext)] })), _jsx(ContextMenu.Separator, {}), editable && menuCell !== null && (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Item, { disabled: !settable || !nullable, onClick: () => commitValue(null), children: [_jsx(Icon, { icon: Ban, size: 14 }), t('grid.setNull')] }), hasDefault && (_jsxs(ContextMenu.Item, { disabled: !settable, onClick: () => commitValue({ kind: 'default' }), children: [_jsx(Icon, { icon: RotateCcw, size: 14 }), t('grid.setDefault')] }))] })), menuContext !== null && menu?.(menuContext)] })] }), rows.length === 0 && empty !== undefined && _jsx("div", { className: "sticky left-0 w-fit px-4 py-6 text-xs text-text-muted", children: empty })] }) }));
512
514
  }
@@ -4,9 +4,10 @@ import clsx from 'clsx';
4
4
  import { Ban, ChevronDown } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Icon, Menu } from '@adecore/ui';
7
+ import { CODE_TEXT } from '../code-text.js';
7
8
  import { joinMembers, membersOf } from './enum-type.js';
8
9
  const LOOK = {
9
- 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',
10
+ cell: `absolute inset-0 z-10 flex items-center gap-2 border border-accent bg-surface-raised px-3 ${CODE_TEXT} outline-0`,
10
11
  field: 'field field-sm flex w-full items-center gap-2 font-mono text-code data-disabled:opacity-50'
11
12
  };
12
13
  /*
@@ -26,11 +26,10 @@ export interface GridHeaderCellProps {
26
26
  multipleSorts: boolean;
27
27
  hasSorts: boolean;
28
28
  pinned: boolean;
29
- stickyLeft: number;
30
29
  lastPinned: boolean;
31
30
  last: boolean;
32
31
  hasHidden: boolean;
33
32
  canHide: boolean;
34
33
  actions: HeaderActions;
35
34
  }
36
- export declare function GridHeaderCell({ column, index, position, width, sortable, selected, id, sort, multipleSorts, hasSorts, pinned, stickyLeft, lastPinned, last, hasHidden, canHide, actions }: GridHeaderCellProps): import("react").JSX.Element;
35
+ export declare function GridHeaderCell({ column, index, position, width, sortable, selected, id, sort, multipleSorts, hasSorts, pinned, lastPinned, last, hasHidden, canHide, actions }: GridHeaderCellProps): import("react").JSX.Element;
@@ -1,10 +1,13 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useRef } from 'react';
3
3
  import clsx from 'clsx';
4
- import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Key, Pin, PinOff } from 'lucide-react';
4
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, Copy, Eye, EyeOff, Pin, PinOff } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { ColumnResizeHandle, ContextMenu, copyText, Icon, IconButton, isApplePlatform, isModHeld, Menu, Tooltip, useColumnResize } from '@adecore/ui';
7
7
  import { formatNumber } from '@adecore/ui/format';
8
+ import { CODE_TEXT } from '../code-text.js';
9
+ import { keyLabelOf } from '../column-keys.js';
10
+ import { KeyIcon } from '../KeyIcon.js';
8
11
  import { isNumericKind } from './display.js';
9
12
  import { isDoubleClick, MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
10
13
  /* The items of the menu a header opens, from its button and from a right click. */
@@ -15,7 +18,9 @@ function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden,
15
18
  /* The facts about a table column that a tooltip can carry: its type, whether it takes NULL, its default. */
16
19
  function ColumnFacts({ column }) {
17
20
  const { t } = useTranslation('database');
21
+ const keys = { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true };
18
22
  const facts = [
23
+ keys.primaryKey || keys.foreignKey ? t(keyLabelOf(keys)) : null,
19
24
  column.type === '' ? null : column.type,
20
25
  column.nullable === undefined ? null : t(column.nullable ? 'grid.column.nullable' : 'grid.column.notNull'),
21
26
  column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
@@ -23,8 +28,8 @@ function ColumnFacts({ column }) {
23
28
  ].filter((fact) => fact !== null);
24
29
  return (_jsxs(_Fragment, { children: [_jsx("span", { className: "block", children: column.name }), facts.length > 0 && _jsx("span", { className: "block", children: facts.join(', ') })] }));
25
30
  }
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 }) {
31
+ /* One column's head: its name, the sort it carries, the key of a key column, a menu and the strip that resizes it. Numbers sit against the end, like their cells. */
32
+ export function GridHeaderCell({ column, index, position, width, sortable, selected, id, sort, multipleSorts, hasSorts, pinned, lastPinned, last, hasHidden, canHide, actions }) {
28
33
  const { t } = useTranslation('database');
29
34
  const cell = useRef(null);
30
35
  const lastPress = useRef(null);
@@ -61,9 +66,11 @@ export function GridHeaderCell({ column, index, position, width, sortable, selec
61
66
  };
62
67
  const menuProps = { column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions };
63
68
  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: () => {
69
+ 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(
70
+ // The name in the face and size of the cells under it, which is how the column is written in SQL too.
71
+ 'group/header flex h-full shrink-0 cursor-default font-medium select-none', CODE_TEXT, 'relative', pinned && 'bg-clip-border', 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 }, onPointerDownCapture: () => {
65
72
  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
73
+ }, 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: [_jsx(KeyIcon, { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true, size: 12 }), _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
74
  ? 'bg-transparent text-accent-text'
68
75
  : '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
76
  }
@@ -0,0 +1,49 @@
1
+ import { type MouseEvent, type PointerEvent } from 'react';
2
+ import { type EditMove } from './CellEditor.tsx';
3
+ import { type NumberNotation } from './display.ts';
4
+ import type { EnumType } from './enum-type.ts';
5
+ import type { CellPosition } from './navigation.ts';
6
+ import type { GridColumn, GridRow } from './types.ts';
7
+ export declare const PINNED_GROUP = "sticky left-0 z-10 flex shrink-0";
8
+ export interface CellEdit extends CellPosition {
9
+ draft: string;
10
+ initial: string;
11
+ loading: boolean;
12
+ }
13
+ export interface RowLook {
14
+ readonly gridId: string;
15
+ readonly columns: readonly GridColumn[];
16
+ readonly order: readonly number[];
17
+ readonly widths: readonly number[];
18
+ readonly pinnedShown: number;
19
+ readonly gutter: number;
20
+ readonly notation: NumberNotation;
21
+ readonly enumTypes: readonly (EnumType | null)[];
22
+ readonly picked: ReadonlySet<number>;
23
+ readonly loadingLabel: string;
24
+ }
25
+ export interface RowActions {
26
+ pressCell(event: PointerEvent, position: CellPosition, isEditing: boolean): void;
27
+ dragTo(position: CellPosition): void;
28
+ beginEdit(position: CellPosition): void;
29
+ openCellMenu(position: CellPosition): void;
30
+ pressRowNumber(index: number, event: MouseEvent): void;
31
+ openRowMenu(index: number): void;
32
+ finishPick(result: {
33
+ readonly value: string | null;
34
+ } | undefined): void;
35
+ finishEdit(commit: boolean, move?: EditMove): void;
36
+ changeDraft(draft: string): void;
37
+ }
38
+ export interface GridRowViewProps {
39
+ row: GridRow;
40
+ index: number;
41
+ selected: boolean;
42
+ focusedColumn: number;
43
+ rangeStart: number;
44
+ rangeEnd: number;
45
+ editing: CellEdit | null;
46
+ look: RowLook;
47
+ actions: RowActions;
48
+ }
49
+ export declare const GridRowView: import("react").MemoExoticComponent<({ row, index, selected, focusedColumn, rangeStart, rangeEnd, editing, look, actions }: GridRowViewProps) => import("react").JSX.Element>;