@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.
Files changed (109) hide show
  1. package/dist/assign-ref.d.ts +2 -0
  2. package/dist/assign-ref.js +9 -0
  3. package/dist/client/client.js +14 -15
  4. package/dist/client/types.d.ts +1 -0
  5. package/dist/client/types.js +2 -0
  6. package/dist/connections/ConnectionForm.js +7 -2
  7. package/dist/connections/ConnectionManager.js +5 -5
  8. package/dist/connections/EngineIcon.js +0 -1
  9. package/dist/console/QueryConsole.js +12 -4
  10. package/dist/console/editing.js +2 -4
  11. package/dist/console/useConsoleHistory.js +3 -2
  12. package/dist/ddl/alter-mysql.js +2 -8
  13. package/dist/ddl/alter-sqlite.js +1 -1
  14. package/dist/ddl/column.js +5 -8
  15. package/dist/ddl/create.d.ts +1 -0
  16. package/dist/ddl/create.js +1 -1
  17. package/dist/ddl/diff.js +25 -23
  18. package/dist/designer/ColumnsEditor.js +8 -6
  19. package/dist/designer/ForeignKeysEditor.js +5 -1
  20. package/dist/designer/OptionsEditor.js +12 -3
  21. package/dist/designer/SqlPreview.d.ts +1 -5
  22. package/dist/designer/SqlPreview.js +1 -1
  23. package/dist/designer/TableDesigner.js +7 -6
  24. package/dist/explorer/DatabaseExplorer.js +12 -13
  25. package/dist/explorer/TableDialog.js +1 -1
  26. package/dist/explorer/tree.js +6 -16
  27. package/dist/grid/DataGrid.js +11 -14
  28. package/dist/grid/EnumPicker.js +4 -3
  29. package/dist/grid/GridHeaderCell.d.ts +1 -1
  30. package/dist/grid/GridHeaderCell.js +2 -2
  31. package/dist/grid/RecordView.js +2 -1
  32. package/dist/grid/ValueDock.js +2 -6
  33. package/dist/grid/aggregates.js +2 -3
  34. package/dist/grid/column-layout.js +3 -2
  35. package/dist/grid/copy-formats.js +2 -3
  36. package/dist/grid/display.d.ts +0 -1
  37. package/dist/grid/display.js +1 -3
  38. package/dist/grid/layout.js +6 -11
  39. package/dist/host/helper-path.js +8 -0
  40. package/dist/host/host.js +11 -16
  41. package/dist/host/validate.js +44 -43
  42. package/dist/sql.d.ts +2 -0
  43. package/dist/sql.js +5 -2
  44. package/dist/structure/StructureView.js +5 -6
  45. package/dist/table/TableView.js +19 -12
  46. package/dist/table/command-field.js +8 -12
  47. package/dist/table/grid-rows.js +2 -1
  48. package/dist/table/layout-store.js +5 -2
  49. package/dist/table/pending.js +11 -3
  50. package/dist/table/useTableTransfer.js +6 -5
  51. package/dist/testing/fake.js +7 -9
  52. package/dist/value/ValueEditor.js +1 -1
  53. package/dist/value/draft.js +1 -6
  54. package/dist/value/json.js +4 -4
  55. package/dist/workbench/DatabaseWorkbench.js +12 -5
  56. package/dist/workbench/tabs.js +3 -4
  57. package/package.json +6 -6
  58. package/src/assign-ref.ts +10 -0
  59. package/src/client/client.ts +17 -18
  60. package/src/client/types.ts +3 -0
  61. package/src/connections/ConnectionForm.tsx +62 -47
  62. package/src/connections/ConnectionManager.tsx +6 -5
  63. package/src/connections/EngineIcon.tsx +0 -2
  64. package/src/console/QueryConsole.tsx +52 -31
  65. package/src/console/editing.ts +2 -4
  66. package/src/console/useConsoleHistory.ts +5 -2
  67. package/src/ddl/alter-mysql.ts +3 -11
  68. package/src/ddl/alter-sqlite.ts +1 -1
  69. package/src/ddl/column.ts +5 -7
  70. package/src/ddl/create.ts +1 -1
  71. package/src/ddl/diff.ts +30 -25
  72. package/src/designer/ColumnsEditor.tsx +145 -125
  73. package/src/designer/ForeignKeysEditor.tsx +44 -27
  74. package/src/designer/OptionsEditor.tsx +95 -75
  75. package/src/designer/SqlPreview.tsx +2 -3
  76. package/src/designer/TableDesigner.tsx +7 -6
  77. package/src/explorer/DatabaseExplorer.tsx +26 -31
  78. package/src/explorer/TableDialog.tsx +1 -1
  79. package/src/explorer/tree.ts +8 -19
  80. package/src/grid/DataGrid.tsx +11 -14
  81. package/src/grid/EnumPicker.tsx +4 -3
  82. package/src/grid/GridHeaderCell.tsx +2 -3
  83. package/src/grid/RecordView.tsx +2 -1
  84. package/src/grid/ValueDock.tsx +2 -5
  85. package/src/grid/aggregates.ts +2 -4
  86. package/src/grid/column-layout.ts +3 -2
  87. package/src/grid/copy-formats.ts +2 -3
  88. package/src/grid/display.ts +1 -5
  89. package/src/grid/layout.ts +10 -11
  90. package/src/host/helper-path.ts +8 -8
  91. package/src/host/host.ts +11 -16
  92. package/src/host/validate.ts +45 -43
  93. package/src/protocol/messages.ts +0 -1
  94. package/src/sql.ts +7 -2
  95. package/src/structure/StructureView.tsx +7 -8
  96. package/src/table/CommandField.tsx +0 -1
  97. package/src/table/TableView.tsx +48 -34
  98. package/src/table/command-field.ts +9 -12
  99. package/src/table/grid-rows.ts +2 -7
  100. package/src/table/layout-store.ts +6 -3
  101. package/src/table/pending.ts +12 -7
  102. package/src/table/useTableTransfer.ts +7 -5
  103. package/src/testing/fake.ts +8 -12
  104. package/src/value/ValueEditor.tsx +1 -1
  105. package/src/value/draft.ts +1 -6
  106. package/src/value/json.ts +4 -4
  107. package/src/workbench/DatabaseWorkbench.tsx +14 -6
  108. package/src/workbench/DesignerTab.tsx +1 -1
  109. package/src/workbench/tabs.ts +5 -8
package/src/ddl/create.ts CHANGED
@@ -43,7 +43,7 @@ export const mysqlOptionsSql = (options: TableOptions, against?: TableOptions):
43
43
 
44
44
  const sqliteOptionsSql = (options: TableOptions): string[] => [...(options.strict ? ['STRICT'] : []), ...(options.withoutRowid ? ['WITHOUT ROWID'] : [])];
45
45
 
46
- const inlineIndexSql = (dialect: Dialect, index: IndexDraft): string =>
46
+ export const inlineIndexSql = (dialect: Dialect, index: IndexDraft): string =>
47
47
  [index.unique ? 'UNIQUE INDEX' : 'INDEX', ...(index.name === '' ? [] : [quote(dialect, index.name)]), `(${columnList(dialect, index.columns)})`].join(' ');
48
48
 
49
49
  /* `CREATE [UNIQUE] INDEX name ON table (columns)`. */
package/src/ddl/diff.ts CHANGED
@@ -73,13 +73,10 @@ const OPTION_FIELDS: readonly (keyof TableOptions)[] = ['engine', 'charset', 'co
73
73
  const matches = (original: { readonly name: string; readonly key: string }, draft: { readonly originalName: string | null; readonly key: string }): boolean =>
74
74
  draft.originalName !== null && (original.name === '' ? draft.key === original.key : draft.originalName === original.name);
75
75
 
76
- /* What a draft changes in the table it came from, told apart by the names items kept. */
77
- export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft): TableDiff => {
78
- const survivors = draft.columns.filter((column) => column.originalName !== null);
79
- const renames = new Map(survivors.map((column) => [column.originalName!, column.name]));
80
- const project = (names: readonly string[]): string[] => names.map((name) => renames.get(name) ?? name);
81
- const originalOf = (name: string): ColumnDraft | undefined => original.columns.find((column) => column.name === name);
76
+ type Project = (names: readonly string[]) => string[];
82
77
 
78
+ const columnChanges = (dialect: Dialect, original: TableDraft, draft: TableDraft) => {
79
+ const survivors = draft.columns.filter((column) => column.originalName !== null);
83
80
  const positions = survivors.map((column) => original.columns.findIndex((candidate) => candidate.name === column.originalName));
84
81
  const staying = longestRun(positions);
85
82
  let survivor = 0;
@@ -92,7 +89,7 @@ export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft
92
89
  return;
93
90
  }
94
91
  const rank = survivor++;
95
- const before = originalOf(column.originalName);
92
+ const before = original.columns.find((candidate) => candidate.name === column.originalName);
96
93
  if (before === undefined) {
97
94
  return;
98
95
  }
@@ -107,7 +104,10 @@ export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft
107
104
  });
108
105
 
109
106
  const droppedColumns = original.columns.filter((column) => !survivors.some((kept) => kept.originalName === column.name));
107
+ return { droppedColumns, keptColumns, addedColumns };
108
+ };
110
109
 
110
+ const indexChanges = (original: TableDraft, draft: TableDraft, project: Project) => {
111
111
  const droppedIndexes: IndexDraft[] = [];
112
112
  const addedIndexes = draft.indexes.filter((index) => index.originalName === null);
113
113
  const renamedIndexes: RenamedIndex[] = [];
@@ -122,39 +122,44 @@ export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft
122
122
  renamedIndexes.push({ from: before, to: after });
123
123
  }
124
124
  }
125
+ return { droppedIndexes, addedIndexes, renamedIndexes };
126
+ };
125
127
 
128
+ const sameForeignKey = (before: ForeignKeyDraft, after: ForeignKeyDraft, project: Project): boolean =>
129
+ before.name === after.name &&
130
+ sameList(project(before.columns), after.columns) &&
131
+ before.referencedSchema === after.referencedSchema &&
132
+ before.referencedTable === after.referencedTable &&
133
+ sameList(before.referencedColumns, after.referencedColumns) &&
134
+ before.onUpdate === after.onUpdate &&
135
+ before.onDelete === after.onDelete;
136
+
137
+ const foreignKeyChanges = (original: TableDraft, draft: TableDraft, project: Project) => {
126
138
  const droppedForeignKeys: ForeignKeyDraft[] = [];
127
139
  const addedForeignKeys = draft.foreignKeys.filter((key) => key.originalName === null);
128
140
  for (const before of original.foreignKeys) {
129
141
  const after = draft.foreignKeys.find((key) => matches(before, key));
130
- const same =
131
- after !== undefined &&
132
- before.name === after.name &&
133
- sameList(project(before.columns), after.columns) &&
134
- before.referencedSchema === after.referencedSchema &&
135
- before.referencedTable === after.referencedTable &&
136
- sameList(before.referencedColumns, after.referencedColumns) &&
137
- before.onUpdate === after.onUpdate &&
138
- before.onDelete === after.onDelete;
139
- if (!same) {
142
+ if (after === undefined || !sameForeignKey(before, after, project)) {
140
143
  droppedForeignKeys.push(before);
141
144
  if (after !== undefined) {
142
145
  addedForeignKeys.push(after);
143
146
  }
144
147
  }
145
148
  }
149
+ return { droppedForeignKeys, addedForeignKeys };
150
+ };
151
+
152
+ /* What a draft changes in the table it came from, told apart by the names items kept. */
153
+ export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft): TableDiff => {
154
+ const renames = new Map(draft.columns.filter((column) => column.originalName !== null).map((column) => [column.originalName!, column.name]));
155
+ const project: Project = (names) => names.map((name) => renames.get(name) ?? name);
146
156
 
147
157
  return {
148
158
  renamed: original.name !== draft.name,
149
- droppedColumns,
150
- keptColumns,
151
- addedColumns,
159
+ ...columnChanges(dialect, original, draft),
152
160
  primaryKeyChanged: !sameList(project(original.primaryKey), draft.primaryKey),
153
- droppedIndexes,
154
- addedIndexes,
155
- renamedIndexes,
156
- droppedForeignKeys,
157
- addedForeignKeys,
161
+ ...indexChanges(original, draft, project),
162
+ ...foreignKeyChanges(original, draft, project),
158
163
  optionsChanged: OPTION_FIELDS.some((field) => original.options[field] !== draft.options[field])
159
164
  };
160
165
  };
@@ -4,7 +4,7 @@ import { ArrowDown, ArrowUp, KeyRound, Plus, Trash2 } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Button, ButtonGroup, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
6
6
  import { formatNumber } from '@adecore/ui/format';
7
- import { typeSuggestionsOf } from '../ddl/index.ts';
7
+ import { typeSuggestionsOf, type ColumnDraft } from '../ddl/index.ts';
8
8
  import type { EditorProps } from './editor-props.ts';
9
9
  import { addColumn, moveColumn, patchColumn, removeColumn, togglePrimaryKey } from './edit.ts';
10
10
 
@@ -65,130 +65,18 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
65
65
  </tr>
66
66
  </thead>
67
67
  <tbody>
68
- {draft.columns.map((column, at) => {
69
- const keyAt = draft.primaryKey.indexOf(column.name);
70
- const locked = disabled || column.generated;
71
- const patch = (change: Parameters<typeof patchColumn>[2]): void => onChange(patchColumn(draft, column.key, change));
72
- return (
73
- <tr key={column.key}>
74
- <td className={clsx(TD, 'whitespace-nowrap')}>
75
- <ButtonGroup>
76
- <IconButton
77
- icon={ArrowUp}
78
- size="xs"
79
- label={t('designer.columns.moveUp')}
80
- disabled={disabled || at === 0}
81
- onClick={() => onChange(moveColumn(draft, column.key, -1))}
82
- />
83
- <IconButton
84
- icon={ArrowDown}
85
- size="xs"
86
- label={t('designer.columns.moveDown')}
87
- disabled={disabled || at === draft.columns.length - 1}
88
- onClick={() => onChange(moveColumn(draft, column.key, 1))}
89
- />
90
- </ButtonGroup>
91
- </td>
92
- <td className={clsx(TD, 'min-w-40')}>
93
- <Input
94
- size="sm"
95
- className="w-full"
96
- aria-label={t('designer.columns.name')}
97
- spellCheck={false}
98
- value={column.name}
99
- disabled={locked}
100
- onChange={(event) => patch({ name: event.target.value })}
101
- />
102
- </td>
103
- <td className={clsx(TD, 'min-w-36')}>
104
- <Input
105
- size="sm"
106
- mono
107
- className="w-full"
108
- aria-label={t('designer.columns.type')}
109
- list={listId}
110
- spellCheck={false}
111
- value={column.type}
112
- disabled={locked}
113
- onChange={(event) => patch({ type: event.target.value })}
114
- />
115
- </td>
116
- <td className={TD}>
117
- <Checkbox
118
- className="mx-auto"
119
- label={t('designer.columns.notNullFor', { name: column.name })}
120
- checked={!column.nullable}
121
- disabled={locked}
122
- onCheckedChange={(checked) => patch({ nullable: !checked })}
123
- />
124
- </td>
125
- <td className={clsx(TD, 'min-w-36')}>
126
- {column.generated ? (
127
- <Pill mono>{column.generatedClause ?? t('designer.columns.generated')}</Pill>
128
- ) : (
129
- <Input
130
- size="sm"
131
- mono
132
- className="w-full"
133
- aria-label={t('designer.columns.default')}
134
- placeholder={t('designer.columns.noDefault')}
135
- spellCheck={false}
136
- value={column.defaultValue ?? ''}
137
- disabled={disabled}
138
- onChange={(event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value })}
139
- />
140
- )}
141
- </td>
142
- {mysql && (
143
- <td className={TD}>
144
- <Checkbox
145
- className="mx-auto"
146
- label={t('designer.columns.autoIncrementFor', { name: column.name })}
147
- checked={column.autoIncrement}
148
- disabled={locked}
149
- onCheckedChange={(checked) => patch({ autoIncrement: checked })}
150
- />
151
- </td>
152
- )}
153
- <td className={clsx(TD, 'whitespace-nowrap')}>
154
- <span className="flex items-center gap-1">
155
- <IconButton
156
- icon={KeyRound}
157
- size="xs"
158
- label={keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey')}
159
- aria-pressed={keyAt >= 0}
160
- disabled={locked}
161
- onClick={() => onChange(togglePrimaryKey(draft, column.key))}
162
- />
163
- {keyAt >= 0 && draft.primaryKey.length > 1 && (
164
- <span className="text-xs text-text-muted">{formatNumber(keyAt + 1)}</span>
165
- )}
166
- </span>
167
- </td>
168
- {mysql && (
169
- <td className={clsx(TD, 'min-w-40')}>
170
- <Input
171
- size="sm"
172
- className="w-full"
173
- aria-label={t('designer.columns.comment')}
174
- value={column.comment}
175
- disabled={locked}
176
- onChange={(event) => patch({ comment: event.target.value })}
177
- />
178
- </td>
179
- )}
180
- <td className={TD}>
181
- <IconButton
182
- icon={Trash2}
183
- size="xs"
184
- label={t('designer.columns.remove')}
185
- disabled={disabled}
186
- onClick={() => onChange(removeColumn(draft, column.key))}
187
- />
188
- </td>
189
- </tr>
190
- );
191
- })}
68
+ {draft.columns.map((column, at) => (
69
+ <ColumnRow
70
+ key={column.key}
71
+ draft={draft}
72
+ column={column}
73
+ at={at}
74
+ mysql={mysql}
75
+ listId={listId}
76
+ disabled={disabled}
77
+ onChange={onChange}
78
+ />
79
+ ))}
192
80
  </tbody>
193
81
  </table>
194
82
  </div>
@@ -202,3 +90,135 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
202
90
  </div>
203
91
  );
204
92
  }
93
+
94
+ interface ColumnRowProps extends Pick<EditorProps, 'draft' | 'disabled' | 'onChange'> {
95
+ column: ColumnDraft;
96
+ at: number;
97
+ mysql: boolean;
98
+ /* The id of the datalist with the suggested types. */
99
+ listId: string;
100
+ }
101
+
102
+ function ColumnRow({ draft, column, at, mysql, listId, disabled, onChange }: ColumnRowProps) {
103
+ const { t } = useTranslation('database');
104
+ const keyAt = draft.primaryKey.indexOf(column.name);
105
+ const locked = disabled || column.generated;
106
+ const patch = (change: Partial<ColumnDraft>): void => onChange(patchColumn(draft, column.key, change));
107
+ return (
108
+ <tr>
109
+ <td className={clsx(TD, 'whitespace-nowrap')}>
110
+ <ButtonGroup>
111
+ <IconButton
112
+ icon={ArrowUp}
113
+ size="xs"
114
+ label={t('designer.columns.moveUp')}
115
+ disabled={disabled || at === 0}
116
+ onClick={() => onChange(moveColumn(draft, column.key, -1))}
117
+ />
118
+ <IconButton
119
+ icon={ArrowDown}
120
+ size="xs"
121
+ label={t('designer.columns.moveDown')}
122
+ disabled={disabled || at === draft.columns.length - 1}
123
+ onClick={() => onChange(moveColumn(draft, column.key, 1))}
124
+ />
125
+ </ButtonGroup>
126
+ </td>
127
+ <td className={clsx(TD, 'min-w-40')}>
128
+ <Input
129
+ size="sm"
130
+ className="w-full"
131
+ aria-label={t('designer.columns.name')}
132
+ spellCheck={false}
133
+ value={column.name}
134
+ disabled={locked}
135
+ onChange={(event) => patch({ name: event.target.value })}
136
+ />
137
+ </td>
138
+ <td className={clsx(TD, 'min-w-36')}>
139
+ <Input
140
+ size="sm"
141
+ mono
142
+ className="w-full"
143
+ aria-label={t('designer.columns.type')}
144
+ list={listId}
145
+ spellCheck={false}
146
+ value={column.type}
147
+ disabled={locked}
148
+ onChange={(event) => patch({ type: event.target.value })}
149
+ />
150
+ </td>
151
+ <td className={TD}>
152
+ <Checkbox
153
+ className="mx-auto"
154
+ label={t('designer.columns.notNullFor', { name: column.name })}
155
+ checked={!column.nullable}
156
+ disabled={locked}
157
+ onCheckedChange={(checked) => patch({ nullable: !checked })}
158
+ />
159
+ </td>
160
+ <td className={clsx(TD, 'min-w-36')}>
161
+ {column.generated ? (
162
+ <Pill mono>{column.generatedClause ?? t('designer.columns.generated')}</Pill>
163
+ ) : (
164
+ <Input
165
+ size="sm"
166
+ mono
167
+ className="w-full"
168
+ aria-label={t('designer.columns.default')}
169
+ placeholder={t('designer.columns.noDefault')}
170
+ spellCheck={false}
171
+ value={column.defaultValue ?? ''}
172
+ disabled={disabled}
173
+ onChange={(event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value })}
174
+ />
175
+ )}
176
+ </td>
177
+ {mysql && (
178
+ <td className={TD}>
179
+ <Checkbox
180
+ className="mx-auto"
181
+ label={t('designer.columns.autoIncrementFor', { name: column.name })}
182
+ checked={column.autoIncrement}
183
+ disabled={locked}
184
+ onCheckedChange={(checked) => patch({ autoIncrement: checked })}
185
+ />
186
+ </td>
187
+ )}
188
+ <td className={clsx(TD, 'whitespace-nowrap')}>
189
+ <span className="flex items-center gap-1">
190
+ <IconButton
191
+ icon={KeyRound}
192
+ size="xs"
193
+ label={keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey')}
194
+ aria-pressed={keyAt >= 0}
195
+ disabled={locked}
196
+ onClick={() => onChange(togglePrimaryKey(draft, column.key))}
197
+ />
198
+ {keyAt >= 0 && draft.primaryKey.length > 1 && <span className="text-xs text-text-muted">{formatNumber(keyAt + 1)}</span>}
199
+ </span>
200
+ </td>
201
+ {mysql && (
202
+ <td className={clsx(TD, 'min-w-40')}>
203
+ <Input
204
+ size="sm"
205
+ className="w-full"
206
+ aria-label={t('designer.columns.comment')}
207
+ value={column.comment}
208
+ disabled={locked}
209
+ onChange={(event) => patch({ comment: event.target.value })}
210
+ />
211
+ </td>
212
+ )}
213
+ <td className={TD}>
214
+ <IconButton
215
+ icon={Trash2}
216
+ size="xs"
217
+ label={t('designer.columns.remove')}
218
+ disabled={disabled}
219
+ onClick={() => onChange(removeColumn(draft, column.key))}
220
+ />
221
+ </td>
222
+ </tr>
223
+ );
224
+ }
@@ -1,7 +1,7 @@
1
1
  import clsx from 'clsx';
2
2
  import { Plus, Trash2 } from 'lucide-react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Button, EmptyState, Field, Icon, IconButton, Input, Select, Spinner } from '@adecore/ui';
4
+ import { Button, EmptyState, Field, Icon, IconButton, Input, Select, Spinner, type SelectItem } from '@adecore/ui';
5
5
  import { FOREIGN_KEY_ACTIONS } from '../ddl/index.ts';
6
6
  import type { EditorProps } from './editor-props.ts';
7
7
  import { NamePicker } from './NamePicker.tsx';
@@ -58,32 +58,20 @@ export function ForeignKeysEditor({ draft, schema, tables, referenceColumns, dis
58
58
  onValueChange={(table) => onChange(setReferencedTable(draft, foreignKey.key, schema, table))}
59
59
  />
60
60
  </Field>
61
- <Field label={t('designer.foreignKeys.onUpdate')} group className="w-40">
62
- <Select
63
- size="sm"
64
- className="w-full"
65
- label={t('designer.foreignKeys.onUpdate')}
66
- value={foreignKey.onUpdate ?? DEFAULT_ACTION}
67
- disabled={disabled}
68
- items={actions}
69
- onValueChange={(action) =>
70
- onChange(patchForeignKey(draft, foreignKey.key, { onUpdate: action === DEFAULT_ACTION ? null : action }))
71
- }
72
- />
73
- </Field>
74
- <Field label={t('designer.foreignKeys.onDelete')} group className="w-40">
75
- <Select
76
- size="sm"
77
- className="w-full"
78
- label={t('designer.foreignKeys.onDelete')}
79
- value={foreignKey.onDelete ?? DEFAULT_ACTION}
80
- disabled={disabled}
81
- items={actions}
82
- onValueChange={(action) =>
83
- onChange(patchForeignKey(draft, foreignKey.key, { onDelete: action === DEFAULT_ACTION ? null : action }))
84
- }
85
- />
86
- </Field>
61
+ <ActionField
62
+ label={t('designer.foreignKeys.onUpdate')}
63
+ value={foreignKey.onUpdate}
64
+ items={actions}
65
+ disabled={disabled}
66
+ onValueChange={(action) => onChange(patchForeignKey(draft, foreignKey.key, { onUpdate: action }))}
67
+ />
68
+ <ActionField
69
+ label={t('designer.foreignKeys.onDelete')}
70
+ value={foreignKey.onDelete}
71
+ items={actions}
72
+ disabled={disabled}
73
+ onValueChange={(action) => onChange(patchForeignKey(draft, foreignKey.key, { onDelete: action }))}
74
+ />
87
75
  <IconButton
88
76
  icon={Trash2}
89
77
  size="xs"
@@ -130,3 +118,32 @@ export function ForeignKeysEditor({ draft, schema, tables, referenceColumns, dis
130
118
  </div>
131
119
  );
132
120
  }
121
+
122
+ /* What a key does on an update or a delete; `null` is the engine's default. */
123
+ function ActionField({
124
+ label,
125
+ value,
126
+ items,
127
+ disabled,
128
+ onValueChange
129
+ }: {
130
+ label: string;
131
+ value: string | null;
132
+ items: SelectItem<string>[];
133
+ disabled: boolean;
134
+ onValueChange(action: string | null): void;
135
+ }) {
136
+ return (
137
+ <Field label={label} group className="w-40">
138
+ <Select
139
+ size="sm"
140
+ className="w-full"
141
+ label={label}
142
+ value={value ?? DEFAULT_ACTION}
143
+ disabled={disabled}
144
+ items={items}
145
+ onValueChange={(action) => onValueChange(action === DEFAULT_ACTION ? null : action)}
146
+ />
147
+ </Field>
148
+ );
149
+ }