@adecore/database 0.21.0 → 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
package/dist/client/client.js
CHANGED
|
@@ -2,8 +2,8 @@ import { DatabaseRequestError } from './types.js';
|
|
|
2
2
|
const cancelledError = () => new DatabaseRequestError('cancelled', 'The request was cancelled.');
|
|
3
3
|
const isUnknownSession = (error) => error instanceof DatabaseRequestError && error.code === 'unknown-session';
|
|
4
4
|
const LEADING_COMMENTS = /^(?:\s+|--[^\n]*(?:\n|$)|#[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/;
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const SCHEMA_STATEMENT = /^(?:create|alter|drop|rename|truncate)\b/i;
|
|
6
|
+
const changesSchema = (sql) => SCHEMA_STATEMENT.test(sql.replace(LEADING_COMMENTS, ''));
|
|
7
7
|
/* Structural equality for the JSON a config is made of; a key set to `undefined` counts as absent. */
|
|
8
8
|
const isEqual = (left, right) => {
|
|
9
9
|
if (left === right) {
|
|
@@ -36,6 +36,14 @@ export const createDatabaseClient = (transport, options = {}) => {
|
|
|
36
36
|
const createId = options.createId ?? (() => crypto.randomUUID());
|
|
37
37
|
const live = new Map();
|
|
38
38
|
const schemaListeners = new Set();
|
|
39
|
+
const channelsOf = (connectionId) => {
|
|
40
|
+
let channels = live.get(connectionId);
|
|
41
|
+
if (channels === undefined) {
|
|
42
|
+
channels = new Map();
|
|
43
|
+
live.set(connectionId, channels);
|
|
44
|
+
}
|
|
45
|
+
return channels;
|
|
46
|
+
};
|
|
39
47
|
const notifySchemaChange = (change) => {
|
|
40
48
|
for (const listener of [...schemaListeners]) {
|
|
41
49
|
try {
|
|
@@ -79,11 +87,7 @@ export const createDatabaseClient = (transport, options = {}) => {
|
|
|
79
87
|
let opening = null;
|
|
80
88
|
/* A session that was closed and is used again is findable again, so `disconnect` and `dispose` reach it. */
|
|
81
89
|
const track = () => {
|
|
82
|
-
|
|
83
|
-
if (channels === undefined) {
|
|
84
|
-
channels = new Map();
|
|
85
|
-
live.set(connection.id, channels);
|
|
86
|
-
}
|
|
90
|
+
const channels = channelsOf(connection.id);
|
|
87
91
|
if (!channels.has(channel)) {
|
|
88
92
|
channels.set(channel, created);
|
|
89
93
|
}
|
|
@@ -169,7 +173,7 @@ export const createDatabaseClient = (transport, options = {}) => {
|
|
|
169
173
|
async execute(sql, executeOptions) {
|
|
170
174
|
const { signal: _signal, ...rest } = executeOptions ?? {};
|
|
171
175
|
const result = await request('execute', (id) => ({ session: id, sql, ...rest }), executeOptions, false);
|
|
172
|
-
if (result.results.some((item) => item.kind !== 'error' &&
|
|
176
|
+
if (result.results.some((item) => item.kind !== 'error' && changesSchema(item.sql))) {
|
|
173
177
|
notifySchemaChange({ connectionId: connection.id, ...(rest.schema === undefined ? {} : { schema: rest.schema }) });
|
|
174
178
|
}
|
|
175
179
|
return result;
|
|
@@ -216,19 +220,14 @@ export const createDatabaseClient = (transport, options = {}) => {
|
|
|
216
220
|
};
|
|
217
221
|
},
|
|
218
222
|
session(connection, channel = '') {
|
|
219
|
-
|
|
220
|
-
const existing = channels?.get(channel);
|
|
223
|
+
const existing = live.get(connection.id)?.get(channel);
|
|
221
224
|
if (existing !== undefined && isEqual(existing.session.connection.config, connection.config)) {
|
|
222
225
|
existing.update(connection);
|
|
223
226
|
return existing.session;
|
|
224
227
|
}
|
|
225
228
|
void existing?.close();
|
|
226
229
|
const created = createSession(connection, channel);
|
|
227
|
-
|
|
228
|
-
channels = new Map();
|
|
229
|
-
live.set(connection.id, channels);
|
|
230
|
-
}
|
|
231
|
-
channels.set(channel, created);
|
|
230
|
+
channelsOf(connection.id).set(channel, created);
|
|
232
231
|
return created.session;
|
|
233
232
|
},
|
|
234
233
|
async disconnect(connectionId) {
|
package/dist/client/types.d.ts
CHANGED
package/dist/client/types.js
CHANGED
|
@@ -11,3 +11,5 @@ export class DatabaseRequestError extends Error {
|
|
|
11
11
|
this.change = change;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/* Whether a request failed because it was cancelled, which is no failure to show. */
|
|
15
|
+
export const isCancelled = (error) => error instanceof DatabaseRequestError && error.code === 'cancelled';
|
|
@@ -51,7 +51,12 @@ function SqliteFields({ config, onConfigChange, touched, touch, onBrowse }) {
|
|
|
51
51
|
});
|
|
52
52
|
touch('path');
|
|
53
53
|
};
|
|
54
|
-
return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.path'), hint: t('connections.form.pathHint'), error: error, orientation: "horizontal", group: onBrowse !== undefined, children:
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.path'), hint: t('connections.form.pathHint'), error: error, orientation: "horizontal", group: onBrowse !== undefined, children: _jsx(PathInput, { value: config.path, placeholder: "/path/to/database.sqlite", label: t('connections.form.path'), busy: browsing.busy, onBlur: () => touch('path'), onValueChange: (path) => onConfigChange({ ...config, path }), onBrowse: onBrowse === undefined ? undefined : () => void browse() }) }), _jsx(Field, { label: t('connections.form.create'), hint: t('connections.form.createHint'), orientation: "horizontal", group: true, children: _jsx(Switch, { checked: config.create === true, label: t('connections.form.create'), onCheckedChange: (create) => onConfigChange({ ...config, create }) }) })] }));
|
|
55
|
+
}
|
|
56
|
+
/* A path typed by hand, with a Browse button beside it when the app has a file dialog. */
|
|
57
|
+
function PathInput({ value, placeholder, label, busy, onBlur, onValueChange, onBrowse }) {
|
|
58
|
+
const { t } = useTranslation('database');
|
|
59
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { mono: true, value: value, placeholder: placeholder, spellCheck: false, "aria-label": onBrowse === undefined ? undefined : label, onBlur: onBlur, onChange: (e) => onValueChange(e.target.value) }), onBrowse !== undefined && (_jsx(Button, { variant: "secondary", disabled: busy, onClick: onBrowse, children: t('connections.form.browse') }))] }));
|
|
55
60
|
}
|
|
56
61
|
function PortField({ label, hint, error, value, placeholder, onBlur, onValueChange }) {
|
|
57
62
|
return (_jsx(Field, { label: label, hint: hint, error: error, orientation: "horizontal", children: _jsx(Input, { type: "number", min: 1, max: 65535, className: "max-w-32", value: value === undefined ? '' : String(value), placeholder: String(placeholder), onBlur: onBlur, onChange: (e) => onValueChange(e.target.value === '' ? undefined : Number(e.target.value)) }) }));
|
|
@@ -97,7 +102,7 @@ function SshFields({ config, tunnel, onConfigChange, touched, touch, onBrowse })
|
|
|
97
102
|
}
|
|
98
103
|
});
|
|
99
104
|
};
|
|
100
|
-
return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.ssh.host'), hint: t('connections.form.ssh.hostHint'), error: hostError, orientation: "horizontal", children: _jsx(Input, { value: tunnel.host, placeholder: "bastion.example.com", spellCheck: false, onBlur: () => touch('sshHost'), onChange: (e) => update({ host: e.target.value }) }) }), _jsx(PortField, { label: t('connections.form.ssh.port'), error: portError, value: tunnel.port, placeholder: 22, onBlur: () => touch('sshPort'), onValueChange: (port) => update({ port }) }), _jsx(Field, { label: t('connections.form.ssh.user'), orientation: "horizontal", children: _jsx(Input, { value: tunnel.user ?? '', autoComplete: "off", spellCheck: false, onChange: (e) => update({ user: optionalText(e.target.value) }) }) }), _jsx(Field, { label: t('connections.form.ssh.identityFile'), hint: t('connections.form.ssh.keyHint'), error: browsing.failure ?? undefined, orientation: "horizontal", group: onBrowse !== undefined, children:
|
|
105
|
+
return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.ssh.host'), hint: t('connections.form.ssh.hostHint'), error: hostError, orientation: "horizontal", children: _jsx(Input, { value: tunnel.host, placeholder: "bastion.example.com", spellCheck: false, onBlur: () => touch('sshHost'), onChange: (e) => update({ host: e.target.value }) }) }), _jsx(PortField, { label: t('connections.form.ssh.port'), error: portError, value: tunnel.port, placeholder: 22, onBlur: () => touch('sshPort'), onValueChange: (port) => update({ port }) }), _jsx(Field, { label: t('connections.form.ssh.user'), orientation: "horizontal", children: _jsx(Input, { value: tunnel.user ?? '', autoComplete: "off", spellCheck: false, onChange: (e) => update({ user: optionalText(e.target.value) }) }) }), _jsx(Field, { label: t('connections.form.ssh.identityFile'), hint: t('connections.form.ssh.keyHint'), error: browsing.failure ?? undefined, orientation: "horizontal", group: onBrowse !== undefined, children: _jsx(PathInput, { value: tunnel.identityFile ?? '', placeholder: "~/.ssh/id_ed25519", label: t('connections.form.ssh.identityFile'), busy: browsing.busy, onValueChange: (identityFile) => update({ identityFile: optionalText(identityFile) }), onBrowse: onBrowse === undefined ? undefined : () => void browse() }) }), _jsx(ServerFields, { config: config, onConfigChange: onConfigChange, touched: touched, touch: touch })] }));
|
|
101
106
|
}
|
|
102
107
|
/* What a container offers a person in a list: its image, its Compose name and the port it publishes. */
|
|
103
108
|
function containerDescription(container, published) {
|
|
@@ -37,15 +37,15 @@ export function ConnectionManager({ value, onValueChange, selected, onSelectedCh
|
|
|
37
37
|
}
|
|
38
38
|
onSelectedChange?.(id);
|
|
39
39
|
};
|
|
40
|
-
const
|
|
41
|
-
const connection = { id: crypto.randomUUID(), name: t('connections.newName'), config: defaultConfig(engine) };
|
|
40
|
+
const append = (connection) => {
|
|
42
41
|
onValueChange([...value, connection]);
|
|
43
42
|
select(connection.id);
|
|
44
43
|
};
|
|
44
|
+
const add = (engine) => {
|
|
45
|
+
append({ id: crypto.randomUUID(), name: t('connections.newName'), config: defaultConfig(engine) });
|
|
46
|
+
};
|
|
45
47
|
const addFromContainer = (container) => {
|
|
46
|
-
|
|
47
|
-
onValueChange([...value, connection]);
|
|
48
|
-
select(connection.id);
|
|
48
|
+
append(connectionFromContainer(crypto.randomUUID(), container));
|
|
49
49
|
};
|
|
50
50
|
const change = (next) => {
|
|
51
51
|
onValueChange(value.map((connection) => (connection.id === next.id ? next : connection)));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from '@adecore/ui';
|
|
3
3
|
import { ENGINE_ICONS } from './engine-icons.js';
|
|
4
|
-
/* A file on disk for SQLite, a server for MySQL and MariaDB. */
|
|
5
4
|
export function EngineIcon({ engine, size = 16, className, ref }) {
|
|
6
5
|
return _jsx(Icon, { ref: ref, icon: ENGINE_ICONS[engine], size: size, className: className });
|
|
7
6
|
}
|
|
@@ -5,7 +5,7 @@ import { Check, CircleAlert, History, Play, Square, X } from 'lucide-react';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Banner, Button, ColumnResizeHandle, Icon, IconButton, messageOf, PromptDialog, Spinner, Tabs, Tooltip, useColumnResize } from '@adecore/ui';
|
|
7
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
-
import {
|
|
8
|
+
import { isCancelled } from '../client/types.js';
|
|
9
9
|
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.js';
|
|
10
10
|
import { splitStatements, statementAt } from '../sql-split.js';
|
|
11
11
|
import { DestructiveDialog } from './DestructiveDialog.js';
|
|
@@ -41,6 +41,14 @@ const parseHeight = (raw) => {
|
|
|
41
41
|
function AppEditor({ render, ...editor }) {
|
|
42
42
|
return render(editor);
|
|
43
43
|
}
|
|
44
|
+
/* Run and Run all, or Cancel while a run is under way. */
|
|
45
|
+
function RunButtons({ running, disabled, onCancel, onRun }) {
|
|
46
|
+
const { t } = useTranslation('database');
|
|
47
|
+
if (running) {
|
|
48
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", onClick: onCancel, children: [_jsx(Icon, { icon: Square, size: 12 }), t('console.cancel')] }), _jsx(Spinner, { size: 14, label: t('console.running') })] }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: t('console.run'), kbd: RUN_SHORTCUT, children: _jsxs(Button, { variant: "primary", size: "sm", disabled: disabled, onClick: () => onRun('selection-or-statement'), children: [_jsx(Icon, { icon: Play, size: 12 }), t('console.run')] }) }), _jsx(Tooltip, { label: t('console.runAll'), kbd: RUN_ALL_SHORTCUT, children: _jsx(Button, { variant: "secondary", size: "sm", disabled: disabled, onClick: () => onRun('all'), children: t('console.runAll') }) })] }));
|
|
51
|
+
}
|
|
44
52
|
/* Type SQL, run it, and read what each statement did. Several statements give a tab each. */
|
|
45
53
|
export function QueryConsole({ connection, schema, onSchemaChange, value, defaultValue = '', onValueChange, defaultHistoryOpen = false, autoFocus = false, renderEditor, toolbarEnd, className, ref }) {
|
|
46
54
|
const { t } = useTranslation('database');
|
|
@@ -80,7 +88,7 @@ export function QueryConsole({ connection, schema, onSchemaChange, value, defaul
|
|
|
80
88
|
const [exporting, setExporting] = useState(false);
|
|
81
89
|
// What the console shows itself when the app takes no notices.
|
|
82
90
|
const [notice, setNotice] = useState(null);
|
|
83
|
-
|
|
91
|
+
// Closed until the first run, so the editor has the whole height before anything ran.
|
|
84
92
|
const [resultsOpen, setResultsOpen] = useState(false);
|
|
85
93
|
const [resultsHeight, setResultsHeight] = useState(() => parseHeight(storage?.get(RESULTS_HEIGHT_KEY)));
|
|
86
94
|
const { startResize } = useColumnResize(resultsPane, {
|
|
@@ -183,7 +191,7 @@ export function QueryConsole({ connection, schema, onSchemaChange, value, defaul
|
|
|
183
191
|
record(outcomeOf(executed.results));
|
|
184
192
|
}
|
|
185
193
|
catch (error) {
|
|
186
|
-
if (controller.signal.aborted || (error
|
|
194
|
+
if (controller.signal.aborted || isCancelled(error)) {
|
|
187
195
|
setRun({ status: 'cancelled' });
|
|
188
196
|
}
|
|
189
197
|
else {
|
|
@@ -304,7 +312,7 @@ export function QueryConsole({ connection, schema, onSchemaChange, value, defaul
|
|
|
304
312
|
placeholder: t('console.placeholder'),
|
|
305
313
|
autoFocus
|
|
306
314
|
};
|
|
307
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), children: [_jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [session !== null && (_jsxs(_Fragment, { children: [
|
|
315
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), children: [_jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [session !== null && (_jsxs(_Fragment, { children: [_jsx(RunButtons, { running: run.status === 'running', disabled: blank || busy, onCancel: () => running.current?.abort(), onRun: (scope) => requestRun(targetOf(scope)) }), _jsx(TransactionControls, { mode: mode, onModeChange: changeMode, open: inTransaction, busy: busy, onCommit: () => void settle('commit'), onRollback: () => void settle('rollback') })] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [toolbarEnd, session !== null && schemas.length > 0 && (_jsx(SchemaPicker, { schemas: schemas, value: activeSchema ?? null, disabled: busy, onValueChange: onSchemaChange ?? setPickedSchema })), session !== null && (_jsxs(Button, { variant: "secondary", size: "sm", "aria-pressed": historyOpen, className: "aria-pressed:bg-surface-active aria-pressed:text-text", onClick: () => setHistoryOpen(!historyOpen), children: [_jsx(Icon, { icon: History, size: 12 }), t('console.history.title')] }))] })] }), _jsx("div", { className: clsx('flex min-h-0 flex-1 flex-col', renderEditor === undefined && 'p-3'), children: renderEditor === undefined ? _jsx(SqlEditor, { ...editorProps }) : _jsx(AppEditor, { render: renderEditor, ...editorProps }) })] }), session !== null && historyOpen && _jsx(HistoryPanel, { entries: history.entries, onPick: pickHistory, onClear: history.clear })] }), session !== null && notice !== null && (_jsx(Banner, { icon: notice.tone === 'error' ? CircleAlert : Check, tone: notice.tone === 'error' ? 'error' : 'neutral', message: notice.description ?? notice.title, className: "shrink-0 pb-2", children: _jsx(Button, { size: "xs", onClick: () => setNotice(null), children: t('console.dismiss') }) })), session !== null && resultsOpen && (_jsxs("section", { ref: resultsPane, "aria-label": t('console.results'), className: "relative flex min-h-24 shrink-0 flex-col border-t border-border", style: { height: resultsHeight ?? '50%', maxHeight: `calc(100% - ${MIN_EDITOR_HEIGHT}px)` }, children: [_jsx(ColumnResizeHandle, { from: "bottom", onPointerDown: startResize }), _jsx(Tabs.Root, { value: index, onValueChange: (next) => setTab(Number(next)), children: _jsx(Tabs.List, { "aria-label": t('console.results'), className: "shrink-0 pr-1.5 pl-3", end: _jsx(IconButton, { icon: X, size: "sm", label: t('console.closeResults'), onClick: () => setResultsOpen(false) }), children: results.map((result, position) => (_jsx(Tabs.Tab, { value: position, children: statementLabel(result.sql) }, position))) }) }), run.status === 'failed' && _jsx(Banner, { icon: CircleAlert, tone: "error", message: run.message, className: "shrink-0 pt-2" }), run.status === 'cancelled' && _jsx(Banner, { icon: CircleAlert, tone: "neutral", message: t('console.cancelled'), className: "shrink-0 pt-2" }), shown !== undefined && (_jsx(StatementResultView, { result: shown, offset: offset, pager: pager, onExport: exportBase === undefined ? undefined : (format) => void exportResult(exportBase, format), exporting: exporting, engine: engine, valuePanelOpen: panelOpen, onValuePanelOpenChange: setPanelOpen, keys: keys, onFollowReference: act === undefined || connection === undefined
|
|
308
316
|
? undefined
|
|
309
317
|
: (reference) => act({
|
|
310
318
|
kind: 'open-table',
|
package/dist/console/editing.js
CHANGED
|
@@ -5,7 +5,6 @@ const lineEndOf = (value, at) => {
|
|
|
5
5
|
const newline = value.indexOf('\n', at);
|
|
6
6
|
return newline === -1 ? value.length : newline;
|
|
7
7
|
};
|
|
8
|
-
/* The whitespace a line begins with. */
|
|
9
8
|
const indentOf = (line) => /^[ \t]*/.exec(line)[0];
|
|
10
9
|
/* The lines a selection touches. A selection that ends at the start of a line leaves that line out. */
|
|
11
10
|
const touchedLines = (value, start, end) => {
|
|
@@ -42,13 +41,12 @@ export const outdentEdit = (value, start, end) => {
|
|
|
42
41
|
if (total === 0) {
|
|
43
42
|
return null;
|
|
44
43
|
}
|
|
45
|
-
const firstLineStart = from;
|
|
46
44
|
return {
|
|
47
45
|
from,
|
|
48
46
|
to,
|
|
49
47
|
insert: lines.map((line, index) => line.slice(removed[index])).join('\n'),
|
|
50
|
-
selectionStart: Math.max(
|
|
51
|
-
selectionEnd: Math.max(
|
|
48
|
+
selectionStart: Math.max(from, start - removed[0]),
|
|
49
|
+
selectionEnd: Math.max(from, end - total)
|
|
52
50
|
};
|
|
53
51
|
};
|
|
54
52
|
/* Enter: a new line that starts with the indentation of the line the caret is on, up to the caret. */
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useDatabaseStorage } from '../client-context.js';
|
|
3
3
|
import { addEntry, historyKey, parseHistory, serializeHistory } from './history.js';
|
|
4
|
+
const load = (storage, key) => ({ key, entries: parseHistory(storage?.get(key) ?? null), changed: false });
|
|
4
5
|
/* The runs of one connection, kept in the app's storage when it has one and for the life of the console when it has not. */
|
|
5
6
|
export function useConsoleHistory(connectionId) {
|
|
6
7
|
const storage = useDatabaseStorage();
|
|
7
8
|
const key = historyKey(connectionId);
|
|
8
|
-
const [state, setState] = useState(() => (
|
|
9
|
+
const [state, setState] = useState(() => load(storage, key));
|
|
9
10
|
if (state.key !== key) {
|
|
10
|
-
setState(
|
|
11
|
+
setState(load(storage, key));
|
|
11
12
|
}
|
|
12
13
|
useEffect(() => {
|
|
13
14
|
if (state.changed) {
|
package/dist/ddl/alter-mysql.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { columnSpecSql } from './column.js';
|
|
2
|
-
import { foreignKeySql, mysqlOptionsSql } from './create.js';
|
|
2
|
+
import { foreignKeySql, inlineIndexSql, mysqlOptionsSql } from './create.js';
|
|
3
3
|
import { columnList, quote, tableName } from './dialect.js';
|
|
4
4
|
const positionSql = (dialect, after) => (after === null ? 'FIRST' : `AFTER ${quote(dialect, after)}`);
|
|
5
|
-
const indexSql = (dialect, index) => [
|
|
6
|
-
'ADD',
|
|
7
|
-
index.unique ? 'UNIQUE INDEX' : 'INDEX',
|
|
8
|
-
...(index.name === '' ? [] : [quote(dialect, index.name)]),
|
|
9
|
-
`(${columnList(dialect, index.columns)})`
|
|
10
|
-
].join(' ');
|
|
11
5
|
const withPosition = (clause, position) => (position === null ? clause : `${clause} ${position}`);
|
|
12
6
|
/*
|
|
13
7
|
* One `ALTER TABLE`, or two when it has to be. A position clause names the columns as they are
|
|
@@ -61,7 +55,7 @@ export const mysqlAlterSql = (dialect, schema, original, draft, diff) => {
|
|
|
61
55
|
late.push(`ADD PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
|
|
62
56
|
}
|
|
63
57
|
for (const index of diff.addedIndexes) {
|
|
64
|
-
late.push(
|
|
58
|
+
late.push(`ADD ${inlineIndexSql(dialect, index)}`);
|
|
65
59
|
}
|
|
66
60
|
for (const foreignKey of diff.addedForeignKeys) {
|
|
67
61
|
late.push(`ADD ${foreignKeySql(dialect, schema, foreignKey)}`);
|
package/dist/ddl/alter-sqlite.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isAutoIndex } from './draft.js';
|
|
|
5
5
|
import { dropIndexSql } from './statements.js';
|
|
6
6
|
const RENAME_COLUMN = [3, 25];
|
|
7
7
|
const DROP_COLUMN = [3, 35];
|
|
8
|
-
/*
|
|
8
|
+
/* Whether `ALTER TABLE .. ADD COLUMN` takes the column; it refuses a key, a stored computed value, or a NOT NULL column with nothing to fill the rows with. */
|
|
9
9
|
const addable = (column) => {
|
|
10
10
|
if (column.autoIncrement || (column.generated && column.generatedClause !== null && /\bSTORED\b/i.test(column.generatedClause))) {
|
|
11
11
|
return false;
|
package/dist/ddl/column.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { sqlLiteral } from '../sql.js';
|
|
1
|
+
import { NUMERIC_TEXT, sqlLiteral } from '../sql.js';
|
|
2
2
|
import { quote } from './dialect.js';
|
|
3
|
-
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
4
3
|
const STRING = /^'(?:[^']|'')*'$/;
|
|
5
4
|
const KEYWORD = /^(?:NULL|TRUE|FALSE|CURRENT_DATE|CURRENT_TIME(?:\(\d*\))?|CURRENT_TIMESTAMP(?:\(\d*\))?|LOCALTIME(?:STAMP)?(?:\(\d*\))?|NOW\(\d*\))$/i;
|
|
6
5
|
/*
|
|
@@ -9,7 +8,7 @@ const KEYWORD = /^(?:NULL|TRUE|FALSE|CURRENT_DATE|CURRENT_TIME(?:\(\d*\))?|CURRE
|
|
|
9
8
|
*/
|
|
10
9
|
export const defaultClause = (expression) => {
|
|
11
10
|
const text = expression.trim();
|
|
12
|
-
if (
|
|
11
|
+
if (NUMERIC_TEXT.test(text) || STRING.test(text) || KEYWORD.test(text)) {
|
|
13
12
|
return text;
|
|
14
13
|
}
|
|
15
14
|
return text.startsWith('(') && text.endsWith(')') ? text : `(${text})`;
|
|
@@ -28,13 +27,11 @@ export const columnSpecSql = (dialect, column, { inlinePrimaryKey = false } = {}
|
|
|
28
27
|
parts.push(column.autoIncrement ? 'PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY');
|
|
29
28
|
}
|
|
30
29
|
// MariaDB takes neither NULL nor NOT NULL after the expression of a computed column.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
else if (!column.nullable) {
|
|
30
|
+
const statesNullability = !(column.generated && dialect.flavor === 'mariadb');
|
|
31
|
+
if (statesNullability && !column.nullable) {
|
|
35
32
|
parts.push('NOT NULL');
|
|
36
33
|
}
|
|
37
|
-
else if (mysql) {
|
|
34
|
+
else if (statesNullability && mysql) {
|
|
38
35
|
// MySQL gives a TIMESTAMP column NOT NULL unless it is told otherwise.
|
|
39
36
|
parts.push('NULL');
|
|
40
37
|
}
|
package/dist/ddl/create.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type ForeignKeyDraft, type IndexDraft, type TableDraft } from './draft.
|
|
|
3
3
|
import type { TableOptions } from './parse.ts';
|
|
4
4
|
export declare const foreignKeySql: (dialect: Dialect, schema: string, foreignKey: ForeignKeyDraft) => string;
|
|
5
5
|
export declare const mysqlOptionsSql: (options: TableOptions, against?: TableOptions) => string[];
|
|
6
|
+
export declare const inlineIndexSql: (dialect: Dialect, index: IndexDraft) => string;
|
|
6
7
|
export declare const createIndexSql: (dialect: Dialect, schema: string, table: string, index: Pick<IndexDraft, 'name' | 'columns' | 'unique'>) => string;
|
|
7
8
|
export interface CreateParts {
|
|
8
9
|
readonly table: string;
|
package/dist/ddl/create.js
CHANGED
|
@@ -35,7 +35,7 @@ export const mysqlOptionsSql = (options, against) => {
|
|
|
35
35
|
];
|
|
36
36
|
};
|
|
37
37
|
const sqliteOptionsSql = (options) => [...(options.strict ? ['STRICT'] : []), ...(options.withoutRowid ? ['WITHOUT ROWID'] : [])];
|
|
38
|
-
const inlineIndexSql = (dialect, index) => [index.unique ? 'UNIQUE INDEX' : 'INDEX', ...(index.name === '' ? [] : [quote(dialect, index.name)]), `(${columnList(dialect, index.columns)})`].join(' ');
|
|
38
|
+
export const inlineIndexSql = (dialect, index) => [index.unique ? 'UNIQUE INDEX' : 'INDEX', ...(index.name === '' ? [] : [quote(dialect, index.name)]), `(${columnList(dialect, index.columns)})`].join(' ');
|
|
39
39
|
/* `CREATE [UNIQUE] INDEX name ON table (columns)`. */
|
|
40
40
|
export const createIndexSql = (dialect, schema, table, index) => `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${dialect.engine === 'sqlite' ? sqliteSchemaPrefix(dialect, schema) : ''}${quote(dialect, index.name)} ON ${tableName(dialect, dialect.engine === 'sqlite' ? '' : schema, table)} (${columnList(dialect, index.columns)})`;
|
|
41
41
|
/* The `CREATE TABLE` of a draft under a name of the caller's choosing, which a rebuild uses for the table it fills before the swap. */
|
package/dist/ddl/diff.js
CHANGED
|
@@ -26,12 +26,8 @@ const longestRun = (positions) => {
|
|
|
26
26
|
};
|
|
27
27
|
const OPTION_FIELDS = ['engine', 'charset', 'collation', 'comment', 'withoutRowid', 'strict'];
|
|
28
28
|
const matches = (original, draft) => draft.originalName !== null && (original.name === '' ? draft.key === original.key : draft.originalName === original.name);
|
|
29
|
-
|
|
30
|
-
export const diffOf = (dialect, original, draft) => {
|
|
29
|
+
const columnChanges = (dialect, original, draft) => {
|
|
31
30
|
const survivors = draft.columns.filter((column) => column.originalName !== null);
|
|
32
|
-
const renames = new Map(survivors.map((column) => [column.originalName, column.name]));
|
|
33
|
-
const project = (names) => names.map((name) => renames.get(name) ?? name);
|
|
34
|
-
const originalOf = (name) => original.columns.find((column) => column.name === name);
|
|
35
31
|
const positions = survivors.map((column) => original.columns.findIndex((candidate) => candidate.name === column.originalName));
|
|
36
32
|
const staying = longestRun(positions);
|
|
37
33
|
let survivor = 0;
|
|
@@ -44,7 +40,7 @@ export const diffOf = (dialect, original, draft) => {
|
|
|
44
40
|
return;
|
|
45
41
|
}
|
|
46
42
|
const rank = survivor++;
|
|
47
|
-
const before =
|
|
43
|
+
const before = original.columns.find((candidate) => candidate.name === column.originalName);
|
|
48
44
|
if (before === undefined) {
|
|
49
45
|
return;
|
|
50
46
|
}
|
|
@@ -58,6 +54,9 @@ export const diffOf = (dialect, original, draft) => {
|
|
|
58
54
|
});
|
|
59
55
|
});
|
|
60
56
|
const droppedColumns = original.columns.filter((column) => !survivors.some((kept) => kept.originalName === column.name));
|
|
57
|
+
return { droppedColumns, keptColumns, addedColumns };
|
|
58
|
+
};
|
|
59
|
+
const indexChanges = (original, draft, project) => {
|
|
61
60
|
const droppedIndexes = [];
|
|
62
61
|
const addedIndexes = draft.indexes.filter((index) => index.originalName === null);
|
|
63
62
|
const renamedIndexes = [];
|
|
@@ -74,36 +73,39 @@ export const diffOf = (dialect, original, draft) => {
|
|
|
74
73
|
renamedIndexes.push({ from: before, to: after });
|
|
75
74
|
}
|
|
76
75
|
}
|
|
76
|
+
return { droppedIndexes, addedIndexes, renamedIndexes };
|
|
77
|
+
};
|
|
78
|
+
const sameForeignKey = (before, after, project) => before.name === after.name &&
|
|
79
|
+
sameList(project(before.columns), after.columns) &&
|
|
80
|
+
before.referencedSchema === after.referencedSchema &&
|
|
81
|
+
before.referencedTable === after.referencedTable &&
|
|
82
|
+
sameList(before.referencedColumns, after.referencedColumns) &&
|
|
83
|
+
before.onUpdate === after.onUpdate &&
|
|
84
|
+
before.onDelete === after.onDelete;
|
|
85
|
+
const foreignKeyChanges = (original, draft, project) => {
|
|
77
86
|
const droppedForeignKeys = [];
|
|
78
87
|
const addedForeignKeys = draft.foreignKeys.filter((key) => key.originalName === null);
|
|
79
88
|
for (const before of original.foreignKeys) {
|
|
80
89
|
const after = draft.foreignKeys.find((key) => matches(before, key));
|
|
81
|
-
|
|
82
|
-
before.name === after.name &&
|
|
83
|
-
sameList(project(before.columns), after.columns) &&
|
|
84
|
-
before.referencedSchema === after.referencedSchema &&
|
|
85
|
-
before.referencedTable === after.referencedTable &&
|
|
86
|
-
sameList(before.referencedColumns, after.referencedColumns) &&
|
|
87
|
-
before.onUpdate === after.onUpdate &&
|
|
88
|
-
before.onDelete === after.onDelete;
|
|
89
|
-
if (!same) {
|
|
90
|
+
if (after === undefined || !sameForeignKey(before, after, project)) {
|
|
90
91
|
droppedForeignKeys.push(before);
|
|
91
92
|
if (after !== undefined) {
|
|
92
93
|
addedForeignKeys.push(after);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
return { droppedForeignKeys, addedForeignKeys };
|
|
98
|
+
};
|
|
99
|
+
/* What a draft changes in the table it came from, told apart by the names items kept. */
|
|
100
|
+
export const diffOf = (dialect, original, draft) => {
|
|
101
|
+
const renames = new Map(draft.columns.filter((column) => column.originalName !== null).map((column) => [column.originalName, column.name]));
|
|
102
|
+
const project = (names) => names.map((name) => renames.get(name) ?? name);
|
|
96
103
|
return {
|
|
97
104
|
renamed: original.name !== draft.name,
|
|
98
|
-
|
|
99
|
-
keptColumns,
|
|
100
|
-
addedColumns,
|
|
105
|
+
...columnChanges(dialect, original, draft),
|
|
101
106
|
primaryKeyChanged: !sameList(project(original.primaryKey), draft.primaryKey),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
renamedIndexes,
|
|
105
|
-
droppedForeignKeys,
|
|
106
|
-
addedForeignKeys,
|
|
107
|
+
...indexChanges(original, draft, project),
|
|
108
|
+
...foreignKeyChanges(original, draft, project),
|
|
107
109
|
optionsChanged: OPTION_FIELDS.some((field) => original.options[field] !== draft.options[field])
|
|
108
110
|
};
|
|
109
111
|
};
|
|
@@ -15,10 +15,12 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
15
15
|
const listId = useId();
|
|
16
16
|
const mysql = dialect.engine === 'mysql';
|
|
17
17
|
const defaultType = typeSuggestionsOf(dialect)[0];
|
|
18
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col', className), children: [draft.columns.length === 0 ? (_jsx(EmptyState, { children: t('designer.columns.empty') })) : (_jsxs("div", { children: [_jsx("datalist", { id: listId, children: typeSuggestionsOf(dialect).map((type) => (_jsx("option", { value: type }, type))) }), _jsxs("table", { className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: TH, children: _jsx("span", { className: "sr-only", children: t('designer.columns.order') }) }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.name') }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.type') }), _jsx("th", { scope: "col", className: clsx(TH, 'text-center'), children: t('designer.columns.notNull') }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.default') }), mysql && (_jsx("th", { scope: "col", className: clsx(TH, 'text-center'), children: t('designer.columns.autoIncrement') })), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.primaryKey') }), mysql && (_jsx("th", { scope: "col", className: TH, children: t('designer.columns.comment') })), _jsx("th", { scope: "col", className: TH, children: _jsx("span", { className: "sr-only", children: t('designer.columns.remove') }) })] }) }), _jsx("tbody", { children: draft.columns.map((column, at) => {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col', className), children: [draft.columns.length === 0 ? (_jsx(EmptyState, { children: t('designer.columns.empty') })) : (_jsxs("div", { children: [_jsx("datalist", { id: listId, children: typeSuggestionsOf(dialect).map((type) => (_jsx("option", { value: type }, type))) }), _jsxs("table", { className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", className: TH, children: _jsx("span", { className: "sr-only", children: t('designer.columns.order') }) }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.name') }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.type') }), _jsx("th", { scope: "col", className: clsx(TH, 'text-center'), children: t('designer.columns.notNull') }), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.default') }), mysql && (_jsx("th", { scope: "col", className: clsx(TH, 'text-center'), children: t('designer.columns.autoIncrement') })), _jsx("th", { scope: "col", className: TH, children: t('designer.columns.primaryKey') }), mysql && (_jsx("th", { scope: "col", className: TH, children: t('designer.columns.comment') })), _jsx("th", { scope: "col", className: TH, children: _jsx("span", { className: "sr-only", children: t('designer.columns.remove') }) })] }) }), _jsx("tbody", { children: draft.columns.map((column, at) => (_jsx(ColumnRow, { draft: draft, column: column, at: at, mysql: mysql, listId: listId, disabled: disabled, onChange: onChange }, column.key))) })] })] })), _jsx("div", { className: "shrink-0 border-t border-border-soft px-3 py-2", children: _jsxs(Button, { variant: "secondary", size: "sm", disabled: disabled, onClick: () => onChange(addColumn(draft, defaultType)), children: [_jsx(Icon, { icon: Plus, size: 12 }), t('designer.columns.add')] }) })] }));
|
|
19
|
+
}
|
|
20
|
+
function ColumnRow({ draft, column, at, mysql, listId, disabled, onChange }) {
|
|
21
|
+
const { t } = useTranslation('database');
|
|
22
|
+
const keyAt = draft.primaryKey.indexOf(column.name);
|
|
23
|
+
const locked = disabled || column.generated;
|
|
24
|
+
const patch = (change) => onChange(patchColumn(draft, column.key, change));
|
|
25
|
+
return (_jsxs("tr", { children: [_jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs(ButtonGroup, { children: [_jsx(IconButton, { icon: ArrowUp, size: "xs", label: t('designer.columns.moveUp'), disabled: disabled || at === 0, onClick: () => onChange(moveColumn(draft, column.key, -1)) }), _jsx(IconButton, { icon: ArrowDown, size: "xs", label: t('designer.columns.moveDown'), disabled: disabled || at === draft.columns.length - 1, onClick: () => onChange(moveColumn(draft, column.key, 1)) })] }) }), _jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.name'), spellCheck: false, value: column.name, disabled: locked, onChange: (event) => patch({ name: event.target.value }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: _jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.type'), list: listId, spellCheck: false, value: column.type, disabled: locked, onChange: (event) => patch({ type: event.target.value }) }) }), _jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.notNullFor', { name: column.name }), checked: !column.nullable, disabled: locked, onCheckedChange: (checked) => patch({ nullable: !checked }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: column.generated ? (_jsx(Pill, { mono: true, children: column.generatedClause ?? t('designer.columns.generated') })) : (_jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.default'), placeholder: t('designer.columns.noDefault'), spellCheck: false, value: column.defaultValue ?? '', disabled: disabled, onChange: (event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value }) })) }), mysql && (_jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.autoIncrementFor', { name: column.name }), checked: column.autoIncrement, disabled: locked, onCheckedChange: (checked) => patch({ autoIncrement: checked }) }) })), _jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: KeyRound, size: "xs", label: keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey'), "aria-pressed": keyAt >= 0, disabled: locked, onClick: () => onChange(togglePrimaryKey(draft, column.key)) }), keyAt >= 0 && draft.primaryKey.length > 1 && _jsx("span", { className: "text-xs text-text-muted", children: formatNumber(keyAt + 1) })] }) }), mysql && (_jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.comment'), value: column.comment, disabled: locked, onChange: (event) => patch({ comment: event.target.value }) }) })), _jsx("td", { className: TD, children: _jsx(IconButton, { icon: Trash2, size: "xs", label: t('designer.columns.remove'), disabled: disabled, onClick: () => onChange(removeColumn(draft, column.key)) }) })] }));
|
|
24
26
|
}
|
|
@@ -19,6 +19,10 @@ export function ForeignKeysEditor({ draft, schema, tables, referenceColumns, dis
|
|
|
19
19
|
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col', className), children: [_jsxs("div", { children: [draft.foreignKeys.length === 0 && _jsx(EmptyState, { children: t('designer.foreignKeys.empty') }), draft.foreignKeys.map((foreignKey) => {
|
|
20
20
|
const referenced = foreignKey.referencedTable === '' ? [] : referenceColumns(foreignKey.referencedTable);
|
|
21
21
|
const choices = tables.includes(foreignKey.referencedTable) || foreignKey.referencedTable === '' ? tables : [...tables, foreignKey.referencedTable];
|
|
22
|
-
return (_jsxs("div", { className: "flex flex-col gap-3 border-b border-border-soft px-3 py-3", children: [_jsxs("div", { className: "flex items-end gap-3", children: [_jsx(Field, { label: t('designer.foreignKeys.name'), className: "w-56", children: _jsx(Input, { size: "sm", spellCheck: false, value: foreignKey.name, disabled: disabled, onChange: (event) => onChange(patchForeignKey(draft, foreignKey.key, { name: event.target.value })) }) }), _jsx(Field, { label: t('designer.foreignKeys.referencedTable'), group: true, className: "w-56", children: _jsx(Select, { size: "sm", className: "w-full", label: t('designer.foreignKeys.referencedTable'), placeholder: t('designer.foreignKeys.pickTable'), value: foreignKey.referencedTable === '' ? null : foreignKey.referencedTable, disabled: disabled, items: choices.map((table) => ({ value: table, label: table })), onValueChange: (table) => onChange(setReferencedTable(draft, foreignKey.key, schema, table)) }) }), _jsx(
|
|
22
|
+
return (_jsxs("div", { className: "flex flex-col gap-3 border-b border-border-soft px-3 py-3", children: [_jsxs("div", { className: "flex items-end gap-3", children: [_jsx(Field, { label: t('designer.foreignKeys.name'), className: "w-56", children: _jsx(Input, { size: "sm", spellCheck: false, value: foreignKey.name, disabled: disabled, onChange: (event) => onChange(patchForeignKey(draft, foreignKey.key, { name: event.target.value })) }) }), _jsx(Field, { label: t('designer.foreignKeys.referencedTable'), group: true, className: "w-56", children: _jsx(Select, { size: "sm", className: "w-full", label: t('designer.foreignKeys.referencedTable'), placeholder: t('designer.foreignKeys.pickTable'), value: foreignKey.referencedTable === '' ? null : foreignKey.referencedTable, disabled: disabled, items: choices.map((table) => ({ value: table, label: table })), onValueChange: (table) => onChange(setReferencedTable(draft, foreignKey.key, schema, table)) }) }), _jsx(ActionField, { label: t('designer.foreignKeys.onUpdate'), value: foreignKey.onUpdate, items: actions, disabled: disabled, onValueChange: (action) => onChange(patchForeignKey(draft, foreignKey.key, { onUpdate: action })) }), _jsx(ActionField, { label: t('designer.foreignKeys.onDelete'), value: foreignKey.onDelete, items: actions, disabled: disabled, onValueChange: (action) => onChange(patchForeignKey(draft, foreignKey.key, { onDelete: action })) }), _jsx(IconButton, { icon: Trash2, size: "xs", className: "mb-1", label: t('designer.foreignKeys.remove'), disabled: disabled, onClick: () => onChange(removeForeignKey(draft, foreignKey.key)) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-3", children: [_jsx(Field, { label: t('designer.foreignKeys.columns'), group: true, children: _jsx(NamePicker, { label: t('designer.foreignKeys.columns'), options: columns, value: foreignKey.columns, disabled: disabled, onToggle: (name) => onChange(toggleForeignKeyColumn(draft, foreignKey.key, 'columns', name)) }) }), _jsx(Field, { label: t('designer.foreignKeys.referencedColumns'), group: true, children: referenced === null ? (_jsx(Spinner, { size: 14, label: t('designer.foreignKeys.loadingColumns') })) : (_jsx(NamePicker, { label: t('designer.foreignKeys.referencedColumns'), options: referenced, value: foreignKey.referencedColumns, disabled: disabled, onToggle: (name) => onChange(toggleForeignKeyColumn(draft, foreignKey.key, 'referencedColumns', name)) })) })] })] }, foreignKey.key));
|
|
23
23
|
})] }), _jsx("div", { className: "shrink-0 px-3 py-2", children: _jsxs(Button, { variant: "secondary", size: "sm", disabled: disabled, onClick: () => onChange(addForeignKey(draft, schema)), children: [_jsx(Icon, { icon: Plus, size: 12 }), t('designer.foreignKeys.add')] }) })] }));
|
|
24
24
|
}
|
|
25
|
+
/* What a key does on an update or a delete; `null` is the engine's default. */
|
|
26
|
+
function ActionField({ label, value, items, disabled, onValueChange }) {
|
|
27
|
+
return (_jsx(Field, { label: label, group: true, className: "w-40", children: _jsx(Select, { size: "sm", className: "w-full", label: label, value: value ?? DEFAULT_ACTION, disabled: disabled, items: items, onValueChange: (action) => onValueChange(action === DEFAULT_ACTION ? null : action) }) }));
|
|
28
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useId } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -10,8 +10,17 @@ export function OptionsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
10
10
|
const { t } = useTranslation('database');
|
|
11
11
|
const id = useId();
|
|
12
12
|
const { options } = draft;
|
|
13
|
+
const patch = (change) => onChange(patchOptions(draft, change));
|
|
13
14
|
if (dialect.engine === 'sqlite') {
|
|
14
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex flex-col gap-4 p-3', className), children: [
|
|
15
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex flex-col gap-4 p-3', className), children: [_jsx(OptionSwitch, { label: t('designer.options.withoutRowid'), hint: t('designer.options.withoutRowidHint'), checked: options.withoutRowid, disabled: disabled, onCheckedChange: (withoutRowid) => patch({ withoutRowid }) }), _jsx(OptionSwitch, { label: t('designer.options.strict'), hint: t('designer.options.strictHint'), checked: options.strict, disabled: disabled, onCheckedChange: (strict) => patch({ strict }) })] }));
|
|
15
16
|
}
|
|
16
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex max-w-xl flex-col gap-4 p-3', className), children: [_jsx(
|
|
17
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex max-w-xl flex-col gap-4 p-3', className), children: [_jsx(SuggestedField, { label: t('designer.options.engine'), listId: `${id}-engines`, suggestions: MYSQL_ENGINES, value: options.engine, disabled: disabled, onValueChange: (engine) => patch({ engine }) }), _jsx(SuggestedField, { label: t('designer.options.charset'), listId: `${id}-charsets`, suggestions: MYSQL_CHARSETS, value: options.charset, disabled: disabled, onValueChange: (charset) => patch({ charset }) }), _jsx(SuggestedField, { label: t('designer.options.collation'), listId: `${id}-collations`, suggestions: collationSuggestionsOf(options.charset), value: options.collation, disabled: disabled, onValueChange: (collation) => patch({ collation }) }), _jsx(Field, { label: t('designer.options.comment'), children: _jsx(TextArea, { rows: 3, value: options.comment, disabled: disabled, onChange: (event) => patch({ comment: event.target.value }) }) })] }));
|
|
18
|
+
}
|
|
19
|
+
function OptionSwitch({ label, hint, checked, disabled, onCheckedChange }) {
|
|
20
|
+
return (_jsxs("label", { className: "flex items-start gap-3", children: [_jsx(Switch, { label: label, checked: checked, disabled: disabled, onCheckedChange: onCheckedChange }), _jsxs("span", { className: "flex flex-col", children: [_jsx("span", { className: "text-sm text-text", children: label }), _jsx("span", { className: "text-xs text-text-muted", children: hint })] })] }));
|
|
21
|
+
}
|
|
22
|
+
/* A free text field with a list of common values to pick from; empty leaves it to the server. */
|
|
23
|
+
function SuggestedField({ label, listId, suggestions, value, disabled, onValueChange }) {
|
|
24
|
+
const { t } = useTranslation('database');
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx("datalist", { id: listId, children: suggestions.map((suggestion) => (_jsx("option", { value: suggestion }, suggestion))) }), _jsx(Field, { label: label, hint: t('designer.options.serverDefault'), children: _jsx(Input, { mono: true, list: listId, spellCheck: false, value: value, disabled: disabled, onChange: (event) => onValueChange(event.target.value) }) })] }));
|
|
17
26
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { Ref } from 'react';
|
|
2
|
-
import type
|
|
3
|
-
import type { Plan } from './plan.ts';
|
|
2
|
+
import { type Plan } from './plan.ts';
|
|
4
3
|
export interface SqlPreviewProps {
|
|
5
4
|
plan: Plan;
|
|
6
5
|
className?: string;
|
|
7
6
|
ref?: Ref<HTMLDivElement>;
|
|
8
7
|
}
|
|
9
|
-
export declare function ProblemText({ problem }: {
|
|
10
|
-
problem: DraftProblem;
|
|
11
|
-
}): import("react").JSX.Element;
|
|
12
8
|
export declare function SqlPreview({ plan, className, ref }: SqlPreviewProps): import("react").JSX.Element;
|
|
@@ -6,7 +6,7 @@ import { Button, Icon, copyText } from '@adecore/ui';
|
|
|
6
6
|
import { CODE_TEXT } from '../code-text.js';
|
|
7
7
|
import { scriptOf } from './plan.js';
|
|
8
8
|
/* The sentence a problem of the draft is read as. */
|
|
9
|
-
|
|
9
|
+
function ProblemText({ problem }) {
|
|
10
10
|
const { t } = useTranslation('database');
|
|
11
11
|
return _jsx(_Fragment, { children: t(`ddl.problem.${problem.code}`, { subject: problem.subject }) });
|
|
12
12
|
}
|