@adecore/database 0.39.0 → 0.40.1

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 (131) hide show
  1. package/dist/console/QueryConsole.js +2 -10
  2. package/dist/console/SchemaPicker.js +0 -1
  3. package/dist/console/StatementResultView.js +1 -1
  4. package/dist/ddl/diff.js +1 -1
  5. package/dist/explorer/tree.js +0 -1
  6. package/dist/export-formats.d.ts +2 -0
  7. package/dist/export-formats.js +1 -0
  8. package/dist/grid/CopyAsMenu.js +0 -1
  9. package/dist/grid/DataGrid.js +10 -10
  10. package/dist/grid/column-layout.d.ts +0 -1
  11. package/dist/grid/column-layout.js +1 -2
  12. package/dist/grid/column-view.js +3 -2
  13. package/dist/grid/display.js +1 -2
  14. package/dist/host/host.js +1 -2
  15. package/dist/host/validate.d.ts +3 -0
  16. package/dist/host/validate.js +18 -15
  17. package/dist/same-list.d.ts +1 -0
  18. package/dist/same-list.js +1 -0
  19. package/dist/stored-size.d.ts +1 -0
  20. package/dist/stored-size.js +10 -0
  21. package/dist/table/TableToolbar.js +1 -1
  22. package/dist/table/TableView.js +2 -10
  23. package/package.json +15 -15
  24. package/src/DatabaseProvider.tsx +2 -0
  25. package/src/KeyIcon.tsx +1 -0
  26. package/src/client/client.ts +13 -0
  27. package/src/client-context.ts +2 -0
  28. package/src/connections/ConnectionForm.tsx +11 -0
  29. package/src/connections/ConnectionManager.tsx +9 -0
  30. package/src/connections/connection-config.ts +21 -0
  31. package/src/connections/docker.ts +2 -0
  32. package/src/connections/use-docker-containers.ts +3 -0
  33. package/src/console/DestructiveDialog.tsx +1 -0
  34. package/src/console/QueryConsole.tsx +38 -10
  35. package/src/console/SchemaPicker.tsx +1 -1
  36. package/src/console/SqlEditor.tsx +10 -0
  37. package/src/console/StatementResultView.tsx +7 -2
  38. package/src/console/TransactionControls.tsx +1 -0
  39. package/src/console/destructive.ts +11 -0
  40. package/src/console/editing.ts +10 -0
  41. package/src/console/history.ts +8 -0
  42. package/src/console/labels.ts +1 -0
  43. package/src/console/outcome.ts +1 -0
  44. package/src/console/result-keys.ts +16 -0
  45. package/src/console/shortcuts.ts +2 -0
  46. package/src/ddl/alter-mysql.ts +16 -0
  47. package/src/ddl/alter-sqlite.ts +21 -0
  48. package/src/ddl/alter.ts +2 -0
  49. package/src/ddl/column.ts +11 -0
  50. package/src/ddl/create.ts +15 -0
  51. package/src/ddl/dialect.ts +4 -0
  52. package/src/ddl/diff.ts +22 -2
  53. package/src/ddl/draft.ts +1 -0
  54. package/src/ddl/parse.ts +31 -0
  55. package/src/ddl/types.ts +3 -0
  56. package/src/ddl/validate.ts +12 -0
  57. package/src/designer/ColumnsEditor.tsx +3 -0
  58. package/src/designer/DesignerView.tsx +2 -0
  59. package/src/designer/ForeignKeysEditor.tsx +4 -0
  60. package/src/designer/IndexesEditor.tsx +1 -0
  61. package/src/designer/NamePicker.tsx +2 -0
  62. package/src/designer/OptionsEditor.tsx +4 -0
  63. package/src/designer/SqlPreview.tsx +2 -0
  64. package/src/designer/TableDesigner.tsx +14 -0
  65. package/src/designer/edit.ts +13 -0
  66. package/src/designer/plan.ts +3 -0
  67. package/src/designer/recover.ts +1 -0
  68. package/src/designer/useReferenceColumns.ts +5 -0
  69. package/src/explorer/DatabaseExplorer.tsx +40 -0
  70. package/src/explorer/RowMenu.tsx +2 -0
  71. package/src/explorer/TableDialog.tsx +6 -0
  72. package/src/explorer/expansion.ts +8 -0
  73. package/src/explorer/tree.ts +73 -2
  74. package/src/explorer/use-explorer-loads.ts +15 -0
  75. package/src/explorer/use-stored-expansion.ts +4 -0
  76. package/src/export-formats.ts +3 -0
  77. package/src/grid/CellEditor.tsx +1 -0
  78. package/src/grid/CopyAsMenu.tsx +1 -1
  79. package/src/grid/DataGrid.tsx +93 -23
  80. package/src/grid/EnumPicker.tsx +6 -0
  81. package/src/grid/GridHeaderCell.tsx +11 -0
  82. package/src/grid/GridRowView.tsx +3 -0
  83. package/src/grid/RecordView.tsx +19 -0
  84. package/src/grid/ValueDock.tsx +2 -0
  85. package/src/grid/aggregates.ts +12 -0
  86. package/src/grid/column-layout.ts +12 -4
  87. package/src/grid/column-selection.ts +9 -0
  88. package/src/grid/column-view.ts +6 -2
  89. package/src/grid/copy-formats.ts +13 -0
  90. package/src/grid/display.ts +17 -3
  91. package/src/grid/edit-value.ts +11 -0
  92. package/src/grid/enum-type.ts +15 -0
  93. package/src/grid/focused-value.ts +4 -0
  94. package/src/grid/layout.ts +12 -0
  95. package/src/grid/navigation.ts +5 -0
  96. package/src/grid/row-selection.ts +7 -0
  97. package/src/grid/sort.ts +1 -0
  98. package/src/grid/whole-cells.ts +8 -0
  99. package/src/host/host.ts +16 -3
  100. package/src/host/spawn.ts +2 -0
  101. package/src/host/validate.ts +37 -15
  102. package/src/same-list.ts +2 -0
  103. package/src/sql-split.ts +28 -0
  104. package/src/sql.ts +20 -0
  105. package/src/stored-size.ts +10 -0
  106. package/src/structure/StructureView.tsx +18 -0
  107. package/src/table/CommandField.tsx +11 -0
  108. package/src/table/ImportDialog.tsx +4 -0
  109. package/src/table/TableStatusBar.tsx +1 -0
  110. package/src/table/TableToolbar.tsx +2 -2
  111. package/src/table/TableView.tsx +50 -10
  112. package/src/table/command-field.ts +40 -0
  113. package/src/table/grid-rows.ts +13 -0
  114. package/src/table/import-mapping.ts +5 -0
  115. package/src/table/layout-store.ts +7 -0
  116. package/src/table/paging.ts +1 -0
  117. package/src/table/pending.ts +18 -0
  118. package/src/table/references.ts +4 -0
  119. package/src/table/useLoaded.ts +3 -0
  120. package/src/table/useStoredLayout.ts +2 -0
  121. package/src/table/useTableTransfer.ts +17 -1
  122. package/src/testing/fake.ts +24 -0
  123. package/src/use-database-resources.ts +1 -0
  124. package/src/use-popup-press.ts +3 -0
  125. package/src/value/ValueEditor.tsx +6 -0
  126. package/src/value/draft.ts +7 -0
  127. package/src/value/hex.ts +8 -0
  128. package/src/value/json.ts +16 -0
  129. package/src/value/views.ts +14 -0
  130. package/src/workbench/DatabaseWorkbench.tsx +22 -0
  131. package/src/workbench/tabs.ts +34 -0
package/src/sql-split.ts CHANGED
@@ -22,6 +22,7 @@ const isWordCode = (code: number): boolean =>
22
22
 
23
23
  const lineEnd = (text: string, at: number): number => {
24
24
  const newline = text.indexOf('\n', at);
25
+
25
26
  return newline === -1 ? text.length : newline + 1;
26
27
  };
27
28
 
@@ -31,9 +32,12 @@ const startsLineComment = (text: string, at: number, engine: Engine): boolean =>
31
32
  if (engine === 'sqlite') {
32
33
  return true;
33
34
  }
35
+
34
36
  const next = text[at + 2];
37
+
35
38
  return next === undefined || WHITESPACE.test(next) || next.charCodeAt(0) < 32;
36
39
  }
40
+
37
41
  return text[at] === '#' && engine === 'mysql';
38
42
  };
39
43
 
@@ -42,6 +46,7 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
42
46
  const quote = text[at]!;
43
47
  const backslashEscapes = engine === 'mysql' && quote !== '`';
44
48
  let i = at + 1;
49
+
45
50
  while (i < text.length) {
46
51
  if (backslashEscapes && text[i] === '\\') {
47
52
  i += 2;
@@ -55,6 +60,7 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
55
60
  i += 1;
56
61
  }
57
62
  }
63
+
58
64
  return text.length;
59
65
  };
60
66
 
@@ -62,9 +68,11 @@ const quotedEnd = (text: string, at: number, engine: Engine): number => {
62
68
  export const tokenize = (text: string, engine: Engine): SqlToken[] => {
63
69
  const tokens: SqlToken[] = [];
64
70
  let i = 0;
71
+
65
72
  while (i < text.length) {
66
73
  const char = text[i]!;
67
74
  const code = text.charCodeAt(i);
75
+
68
76
  if (WHITESPACE.test(char)) {
69
77
  i += 1;
70
78
  } else if (char === "'" || char === '"' || char === '`') {
@@ -90,9 +98,11 @@ export const tokenize = (text: string, engine: Engine): SqlToken[] => {
90
98
  i += 1;
91
99
  } else if (isWordCode(code)) {
92
100
  let end = i + 1;
101
+
93
102
  while (end < text.length && isWordCode(text.charCodeAt(end))) {
94
103
  end += 1;
95
104
  }
105
+
96
106
  tokens.push({ kind: 'word', start: i, end });
97
107
  i = end;
98
108
  } else {
@@ -100,6 +110,7 @@ export const tokenize = (text: string, engine: Engine): SqlToken[] => {
100
110
  i += 1;
101
111
  }
102
112
  }
113
+
103
114
  return tokens;
104
115
  };
105
116
 
@@ -112,12 +123,15 @@ class TriggerState {
112
123
 
113
124
  word(word: string): void {
114
125
  const upper = word.toUpperCase();
126
+
115
127
  if (this.leading.length < 3) {
116
128
  this.leading.push(upper);
117
129
  }
130
+
118
131
  if (!this.isTrigger()) {
119
132
  return;
120
133
  }
134
+
121
135
  if (upper === 'BEGIN' && !this.inBody) {
122
136
  this.inBody = true;
123
137
  } else if (upper === 'CASE' && this.inBody) {
@@ -137,6 +151,7 @@ class TriggerState {
137
151
 
138
152
  private isTrigger(): boolean {
139
153
  const [first, second, third] = this.leading;
154
+
140
155
  return first === 'CREATE' && (second === 'TRIGGER' || ((second === 'TEMP' || second === 'TEMPORARY') && third === 'TRIGGER'));
141
156
  }
142
157
  }
@@ -144,12 +159,15 @@ class TriggerState {
144
159
  const trimmed = (text: string, from: number, to: number): SqlStatement => {
145
160
  let start = from;
146
161
  let end = to;
162
+
147
163
  while (start < end && WHITESPACE.test(text[start]!)) {
148
164
  start += 1;
149
165
  }
166
+
150
167
  while (end > start && WHITESPACE.test(text[end - 1]!)) {
151
168
  end -= 1;
152
169
  }
170
+
153
171
  return { text: text.slice(start, end), start, end };
154
172
  };
155
173
 
@@ -170,12 +188,14 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
170
188
  if (hasContent) {
171
189
  statements.push(trimmed(text, start, token.start));
172
190
  }
191
+
173
192
  start = token.end;
174
193
  hasContent = false;
175
194
  trigger = new TriggerState();
176
195
  }
177
196
  } else if (token.kind !== 'comment') {
178
197
  hasContent = true;
198
+
179
199
  if (token.kind === 'word' && engine === 'sqlite') {
180
200
  trigger.word(text.slice(token.start, token.end));
181
201
  }
@@ -185,6 +205,7 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
185
205
  if (hasContent) {
186
206
  statements.push(trimmed(text, start, text.length));
187
207
  }
208
+
188
209
  return statements;
189
210
  };
190
211
 
@@ -195,16 +216,21 @@ export const splitStatements = (text: string, engine: Engine): SqlStatement[] =>
195
216
  */
196
217
  export const statementAt = (text: string, offset: number, engine: Engine): SqlStatement | null => {
197
218
  const statements = splitStatements(text, engine);
219
+
198
220
  for (const [index, statement] of statements.entries()) {
199
221
  if (offset >= statement.start && offset <= statement.end) {
200
222
  return statement;
201
223
  }
224
+
202
225
  const next = statements[index + 1];
226
+
203
227
  if (offset > statement.end && (next === undefined || offset < next.start)) {
204
228
  const gap = text.slice(statement.end, offset);
229
+
205
230
  return gap.includes('\n') && next !== undefined ? next : statement;
206
231
  }
207
232
  }
233
+
208
234
  return statements[0] ?? null;
209
235
  };
210
236
 
@@ -214,7 +240,9 @@ export const firstKeyword = (text: string, engine: Engine): string => {
214
240
  if (token.kind === 'comment' || (token.kind === 'punct' && text[token.start] === '(')) {
215
241
  continue;
216
242
  }
243
+
217
244
  return token.kind === 'word' ? (/^[A-Za-z]*/.exec(text.slice(token.start, token.end))?.[0] ?? '').toUpperCase() : '';
218
245
  }
246
+
219
247
  return '';
220
248
  };
package/src/sql.ts CHANGED
@@ -24,12 +24,14 @@ export const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
24
24
 
25
25
  /* Whether a value of this kind is a number, which a grid aligns against the end of its cell. */
26
26
  export const isNumericKind = (kind: ValueKind): boolean => NUMERIC_KINDS.has(kind);
27
+
27
28
  const ORDER_ITEM = /\s*(`(?:[^`]|``)+`|"(?:[^"]|"")+"|[A-Za-z_][\w$]*)(?:\s+(asc|desc))?\s*(,|$)/iy;
28
29
 
29
30
  const markOf = (engine: Engine): string => (engine === 'mysql' ? '`' : '"');
30
31
 
31
32
  export const quoteIdentifier = (engine: Engine, name: string): string => {
32
33
  const mark = markOf(engine);
34
+
33
35
  return `${mark}${name.replaceAll(mark, mark + mark)}${mark}`;
34
36
  };
35
37
 
@@ -42,6 +44,7 @@ export const qualifiedName = (target: SqlTarget): string =>
42
44
  const stringLiteral = (engine: Engine, text: string): string => {
43
45
  // MySQL reads a backslash as an escape unless the server runs with NO_BACKSLASH_ESCAPES.
44
46
  const escaped = engine === 'mysql' ? text.replaceAll('\\', '\\\\') : text;
47
+
45
48
  return `'${escaped.replaceAll("'", "''")}'`;
46
49
  };
47
50
 
@@ -52,6 +55,7 @@ export const sqlLiteral = (engine: Engine, value: LiteralValue, kind?: ValueKind
52
55
  if (value === null) {
53
56
  return 'NULL';
54
57
  }
58
+
55
59
  switch (typeof value) {
56
60
  case 'boolean':
57
61
  return engine === 'mysql' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0';
@@ -59,9 +63,11 @@ export const sqlLiteral = (engine: Engine, value: LiteralValue, kind?: ValueKind
59
63
  return Number.isFinite(value) ? String(value) : 'NULL';
60
64
  case 'string': {
61
65
  const trimmed = value.trim();
66
+
62
67
  return kind !== undefined && isNumericKind(kind) && NUMERIC_TEXT.test(trimmed) ? trimmed : stringLiteral(engine, value);
63
68
  }
64
69
  }
70
+
65
71
  switch (value.kind) {
66
72
  case 'default':
67
73
  return 'DEFAULT';
@@ -96,43 +102,56 @@ export const orderByClause = (engine: Engine, sorts: readonly ColumnSort[]): str
96
102
 
97
103
  const unquote = (engine: Engine, word: string): string | null => {
98
104
  const mark = word[0]!;
105
+
99
106
  if (mark !== '`' && mark !== '"') {
100
107
  return word;
101
108
  }
109
+
102
110
  // A double quoted word is a string in MySQL, which sorts by a constant and so is not a column.
103
111
  if (mark === '"' && engine === 'mysql') {
104
112
  return null;
105
113
  }
114
+
106
115
  return word.slice(1, -1).replaceAll(mark + mark, mark);
107
116
  };
108
117
 
109
118
  const readOrderBy = (engine: Engine, text: string, resolve: (word: string) => string | undefined): ColumnSort[] | null => {
110
119
  const source = text.trim();
120
+
111
121
  if (source === '') {
112
122
  return [];
113
123
  }
124
+
114
125
  const sorts: ColumnSort[] = [];
115
126
  let position = 0;
127
+
116
128
  while (position < source.length) {
117
129
  ORDER_ITEM.lastIndex = position;
118
130
  const match = ORDER_ITEM.exec(source);
131
+
119
132
  if (match === null) {
120
133
  return null;
121
134
  }
135
+
122
136
  const word = unquote(engine, match[1]!);
123
137
  const column = word === null ? undefined : resolve(word);
138
+
124
139
  if (column === undefined || sorts.some((sort) => sort.column === column)) {
125
140
  return null;
126
141
  }
142
+
127
143
  sorts.push({ column, direction: match[2]?.toLowerCase() === 'desc' ? 'desc' : 'asc' });
128
144
  position = ORDER_ITEM.lastIndex;
145
+
129
146
  if (match[3] === '') {
130
147
  return sorts;
131
148
  }
149
+
132
150
  if (position >= source.length) {
133
151
  return null;
134
152
  }
135
153
  }
154
+
136
155
  return sorts;
137
156
  };
138
157
 
@@ -153,5 +172,6 @@ export const parseOrderByLoose = (engine: Engine, text: string): ColumnSort[] |
153
172
  /* The column a word names, which may be quoted the way the engine quotes, or `undefined` when none of the columns is meant. */
154
173
  export const findColumn = (engine: Engine, word: string, columns: readonly string[]): string | undefined => {
155
174
  const bare = unquote(engine, word);
175
+
156
176
  return bare === null ? undefined : matchColumn(columns, bare);
157
177
  };
@@ -0,0 +1,10 @@
1
+ /* A size in pixels a view wrote to the app's storage, or `null` when the text is missing, broken or not at least `minimum`. */
2
+ export const parseStoredSize = (raw: string | null | undefined, minimum: number): number | null => {
3
+ try {
4
+ const value: unknown = JSON.parse(raw ?? 'null');
5
+
6
+ return typeof value === 'number' && Number.isFinite(value) && value >= minimum ? Math.round(value) : null;
7
+ } catch {
8
+ return null;
9
+ }
10
+ };
@@ -52,11 +52,13 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
52
52
 
53
53
  useEffect(() => {
54
54
  let current = true;
55
+
55
56
  const settle = (outcome: Load): void => {
56
57
  if (current) {
57
58
  setAnswer({ connection, schema, table, attempt, load: outcome });
58
59
  }
59
60
  };
61
+
60
62
  client
61
63
  .session(connection)
62
64
  .structure(schema, table)
@@ -64,6 +66,7 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
64
66
  (structure) => settle({ status: 'ready', structure }),
65
67
  (e: unknown) => settle({ status: 'error', message: messageOf(e, t('structure.failed')) })
66
68
  );
69
+
67
70
  return () => {
68
71
  current = false;
69
72
  };
@@ -92,6 +95,7 @@ export function StructureView({ connection, schema, table, toolbarStart, classNa
92
95
 
93
96
  export function StructureTabs({ structure, start }: { structure: TableStructure; start?: ReactNode }) {
94
97
  const { t } = useTranslation('database');
98
+
95
99
  const head =
96
100
  start === undefined || start === null || start === false ? undefined : (
97
101
  <span className="flex items-center gap-4">
@@ -99,6 +103,7 @@ export function StructureTabs({ structure, start }: { structure: TableStructure;
99
103
  <Separator />
100
104
  </span>
101
105
  );
106
+
102
107
  return (
103
108
  <Tabs.Root defaultValue="columns" className="flex min-h-0 grow flex-col">
104
109
  <Tabs.List className="shrink-0 px-3" start={head}>
@@ -151,11 +156,13 @@ function Grid({ heads, children }: { heads: readonly string[]; children: ReactNo
151
156
 
152
157
  function YesNo({ value }: { value: boolean }) {
153
158
  const { t } = useTranslation('database');
159
+
154
160
  return <>{value ? t('structure.yes') : t('structure.no')}</>;
155
161
  }
156
162
 
157
163
  function ColumnsTable({ structure }: { structure: TableStructure }) {
158
164
  const { t } = useTranslation('database');
165
+
159
166
  const heads = [
160
167
  t('structure.column.name'),
161
168
  t('structure.column.type'),
@@ -164,10 +171,13 @@ function ColumnsTable({ structure }: { structure: TableStructure }) {
164
171
  t('structure.column.extra'),
165
172
  t('structure.column.comment')
166
173
  ];
174
+
167
175
  const foreign = new Set(structure.foreignKeys.flatMap((key) => key.columns));
176
+
168
177
  if (structure.columns.length === 0) {
169
178
  return <EmptyState>{t('structure.noColumns')}</EmptyState>;
170
179
  }
180
+
171
181
  return (
172
182
  <Grid heads={heads}>
173
183
  {structure.columns.map((column) => (
@@ -179,6 +189,7 @@ function ColumnsTable({ structure }: { structure: TableStructure }) {
179
189
 
180
190
  function ColumnRow({ column, primary, foreign }: { column: ColumnInfo; primary: boolean; foreign: boolean }) {
181
191
  const { t } = useTranslation('database');
192
+
182
193
  return (
183
194
  <tr>
184
195
  <td className={TD}>
@@ -211,9 +222,11 @@ function ColumnRow({ column, primary, foreign }: { column: ColumnInfo; primary:
211
222
 
212
223
  function IndexesTable({ indexes }: { indexes: readonly IndexInfo[] }) {
213
224
  const { t } = useTranslation('database');
225
+
214
226
  if (indexes.length === 0) {
215
227
  return <EmptyState>{t('structure.noIndexes')}</EmptyState>;
216
228
  }
229
+
217
230
  return (
218
231
  <Grid heads={[t('structure.index.name'), t('structure.index.columns'), t('structure.index.kind')]}>
219
232
  {indexes.map((index) => (
@@ -234,10 +247,13 @@ function IndexesTable({ indexes }: { indexes: readonly IndexInfo[] }) {
234
247
 
235
248
  function ForeignKeysTable({ foreignKeys }: { foreignKeys: readonly ForeignKeyInfo[] }) {
236
249
  const { t } = useTranslation('database');
250
+
237
251
  if (foreignKeys.length === 0) {
238
252
  return <EmptyState>{t('structure.noForeignKeys')}</EmptyState>;
239
253
  }
254
+
240
255
  const action = (value: string | null): string => value ?? t('structure.engineDefault');
256
+
241
257
  return (
242
258
  <Grid
243
259
  heads={[
@@ -263,9 +279,11 @@ function ForeignKeysTable({ foreignKeys }: { foreignKeys: readonly ForeignKeyInf
263
279
 
264
280
  function Ddl({ ddl }: { ddl: string | null }) {
265
281
  const { t } = useTranslation('database');
282
+
266
283
  if (ddl === null) {
267
284
  return <EmptyState>{t('structure.noDdl')}</EmptyState>;
268
285
  }
286
+
269
287
  return (
270
288
  <div className="relative">
271
289
  <Button variant="secondary" size="xs" className="absolute top-2 right-3" onClick={() => copyText(ddl)}>
@@ -59,6 +59,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
59
59
  const [dismissed, setDismissed] = useState(false);
60
60
  const suggestions = useMemo(() => suggest(engine, text, columns), [engine, text, columns]);
61
61
  const open = focused && !dismissed && suggestions.length > 0;
62
+
62
63
  const optionId = (index: number): string => `${listId}-${index}`;
63
64
 
64
65
  useImperativeHandle(inputRef, () => input.current!, []);
@@ -86,18 +87,22 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
86
87
  case 'condition':
87
88
  case 'sql':
88
89
  commit(suggestion.chip);
90
+
89
91
  break;
90
92
  case 'filter':
91
93
  change(filterStartText(suggestion.column));
92
94
  input.current?.focus();
95
+
93
96
  break;
94
97
  case 'sort':
95
98
  onChipsChange(toggleSort(chips, suggestion.column.name));
96
99
  clear();
100
+
97
101
  break;
98
102
  case 'jump':
99
103
  clear();
100
104
  onJumpToColumn(suggestion.column.name);
105
+
101
106
  break;
102
107
  }
103
108
  };
@@ -122,6 +127,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
122
127
  if (event.nativeEvent.isComposing) {
123
128
  return;
124
129
  }
130
+
125
131
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
126
132
  if (suggestions.length > 0) {
127
133
  event.preventDefault();
@@ -131,6 +137,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
131
137
  } else if (event.key === 'Enter') {
132
138
  event.preventDefault();
133
139
  const suggestion = suggestions[Math.min(active, suggestions.length - 1)];
140
+
134
141
  if (suggestion !== undefined) {
135
142
  pick(suggestion);
136
143
  } else if (editing !== null) {
@@ -153,6 +160,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
153
160
 
154
161
  const handleBlur = (): void => {
155
162
  setFocused(false);
163
+
156
164
  if (editing !== null) {
157
165
  clear();
158
166
  }
@@ -207,6 +215,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
207
215
  const column = columnOf(suggestion);
208
216
  const previous = suggestions[index - 1];
209
217
  const startsGroup = column !== null && (previous === undefined || columnOf(previous)?.name !== column.name);
218
+
210
219
  return (
211
220
  <div key={`${suggestion.kind}:${column?.name ?? ''}`} role="presentation">
212
221
  {startsGroup && (
@@ -240,6 +249,7 @@ export function CommandField({ chips, columns, engine, onChipsChange, onJumpToCo
240
249
 
241
250
  function SuggestionLabel({ suggestion }: { suggestion: Suggestion }) {
242
251
  const { t } = useTranslation('database');
252
+
243
253
  switch (suggestion.kind) {
244
254
  case 'condition':
245
255
  return (
@@ -280,6 +290,7 @@ interface ChipViewProps {
280
290
  /* A filter opens its text for editing, a sort flips its direction, and a raw ORDER BY is only shown. */
281
291
  function ChipView({ chip, onActivate, onRemove }: ChipViewProps) {
282
292
  const { t } = useTranslation('database');
293
+
283
294
  return (
284
295
  <span className="inline-flex h-5 max-w-72 min-w-0 shrink-0 items-center rounded-md bg-text/10 font-mono text-code text-text">
285
296
  {chip.kind === 'filter' && (
@@ -54,10 +54,13 @@ export function ImportForm({
54
54
  }: ImportFormProps) {
55
55
  const { t } = useTranslation('database');
56
56
  const items = [{ value: SKIP, label: t('table.import.skip') }, ...tableColumns.map((column, index) => ({ value: String(index), label: column.name }))];
57
+
57
58
  const valueOf = (name: string | null): string => {
58
59
  const index = name === null ? -1 : tableColumns.findIndex((column) => column.name === name);
60
+
59
61
  return index < 0 ? SKIP : String(index);
60
62
  };
63
+
61
64
  const canImport = sample !== null && hasMapping(mapping) && !busy;
62
65
 
63
66
  return (
@@ -156,6 +159,7 @@ export interface ImportDialogProps extends ImportFormProps {
156
159
  /* The import of a file into the table: pick how the columns map, look at the rows, confirm. */
157
160
  export function ImportDialog({ open, onOpenChange, ...form }: ImportDialogProps) {
158
161
  const { t } = useTranslation('database');
162
+
159
163
  return (
160
164
  <Dialog.Root open={open} onOpenChange={(next) => !form.busy && onOpenChange(next)}>
161
165
  <Dialog.Popup className="flex max-h-[calc(100vh-96px)] w-[720px] max-w-[calc(100vw-48px)] flex-col p-5">
@@ -66,6 +66,7 @@ function SelectionSummary({ selection }: { selection: SelectionFigures }) {
66
66
  const { columns, aggregates } = selection;
67
67
  const { count, numeric } = aggregates;
68
68
  const notation = useNumberNotation();
69
+
69
70
  const figure = (label: string, value: string) => (
70
71
  <span className="shrink-0">
71
72
  <span className="text-text-faint">{label}</span> <span className="text-text">{value}</span>
@@ -18,6 +18,7 @@ import {
18
18
  import { useTranslation } from 'react-i18next';
19
19
  import { Button, ButtonGroup, Icon, IconButton, Menu, Separator } from '@adecore/ui';
20
20
  import { formatNumber } from '@adecore/ui/format';
21
+ import { EXPORT_FORMATS } from '../export-formats.ts';
21
22
  import type { Engine, FileFormat } from '../protocol/index.ts';
22
23
  import { CommandField } from './CommandField.tsx';
23
24
  import type { Chip, CommandColumn } from './command-field.ts';
@@ -32,8 +33,6 @@ export interface TransferMenu {
32
33
  onImport?(): void;
33
34
  }
34
35
 
35
- const EXPORT_FORMATS: readonly FileFormat[] = ['csv', 'tsv', 'json', 'sql'];
36
-
37
36
  export interface TableToolbarProps {
38
37
  /* The app's own content before the command field, such as where the table is, with a separator after it and after the field. */
39
38
  start?: ReactNode;
@@ -110,6 +109,7 @@ export function TableToolbar({
110
109
  const { t } = useTranslation('database');
111
110
  const readOnly = readOnlyReason !== null;
112
111
  const hasStart = start !== undefined && start !== null && start !== false;
112
+
113
113
  const tooltipOf = (label: string): string => (readOnly ? t('table.unavailable', { action: label, reason: readOnlyReason }) : label);
114
114
 
115
115
  return (