@adecore/database 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +251 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -1,7 +1,19 @@
1
1
  import type { ExplorerSelection } from '../actions.ts';
2
2
  import type { Connection, TableRef } from '../client/types.ts';
3
- import type { ColumnInfo, Engine, SchemaInfo, TableInfo, TableKind, TableStructure } from '../protocol/index.ts';
3
+ import type {
4
+ CheckInfo,
5
+ ColumnInfo,
6
+ Engine,
7
+ ForeignKeyInfo,
8
+ IndexInfo,
9
+ SchemaInfo,
10
+ TableInfo,
11
+ TableKind,
12
+ TableStructure,
13
+ TriggerInfo
14
+ } from '../protocol/index.ts';
4
15
  import { qualifiedName } from '../sql.ts';
16
+ import type { ExplorerFolder, ExplorerItem } from './folders.ts';
5
17
 
6
18
  /* Where a lazy list stands: not asked for yet, on its way, failed, or here. */
7
19
  export type Load<T> =
@@ -26,7 +38,12 @@ export const connectionKey = (connectionId: string): string => `c:${connectionId
26
38
  export const schemaKey = (connectionId: string, schema: string): string => `s:${connectionId}${SEPARATOR}${schema}`;
27
39
  export const folderKey = (connectionId: string, schema: string, group: TableKind): string => `f:${connectionId}${SEPARATOR}${schema}${SEPARATOR}${group}`;
28
40
  export const tableKey = (ref: TableRef): string => `t:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}`;
29
- export const isFolderKey = (key: string): boolean => key.startsWith('f:');
41
+ /* The folders of a table, in the order they come under it. */
42
+ export const PARTS = ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'] as const;
43
+ export type TablePart = (typeof PARTS)[number];
44
+ export const partKey = (ref: TableRef, part: TablePart): string => `p:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}${SEPARATOR}${part}`;
45
+ /* A folder of a schema, and the columns of a table, are open until the person closes them; everything else is closed until they open it. */
46
+ export const startsOpen = (key: string): boolean => key.startsWith('f:') || (key.startsWith('p:') && key.endsWith(`${SEPARATOR}columns`));
30
47
  /* The connection a key of a connection, schema, folder or table belongs to. */
31
48
  export const connectionOfKey = (key: string): string => {
32
49
  const rest = key.slice(2);
@@ -34,6 +51,10 @@ export const connectionOfKey = (key: string): string => {
34
51
  return end === -1 ? rest : rest.slice(0, end);
35
52
  };
36
53
  export const columnKey = (ref: TableRef, column: string): string => `${tableKey(ref)}${SEPARATOR}${column}`;
54
+ /* A folder of the app's own under a connection, and an item in it. */
55
+ export const appFolderKey = (connectionId: string, folder: string): string => `x:${connectionId}${SEPARATOR}${folder}`;
56
+ const appItemKey = (connectionId: string, folder: string, item: string): string => `${appFolderKey(connectionId, folder)}${SEPARATOR}${item}`;
57
+ const entryKey = (ref: TableRef, part: TablePart, index: number): string => `${partKey(ref, part)}${SEPARATOR}${index}`;
37
58
 
38
59
  export const loadKey = (target: LoadTarget): string => {
39
60
  if (target.schema === undefined) {
@@ -102,6 +123,30 @@ export interface ColumnRow extends RowBase {
102
123
  readonly foreignKey: boolean;
103
124
  }
104
125
 
126
+ /* One of the folders a table opens into, which exists only when it holds something. */
127
+ export interface PartRow extends RowBase {
128
+ readonly kind: 'part';
129
+ readonly ref: TableRef;
130
+ readonly part: TablePart;
131
+ readonly expanded: boolean;
132
+ readonly count: number;
133
+ }
134
+
135
+ /* What a folder of a table holds besides its columns. */
136
+ export type TableEntry =
137
+ | { readonly type: 'key'; readonly name: string | null; readonly columns: readonly string[]; readonly primary: boolean }
138
+ | { readonly type: 'foreignKey'; readonly foreignKey: ForeignKeyInfo }
139
+ | { readonly type: 'index'; readonly index: IndexInfo }
140
+ | { readonly type: 'check'; readonly check: CheckInfo }
141
+ | { readonly type: 'trigger'; readonly trigger: TriggerInfo };
142
+
143
+ export interface EntryRow extends RowBase {
144
+ readonly kind: 'entry';
145
+ /* The table the entry belongs to. */
146
+ readonly ref: TableRef;
147
+ readonly entry: TableEntry;
148
+ }
149
+
105
150
  export interface LoadingRow extends RowBase {
106
151
  readonly kind: 'loading';
107
152
  }
@@ -112,21 +157,59 @@ export interface ErrorRow extends RowBase {
112
157
  readonly retry: LoadTarget;
113
158
  }
114
159
 
160
+ /* A folder of the app's own under a connection. */
161
+ export interface AppFolderRow extends RowBase {
162
+ readonly kind: 'appFolder';
163
+ readonly connection: Connection;
164
+ readonly folder: ExplorerFolder;
165
+ readonly expanded: boolean;
166
+ /* Its items, or those that match the filter. */
167
+ readonly count: number;
168
+ }
169
+
170
+ export interface AppItemRow extends RowBase {
171
+ readonly kind: 'appItem';
172
+ readonly connection: Connection;
173
+ readonly item: ExplorerItem;
174
+ }
175
+
115
176
  /* A connection without schemas, a schema without tables or a table without columns. */
116
177
  export interface EmptyRow extends RowBase {
117
178
  readonly kind: 'empty';
118
179
  readonly of: 'schemas' | 'tables' | 'columns';
119
180
  }
120
181
 
121
- export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | ColumnRow | LoadingRow | ErrorRow | EmptyRow;
122
-
123
- export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow;
182
+ export type TreeRow =
183
+ | ConnectionRow
184
+ | SchemaRow
185
+ | FolderRow
186
+ | TableRow
187
+ | PartRow
188
+ | ColumnRow
189
+ | EntryRow
190
+ | AppFolderRow
191
+ | AppItemRow
192
+ | LoadingRow
193
+ | ErrorRow
194
+ | EmptyRow;
195
+
196
+ export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow | PartRow | AppFolderRow;
124
197
 
125
198
  export const isExpandable = (row: TreeRow): row is ExpandableRow =>
126
- row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table';
199
+ row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table' || row.kind === 'part' || row.kind === 'appFolder';
127
200
 
128
201
  /* The table a row stands for, when it stands for one. */
129
- export const tableOf = (row: TreeRow): TableRef | null => (row.kind === 'table' || row.kind === 'column' ? row.ref : null);
202
+ export const tableOf = (row: TreeRow): TableRef | null => (row.kind === 'table' || row.kind === 'column' || row.kind === 'entry' ? row.ref : null);
203
+
204
+ /* The table activating a row opens: its own, or for a foreign key the table it references. */
205
+ export const openedTableOf = (row: TreeRow): TableRef | null =>
206
+ row.kind === 'entry' && row.entry.type === 'foreignKey'
207
+ ? { connectionId: row.ref.connectionId, schema: row.entry.foreignKey.referencedSchema, table: row.entry.foreignKey.referencedTable }
208
+ : tableOf(row);
209
+
210
+ /* Whether a list of tables holds `name` as a table or a view; `undefined` while the list has not loaded or lacks it. */
211
+ export const tableKindIn = (tables: Load<readonly TableInfo[]>, name: string): TableKind | undefined =>
212
+ tables.status === 'ready' ? tables.value.find((table) => table.name === name)?.kind : undefined;
130
213
 
131
214
  /* What picking a row selects: its connection, its schema, or its table (a column stands for its table). Status rows select nothing. */
132
215
  export const selectionOf = (row: TreeRow): ExplorerSelection | null => {
@@ -137,7 +220,9 @@ export const selectionOf = (row: TreeRow): ExplorerSelection | null => {
137
220
  case 'folder':
138
221
  return { connectionId: row.connection.id, schema: row.schema };
139
222
  case 'table':
223
+ case 'part':
140
224
  case 'column':
225
+ case 'entry':
141
226
  return { connectionId: row.ref.connectionId, schema: row.ref.schema, table: row.ref.table };
142
227
  default:
143
228
  return null;
@@ -154,9 +239,14 @@ export const selectionKey = (selection: ExplorerSelection): string => {
154
239
  : tableKey({ connectionId: selection.connectionId, schema: selection.schema, table: selection.table });
155
240
  };
156
241
 
157
- /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, and none of the rows that only report. */
242
+ /* Whether a row has a menu: the rows of a connection, a schema, a table or a column, an item the app gave one, and none of the rows that only report. */
158
243
  export const hasMenu = (row: TreeRow): boolean =>
159
- row.kind === 'connection' || row.kind === 'schema' || row.kind === 'folder' || row.kind === 'table' || row.kind === 'column';
244
+ row.kind === 'connection' ||
245
+ row.kind === 'schema' ||
246
+ row.kind === 'folder' ||
247
+ row.kind === 'table' ||
248
+ row.kind === 'column' ||
249
+ (row.kind === 'appItem' && row.item.menu !== undefined);
160
250
 
161
251
  /* Only the rows a selection can point at are drawn selected; a folder or a column leaves that to the row above it. */
162
252
  export const isSelectable = (row: TreeRow): boolean => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'table';
@@ -175,6 +265,8 @@ export interface TreeInput {
175
265
  tables(connectionId: string, schema: string): Load<readonly TableInfo[]>;
176
266
  structures(connectionId: string, schema: string, table: string): Load<TableStructure>;
177
267
  versions(connectionId: string): string | null;
268
+ /* The app's own folders under a connection. */
269
+ folders?(connectionId: string): readonly ExplorerFolder[];
178
270
  }
179
271
 
180
272
  /* The schemas a list shows. */
@@ -207,6 +299,23 @@ const statusDraft = (row: StatusRow, level: number, parent: string, focusable: b
207
299
 
208
300
  const matches = (name: string, query: string): boolean => name.toLowerCase().includes(query);
209
301
 
302
+ /* What each folder of a table besides its columns holds: the primary key and the unique keys, the foreign keys, the other indexes, the checks and the triggers. */
303
+ export const tableEntries = (structure: TableStructure): Readonly<Record<Exclude<TablePart, 'columns'>, readonly TableEntry[]>> => {
304
+ const primaryIndex = structure.indexes.find((index) => index.primary);
305
+ const primary: TableEntry[] =
306
+ structure.primaryKey.length === 0 ? [] : [{ type: 'key', name: primaryIndex?.name ?? null, columns: structure.primaryKey, primary: true }];
307
+ const unique = structure.indexes
308
+ .filter((index) => index.unique && !index.primary)
309
+ .map((index): TableEntry => ({ type: 'key', name: index.name, columns: index.columns, primary: false }));
310
+ return {
311
+ keys: [...primary, ...unique],
312
+ foreignKeys: structure.foreignKeys.map((foreignKey) => ({ type: 'foreignKey', foreignKey })),
313
+ indexes: structure.indexes.filter((index) => !index.primary).map((index) => ({ type: 'index', index })),
314
+ checks: (structure.checks ?? []).map((check) => ({ type: 'check', check })),
315
+ triggers: (structure.triggers ?? []).map((trigger) => ({ type: 'trigger', trigger }))
316
+ };
317
+ };
318
+
210
319
  /*
211
320
  * The rows a tree shows, top to bottom. With a filter only the loaded tables that match stay, under
212
321
  * their folder, schema and connection, which then count as open whatever the person left them at.
@@ -215,22 +324,10 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
215
324
  const query = input.filter.trim().toLowerCase();
216
325
  const filtering = query !== '';
217
326
 
218
- const columnDrafts = (ref: TableRef, level: number, parent: string): Draft[] => {
219
- const load = input.structures(ref.connectionId, ref.schema, ref.table);
220
- if (load.status !== 'ready') {
221
- const row: StatusRow =
222
- load.status === 'error'
223
- ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
224
- : { kind: 'loading' };
225
- return [statusDraft(row, level, parent, load.status === 'error')];
226
- }
227
- const { columns, primaryKey, foreignKeys } = load.value;
228
- if (columns.length === 0) {
229
- return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
230
- }
231
- const primary = new Set(primaryKey);
232
- const foreign = new Set(foreignKeys.flatMap((foreignKey) => foreignKey.columns));
233
- return columns.map((column): Draft => ({
327
+ const columnRows = (ref: TableRef, structure: TableStructure, level: number, parent: string): Draft[] => {
328
+ const primary = new Set(structure.primaryKey);
329
+ const foreign = new Set(structure.foreignKeys.flatMap((foreignKey) => foreignKey.columns));
330
+ return structure.columns.map((column): Draft => ({
234
331
  make: (place) => ({
235
332
  kind: 'column',
236
333
  key: columnKey(ref, column.name),
@@ -247,12 +344,62 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
247
344
  }));
248
345
  };
249
346
 
347
+ /* The folders a table opens into, each only when it holds something. A view has only its columns, straight under it. */
348
+ const tableChildren = (ref: TableRef, kind: TableKind, level: number, parent: string): Draft[] => {
349
+ const load = input.structures(ref.connectionId, ref.schema, ref.table);
350
+ if (load.status !== 'ready') {
351
+ const row: StatusRow =
352
+ load.status === 'error'
353
+ ? { kind: 'error', message: load.message, retry: { connectionId: ref.connectionId, schema: ref.schema, table: ref.table } }
354
+ : { kind: 'loading' };
355
+ return [statusDraft(row, level, parent, load.status === 'error')];
356
+ }
357
+ const structure = load.value;
358
+ if (structure.columns.length === 0) {
359
+ return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
360
+ }
361
+ if (kind === 'view') {
362
+ return columnRows(ref, structure, level, parent);
363
+ }
364
+ const entries = tableEntries(structure);
365
+ return PARTS.flatMap((part): Draft[] => {
366
+ const count = part === 'columns' ? structure.columns.length : entries[part].length;
367
+ if (count === 0) {
368
+ return [];
369
+ }
370
+ const key = partKey(ref, part);
371
+ const open = startsOpen(key) ? !input.collapsed.has(key) : input.expanded.has(key);
372
+ const below = (): Draft[] =>
373
+ part === 'columns'
374
+ ? columnRows(ref, structure, level + 1, key)
375
+ : entries[part].map((entry, index): Draft => ({
376
+ make: (place) => ({
377
+ kind: 'entry',
378
+ key: entryKey(ref, part, index),
379
+ level: level + 1,
380
+ parent: key,
381
+ focusable: true,
382
+ ref,
383
+ entry,
384
+ ...place
385
+ }),
386
+ below: []
387
+ }));
388
+ return [
389
+ {
390
+ make: (place) => ({ kind: 'part', key, level, parent, focusable: true, ref, part, expanded: open, count, ...place }),
391
+ below: open ? settle(below()) : []
392
+ }
393
+ ];
394
+ });
395
+ };
396
+
250
397
  const tableDraft = (ref: TableRef, table: TableInfo, level: number, parent: string): Draft => {
251
398
  const key = tableKey(ref);
252
399
  const open = input.expanded.has(key);
253
400
  return {
254
401
  make: (place) => ({ kind: 'table', key, level, parent, focusable: true, ref, table, expanded: open, ...place }),
255
- below: open ? settle(columnDrafts(ref, level + 1, key)) : []
402
+ below: open ? settle(tableChildren(ref, table.kind, level + 1, key)) : []
256
403
  };
257
404
  };
258
405
 
@@ -314,10 +461,52 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
314
461
  };
315
462
  };
316
463
 
464
+ /* The app's folders under a connection, each while it holds items; with a filter only the items that match, in folders that count as open. */
465
+ const appFolderDrafts = (connection: Connection, level: number, parent: string): Draft[] =>
466
+ (input.folders?.(connection.id) ?? []).flatMap((folder): Draft[] => {
467
+ const items = filtering ? folder.items.filter((item) => matches(item.label, query)) : folder.items;
468
+ if (items.length === 0) {
469
+ return [];
470
+ }
471
+ const key = appFolderKey(connection.id, folder.id);
472
+ const open = filtering || input.expanded.has(key);
473
+ const rows = items.map((item): Draft => ({
474
+ make: (place) => ({
475
+ kind: 'appItem',
476
+ key: appItemKey(connection.id, folder.id, item.id),
477
+ level: level + 1,
478
+ parent: key,
479
+ focusable: true,
480
+ connection,
481
+ item,
482
+ ...place
483
+ }),
484
+ below: []
485
+ }));
486
+ return [
487
+ {
488
+ make: (place) => ({
489
+ kind: 'appFolder',
490
+ key,
491
+ level,
492
+ parent,
493
+ focusable: true,
494
+ connection,
495
+ folder,
496
+ expanded: open,
497
+ count: items.length,
498
+ ...place
499
+ }),
500
+ below: open ? settle(rows) : []
501
+ }
502
+ ];
503
+ });
504
+
317
505
  const connectionDraft = (connection: Connection): Draft | null => {
318
506
  const key = connectionKey(connection.id);
319
507
  const load = input.schemas(connection.id);
320
- const open = filtering ? load.status === 'ready' : input.expanded.has(key);
508
+ const appFolders = appFolderDrafts(connection, 2, key);
509
+ const open = filtering ? load.status === 'ready' || appFolders.length > 0 : input.expanded.has(key);
321
510
  let children: Draft[] = [];
322
511
  let schemaCount: ConnectionRow['schemaCount'] = null;
323
512
  if (load.status === 'ready') {
@@ -333,11 +522,13 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
333
522
  } else {
334
523
  children = visible.flatMap((entry) => schemaDraft(connection, entry.name, key) ?? []);
335
524
  }
336
- } else if (open) {
525
+ } else if (open && !filtering) {
337
526
  const row: StatusRow =
338
527
  load.status === 'error' ? { kind: 'error', message: load.message, retry: { connectionId: connection.id } } : { kind: 'loading' };
339
528
  children = [statusDraft(row, 2, key, load.status === 'error')];
340
529
  }
530
+ // Behind a status row too: what the app keeps of a connection does not wait on its server.
531
+ children = [...children, ...(open ? appFolders : [])];
341
532
  if (filtering && children.length === 0) {
342
533
  return null;
343
534
  }
@@ -1,5 +1,7 @@
1
1
  import { useEffect, useRef, type KeyboardEvent } from 'react';
2
+ import clsx from 'clsx';
2
3
  import { Input } from '@adecore/ui';
4
+ import { CODE_TEXT } from '../code-text.ts';
3
5
 
4
6
  export type EditMove = 'next' | 'previous';
5
7
 
@@ -40,11 +42,10 @@ export function CellEditor({ value, label, onValueChange, onCommit, onCancel }:
40
42
  return (
41
43
  <Input
42
44
  ref={input}
43
- mono
44
45
  size="sm"
45
46
  aria-label={label}
46
47
  value={value}
47
- className="absolute inset-0 z-10 h-full min-w-0 rounded-none px-3"
48
+ className={clsx(CODE_TEXT, 'absolute inset-0 z-10 h-full min-w-0 rounded-none px-3')}
48
49
  onChange={(event) => onValueChange(event.target.value)}
49
50
  onKeyDown={handleKeyDown}
50
51
  onBlur={() => onCommit()}