@adecore/database 0.21.1 → 0.21.2
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/assign-ref.d.ts +2 -0
- package/dist/assign-ref.js +9 -0
- package/dist/client/client.js +14 -15
- package/dist/client/types.d.ts +1 -0
- package/dist/client/types.js +2 -0
- package/dist/connections/ConnectionForm.js +7 -2
- package/dist/connections/ConnectionManager.js +5 -5
- package/dist/connections/EngineIcon.js +0 -1
- package/dist/console/QueryConsole.js +12 -4
- package/dist/console/editing.js +2 -4
- package/dist/console/useConsoleHistory.js +3 -2
- package/dist/ddl/alter-mysql.js +2 -8
- package/dist/ddl/alter-sqlite.js +1 -1
- package/dist/ddl/column.js +5 -8
- package/dist/ddl/create.d.ts +1 -0
- package/dist/ddl/create.js +1 -1
- package/dist/ddl/diff.js +25 -23
- package/dist/designer/ColumnsEditor.js +8 -6
- package/dist/designer/ForeignKeysEditor.js +5 -1
- package/dist/designer/OptionsEditor.js +12 -3
- package/dist/designer/SqlPreview.d.ts +1 -5
- package/dist/designer/SqlPreview.js +1 -1
- package/dist/designer/TableDesigner.js +7 -6
- package/dist/explorer/DatabaseExplorer.js +12 -13
- package/dist/explorer/TableDialog.js +1 -1
- package/dist/explorer/tree.js +6 -16
- package/dist/grid/DataGrid.js +11 -14
- package/dist/grid/EnumPicker.js +4 -3
- package/dist/grid/GridHeaderCell.d.ts +1 -1
- package/dist/grid/GridHeaderCell.js +2 -2
- package/dist/grid/RecordView.js +2 -1
- package/dist/grid/ValueDock.js +2 -6
- package/dist/grid/aggregates.js +2 -3
- package/dist/grid/column-layout.js +3 -2
- package/dist/grid/copy-formats.js +2 -3
- package/dist/grid/display.d.ts +0 -1
- package/dist/grid/display.js +1 -3
- package/dist/grid/layout.js +6 -11
- package/dist/host/helper-path.js +8 -0
- package/dist/host/host.js +11 -16
- package/dist/host/validate.js +44 -43
- package/dist/sql.d.ts +2 -0
- package/dist/sql.js +5 -2
- package/dist/structure/StructureView.js +5 -6
- package/dist/table/TableView.js +19 -12
- package/dist/table/command-field.js +8 -12
- package/dist/table/grid-rows.js +2 -1
- package/dist/table/layout-store.js +5 -2
- package/dist/table/pending.js +11 -3
- package/dist/table/useTableTransfer.js +6 -5
- package/dist/testing/fake.js +7 -9
- package/dist/value/ValueEditor.js +1 -1
- package/dist/value/draft.js +1 -6
- package/dist/value/json.js +4 -4
- package/dist/workbench/DatabaseWorkbench.js +12 -5
- package/dist/workbench/tabs.js +3 -4
- package/package.json +6 -6
- package/src/assign-ref.ts +10 -0
- package/src/client/client.ts +17 -18
- package/src/client/types.ts +3 -0
- package/src/connections/ConnectionForm.tsx +62 -47
- package/src/connections/ConnectionManager.tsx +6 -5
- package/src/connections/EngineIcon.tsx +0 -2
- package/src/console/QueryConsole.tsx +52 -31
- package/src/console/editing.ts +2 -4
- package/src/console/useConsoleHistory.ts +5 -2
- package/src/ddl/alter-mysql.ts +3 -11
- package/src/ddl/alter-sqlite.ts +1 -1
- package/src/ddl/column.ts +5 -7
- package/src/ddl/create.ts +1 -1
- package/src/ddl/diff.ts +30 -25
- package/src/designer/ColumnsEditor.tsx +145 -125
- package/src/designer/ForeignKeysEditor.tsx +44 -27
- package/src/designer/OptionsEditor.tsx +95 -75
- package/src/designer/SqlPreview.tsx +2 -3
- package/src/designer/TableDesigner.tsx +7 -6
- package/src/explorer/DatabaseExplorer.tsx +26 -31
- package/src/explorer/TableDialog.tsx +1 -1
- package/src/explorer/tree.ts +8 -19
- package/src/grid/DataGrid.tsx +11 -14
- package/src/grid/EnumPicker.tsx +4 -3
- package/src/grid/GridHeaderCell.tsx +2 -3
- package/src/grid/RecordView.tsx +2 -1
- package/src/grid/ValueDock.tsx +2 -5
- package/src/grid/aggregates.ts +2 -4
- package/src/grid/column-layout.ts +3 -2
- package/src/grid/copy-formats.ts +2 -3
- package/src/grid/display.ts +1 -5
- package/src/grid/layout.ts +10 -11
- package/src/host/helper-path.ts +8 -8
- package/src/host/host.ts +11 -16
- package/src/host/validate.ts +45 -43
- package/src/protocol/messages.ts +0 -1
- package/src/sql.ts +7 -2
- package/src/structure/StructureView.tsx +7 -8
- package/src/table/CommandField.tsx +0 -1
- package/src/table/TableView.tsx +48 -34
- package/src/table/command-field.ts +9 -12
- package/src/table/grid-rows.ts +2 -7
- package/src/table/layout-store.ts +6 -3
- package/src/table/pending.ts +12 -7
- package/src/table/useTableTransfer.ts +7 -5
- package/src/testing/fake.ts +8 -12
- package/src/value/ValueEditor.tsx +1 -1
- package/src/value/draft.ts +1 -6
- package/src/value/json.ts +4 -4
- package/src/workbench/DatabaseWorkbench.tsx +14 -6
- package/src/workbench/DesignerTab.tsx +1 -1
- package/src/workbench/tabs.ts +5 -8
|
@@ -11,91 +11,111 @@ export function OptionsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
11
11
|
const { t } = useTranslation('database');
|
|
12
12
|
const id = useId();
|
|
13
13
|
const { options } = draft;
|
|
14
|
+
const patch = (change: Parameters<typeof patchOptions>[1]): void => onChange(patchOptions(draft, change));
|
|
14
15
|
if (dialect.engine === 'sqlite') {
|
|
15
16
|
return (
|
|
16
17
|
<div ref={ref} className={clsx('flex flex-col gap-4 p-3', className)}>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
label={t('designer.options.strict')}
|
|
32
|
-
checked={options.strict}
|
|
33
|
-
disabled={disabled}
|
|
34
|
-
onCheckedChange={(checked) => onChange(patchOptions(draft, { strict: checked }))}
|
|
35
|
-
/>
|
|
36
|
-
<span className="flex flex-col">
|
|
37
|
-
<span className="text-sm text-text">{t('designer.options.strict')}</span>
|
|
38
|
-
<span className="text-xs text-text-muted">{t('designer.options.strictHint')}</span>
|
|
39
|
-
</span>
|
|
40
|
-
</label>
|
|
18
|
+
<OptionSwitch
|
|
19
|
+
label={t('designer.options.withoutRowid')}
|
|
20
|
+
hint={t('designer.options.withoutRowidHint')}
|
|
21
|
+
checked={options.withoutRowid}
|
|
22
|
+
disabled={disabled}
|
|
23
|
+
onCheckedChange={(withoutRowid) => patch({ withoutRowid })}
|
|
24
|
+
/>
|
|
25
|
+
<OptionSwitch
|
|
26
|
+
label={t('designer.options.strict')}
|
|
27
|
+
hint={t('designer.options.strictHint')}
|
|
28
|
+
checked={options.strict}
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
onCheckedChange={(strict) => patch({ strict })}
|
|
31
|
+
/>
|
|
41
32
|
</div>
|
|
42
33
|
);
|
|
43
34
|
}
|
|
44
35
|
return (
|
|
45
36
|
<div ref={ref} className={clsx('flex max-w-xl flex-col gap-4 p-3', className)}>
|
|
46
|
-
<
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</Field>
|
|
71
|
-
<Field label={t('designer.options.charset')} hint={t('designer.options.serverDefault')}>
|
|
72
|
-
<Input
|
|
73
|
-
mono
|
|
74
|
-
list={`${id}-charsets`}
|
|
75
|
-
spellCheck={false}
|
|
76
|
-
value={options.charset}
|
|
77
|
-
disabled={disabled}
|
|
78
|
-
onChange={(event) => onChange(patchOptions(draft, { charset: event.target.value }))}
|
|
79
|
-
/>
|
|
80
|
-
</Field>
|
|
81
|
-
<Field label={t('designer.options.collation')} hint={t('designer.options.serverDefault')}>
|
|
82
|
-
<Input
|
|
83
|
-
mono
|
|
84
|
-
list={`${id}-collations`}
|
|
85
|
-
spellCheck={false}
|
|
86
|
-
value={options.collation}
|
|
87
|
-
disabled={disabled}
|
|
88
|
-
onChange={(event) => onChange(patchOptions(draft, { collation: event.target.value }))}
|
|
89
|
-
/>
|
|
90
|
-
</Field>
|
|
37
|
+
<SuggestedField
|
|
38
|
+
label={t('designer.options.engine')}
|
|
39
|
+
listId={`${id}-engines`}
|
|
40
|
+
suggestions={MYSQL_ENGINES}
|
|
41
|
+
value={options.engine}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
onValueChange={(engine) => patch({ engine })}
|
|
44
|
+
/>
|
|
45
|
+
<SuggestedField
|
|
46
|
+
label={t('designer.options.charset')}
|
|
47
|
+
listId={`${id}-charsets`}
|
|
48
|
+
suggestions={MYSQL_CHARSETS}
|
|
49
|
+
value={options.charset}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
onValueChange={(charset) => patch({ charset })}
|
|
52
|
+
/>
|
|
53
|
+
<SuggestedField
|
|
54
|
+
label={t('designer.options.collation')}
|
|
55
|
+
listId={`${id}-collations`}
|
|
56
|
+
suggestions={collationSuggestionsOf(options.charset)}
|
|
57
|
+
value={options.collation}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
onValueChange={(collation) => patch({ collation })}
|
|
60
|
+
/>
|
|
91
61
|
<Field label={t('designer.options.comment')}>
|
|
92
|
-
<TextArea
|
|
93
|
-
rows={3}
|
|
94
|
-
value={options.comment}
|
|
95
|
-
disabled={disabled}
|
|
96
|
-
onChange={(event) => onChange(patchOptions(draft, { comment: event.target.value }))}
|
|
97
|
-
/>
|
|
62
|
+
<TextArea rows={3} value={options.comment} disabled={disabled} onChange={(event) => patch({ comment: event.target.value })} />
|
|
98
63
|
</Field>
|
|
99
64
|
</div>
|
|
100
65
|
);
|
|
101
66
|
}
|
|
67
|
+
|
|
68
|
+
function OptionSwitch({
|
|
69
|
+
label,
|
|
70
|
+
hint,
|
|
71
|
+
checked,
|
|
72
|
+
disabled,
|
|
73
|
+
onCheckedChange
|
|
74
|
+
}: {
|
|
75
|
+
label: string;
|
|
76
|
+
hint: string;
|
|
77
|
+
checked: boolean;
|
|
78
|
+
disabled: boolean;
|
|
79
|
+
onCheckedChange(checked: boolean): void;
|
|
80
|
+
}) {
|
|
81
|
+
return (
|
|
82
|
+
<label className="flex items-start gap-3">
|
|
83
|
+
<Switch label={label} checked={checked} disabled={disabled} onCheckedChange={onCheckedChange} />
|
|
84
|
+
<span className="flex flex-col">
|
|
85
|
+
<span className="text-sm text-text">{label}</span>
|
|
86
|
+
<span className="text-xs text-text-muted">{hint}</span>
|
|
87
|
+
</span>
|
|
88
|
+
</label>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* A free text field with a list of common values to pick from; empty leaves it to the server. */
|
|
93
|
+
function SuggestedField({
|
|
94
|
+
label,
|
|
95
|
+
listId,
|
|
96
|
+
suggestions,
|
|
97
|
+
value,
|
|
98
|
+
disabled,
|
|
99
|
+
onValueChange
|
|
100
|
+
}: {
|
|
101
|
+
label: string;
|
|
102
|
+
listId: string;
|
|
103
|
+
suggestions: readonly string[];
|
|
104
|
+
value: string;
|
|
105
|
+
disabled: boolean;
|
|
106
|
+
onValueChange(value: string): void;
|
|
107
|
+
}) {
|
|
108
|
+
const { t } = useTranslation('database');
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<datalist id={listId}>
|
|
112
|
+
{suggestions.map((suggestion) => (
|
|
113
|
+
<option key={suggestion} value={suggestion} />
|
|
114
|
+
))}
|
|
115
|
+
</datalist>
|
|
116
|
+
<Field label={label} hint={t('designer.options.serverDefault')}>
|
|
117
|
+
<Input mono list={listId} spellCheck={false} value={value} disabled={disabled} onChange={(event) => onValueChange(event.target.value)} />
|
|
118
|
+
</Field>
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -5,8 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
5
5
|
import { Button, Icon, copyText } from '@adecore/ui';
|
|
6
6
|
import { CODE_TEXT } from '../code-text.ts';
|
|
7
7
|
import type { DraftProblem } from '../ddl/index.ts';
|
|
8
|
-
import type
|
|
9
|
-
import { scriptOf } from './plan.ts';
|
|
8
|
+
import { scriptOf, type Plan } from './plan.ts';
|
|
10
9
|
|
|
11
10
|
export interface SqlPreviewProps {
|
|
12
11
|
plan: Plan;
|
|
@@ -15,7 +14,7 @@ export interface SqlPreviewProps {
|
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
/* The sentence a problem of the draft is read as. */
|
|
18
|
-
|
|
17
|
+
function ProblemText({ problem }: { problem: DraftProblem }) {
|
|
19
18
|
const { t } = useTranslation('database');
|
|
20
19
|
return <>{t(`ddl.problem.${problem.code}`, { subject: problem.subject })}</>;
|
|
21
20
|
}
|
|
@@ -104,13 +104,16 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
|
|
|
104
104
|
const session = client.session(connection);
|
|
105
105
|
const statements = plan.statements;
|
|
106
106
|
const wasNew = snapshot.structure === null;
|
|
107
|
+
const settle = (message: string | null): void => {
|
|
108
|
+
setFailure(message);
|
|
109
|
+
setConfirming(false);
|
|
110
|
+
};
|
|
107
111
|
let outcome: ExecuteResult;
|
|
108
112
|
applying.current = true;
|
|
109
113
|
try {
|
|
110
114
|
outcome = await session.execute(statements.join(';\n'), { schema });
|
|
111
115
|
} catch (e) {
|
|
112
|
-
|
|
113
|
-
setConfirming(false);
|
|
116
|
+
settle(messageOf(e, t('designer.failed')));
|
|
114
117
|
return;
|
|
115
118
|
} finally {
|
|
116
119
|
applying.current = false;
|
|
@@ -118,12 +121,10 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
|
|
|
118
121
|
const failed = outcome.results.find((result) => result.kind === 'error');
|
|
119
122
|
if (failed?.kind === 'error') {
|
|
120
123
|
await recoverFrom(session, statements, failed.sql, outcome.inTransaction);
|
|
121
|
-
|
|
122
|
-
setConfirming(false);
|
|
124
|
+
settle(failed.error.message);
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
125
|
-
|
|
126
|
-
setConfirming(false);
|
|
127
|
+
settle(null);
|
|
127
128
|
// The client has told its listeners already: `execute` does for a statement that creates, alters or drops.
|
|
128
129
|
if (wasNew || draft.name !== draft.originalName) {
|
|
129
130
|
setSubject({ requested: table, current: draft.name });
|
|
@@ -11,6 +11,7 @@ import type { TableKind } from '../protocol/index.ts';
|
|
|
11
11
|
import { EngineIcon } from '../connections/EngineIcon.tsx';
|
|
12
12
|
import { keyLabelOf, type ColumnKeys } from '../column-keys.ts';
|
|
13
13
|
import { KeyIcon } from '../KeyIcon.tsx';
|
|
14
|
+
import { usePopupPress } from '../use-popup-press.ts';
|
|
14
15
|
import type { ExplorerFolder } from './folders.ts';
|
|
15
16
|
import { RowMenu } from './RowMenu.tsx';
|
|
16
17
|
import { TableDialog, type TableRequest } from './TableDialog.tsx';
|
|
@@ -31,12 +32,10 @@ import {
|
|
|
31
32
|
tableOf,
|
|
32
33
|
type EntryRow,
|
|
33
34
|
type ErrorRow,
|
|
34
|
-
type LoadTarget,
|
|
35
35
|
type ExpandableRow,
|
|
36
36
|
type TreeRow
|
|
37
37
|
} from './tree.ts';
|
|
38
38
|
import { useExplorerLoads } from './use-explorer-loads.ts';
|
|
39
|
-
import { usePopupPress } from '../use-popup-press.ts';
|
|
40
39
|
import { useStoredExpansion } from './use-stored-expansion.ts';
|
|
41
40
|
|
|
42
41
|
export interface DatabaseExplorerProps {
|
|
@@ -228,10 +227,6 @@ export function DatabaseExplorer({
|
|
|
228
227
|
}
|
|
229
228
|
};
|
|
230
229
|
|
|
231
|
-
const refresh = (target: LoadTarget): void => {
|
|
232
|
-
loads.reset(target);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
230
|
const disconnect = (connection: Connection): void => {
|
|
236
231
|
void client.disconnect(connection.id).catch(() => undefined);
|
|
237
232
|
loads.reset({ connectionId: connection.id });
|
|
@@ -259,9 +254,10 @@ export function DatabaseExplorer({
|
|
|
259
254
|
row.kind === 'appItem' ? (
|
|
260
255
|
(row.item.menu ?? null)
|
|
261
256
|
) : connection !== undefined && hasMenu(row) ? (
|
|
262
|
-
<RowMenu row={row} connection={connection} onRefresh={
|
|
257
|
+
<RowMenu row={row} connection={connection} onRefresh={loads.reset} onDisconnect={disconnect} onRequest={setRequest} />
|
|
263
258
|
) : null;
|
|
264
259
|
const selectedRow = isSelected(row);
|
|
260
|
+
const draggable = row.kind === 'table' && onTableDragStart !== undefined;
|
|
265
261
|
const props = {
|
|
266
262
|
level: row.level,
|
|
267
263
|
selected: selectedRow,
|
|
@@ -281,12 +277,9 @@ export function DatabaseExplorer({
|
|
|
281
277
|
rowElements.current.set(row.key, element);
|
|
282
278
|
}
|
|
283
279
|
},
|
|
284
|
-
draggable
|
|
285
|
-
onDragStart:
|
|
286
|
-
|
|
287
|
-
? (event: DragEvent<HTMLElement>) => onTableDragStart(row.ref, event, row.table.kind)
|
|
288
|
-
: undefined,
|
|
289
|
-
onDragEnd: row.kind === 'table' && onTableDragStart !== undefined ? onTableDragEnd : undefined,
|
|
280
|
+
draggable,
|
|
281
|
+
onDragStart: draggable ? (event: DragEvent<HTMLElement>) => onTableDragStart(row.ref, event, row.table.kind) : undefined,
|
|
282
|
+
onDragEnd: draggable ? onTableDragEnd : undefined,
|
|
290
283
|
onClick: (event: MouseEvent) => click(row, event.detail),
|
|
291
284
|
onDoubleClick: tableOf(row) === null && row.kind !== 'appItem' ? undefined : () => open(row),
|
|
292
285
|
onFocus: () => {
|
|
@@ -374,12 +367,9 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
374
367
|
);
|
|
375
368
|
case 'folder':
|
|
376
369
|
return (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<Tree.Label>{t(row.group === 'view' ? 'explorer.views' : 'explorer.tables')}</Tree.Label>
|
|
381
|
-
<span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
|
|
382
|
-
</>
|
|
370
|
+
<FolderContent expanded={row.expanded} count={row.count} onToggle={onToggle}>
|
|
371
|
+
{t(row.group === 'view' ? 'explorer.views' : 'explorer.tables')}
|
|
372
|
+
</FolderContent>
|
|
383
373
|
);
|
|
384
374
|
case 'table':
|
|
385
375
|
return (
|
|
@@ -391,21 +381,15 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
391
381
|
);
|
|
392
382
|
case 'part':
|
|
393
383
|
return (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
<Tree.Label>{t(`explorer.parts.${row.part}`)}</Tree.Label>
|
|
398
|
-
<span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
|
|
399
|
-
</>
|
|
384
|
+
<FolderContent expanded={row.expanded} count={row.count} onToggle={onToggle}>
|
|
385
|
+
{t(`explorer.parts.${row.part}`)}
|
|
386
|
+
</FolderContent>
|
|
400
387
|
);
|
|
401
388
|
case 'appFolder':
|
|
402
389
|
return (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
<Tree.Label>{row.folder.label}</Tree.Label>
|
|
407
|
-
<span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
|
|
408
|
-
</>
|
|
390
|
+
<FolderContent expanded={row.expanded} count={row.count} onToggle={onToggle}>
|
|
391
|
+
{row.folder.label}
|
|
392
|
+
</FolderContent>
|
|
409
393
|
);
|
|
410
394
|
case 'appItem':
|
|
411
395
|
return (
|
|
@@ -446,6 +430,17 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
446
430
|
}
|
|
447
431
|
}
|
|
448
432
|
|
|
433
|
+
function FolderContent({ expanded, count, onToggle, children }: { expanded: boolean; count: number; onToggle?: () => void; children: ReactNode }) {
|
|
434
|
+
return (
|
|
435
|
+
<>
|
|
436
|
+
<Tree.Chevron expanded={expanded} onExpandedChange={onToggle} />
|
|
437
|
+
<Icon icon={Folder} size={16} className="shrink-0 text-text-muted" />
|
|
438
|
+
<Tree.Label>{children}</Tree.Label>
|
|
439
|
+
<span className="shrink-0 text-text-faint tabular-nums">{formatNumber(count)}</span>
|
|
440
|
+
</>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
449
444
|
const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables', columns: 'explorer.noColumns' } as const;
|
|
450
445
|
|
|
451
446
|
function SchemaCount({ shown, total }: { shown: number; total: number }) {
|
|
@@ -32,7 +32,7 @@ export function TableDialog({ request, onClose, onDone }: TableDialogProps) {
|
|
|
32
32
|
const dialect = dialectOf(await session.server());
|
|
33
33
|
const { results } = await session.execute(statementOf(dialect, change, ref, kind, to));
|
|
34
34
|
const failed = results.find((result) => result.kind === 'error');
|
|
35
|
-
if (failed
|
|
35
|
+
if (failed?.kind === 'error') {
|
|
36
36
|
throw new Error(failed.error.message);
|
|
37
37
|
}
|
|
38
38
|
// The client tells its listeners after a `RENAME`, a `DROP` and a `TRUNCATE`, but not after a SQLite emptying, which is a `DELETE FROM`.
|
package/src/explorer/tree.ts
CHANGED
|
@@ -230,14 +230,7 @@ export const selectionOf = (row: TreeRow): ExplorerSelection | null => {
|
|
|
230
230
|
};
|
|
231
231
|
|
|
232
232
|
/* The key of the row a selection points at: its table, else its schema, else its connection. */
|
|
233
|
-
export const selectionKey = (selection: ExplorerSelection): string =>
|
|
234
|
-
if (selection.schema === undefined) {
|
|
235
|
-
return connectionKey(selection.connectionId);
|
|
236
|
-
}
|
|
237
|
-
return selection.table === undefined
|
|
238
|
-
? schemaKey(selection.connectionId, selection.schema)
|
|
239
|
-
: tableKey({ connectionId: selection.connectionId, schema: selection.schema, table: selection.table });
|
|
240
|
-
};
|
|
233
|
+
export const selectionKey = (selection: ExplorerSelection): string => loadKey(selection);
|
|
241
234
|
|
|
242
235
|
/* 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. */
|
|
243
236
|
export const hasMenu = (row: TreeRow): boolean =>
|
|
@@ -297,6 +290,10 @@ const statusDraft = (row: StatusRow, level: number, parent: string, focusable: b
|
|
|
297
290
|
below: []
|
|
298
291
|
});
|
|
299
292
|
|
|
293
|
+
/* The row of a load that is not ready: its error, which can be retried and so takes the focus, or that it is loading. */
|
|
294
|
+
const pendingDraft = (load: Exclude<Load<unknown>, { status: 'ready' }>, retry: LoadTarget, level: number, parent: string): Draft =>
|
|
295
|
+
statusDraft(load.status === 'error' ? { kind: 'error', message: load.message, retry } : { kind: 'loading' }, level, parent, load.status === 'error');
|
|
296
|
+
|
|
300
297
|
const matches = (name: string, query: string): boolean => name.toLowerCase().includes(query);
|
|
301
298
|
|
|
302
299
|
/* 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. */
|
|
@@ -348,11 +345,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
348
345
|
const tableChildren = (ref: TableRef, kind: TableKind, level: number, parent: string): Draft[] => {
|
|
349
346
|
const load = input.structures(ref.connectionId, ref.schema, ref.table);
|
|
350
347
|
if (load.status !== 'ready') {
|
|
351
|
-
|
|
352
|
-
load.status === 'error'
|
|
353
|
-
? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
|
|
354
|
-
: { kind: 'loading' };
|
|
355
|
-
return [statusDraft(row, level, parent, load.status === 'error')];
|
|
348
|
+
return [pendingDraft(load, { connectionId: ref.connectionId, schema: ref.schema, table: ref.table }, level, parent)];
|
|
356
349
|
}
|
|
357
350
|
const structure = load.value;
|
|
358
351
|
if (structure.columns.length === 0) {
|
|
@@ -443,9 +436,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
443
436
|
if (filtering) {
|
|
444
437
|
return { drafts: [], tables: 0 };
|
|
445
438
|
}
|
|
446
|
-
|
|
447
|
-
load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id, schema } } : { kind: 'loading' };
|
|
448
|
-
return { drafts: [statusDraft(row, level, parent, load.status === 'error')], tables: 0 };
|
|
439
|
+
return { drafts: [pendingDraft(load, { connectionId: connection.id, schema }, level, parent)], tables: 0 };
|
|
449
440
|
};
|
|
450
441
|
|
|
451
442
|
const schemaDraft = (connection: Connection, schema: string, parent: string): Draft | null => {
|
|
@@ -523,9 +514,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
523
514
|
children = visible.flatMap((entry) => schemaDraft(connection, entry.name, key) ?? []);
|
|
524
515
|
}
|
|
525
516
|
} else if (open && !filtering) {
|
|
526
|
-
|
|
527
|
-
load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id } } : { kind: 'loading' };
|
|
528
|
-
children = [statusDraft(row, 2, key, load.status === 'error')];
|
|
517
|
+
children = [pendingDraft(load, { connectionId: connection.id }, 2, key)];
|
|
529
518
|
}
|
|
530
519
|
// Behind a status row too: what the app keeps of a connection does not wait on its server.
|
|
531
520
|
children = [...children, ...(open ? appFolders : [])];
|
package/src/grid/DataGrid.tsx
CHANGED
|
@@ -16,7 +16,11 @@ import { Ban, Copy, RotateCcw } from 'lucide-react';
|
|
|
16
16
|
import { useTranslation } from 'react-i18next';
|
|
17
17
|
import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, isApplePlatform, isModHeld, Kbd, useContentSize } from '@adecore/ui';
|
|
18
18
|
import type { EditValue, Value } from '../protocol/index.ts';
|
|
19
|
+
import { assignRef } from '../assign-ref.ts';
|
|
19
20
|
import type { SqlTarget } from '../sql.ts';
|
|
21
|
+
import { useNumberNotation } from '../client-context.ts';
|
|
22
|
+
import { usePopupPress } from '../use-popup-press.ts';
|
|
23
|
+
import { useStableCallback } from '../use-stable-callback.ts';
|
|
20
24
|
import type { RangeBlock } from './aggregates.ts';
|
|
21
25
|
import { fullRect, rectBetween, rectContains } from './cell-range.ts';
|
|
22
26
|
import type { EditMove } from './CellEditor.tsx';
|
|
@@ -49,9 +53,6 @@ import { moveFocus, type CellPosition, type NavigationKey } from './navigation.t
|
|
|
49
53
|
import { selectRow } from './row-selection.ts';
|
|
50
54
|
import { sortOnly, sortStateOf, type GridSort } from './sort.ts';
|
|
51
55
|
import type { ColumnRequest, FocusedCell, FocusRequest, GridColumn, GridMenuContext, GridRow } from './types.ts';
|
|
52
|
-
import { useNumberNotation } from '../client-context.ts';
|
|
53
|
-
import { usePopupPress } from '../use-popup-press.ts';
|
|
54
|
-
import { useStableCallback } from '../use-stable-callback.ts';
|
|
55
56
|
|
|
56
57
|
export interface DataGridProps {
|
|
57
58
|
columns: readonly GridColumn[];
|
|
@@ -174,7 +175,7 @@ export function DataGrid({
|
|
|
174
175
|
const anchor = useRef(-1);
|
|
175
176
|
const pressedInPopup = usePopupPress();
|
|
176
177
|
const notation = useNumberNotation();
|
|
177
|
-
|
|
178
|
+
// Set once an edit commits or cancels, so the blur that follows the input's removal does not commit it again.
|
|
178
179
|
const settled = useRef(true);
|
|
179
180
|
const dragging = useRef(false);
|
|
180
181
|
const dragOrigin = useRef<CellPosition | null>(null);
|
|
@@ -252,9 +253,6 @@ export function DataGrid({
|
|
|
252
253
|
|
|
253
254
|
useEffect(() => {
|
|
254
255
|
reportFocus.current = onFocusedCellChange;
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
useEffect(() => {
|
|
258
256
|
reportRange.current = onRangeChange;
|
|
259
257
|
reportLayout.current = onLayoutChange;
|
|
260
258
|
});
|
|
@@ -281,8 +279,11 @@ export function DataGrid({
|
|
|
281
279
|
}, [blockKey]);
|
|
282
280
|
|
|
283
281
|
useEffect(() => {
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
if (focusRequest === null || focusRequest === undefined) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const index = rows.findIndex((row) => row.key === focusRequest.rowKey);
|
|
286
|
+
if (index >= 0) {
|
|
286
287
|
focusAt({ row: index, column: focusRequest.column });
|
|
287
288
|
if (focusRequest.quiet !== true) {
|
|
288
289
|
scroller.current?.focus();
|
|
@@ -318,11 +319,7 @@ export function DataGrid({
|
|
|
318
319
|
(node: HTMLDivElement | null) => {
|
|
319
320
|
scroller.current = node;
|
|
320
321
|
measure(node);
|
|
321
|
-
|
|
322
|
-
ref(node);
|
|
323
|
-
} else if (ref) {
|
|
324
|
-
ref.current = node;
|
|
325
|
-
}
|
|
322
|
+
assignRef(ref, node);
|
|
326
323
|
},
|
|
327
324
|
[measure, ref]
|
|
328
325
|
);
|
package/src/grid/EnumPicker.tsx
CHANGED
|
@@ -37,9 +37,10 @@ const LOOK = {
|
|
|
37
37
|
*/
|
|
38
38
|
export function EnumPicker({ type, value, nullable, label, autoOpen = false, disabled = false, onDone, look = 'cell', className, ref }: EnumPickerProps) {
|
|
39
39
|
const { t } = useTranslation('database');
|
|
40
|
-
const settled = useRef(false);
|
|
41
|
-
const [members, setMembers] = useState<ReadonlySet<string>>(() => new Set(type.kind === 'set' && value !== null ? membersOf(value) : []));
|
|
42
40
|
const set = type.kind === 'set';
|
|
41
|
+
const membersOfValue = (): ReadonlySet<string> => new Set(set && value !== null ? membersOf(value) : []);
|
|
42
|
+
const settled = useRef(false);
|
|
43
|
+
const [members, setMembers] = useState(membersOfValue);
|
|
43
44
|
|
|
44
45
|
const settle = (result: { readonly value: string | null } | undefined): void => {
|
|
45
46
|
if (!settled.current) {
|
|
@@ -51,7 +52,7 @@ export function EnumPicker({ type, value, nullable, label, autoOpen = false, dis
|
|
|
51
52
|
const handleOpenChange = (open: boolean): void => {
|
|
52
53
|
if (open) {
|
|
53
54
|
settled.current = false;
|
|
54
|
-
setMembers(
|
|
55
|
+
setMembers(membersOfValue());
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
const joined = joinMembers(type, members);
|
|
@@ -7,9 +7,8 @@ import { formatNumber } from '@adecore/ui/format';
|
|
|
7
7
|
import { CODE_TEXT } from '../code-text.ts';
|
|
8
8
|
import { keyLabelOf } from '../column-keys.ts';
|
|
9
9
|
import { KeyIcon } from '../KeyIcon.tsx';
|
|
10
|
-
import type
|
|
10
|
+
import { isNumericKind, type SortDirection } from '../sql.ts';
|
|
11
11
|
import type { ColumnClick } from './column-selection.ts';
|
|
12
|
-
import { isNumericKind } from './display.ts';
|
|
13
12
|
import { isDoubleClick, MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.ts';
|
|
14
13
|
import type { GridColumn } from './types.ts';
|
|
15
14
|
|
|
@@ -140,7 +139,7 @@ export function GridHeaderCell({
|
|
|
140
139
|
const { t } = useTranslation('database');
|
|
141
140
|
const cell = useRef<HTMLDivElement>(null);
|
|
142
141
|
const lastPress = useRef<number | null>(null);
|
|
143
|
-
|
|
142
|
+
// Set by a press on the resize strip, since the click that ends the drag would otherwise pick the column.
|
|
144
143
|
const resized = useRef(false);
|
|
145
144
|
const { startResize } = useColumnResize(cell, {
|
|
146
145
|
size: width,
|
package/src/grid/RecordView.tsx
CHANGED
|
@@ -8,7 +8,8 @@ import { useNumberNotation } from '../client-context.ts';
|
|
|
8
8
|
import { CODE_TEXT } from '../code-text.ts';
|
|
9
9
|
import type { EditValue, Value } from '../protocol/index.ts';
|
|
10
10
|
import { ValueEditor } from '../value/ValueEditor.tsx';
|
|
11
|
-
import {
|
|
11
|
+
import { isNumericKind } from '../sql.ts';
|
|
12
|
+
import { cellView, copyTextOf, isPreview, numeralText, type NumberNotation } from './display.ts';
|
|
12
13
|
import { draftOf, parseDraft } from './edit-value.ts';
|
|
13
14
|
import { parseEnumType } from './enum-type.ts';
|
|
14
15
|
import { EnumPicker } from './EnumPicker.tsx';
|
package/src/grid/ValueDock.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useState, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { ColumnResizeHandle, useColumnResize } from '@adecore/ui';
|
|
4
|
+
import { assignRef } from '../assign-ref.ts';
|
|
4
5
|
|
|
5
6
|
export const VALUE_PANEL_WIDTH = 320;
|
|
6
7
|
export const VALUE_PANEL_MIN_WIDTH = 240;
|
|
@@ -41,11 +42,7 @@ export function ValueDock({ open, panel, children, width: given, onWidthChange,
|
|
|
41
42
|
<div
|
|
42
43
|
ref={(node) => {
|
|
43
44
|
root.current = node;
|
|
44
|
-
|
|
45
|
-
ref(node);
|
|
46
|
-
} else if (ref) {
|
|
47
|
-
ref.current = node;
|
|
48
|
-
}
|
|
45
|
+
assignRef(ref, node);
|
|
49
46
|
}}
|
|
50
47
|
className={clsx('flex min-h-0 min-w-0 flex-1', className)}
|
|
51
48
|
>
|
package/src/grid/aggregates.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ValueKind } from '../protocol/index.ts';
|
|
2
|
+
import { isNumericKind, NUMERIC_TEXT } from '../sql.ts';
|
|
2
3
|
import type { Shown } from './display.ts';
|
|
3
4
|
|
|
4
|
-
const NUMERIC_KINDS: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float']);
|
|
5
|
-
const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
6
|
-
|
|
7
5
|
/* A block of rows, and the columns (indexes into the columns of the grid) it spans. */
|
|
8
6
|
export interface RangeBlock {
|
|
9
7
|
readonly top: number;
|
|
@@ -62,7 +60,7 @@ export const aggregateBlock = (rows: readonly (readonly Shown[])[], kinds: reado
|
|
|
62
60
|
for (const column of block.columns) {
|
|
63
61
|
count++;
|
|
64
62
|
const kind = kinds[column];
|
|
65
|
-
const found = kind !== undefined &&
|
|
63
|
+
const found = kind !== undefined && isNumericKind(kind) ? numberOf(cells[column] ?? null) : null;
|
|
66
64
|
if (found === null) {
|
|
67
65
|
continue;
|
|
68
66
|
}
|
|
@@ -51,10 +51,11 @@ export const captureLayout = (columns: readonly { readonly name: string }[], sta
|
|
|
51
51
|
widths[column.name] = width;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
const namesOf = (indexes: readonly number[]): string[] => indexes.flatMap((index) => columns[index]?.name ?? []);
|
|
54
55
|
return {
|
|
55
56
|
widths,
|
|
56
|
-
hidden: [...state.view.hidden].sort((a, b) => a - b)
|
|
57
|
-
pinned: state.view.pinned
|
|
57
|
+
hidden: namesOf([...state.view.hidden].sort((a, b) => a - b)),
|
|
58
|
+
pinned: namesOf(state.view.pinned)
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
61
|
|
package/src/grid/copy-formats.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ValueKind } from '../protocol/index.ts';
|
|
2
|
-
import { qualifiedName, quoteIdentifier, sqlLiteral, type SqlTarget } from '../sql.ts';
|
|
2
|
+
import { isNumericKind, qualifiedName, quoteIdentifier, sqlLiteral, type SqlTarget } from '../sql.ts';
|
|
3
3
|
import { copyTextOf, type Shown } from './display.ts';
|
|
4
4
|
|
|
5
5
|
export type CopyFormat = 'tsv' | 'csv' | 'json' | 'sql';
|
|
@@ -19,7 +19,6 @@ export interface CopyInput {
|
|
|
19
19
|
readonly target?: SqlTarget;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const NUMERIC_KINDS: ReadonlySet<ValueKind> = new Set(['integer', 'decimal', 'float']);
|
|
23
22
|
const JSON_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
24
23
|
|
|
25
24
|
const quoteField = (text: string): string => `"${text.replaceAll('"', '""')}"`;
|
|
@@ -45,7 +44,7 @@ const jsonOf = (cell: Shown, kind: ValueKind): string => {
|
|
|
45
44
|
return Number.isFinite(cell) ? String(cell) : 'null';
|
|
46
45
|
}
|
|
47
46
|
if (typeof cell === 'string') {
|
|
48
|
-
if (
|
|
47
|
+
if (isNumericKind(kind) && JSON_NUMBER.test(cell)) {
|
|
49
48
|
return cell;
|
|
50
49
|
}
|
|
51
50
|
if (kind === 'json') {
|