@adecore/database 0.39.0 → 0.40.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/dist/console/QueryConsole.js +2 -10
- package/dist/console/SchemaPicker.js +0 -1
- package/dist/console/StatementResultView.js +1 -1
- package/dist/ddl/diff.js +1 -1
- package/dist/explorer/tree.js +0 -1
- package/dist/export-formats.d.ts +2 -0
- package/dist/export-formats.js +1 -0
- package/dist/grid/CopyAsMenu.js +0 -1
- package/dist/grid/DataGrid.js +10 -10
- package/dist/grid/column-layout.d.ts +0 -1
- package/dist/grid/column-layout.js +1 -2
- package/dist/grid/column-view.js +3 -2
- package/dist/grid/display.js +1 -2
- package/dist/host/host.js +1 -2
- package/dist/host/validate.d.ts +3 -0
- package/dist/host/validate.js +18 -15
- package/dist/same-list.d.ts +1 -0
- package/dist/same-list.js +1 -0
- package/dist/stored-size.d.ts +1 -0
- package/dist/stored-size.js +10 -0
- package/dist/table/TableToolbar.js +1 -1
- package/dist/table/TableView.js +2 -10
- package/package.json +15 -15
- package/src/DatabaseProvider.tsx +2 -0
- package/src/KeyIcon.tsx +1 -0
- package/src/client/client.ts +13 -0
- package/src/client-context.ts +2 -0
- package/src/connections/ConnectionForm.tsx +11 -0
- package/src/connections/ConnectionManager.tsx +9 -0
- package/src/connections/connection-config.ts +21 -0
- package/src/connections/docker.ts +2 -0
- package/src/connections/use-docker-containers.ts +3 -0
- package/src/console/DestructiveDialog.tsx +1 -0
- package/src/console/QueryConsole.tsx +38 -10
- package/src/console/SchemaPicker.tsx +1 -1
- package/src/console/SqlEditor.tsx +10 -0
- package/src/console/StatementResultView.tsx +7 -2
- package/src/console/TransactionControls.tsx +1 -0
- package/src/console/destructive.ts +11 -0
- package/src/console/editing.ts +10 -0
- package/src/console/history.ts +8 -0
- package/src/console/labels.ts +1 -0
- package/src/console/outcome.ts +1 -0
- package/src/console/result-keys.ts +16 -0
- package/src/console/shortcuts.ts +2 -0
- package/src/ddl/alter-mysql.ts +16 -0
- package/src/ddl/alter-sqlite.ts +21 -0
- package/src/ddl/alter.ts +2 -0
- package/src/ddl/column.ts +11 -0
- package/src/ddl/create.ts +15 -0
- package/src/ddl/dialect.ts +4 -0
- package/src/ddl/diff.ts +22 -2
- package/src/ddl/draft.ts +1 -0
- package/src/ddl/parse.ts +31 -0
- package/src/ddl/types.ts +3 -0
- package/src/ddl/validate.ts +12 -0
- package/src/designer/ColumnsEditor.tsx +3 -0
- package/src/designer/DesignerView.tsx +2 -0
- package/src/designer/ForeignKeysEditor.tsx +4 -0
- package/src/designer/IndexesEditor.tsx +1 -0
- package/src/designer/NamePicker.tsx +2 -0
- package/src/designer/OptionsEditor.tsx +4 -0
- package/src/designer/SqlPreview.tsx +2 -0
- package/src/designer/TableDesigner.tsx +14 -0
- package/src/designer/edit.ts +13 -0
- package/src/designer/plan.ts +3 -0
- package/src/designer/recover.ts +1 -0
- package/src/designer/useReferenceColumns.ts +5 -0
- package/src/explorer/DatabaseExplorer.tsx +40 -0
- package/src/explorer/RowMenu.tsx +2 -0
- package/src/explorer/TableDialog.tsx +6 -0
- package/src/explorer/expansion.ts +8 -0
- package/src/explorer/tree.ts +73 -2
- package/src/explorer/use-explorer-loads.ts +15 -0
- package/src/explorer/use-stored-expansion.ts +4 -0
- package/src/export-formats.ts +3 -0
- package/src/grid/CellEditor.tsx +1 -0
- package/src/grid/CopyAsMenu.tsx +1 -1
- package/src/grid/DataGrid.tsx +93 -23
- package/src/grid/EnumPicker.tsx +6 -0
- package/src/grid/GridHeaderCell.tsx +11 -0
- package/src/grid/GridRowView.tsx +3 -0
- package/src/grid/RecordView.tsx +19 -0
- package/src/grid/ValueDock.tsx +2 -0
- package/src/grid/aggregates.ts +12 -0
- package/src/grid/column-layout.ts +12 -4
- package/src/grid/column-selection.ts +9 -0
- package/src/grid/column-view.ts +6 -2
- package/src/grid/copy-formats.ts +13 -0
- package/src/grid/display.ts +17 -3
- package/src/grid/edit-value.ts +11 -0
- package/src/grid/enum-type.ts +15 -0
- package/src/grid/focused-value.ts +4 -0
- package/src/grid/layout.ts +12 -0
- package/src/grid/navigation.ts +5 -0
- package/src/grid/row-selection.ts +7 -0
- package/src/grid/sort.ts +1 -0
- package/src/grid/whole-cells.ts +8 -0
- package/src/host/host.ts +16 -3
- package/src/host/spawn.ts +2 -0
- package/src/host/validate.ts +37 -15
- package/src/same-list.ts +2 -0
- package/src/sql-split.ts +28 -0
- package/src/sql.ts +20 -0
- package/src/stored-size.ts +10 -0
- package/src/structure/StructureView.tsx +18 -0
- package/src/table/CommandField.tsx +11 -0
- package/src/table/ImportDialog.tsx +4 -0
- package/src/table/TableStatusBar.tsx +1 -0
- package/src/table/TableToolbar.tsx +2 -2
- package/src/table/TableView.tsx +50 -10
- package/src/table/command-field.ts +40 -0
- package/src/table/grid-rows.ts +13 -0
- package/src/table/import-mapping.ts +5 -0
- package/src/table/layout-store.ts +7 -0
- package/src/table/paging.ts +1 -0
- package/src/table/pending.ts +18 -0
- package/src/table/references.ts +4 -0
- package/src/table/useLoaded.ts +3 -0
- package/src/table/useStoredLayout.ts +2 -0
- package/src/table/useTableTransfer.ts +17 -1
- package/src/testing/fake.ts +24 -0
- package/src/use-database-resources.ts +1 -0
- package/src/use-popup-press.ts +3 -0
- package/src/value/ValueEditor.tsx +6 -0
- package/src/value/draft.ts +7 -0
- package/src/value/hex.ts +8 -0
- package/src/value/json.ts +16 -0
- package/src/value/views.ts +14 -0
- package/src/workbench/DatabaseWorkbench.tsx +22 -0
- package/src/workbench/tabs.ts +34 -0
|
@@ -56,6 +56,7 @@ type MenuProps = Pick<GridHeaderCellProps, 'column' | 'index' | 'sortable' | 'ha
|
|
|
56
56
|
/* The items of the menu a header opens, from its button and from a right click. */
|
|
57
57
|
function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden, canHide, actions }: MenuProps) {
|
|
58
58
|
const { t } = useTranslation('database');
|
|
59
|
+
|
|
59
60
|
return (
|
|
60
61
|
<>
|
|
61
62
|
{sortable && (
|
|
@@ -102,6 +103,7 @@ function HeaderMenuItems({ column, index, sortable, hasSorts, pinned, hasHidden,
|
|
|
102
103
|
function ColumnFacts({ column }: { column: GridColumn }) {
|
|
103
104
|
const { t } = useTranslation('database');
|
|
104
105
|
const keys = { primaryKey: column.primaryKey === true, foreignKey: column.foreignKey === true };
|
|
106
|
+
|
|
105
107
|
const facts = [
|
|
106
108
|
keys.primaryKey || keys.foreignKey ? t(keyLabelOf(keys)) : null,
|
|
107
109
|
column.type === '' ? null : column.type,
|
|
@@ -109,6 +111,7 @@ function ColumnFacts({ column }: { column: GridColumn }) {
|
|
|
109
111
|
column.defaultValue === undefined || column.defaultValue === null ? null : t('grid.column.default', { value: column.defaultValue }),
|
|
110
112
|
column.autoIncrement === true ? t('grid.column.autoIncrement') : null
|
|
111
113
|
].filter((fact) => fact !== null);
|
|
114
|
+
|
|
112
115
|
return (
|
|
113
116
|
<>
|
|
114
117
|
<span className="block">{column.name}</span>
|
|
@@ -141,6 +144,7 @@ export function GridHeaderCell({
|
|
|
141
144
|
const lastPress = useRef<number | null>(null);
|
|
142
145
|
// Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column.
|
|
143
146
|
const resized = useRef(false);
|
|
147
|
+
|
|
144
148
|
const { startResize } = useColumnResize(cell, {
|
|
145
149
|
size: width,
|
|
146
150
|
min: MIN_COLUMN_WIDTH,
|
|
@@ -151,25 +155,32 @@ export function GridHeaderCell({
|
|
|
151
155
|
|
|
152
156
|
const pressHandle = (event: PointerEvent<HTMLElement>): void => {
|
|
153
157
|
resized.current = true;
|
|
158
|
+
|
|
154
159
|
if (isDoubleClick(lastPress.current, event.timeStamp)) {
|
|
155
160
|
lastPress.current = null;
|
|
156
161
|
actions.onFit(index);
|
|
162
|
+
|
|
157
163
|
return;
|
|
158
164
|
}
|
|
165
|
+
|
|
159
166
|
lastPress.current = event.timeStamp;
|
|
160
167
|
startResize(event);
|
|
161
168
|
};
|
|
162
169
|
|
|
163
170
|
const handleClick = (event: MouseEvent<HTMLDivElement>): void => {
|
|
164
171
|
const target = event.target as Element;
|
|
172
|
+
|
|
165
173
|
// A click inside a menu popup reaches here through the React tree, not the DOM.
|
|
166
174
|
if (!event.currentTarget.contains(target) || target.closest('button') !== null) {
|
|
167
175
|
return;
|
|
168
176
|
}
|
|
177
|
+
|
|
169
178
|
if (resized.current) {
|
|
170
179
|
resized.current = false;
|
|
180
|
+
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
183
|
+
|
|
173
184
|
actions.onSelect(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
|
|
174
185
|
};
|
|
175
186
|
|
package/src/grid/GridRowView.tsx
CHANGED
|
@@ -85,6 +85,7 @@ export interface GridRowViewProps {
|
|
|
85
85
|
export const GridRowView = memo(function GridRowView({ row, index, selected, focusedColumn, rangeStart, rangeEnd, editing, look, actions }: GridRowViewProps) {
|
|
86
86
|
const { columns, order, widths, pinnedShown, gutter, notation, enumTypes, picked } = look;
|
|
87
87
|
const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
|
|
88
|
+
|
|
88
89
|
const cells = order.map((columnIndex, position) => {
|
|
89
90
|
const column = columns[columnIndex]!;
|
|
90
91
|
const cell = row.cells[columnIndex] ?? null;
|
|
@@ -96,6 +97,7 @@ export const GridRowView = memo(function GridRowView({ row, index, selected, foc
|
|
|
96
97
|
const isEditing = editing !== null && editing.column === columnIndex;
|
|
97
98
|
const pinned = position < pinnedShown;
|
|
98
99
|
const enumType = enumTypes[columnIndex] ?? null;
|
|
100
|
+
|
|
99
101
|
return (
|
|
100
102
|
<div
|
|
101
103
|
key={columnIndex}
|
|
@@ -154,6 +156,7 @@ export const GridRowView = memo(function GridRowView({ row, index, selected, foc
|
|
|
154
156
|
</div>
|
|
155
157
|
);
|
|
156
158
|
});
|
|
159
|
+
|
|
157
160
|
return (
|
|
158
161
|
<div
|
|
159
162
|
role="row"
|
package/src/grid/RecordView.tsx
CHANGED
|
@@ -56,13 +56,16 @@ const textOf = (cell: GridRow['cells'][number], kind: GridColumn['kind'], notati
|
|
|
56
56
|
if (isPreview(cell)) {
|
|
57
57
|
return cellView(cell, kind, notation).text;
|
|
58
58
|
}
|
|
59
|
+
|
|
59
60
|
if (cell !== null && typeof cell === 'object' && cell.kind === 'default') {
|
|
60
61
|
return '';
|
|
61
62
|
}
|
|
63
|
+
|
|
62
64
|
// A field that can be edited holds the server's text, which is what an edit starts from.
|
|
63
65
|
if (readOnly && isNumericKind(kind) && (typeof cell === 'number' || typeof cell === 'string')) {
|
|
64
66
|
return numeralText(cell, notation);
|
|
65
67
|
}
|
|
68
|
+
|
|
66
69
|
return draftOf(cell);
|
|
67
70
|
};
|
|
68
71
|
|
|
@@ -116,6 +119,7 @@ export function RecordView({
|
|
|
116
119
|
if (editing !== null && !editing.loading && row !== undefined && editing.draft !== editing.initial) {
|
|
117
120
|
onCommit?.(row.key, editing.column, parseDraft(editing.draft, columns[editing.column]!.kind));
|
|
118
121
|
}
|
|
122
|
+
|
|
119
123
|
setEditing(null);
|
|
120
124
|
};
|
|
121
125
|
|
|
@@ -127,19 +131,25 @@ export function RecordView({
|
|
|
127
131
|
|
|
128
132
|
const beginEdit = async (column: number): Promise<void> => {
|
|
129
133
|
const cell = row?.cells[column] ?? null;
|
|
134
|
+
|
|
130
135
|
if (row === undefined || !canEdit(column) || !isPreview(cell) || loadValue === undefined) {
|
|
131
136
|
return;
|
|
132
137
|
}
|
|
138
|
+
|
|
133
139
|
setEditing({ column, draft: '', initial: '', loading: true });
|
|
134
140
|
const value = await loadValue(row.key, column);
|
|
141
|
+
|
|
135
142
|
setEditing((now) => {
|
|
136
143
|
if (now === null || !now.loading || now.column !== column) {
|
|
137
144
|
return now;
|
|
138
145
|
}
|
|
146
|
+
|
|
139
147
|
if (value === undefined) {
|
|
140
148
|
return null;
|
|
141
149
|
}
|
|
150
|
+
|
|
142
151
|
const text = draftOf(value);
|
|
152
|
+
|
|
143
153
|
return { column, draft: text, initial: text, loading: false };
|
|
144
154
|
});
|
|
145
155
|
};
|
|
@@ -155,9 +165,11 @@ export function RecordView({
|
|
|
155
165
|
const toggleOpened = (column: number): void => {
|
|
156
166
|
setOpened((now) => {
|
|
157
167
|
const next = new Set(now);
|
|
168
|
+
|
|
158
169
|
if (!next.delete(column)) {
|
|
159
170
|
next.add(column);
|
|
160
171
|
}
|
|
172
|
+
|
|
161
173
|
return next;
|
|
162
174
|
});
|
|
163
175
|
};
|
|
@@ -171,10 +183,13 @@ export function RecordView({
|
|
|
171
183
|
if (event.nativeEvent.isComposing) {
|
|
172
184
|
return;
|
|
173
185
|
}
|
|
186
|
+
|
|
174
187
|
const mod = isModHeld(event, apple);
|
|
188
|
+
|
|
175
189
|
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
176
190
|
event.preventDefault();
|
|
177
191
|
const direction = event.key === 'ArrowUp' ? -1 : 1;
|
|
192
|
+
|
|
178
193
|
if (mod) {
|
|
179
194
|
moveRow(index + direction);
|
|
180
195
|
} else {
|
|
@@ -241,6 +256,7 @@ export function RecordView({
|
|
|
241
256
|
const placeholder = cell === null ? 'NULL' : typeof cell === 'object' && cell.kind === 'default' ? 'DEFAULT' : undefined;
|
|
242
257
|
const expandable = enumType === null && opensUp(cell, column.kind);
|
|
243
258
|
const expanded = expandable && opened.has(position);
|
|
259
|
+
|
|
244
260
|
return (
|
|
245
261
|
<ContextMenu.Root key={position}>
|
|
246
262
|
<ContextMenu.Trigger
|
|
@@ -382,12 +398,15 @@ function WholeValue({
|
|
|
382
398
|
if (known !== undefined || loadValue === undefined) {
|
|
383
399
|
return;
|
|
384
400
|
}
|
|
401
|
+
|
|
385
402
|
let live = true;
|
|
403
|
+
|
|
386
404
|
void loadValue(row.key, column).then((whole) => {
|
|
387
405
|
if (live) {
|
|
388
406
|
setFetched({ cell, value: whole });
|
|
389
407
|
}
|
|
390
408
|
});
|
|
409
|
+
|
|
391
410
|
return () => {
|
|
392
411
|
live = false;
|
|
393
412
|
};
|
package/src/grid/ValueDock.tsx
CHANGED
|
@@ -25,6 +25,7 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
|
|
|
25
25
|
const dock = useRef<HTMLDivElement>(null);
|
|
26
26
|
const [own, setOwn] = useState(VALUE_PANEL_WIDTH);
|
|
27
27
|
const width = given ?? own;
|
|
28
|
+
|
|
28
29
|
const { startResize } = useColumnResize(dock, {
|
|
29
30
|
size: width,
|
|
30
31
|
min: VALUE_PANEL_MIN_WIDTH,
|
|
@@ -34,6 +35,7 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
|
|
|
34
35
|
if (given === undefined) {
|
|
35
36
|
setOwn(next);
|
|
36
37
|
}
|
|
38
|
+
|
|
37
39
|
onWidthChange?.(next);
|
|
38
40
|
}
|
|
39
41
|
});
|
package/src/grid/aggregates.ts
CHANGED
|
@@ -32,10 +32,13 @@ const numberOf = (cell: Shown): { value: number; text: string } | null => {
|
|
|
32
32
|
if (typeof cell === 'number') {
|
|
33
33
|
return Number.isFinite(cell) ? { value: cell, text: String(cell) } : null;
|
|
34
34
|
}
|
|
35
|
+
|
|
35
36
|
if (typeof cell === 'string') {
|
|
36
37
|
const text = cell.trim();
|
|
38
|
+
|
|
37
39
|
return NUMERIC_TEXT.test(text) ? { value: Number(text), text } : null;
|
|
38
40
|
}
|
|
41
|
+
|
|
39
42
|
return null;
|
|
40
43
|
};
|
|
41
44
|
|
|
@@ -52,31 +55,40 @@ export const aggregateBlock = (rows: readonly (readonly Shown[])[], kinds: reado
|
|
|
52
55
|
let sum = 0;
|
|
53
56
|
let minimum: { value: number; text: string } | null = null;
|
|
54
57
|
let maximum: { value: number; text: string } | null = null;
|
|
58
|
+
|
|
55
59
|
for (let row = block.top; row <= block.bottom; row++) {
|
|
56
60
|
const cells = rows[row];
|
|
61
|
+
|
|
57
62
|
if (cells === undefined) {
|
|
58
63
|
continue;
|
|
59
64
|
}
|
|
65
|
+
|
|
60
66
|
for (const column of block.columns) {
|
|
61
67
|
count++;
|
|
62
68
|
const kind = kinds[column];
|
|
63
69
|
const found = kind !== undefined && isNumericKind(kind) ? numberOf(cells[column] ?? null) : null;
|
|
70
|
+
|
|
64
71
|
if (found === null) {
|
|
65
72
|
continue;
|
|
66
73
|
}
|
|
74
|
+
|
|
67
75
|
numbers++;
|
|
68
76
|
sum += found.value;
|
|
77
|
+
|
|
69
78
|
if (minimum === null || found.value < minimum.value) {
|
|
70
79
|
minimum = found;
|
|
71
80
|
}
|
|
81
|
+
|
|
72
82
|
if (maximum === null || found.value > maximum.value) {
|
|
73
83
|
maximum = found;
|
|
74
84
|
}
|
|
75
85
|
}
|
|
76
86
|
}
|
|
87
|
+
|
|
77
88
|
if (numbers === 0 || minimum === null || maximum === null) {
|
|
78
89
|
return { count, numeric: null };
|
|
79
90
|
}
|
|
91
|
+
|
|
80
92
|
return {
|
|
81
93
|
count,
|
|
82
94
|
numeric: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColumnView } from './column-view.ts';
|
|
2
2
|
import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.ts';
|
|
3
|
+
import { sameList } from '../same-list.ts';
|
|
3
4
|
|
|
4
5
|
/* What a person did to the columns of a grid, by column name so it outlives a change of the column order. */
|
|
5
6
|
export interface GridLayout {
|
|
@@ -10,8 +11,6 @@ export interface GridLayout {
|
|
|
10
11
|
readonly pinned: readonly string[];
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export const NO_GRID_LAYOUT: GridLayout = { widths: {}, hidden: [], pinned: [] };
|
|
14
|
-
|
|
15
14
|
export interface ColumnState {
|
|
16
15
|
readonly resized: Readonly<Record<number, number>>;
|
|
17
16
|
readonly view: ColumnView;
|
|
@@ -22,36 +21,46 @@ export const restoreLayout = (columns: readonly { readonly name: string }[], lay
|
|
|
22
21
|
const resized: Record<number, number> = {};
|
|
23
22
|
const hidden = new Set<number>();
|
|
24
23
|
const pinned: number[] = [];
|
|
24
|
+
|
|
25
25
|
if (layout !== undefined) {
|
|
26
26
|
columns.forEach((column, index) => {
|
|
27
27
|
const width = layout.widths[column.name];
|
|
28
|
+
|
|
28
29
|
if (width !== undefined) {
|
|
29
30
|
resized[index] = Math.max(MIN_COLUMN_WIDTH, Math.min(Math.round(width), MAX_RESIZED_WIDTH));
|
|
30
31
|
}
|
|
32
|
+
|
|
31
33
|
if (layout.hidden.includes(column.name)) {
|
|
32
34
|
hidden.add(index);
|
|
33
35
|
}
|
|
34
36
|
});
|
|
37
|
+
|
|
35
38
|
for (const name of layout.pinned) {
|
|
36
39
|
const index = columns.findIndex((column) => column.name === name);
|
|
40
|
+
|
|
37
41
|
if (index >= 0 && !hidden.has(index) && !pinned.includes(index)) {
|
|
38
42
|
pinned.push(index);
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
|
|
42
47
|
return { resized, view: { hidden: hidden.size >= columns.length ? new Set() : hidden, pinned } };
|
|
43
48
|
};
|
|
44
49
|
|
|
45
50
|
/* The layout of a grid's state, with the columns by name. */
|
|
46
51
|
export const captureLayout = (columns: readonly { readonly name: string }[], state: ColumnState): GridLayout => {
|
|
47
52
|
const widths: Record<string, number> = {};
|
|
53
|
+
|
|
48
54
|
for (const [index, width] of Object.entries(state.resized)) {
|
|
49
55
|
const column = columns[Number(index)];
|
|
56
|
+
|
|
50
57
|
if (column !== undefined) {
|
|
51
58
|
widths[column.name] = width;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
61
|
+
|
|
54
62
|
const namesOf = (indexes: readonly number[]): string[] => indexes.flatMap((index) => columns[index]?.name ?? []);
|
|
63
|
+
|
|
55
64
|
return {
|
|
56
65
|
widths,
|
|
57
66
|
hidden: namesOf([...state.view.hidden].sort((a, b) => a - b)),
|
|
@@ -59,10 +68,9 @@ export const captureLayout = (columns: readonly { readonly name: string }[], sta
|
|
|
59
68
|
};
|
|
60
69
|
};
|
|
61
70
|
|
|
62
|
-
const sameList = (a: readonly string[], b: readonly string[]): boolean => a.length === b.length && a.every((item, index) => item === b[index]);
|
|
63
|
-
|
|
64
71
|
export const sameLayout = (a: GridLayout, b: GridLayout): boolean => {
|
|
65
72
|
const names = Object.keys(a.widths);
|
|
73
|
+
|
|
66
74
|
return (
|
|
67
75
|
names.length === Object.keys(b.widths).length &&
|
|
68
76
|
names.every((name) => a.widths[name] === b.widths[name]) &&
|
|
@@ -19,22 +19,31 @@ export const NO_COLUMN_SELECTION: ColumnSelection = { columns: new Set(), anchor
|
|
|
19
19
|
*/
|
|
20
20
|
export const selectColumn = (current: ColumnSelection, order: readonly number[], index: number, click: ColumnClick): ColumnSelection => {
|
|
21
21
|
const at = order.indexOf(index);
|
|
22
|
+
|
|
22
23
|
if (at < 0) {
|
|
23
24
|
return current;
|
|
24
25
|
}
|
|
26
|
+
|
|
25
27
|
const anchorAt = current.anchor === null ? -1 : order.indexOf(current.anchor);
|
|
28
|
+
|
|
26
29
|
if (click.shiftKey && anchorAt >= 0) {
|
|
27
30
|
const range = order.slice(Math.min(anchorAt, at), Math.max(anchorAt, at) + 1);
|
|
31
|
+
|
|
28
32
|
return { columns: new Set(click.mod ? [...current.columns, ...range] : range), anchor: current.anchor };
|
|
29
33
|
}
|
|
34
|
+
|
|
30
35
|
if (click.mod) {
|
|
31
36
|
const next = new Set(current.columns);
|
|
37
|
+
|
|
32
38
|
if (next.delete(index)) {
|
|
33
39
|
return { columns: next, anchor: next.size === 0 ? null : current.anchor };
|
|
34
40
|
}
|
|
41
|
+
|
|
35
42
|
next.add(index);
|
|
43
|
+
|
|
36
44
|
return { columns: next, anchor: index };
|
|
37
45
|
}
|
|
46
|
+
|
|
38
47
|
return { columns: new Set([index]), anchor: index };
|
|
39
48
|
};
|
|
40
49
|
|
package/src/grid/column-view.ts
CHANGED
|
@@ -7,15 +7,19 @@ export interface ColumnView {
|
|
|
7
7
|
|
|
8
8
|
export const NO_COLUMN_VIEW: ColumnView = { hidden: new Set(), pinned: [] };
|
|
9
9
|
|
|
10
|
+
const shownPins = (count: number, view: ColumnView): number[] => view.pinned.filter((index) => index < count && !view.hidden.has(index));
|
|
11
|
+
|
|
10
12
|
/* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
|
|
11
13
|
export const displayOrder = (count: number, view: ColumnView): number[] => {
|
|
12
|
-
const pinned =
|
|
14
|
+
const pinned = shownPins(count, view);
|
|
13
15
|
const rest: number[] = [];
|
|
16
|
+
|
|
14
17
|
for (let i = 0; i < count; i++) {
|
|
15
18
|
if (!view.hidden.has(i) && !pinned.includes(i)) {
|
|
16
19
|
rest.push(i);
|
|
17
20
|
}
|
|
18
21
|
}
|
|
22
|
+
|
|
19
23
|
return [...pinned, ...rest];
|
|
20
24
|
};
|
|
21
25
|
|
|
@@ -32,4 +36,4 @@ export const togglePin = (view: ColumnView, index: number): ColumnView => ({
|
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
/* How many of the first columns of `displayOrder` are pinned. */
|
|
35
|
-
export const pinnedCount = (count: number, view: ColumnView): number =>
|
|
39
|
+
export const pinnedCount = (count: number, view: ColumnView): number => shownPins(count, view).length;
|
package/src/grid/copy-formats.ts
CHANGED
|
@@ -34,19 +34,24 @@ const jsonOf = (cell: Shown, kind: ValueKind): string => {
|
|
|
34
34
|
if (cell === null) {
|
|
35
35
|
return 'null';
|
|
36
36
|
}
|
|
37
|
+
|
|
37
38
|
if (typeof cell === 'boolean') {
|
|
38
39
|
return String(cell);
|
|
39
40
|
}
|
|
41
|
+
|
|
40
42
|
if (typeof cell === 'number') {
|
|
41
43
|
if (kind === 'boolean' && (cell === 0 || cell === 1)) {
|
|
42
44
|
return String(cell === 1);
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
return Number.isFinite(cell) ? String(cell) : 'null';
|
|
45
48
|
}
|
|
49
|
+
|
|
46
50
|
if (typeof cell === 'string') {
|
|
47
51
|
if (isNumericKind(kind) && JSON_NUMBER.test(cell)) {
|
|
48
52
|
return cell;
|
|
49
53
|
}
|
|
54
|
+
|
|
50
55
|
if (kind === 'json') {
|
|
51
56
|
try {
|
|
52
57
|
return JSON.stringify(JSON.parse(cell));
|
|
@@ -54,8 +59,10 @@ const jsonOf = (cell: Shown, kind: ValueKind): string => {
|
|
|
54
59
|
return JSON.stringify(cell);
|
|
55
60
|
}
|
|
56
61
|
}
|
|
62
|
+
|
|
57
63
|
return JSON.stringify(cell);
|
|
58
64
|
}
|
|
65
|
+
|
|
59
66
|
return cell.kind === 'default' ? 'null' : JSON.stringify(copyTextOf(cell));
|
|
60
67
|
};
|
|
61
68
|
|
|
@@ -70,13 +77,17 @@ const toJson = ({ columns, rows }: CopyInput): string => {
|
|
|
70
77
|
if (rows.length === 0) {
|
|
71
78
|
return '[]';
|
|
72
79
|
}
|
|
80
|
+
|
|
73
81
|
const objects = rows.map((row) => {
|
|
74
82
|
if (columns.length === 0) {
|
|
75
83
|
return ' {}';
|
|
76
84
|
}
|
|
85
|
+
|
|
77
86
|
const fields = columns.map((column, index) => ` ${JSON.stringify(column.name)}: ${jsonOf(row[index] ?? null, column.kind)}`);
|
|
87
|
+
|
|
78
88
|
return ` {\n${fields.join(',\n')}\n }`;
|
|
79
89
|
});
|
|
90
|
+
|
|
80
91
|
return `[\n${objects.join(',\n')}\n]`;
|
|
81
92
|
};
|
|
82
93
|
|
|
@@ -84,7 +95,9 @@ const toSql = ({ columns, rows, target }: CopyInput): string => {
|
|
|
84
95
|
if (target === undefined) {
|
|
85
96
|
throw new Error('Copying as SQL needs the table the rows belong to.');
|
|
86
97
|
}
|
|
98
|
+
|
|
87
99
|
const head = `INSERT INTO ${qualifiedName(target)} (${columns.map((column) => quoteIdentifier(target.engine, column.name)).join(', ')}) VALUES`;
|
|
100
|
+
|
|
88
101
|
return rows.map((row) => `${head} (${columns.map((column, index) => sqlLiteral(target.engine, row[index] ?? null, column.kind)).join(', ')});`).join('\n');
|
|
89
102
|
};
|
|
90
103
|
|
package/src/grid/display.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatBytes, formatNumeral } from '@adecore/ui/format';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Cell, EditValue, ValueKind } from '../protocol/index.ts';
|
|
3
3
|
import { isNumericKind } from '../sql.ts';
|
|
4
4
|
import type { GridColumn } from './types.ts';
|
|
5
5
|
|
|
@@ -21,6 +21,7 @@ export type NumberNotation = 'database' | 'region';
|
|
|
21
21
|
|
|
22
22
|
/* A cell never draws more than this, however long the text: the DOM stays small and the cell is one line anyway. */
|
|
23
23
|
export const DISPLAY_LIMIT = 200;
|
|
24
|
+
|
|
24
25
|
/* How many bytes of a binary value the cell spells out. */
|
|
25
26
|
export const BINARY_PREVIEW_BYTES = 8;
|
|
26
27
|
|
|
@@ -33,9 +34,11 @@ export const isPreview = (cell: Shown): boolean => {
|
|
|
33
34
|
if (!isObject(cell)) {
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
37
|
+
|
|
36
38
|
if (cell.kind === 'longText') {
|
|
37
39
|
return true;
|
|
38
40
|
}
|
|
41
|
+
|
|
39
42
|
return cell.kind === 'binary' && cell.hex.length < cell.length * 2;
|
|
40
43
|
};
|
|
41
44
|
|
|
@@ -44,13 +47,16 @@ export const numeralText = (numeral: string | number, notation: NumberNotation):
|
|
|
44
47
|
if (notation === 'database') {
|
|
45
48
|
return String(numeral);
|
|
46
49
|
}
|
|
50
|
+
|
|
47
51
|
const formatted = formatNumeral(numeral);
|
|
52
|
+
|
|
48
53
|
return formatted === String(numeral).trim() ? String(numeral) : formatted;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
/* Identifiers are labels, even when the database stores their characters as a number. */
|
|
52
57
|
export function columnNotation(column: GridColumn, notation: NumberNotation): NumberNotation {
|
|
53
58
|
const identifierName = (name: string): boolean => /^(?:id|.+_id)$/i.test(name) || /(?:Id|ID)$/.test(name);
|
|
59
|
+
|
|
54
60
|
return column.primaryKey === true ||
|
|
55
61
|
column.foreignKey === true ||
|
|
56
62
|
column.autoIncrement === true ||
|
|
@@ -65,18 +71,23 @@ const cut = (text: string): string => (text.length > DISPLAY_LIMIT ? `${text.sli
|
|
|
65
71
|
export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation = 'database'): CellView => {
|
|
66
72
|
const numeric = isNumericKind(kind);
|
|
67
73
|
const align = numeric ? 'end' : 'start';
|
|
74
|
+
|
|
68
75
|
if (cell === null) {
|
|
69
76
|
return { text: 'NULL', tone: 'null', align };
|
|
70
77
|
}
|
|
78
|
+
|
|
71
79
|
if (typeof cell === 'boolean') {
|
|
72
80
|
return { text: cell ? 'true' : 'false', tone: 'value', align };
|
|
73
81
|
}
|
|
82
|
+
|
|
74
83
|
if (typeof cell === 'number') {
|
|
75
84
|
return { text: numeric ? numeralText(cell, notation) : String(cell), tone: 'value', align };
|
|
76
85
|
}
|
|
86
|
+
|
|
77
87
|
if (typeof cell === 'string') {
|
|
78
88
|
return { text: cut(numeric ? numeralText(cell, notation) : cell), tone: 'value', align };
|
|
79
89
|
}
|
|
90
|
+
|
|
80
91
|
switch (cell.kind) {
|
|
81
92
|
case 'default':
|
|
82
93
|
return { text: 'DEFAULT', tone: 'default', align };
|
|
@@ -85,6 +96,7 @@ export const cellView = (cell: Shown, kind: ValueKind, notation: NumberNotation
|
|
|
85
96
|
case 'binary': {
|
|
86
97
|
const shown = cell.hex.slice(0, BINARY_PREVIEW_BYTES * 2);
|
|
87
98
|
const truncated = shown.length < cell.length * 2;
|
|
99
|
+
|
|
88
100
|
return { text: `0x${shown}${truncated ? ELLIPSIS : ''}`, tone: 'value', align: 'start', suffix: formatBytes(cell.length) };
|
|
89
101
|
}
|
|
90
102
|
}
|
|
@@ -95,9 +107,11 @@ export const copyTextOf = (cell: Shown): string => {
|
|
|
95
107
|
if (cell === null) {
|
|
96
108
|
return 'NULL';
|
|
97
109
|
}
|
|
110
|
+
|
|
98
111
|
if (typeof cell !== 'object') {
|
|
99
112
|
return String(cell);
|
|
100
113
|
}
|
|
114
|
+
|
|
101
115
|
switch (cell.kind) {
|
|
102
116
|
case 'default':
|
|
103
117
|
return 'DEFAULT';
|
|
@@ -111,8 +125,8 @@ export const copyTextOf = (cell: Shown): string => {
|
|
|
111
125
|
/* An edit as a cell, so the grid draws a pending value the way it draws a loaded one. */
|
|
112
126
|
export const shownOfEdit = (value: EditValue): Shown => {
|
|
113
127
|
if (value !== null && typeof value === 'object' && value.kind === 'binary') {
|
|
114
|
-
|
|
115
|
-
return { kind: 'binary', hex: binary.hex, length: binary.hex.length / 2 };
|
|
128
|
+
return { kind: 'binary', hex: value.hex, length: value.hex.length / 2 };
|
|
116
129
|
}
|
|
130
|
+
|
|
117
131
|
return value as Shown;
|
|
118
132
|
};
|
package/src/grid/edit-value.ts
CHANGED
|
@@ -11,42 +11,53 @@ export const draftOf = (value: Cell | Value | { readonly kind: 'default' }): str
|
|
|
11
11
|
if (value === null) {
|
|
12
12
|
return '';
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
if (typeof value !== 'object') {
|
|
15
16
|
return String(value);
|
|
16
17
|
}
|
|
18
|
+
|
|
17
19
|
if (value.kind === 'binary') {
|
|
18
20
|
return `0x${value.hex}`;
|
|
19
21
|
}
|
|
22
|
+
|
|
20
23
|
if (value.kind === 'longText') {
|
|
21
24
|
return value.preview;
|
|
22
25
|
}
|
|
26
|
+
|
|
23
27
|
return '';
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
/* What a person typed, as the value to send. A number the text spells exactly stays a number; anything else goes as text and the server decides. */
|
|
27
31
|
export const parseDraft = (text: string, kind: ValueKind): EditValue => {
|
|
28
32
|
const trimmed = text.trim();
|
|
33
|
+
|
|
29
34
|
if (trimmed === '' && EMPTY_IS_NULL.has(kind)) {
|
|
30
35
|
return null;
|
|
31
36
|
}
|
|
37
|
+
|
|
32
38
|
switch (kind) {
|
|
33
39
|
case 'integer': {
|
|
34
40
|
const number = Number(trimmed);
|
|
41
|
+
|
|
35
42
|
return INTEGER.test(trimmed) && Number.isSafeInteger(number) ? number : trimmed;
|
|
36
43
|
}
|
|
37
44
|
case 'float': {
|
|
38
45
|
const number = Number(trimmed);
|
|
46
|
+
|
|
39
47
|
return Number.isFinite(number) ? number : trimmed;
|
|
40
48
|
}
|
|
41
49
|
case 'boolean': {
|
|
42
50
|
const lower = trimmed.toLowerCase();
|
|
51
|
+
|
|
43
52
|
if (lower === 'true' || lower === '1') {
|
|
44
53
|
return true;
|
|
45
54
|
}
|
|
55
|
+
|
|
46
56
|
return lower === 'false' || lower === '0' ? false : trimmed;
|
|
47
57
|
}
|
|
48
58
|
case 'binary': {
|
|
49
59
|
const match = HEX.exec(trimmed);
|
|
60
|
+
|
|
50
61
|
return match !== null && match[1]!.length % 2 === 0 ? { kind: 'binary', hex: match[1]!.toLowerCase() } : trimmed;
|
|
51
62
|
}
|
|
52
63
|
case 'decimal':
|
package/src/grid/enum-type.ts
CHANGED
|
@@ -11,44 +11,59 @@ const DECLARATION = /^(enum|set)\s*\(/i;
|
|
|
11
11
|
*/
|
|
12
12
|
export const parseEnumType = (type: string): EnumType | null => {
|
|
13
13
|
const head = DECLARATION.exec(type.trim());
|
|
14
|
+
|
|
14
15
|
if (head === null) {
|
|
15
16
|
return null;
|
|
16
17
|
}
|
|
18
|
+
|
|
17
19
|
const source = type.trim().slice(head[0].length);
|
|
18
20
|
const values: string[] = [];
|
|
19
21
|
let position = 0;
|
|
22
|
+
|
|
20
23
|
while (position < source.length) {
|
|
21
24
|
if (source[position] !== "'") {
|
|
22
25
|
return null;
|
|
23
26
|
}
|
|
27
|
+
|
|
24
28
|
position++;
|
|
25
29
|
let value = '';
|
|
30
|
+
|
|
26
31
|
for (;;) {
|
|
27
32
|
if (position >= source.length) {
|
|
28
33
|
return null;
|
|
29
34
|
}
|
|
35
|
+
|
|
30
36
|
if (source[position] === "'") {
|
|
31
37
|
if (source[position + 1] === "'") {
|
|
32
38
|
value += "'";
|
|
33
39
|
position += 2;
|
|
40
|
+
|
|
34
41
|
continue;
|
|
35
42
|
}
|
|
43
|
+
|
|
36
44
|
position++;
|
|
45
|
+
|
|
37
46
|
break;
|
|
38
47
|
}
|
|
48
|
+
|
|
39
49
|
value += source[position];
|
|
40
50
|
position++;
|
|
41
51
|
}
|
|
52
|
+
|
|
42
53
|
values.push(value);
|
|
43
54
|
const next = source[position];
|
|
55
|
+
|
|
44
56
|
if (next === ')') {
|
|
45
57
|
return source.slice(position + 1).trim() === '' ? tidy(head[1]!, values) : null;
|
|
46
58
|
}
|
|
59
|
+
|
|
47
60
|
if (next !== ',') {
|
|
48
61
|
return null;
|
|
49
62
|
}
|
|
63
|
+
|
|
50
64
|
position++;
|
|
51
65
|
}
|
|
66
|
+
|
|
52
67
|
return null;
|
|
53
68
|
};
|
|
54
69
|
|