@adecore/database 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DatabaseProvider.d.ts +4 -3
- package/dist/DatabaseProvider.js +15 -3
- package/dist/KeyIcon.d.ts +4 -0
- package/dist/KeyIcon.js +14 -0
- package/dist/actions.d.ts +12 -0
- package/dist/client-context.d.ts +3 -1
- package/dist/client-context.js +2 -0
- package/dist/code-text.d.ts +1 -0
- package/dist/code-text.js +2 -0
- package/dist/column-keys.d.ts +5 -0
- package/dist/column-keys.js +2 -0
- package/dist/connections/ConnectionManager.d.ts +3 -2
- package/dist/connections/ConnectionManager.js +27 -16
- package/dist/connections/connection-config.js +2 -0
- package/dist/console/QueryConsole.d.ts +8 -3
- package/dist/console/QueryConsole.js +110 -50
- package/dist/console/SqlEditor.d.ts +2 -12
- package/dist/console/SqlEditor.js +29 -18
- package/dist/console/StatementResultView.d.ts +5 -1
- package/dist/console/StatementResultView.js +18 -5
- package/dist/console/TransactionControls.js +3 -6
- package/dist/console/editor-slot.d.ts +18 -0
- package/dist/console/editor-slot.js +1 -0
- package/dist/console/result-keys.d.ts +15 -0
- package/dist/console/result-keys.js +79 -0
- package/dist/designer/ColumnsEditor.js +2 -2
- package/dist/designer/DesignerView.d.ts +3 -2
- package/dist/designer/DesignerView.js +5 -4
- package/dist/designer/SqlPreview.js +2 -1
- package/dist/designer/TableDesigner.d.ts +3 -2
- package/dist/designer/TableDesigner.js +2 -2
- package/dist/explorer/DatabaseExplorer.d.ts +4 -1
- package/dist/explorer/DatabaseExplorer.js +79 -23
- package/dist/explorer/RowMenu.js +2 -2
- package/dist/explorer/expansion.js +2 -2
- package/dist/explorer/folders.d.ts +16 -0
- package/dist/explorer/folders.js +1 -0
- package/dist/explorer/tree.d.ts +55 -4
- package/dist/explorer/tree.js +137 -23
- package/dist/grid/CellEditor.js +3 -1
- package/dist/grid/DataGrid.d.ts +2 -2
- package/dist/grid/DataGrid.js +61 -59
- package/dist/grid/EnumPicker.js +2 -1
- package/dist/grid/GridHeaderCell.d.ts +1 -2
- package/dist/grid/GridHeaderCell.js +12 -5
- package/dist/grid/GridRowView.d.ts +49 -0
- package/dist/grid/GridRowView.js +51 -0
- package/dist/grid/RecordView.d.ts +4 -2
- package/dist/grid/RecordView.js +71 -22
- package/dist/grid/ValueDock.d.ts +3 -1
- package/dist/grid/ValueDock.js +9 -3
- package/dist/grid/cell-id.d.ts +1 -0
- package/dist/grid/cell-id.js +2 -0
- package/dist/grid/layout.d.ts +3 -2
- package/dist/grid/layout.js +15 -6
- package/dist/grid/types.d.ts +8 -1
- package/dist/grid/types.js +4 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/locales/en.json +32 -15
- package/dist/locales/nl.json +32 -15
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/messages.d.ts +1 -1
- package/dist/protocol/messages.js +1 -1
- package/dist/protocol/schema.d.ts +17 -0
- package/dist/structure/StructureView.d.ts +5 -3
- package/dist/structure/StructureView.js +8 -6
- package/dist/table/CommandField.js +2 -2
- package/dist/table/TableStatusBar.js +2 -2
- package/dist/table/TableToolbar.d.ts +3 -4
- package/dist/table/TableToolbar.js +4 -3
- package/dist/table/TableView.d.ts +4 -2
- package/dist/table/TableView.js +63 -91
- package/dist/table/grid-rows.js +4 -1
- package/dist/table/useTableTransfer.d.ts +3 -1
- package/dist/table/useTableTransfer.js +19 -5
- package/dist/testing/fake.d.ts +3 -1
- package/dist/testing/fake.js +7 -1
- package/dist/value/ValueEditor.d.ts +9 -0
- package/dist/value/ValueEditor.js +102 -0
- package/dist/value/ValuePanel.d.ts +1 -1
- package/dist/value/ValuePanel.js +3 -94
- package/dist/workbench/DatabaseWorkbench.js +4 -7
- package/package.json +6 -6
- package/src/DatabaseProvider.tsx +24 -5
- package/src/KeyIcon.tsx +15 -0
- package/src/actions.ts +28 -2
- package/src/client-context.ts +5 -1
- package/src/code-text.ts +2 -0
- package/src/column-keys.ts +8 -0
- package/src/connections/ConnectionManager.tsx +98 -68
- package/src/connections/connection-config.ts +2 -0
- package/src/console/QueryConsole.tsx +251 -150
- package/src/console/SqlEditor.tsx +33 -37
- package/src/console/StatementResultView.tsx +61 -27
- package/src/console/TransactionControls.tsx +8 -11
- package/src/console/editor-slot.ts +27 -0
- package/src/console/result-keys.ts +109 -0
- package/src/designer/ColumnsEditor.tsx +3 -3
- package/src/designer/DesignerView.tsx +14 -3
- package/src/designer/SqlPreview.tsx +2 -1
- package/src/designer/TableDesigner.tsx +5 -2
- package/src/explorer/DatabaseExplorer.tsx +170 -26
- package/src/explorer/RowMenu.tsx +2 -2
- package/src/explorer/expansion.ts +4 -4
- package/src/explorer/folders.ts +25 -0
- package/src/explorer/tree.ts +219 -28
- package/src/grid/CellEditor.tsx +3 -2
- package/src/grid/DataGrid.tsx +114 -176
- package/src/grid/EnumPicker.tsx +2 -1
- package/src/grid/GridHeaderCell.tsx +14 -9
- package/src/grid/GridRowView.tsx +184 -0
- package/src/grid/RecordView.tsx +188 -75
- package/src/grid/ValueDock.tsx +12 -3
- package/src/grid/cell-id.ts +2 -0
- package/src/grid/layout.ts +16 -6
- package/src/grid/types.ts +15 -3
- package/src/index.ts +5 -1
- package/src/locales/en.json +32 -15
- package/src/locales/nl.json +32 -15
- package/src/protocol/index.ts +13 -1
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/schema.ts +28 -0
- package/src/structure/StructureView.tsx +16 -6
- package/src/table/CommandField.tsx +2 -2
- package/src/table/TableStatusBar.tsx +3 -3
- package/src/table/TableToolbar.tsx +107 -92
- package/src/table/TableView.tsx +95 -132
- package/src/table/grid-rows.ts +5 -3
- package/src/table/useTableTransfer.ts +21 -6
- package/src/testing/fake.ts +18 -1
- package/src/value/ValueEditor.tsx +213 -0
- package/src/value/ValuePanel.tsx +4 -206
- package/src/workbench/DatabaseWorkbench.tsx +9 -10
package/dist/explorer/tree.js
CHANGED
|
@@ -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
|
-
|
|
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' ||
|
|
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
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
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(
|
|
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
|
|
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
|
}
|
package/dist/grid/CellEditor.js
CHANGED
|
@@ -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,
|
|
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
|
}
|
package/dist/grid/DataGrid.d.ts
CHANGED
|
@@ -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?:
|
|
30
|
+
focusRequest?: FocusRequest | null;
|
|
31
31
|
columnRequest?: ColumnRequest | null;
|
|
32
32
|
empty?: ReactNode;
|
|
33
33
|
className?: string;
|
package/dist/grid/DataGrid.js
CHANGED
|
@@ -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 {
|
|
4
|
+
import { Ban, Copy, RotateCcw } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon,
|
|
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 {
|
|
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 [
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
466
|
-
|
|
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) =>
|
|
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
|
}
|
package/dist/grid/EnumPicker.js
CHANGED
|
@@ -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:
|
|
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,
|
|
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,
|
|
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,
|
|
27
|
-
export function GridHeaderCell({ column, index, position, width, sortable, selected, id, sort, multipleSorts, hasSorts, pinned,
|
|
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(
|
|
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
|
|
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>;
|