@agent-native/core-corpus 0.213.1 → 0.214.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.
- package/corpus/README.md +1 -1
- package/corpus/templates/community/qr/package.json +1 -1
- package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
- package/corpus/templates/content/CHANGELOG.md +5 -0
- package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
- package/corpus/templates/content/actions/_property-utils.ts +3 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
- package/corpus/templates/content/actions/view-screen.ts +12 -0
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +91 -16
- package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
- package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
- package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
- package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
- package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
- package/corpus/templates/content/app/global.css +13 -0
- package/corpus/templates/content/app/i18n-data.ts +32 -0
- package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
- package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
- package/corpus/templates/content/shared/api.ts +18 -0
- package/corpus/templates/content/shared/database-item-layout.ts +591 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -88,7 +88,7 @@ stable IDs identify edits. Sparse patches preserve omitted fields, and empty
|
|
|
88
88
|
lists explicitly clear supported settings.
|
|
89
89
|
|
|
90
90
|
Ordinary setup supports stored property types, additive options, metadata edits,
|
|
91
|
-
the existing text natural key, and
|
|
91
|
+
the existing text natural key, table presentation, and list row layouts. It refuses destructive
|
|
92
92
|
type/option changes, source-managed schema edits, and relationship/computed-field
|
|
93
93
|
authoring. Source status is readable, but this surface does not define joins,
|
|
94
94
|
row unions, source bindings, or write-mode changes. Discovery of a source-backed
|
|
@@ -230,6 +230,20 @@ all fields for the current view, and it includes a New property control for
|
|
|
230
230
|
adding fields without returning to the table header. The New property picker
|
|
231
231
|
supports searching property types by label or machine name.
|
|
232
232
|
|
|
233
|
+
List views can save an item layout (`itemLayout`) that decides what each row
|
|
234
|
+
shows: a start slot (page icon, a checkbox property, a status property, or
|
|
235
|
+
nothing), the title weight, line 2 (a text, URL, or non-body Blocks property,
|
|
236
|
+
clamped to 1-3 lines), and ordered property IDs for line 3 and the end of the
|
|
237
|
+
row; end values move under the title on narrow screens and empty values take
|
|
238
|
+
no space. Ticking a row's checkbox writes the property through
|
|
239
|
+
`set-document-property` and strikes the title through without moving the row.
|
|
240
|
+
A view without `itemLayout` keeps the default rows. Agents update it with an
|
|
241
|
+
`update-content-database-view` `update` patch on the list view: pass
|
|
242
|
+
`itemLayoutPreset` (`checklist`, `reminders`, `tracker`, `digest`, or
|
|
243
|
+
`default`), sparse `itemLayout` slots (omitted slots keep their value), or
|
|
244
|
+
`itemLayout: null` to restore the default rows. List views accept only name,
|
|
245
|
+
icon, and these layout fields on this path.
|
|
246
|
+
|
|
233
247
|
In unconstrained table views, row drag handles can reorder collection item
|
|
234
248
|
pages through `move-database-item`; pass both `databaseId` and `itemId` so the
|
|
235
249
|
membership target is exact, and clear search, sort, and filters before manual
|
|
@@ -11,6 +11,10 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
11
11
|
|
|
12
12
|
## 2026-10-19
|
|
13
13
|
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- List rows can be customized: start from a preset like **Checklist**, then choose what shows before, under, and after each title
|
|
17
|
+
|
|
14
18
|
### Improved
|
|
15
19
|
|
|
16
20
|
- You can comment on text in any Blocks field tab; each tab shows its open comments, and the Comments panel groups and filters them by tab
|
|
@@ -18,6 +22,7 @@ time from the command menu (Cmd+K → "What's new").
|
|
|
18
22
|
|
|
19
23
|
### Fixed
|
|
20
24
|
|
|
25
|
+
- List views no longer run off the side of the screen on phones
|
|
21
26
|
- Edits made from a block's menu or a table's row and column menus, such as deleting a table, adding a row or turning off the header row, are now saved instead of coming back after a reload
|
|
22
27
|
- Starting a comment puts the cursor in the comment box, ready to type.
|
|
23
28
|
- Table row and column options now appear for the row or column under the pointer, Escape selects the cell before leaving the editor, wide tables keep their add-column control on screen on phones, and a tap near a table's edge on a touch screen edits the cell there instead of adding a row or column
|
|
@@ -8,11 +8,30 @@ import { syncPrivateViewIconReferences } from "../server/lib/private-icon-refere
|
|
|
8
8
|
import {
|
|
9
9
|
CONTENT_DATABASE_FILTER_OPERATORS,
|
|
10
10
|
type ContentDatabaseFilter,
|
|
11
|
+
type ContentDatabaseItemLayout,
|
|
11
12
|
type ContentDatabaseView,
|
|
12
13
|
type ContentDatabaseViewConfig,
|
|
13
14
|
} from "../shared/api.js";
|
|
15
|
+
import {
|
|
16
|
+
CONTENT_DATABASE_ITEM_LAYOUT_PRESET_IDS,
|
|
17
|
+
CONTENT_DATABASE_ITEM_LEADING_KINDS,
|
|
18
|
+
CONTENT_DATABASE_ITEM_TITLE_WEIGHTS,
|
|
19
|
+
MAX_ITEM_LAYOUT_META_PROPERTIES,
|
|
20
|
+
contentDatabaseItemLayoutPresets,
|
|
21
|
+
contentDatabaseItemLayoutReferenceErrors,
|
|
22
|
+
dropDeletedContentDatabaseItemLayoutReferences,
|
|
23
|
+
itemLayoutLeadingCandidates,
|
|
24
|
+
normalizeContentDatabaseItemLayout,
|
|
25
|
+
pruneContentDatabaseItemLayout,
|
|
26
|
+
savableContentDatabaseItemLayout,
|
|
27
|
+
startingContentDatabaseItemLayout,
|
|
28
|
+
} from "../shared/database-item-layout.js";
|
|
14
29
|
import { contentDatabaseFilterOperatorsForType } from "../shared/database-query.js";
|
|
15
|
-
import
|
|
30
|
+
import {
|
|
31
|
+
normalizePropertyVisibility,
|
|
32
|
+
parsePropertyOptions,
|
|
33
|
+
type DocumentPropertyType,
|
|
34
|
+
} from "../shared/properties.js";
|
|
16
35
|
import {
|
|
17
36
|
runDatabaseSetupMutation,
|
|
18
37
|
setupError,
|
|
@@ -76,6 +95,77 @@ const tablePatchFields = {
|
|
|
76
95
|
openPagesIn: z.enum(["preview", "full_page"]).optional(),
|
|
77
96
|
};
|
|
78
97
|
|
|
98
|
+
const itemLayoutPropertyIdSchema = z.string().trim().min(1).max(200);
|
|
99
|
+
const itemLayoutLinesSchema = z.union([
|
|
100
|
+
z.literal(1),
|
|
101
|
+
z.literal(2),
|
|
102
|
+
z.literal(3),
|
|
103
|
+
]);
|
|
104
|
+
const itemLayoutMetaSchema = z
|
|
105
|
+
.array(itemLayoutPropertyIdSchema)
|
|
106
|
+
.max(MAX_ITEM_LAYOUT_META_PROPERTIES);
|
|
107
|
+
|
|
108
|
+
export const contentDatabaseItemLayoutSchema = z
|
|
109
|
+
.object({
|
|
110
|
+
leading: z
|
|
111
|
+
.object({
|
|
112
|
+
kind: z.enum(CONTENT_DATABASE_ITEM_LEADING_KINDS),
|
|
113
|
+
propertyId: itemLayoutPropertyIdSchema.nullable(),
|
|
114
|
+
})
|
|
115
|
+
.strict(),
|
|
116
|
+
titleWeight: z.enum(CONTENT_DATABASE_ITEM_TITLE_WEIGHTS),
|
|
117
|
+
line2: z
|
|
118
|
+
.object({
|
|
119
|
+
propertyId: itemLayoutPropertyIdSchema.nullable(),
|
|
120
|
+
lines: itemLayoutLinesSchema,
|
|
121
|
+
})
|
|
122
|
+
.strict(),
|
|
123
|
+
line3: itemLayoutMetaSchema,
|
|
124
|
+
trailing: itemLayoutMetaSchema,
|
|
125
|
+
})
|
|
126
|
+
.strict();
|
|
127
|
+
|
|
128
|
+
const itemLayoutPatchSchema = z
|
|
129
|
+
.object({
|
|
130
|
+
leading: z
|
|
131
|
+
.object({
|
|
132
|
+
kind: z.enum(CONTENT_DATABASE_ITEM_LEADING_KINDS),
|
|
133
|
+
propertyId: itemLayoutPropertyIdSchema.nullable().optional(),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.optional(),
|
|
137
|
+
titleWeight: z.enum(CONTENT_DATABASE_ITEM_TITLE_WEIGHTS).optional(),
|
|
138
|
+
line2: z
|
|
139
|
+
.object({
|
|
140
|
+
propertyId: itemLayoutPropertyIdSchema.nullable().optional(),
|
|
141
|
+
lines: itemLayoutLinesSchema.optional(),
|
|
142
|
+
})
|
|
143
|
+
.strict()
|
|
144
|
+
.optional(),
|
|
145
|
+
line3: itemLayoutMetaSchema.optional(),
|
|
146
|
+
trailing: itemLayoutMetaSchema.optional(),
|
|
147
|
+
})
|
|
148
|
+
.strict()
|
|
149
|
+
.describe(
|
|
150
|
+
"List views only. Slots to change in each row; omitted slots keep their saved value. leading.kind checkbox or status uses the first property of that type unless propertyId names one. line2 shows a text, URL, or Blocks property; line3 and trailing list property IDs in order. Trailing values move under the title on narrow screens. Pass null to restore the default rows.",
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const listPatchFields = {
|
|
154
|
+
itemLayout: itemLayoutPatchSchema.nullable().optional(),
|
|
155
|
+
itemLayoutPreset: z
|
|
156
|
+
.enum(CONTENT_DATABASE_ITEM_LAYOUT_PRESET_IDS)
|
|
157
|
+
.optional()
|
|
158
|
+
.describe(
|
|
159
|
+
"List views only. Starts the rows from a preset before itemLayout applies: checklist (checkbox, bold title, two lines of text), reminders (checkbox, one line of text, date, person and tag under it), tracker (status dot, tag, person and date on the right), digest (page icon, bold title, three lines of text, details under it), or default.",
|
|
160
|
+
),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const LIST_VIEW_PATCH_KEYS = new Set([
|
|
164
|
+
"name",
|
|
165
|
+
"icon",
|
|
166
|
+
...Object.keys(listPatchFields),
|
|
167
|
+
]);
|
|
168
|
+
|
|
79
169
|
const boundedTablePatchSchema = z
|
|
80
170
|
.object(tablePatchFields)
|
|
81
171
|
.strict()
|
|
@@ -91,11 +181,12 @@ const boundedTablePatchSchema = z
|
|
|
91
181
|
}
|
|
92
182
|
});
|
|
93
183
|
|
|
94
|
-
const
|
|
184
|
+
const updateViewPatchSchema = z
|
|
95
185
|
.object({
|
|
96
186
|
name: z.string().trim().min(1).max(500).optional(),
|
|
97
187
|
icon: iconValueSchema.nullable().optional(),
|
|
98
188
|
...tablePatchFields,
|
|
189
|
+
...listPatchFields,
|
|
99
190
|
})
|
|
100
191
|
.strict()
|
|
101
192
|
.refine(
|
|
@@ -125,7 +216,7 @@ export const updateContentDatabaseViewAgentSchema = z.discriminatedUnion(
|
|
|
125
216
|
expectedConfigurationRevision: z.string().min(1),
|
|
126
217
|
operation: z.literal("update"),
|
|
127
218
|
viewId: z.string().trim().min(1).max(200),
|
|
128
|
-
patch:
|
|
219
|
+
patch: updateViewPatchSchema,
|
|
129
220
|
})
|
|
130
221
|
.strict(),
|
|
131
222
|
],
|
|
@@ -147,7 +238,7 @@ function uniqueReferences(values: readonly string[], field: string) {
|
|
|
147
238
|
|
|
148
239
|
function validateTablePatch(
|
|
149
240
|
patch:
|
|
150
|
-
| z.infer<typeof
|
|
241
|
+
| z.infer<typeof updateViewPatchSchema>
|
|
151
242
|
| z.infer<typeof boundedTablePatchSchema>,
|
|
152
243
|
definitions: readonly (typeof schema.documentPropertyDefinitions.$inferSelect)[],
|
|
153
244
|
) {
|
|
@@ -228,6 +319,174 @@ function validateTablePatch(
|
|
|
228
319
|
}
|
|
229
320
|
}
|
|
230
321
|
|
|
322
|
+
type PropertyDefinitionRow =
|
|
323
|
+
typeof schema.documentPropertyDefinitions.$inferSelect;
|
|
324
|
+
|
|
325
|
+
function itemLayoutProperties(definitions: readonly PropertyDefinitionRow[]) {
|
|
326
|
+
return definitions.map((definition) => ({
|
|
327
|
+
definition: {
|
|
328
|
+
id: definition.id,
|
|
329
|
+
type: definition.type as DocumentPropertyType,
|
|
330
|
+
options: parsePropertyOptions(definition.optionsJson),
|
|
331
|
+
visibility: normalizePropertyVisibility(definition.visibility),
|
|
332
|
+
},
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Checks the layout a full-view save carries against the view's saved one:
|
|
338
|
+
* new slot references the rows can't show are dropped, saved ones stay.
|
|
339
|
+
*/
|
|
340
|
+
export function withSavableItemLayout<
|
|
341
|
+
T extends { itemLayout?: unknown; hiddenPropertyIds?: unknown },
|
|
342
|
+
>(
|
|
343
|
+
view: T,
|
|
344
|
+
savedLayout: unknown,
|
|
345
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
346
|
+
): T {
|
|
347
|
+
const layout = normalizeContentDatabaseItemLayout(view.itemLayout);
|
|
348
|
+
return layout
|
|
349
|
+
? {
|
|
350
|
+
...view,
|
|
351
|
+
itemLayout: savableContentDatabaseItemLayout(
|
|
352
|
+
layout,
|
|
353
|
+
normalizeContentDatabaseItemLayout(savedLayout),
|
|
354
|
+
itemLayoutProperties(definitions),
|
|
355
|
+
Array.isArray(view.hiddenPropertyIds) ? view.hiddenPropertyIds : [],
|
|
356
|
+
),
|
|
357
|
+
}
|
|
358
|
+
: view;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A view's properties in the order its UI lists them, so presets and slot
|
|
363
|
+
* defaults pick the same first checkbox, status or text field the UI does.
|
|
364
|
+
*/
|
|
365
|
+
function viewOrderedDefinitions(
|
|
366
|
+
view: ContentDatabaseView,
|
|
367
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
368
|
+
) {
|
|
369
|
+
const order = view.propertyOrderIds ?? [];
|
|
370
|
+
const rank = (id: string) => {
|
|
371
|
+
const index = order.indexOf(id);
|
|
372
|
+
return index < 0 ? order.length : index;
|
|
373
|
+
};
|
|
374
|
+
return [...definitions].sort(
|
|
375
|
+
(a, b) => rank(a.id) - rank(b.id) || a.position - b.position,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The default rows' properties as the view's settings define them: those not
|
|
381
|
+
* hidden, in its order. The rows also leave out a "hide when empty" property
|
|
382
|
+
* that none of the loaded rows fills; the server doesn't see which rows the
|
|
383
|
+
* view loads, so it keeps those, and their blank values take no space.
|
|
384
|
+
*/
|
|
385
|
+
function defaultListRowProperties(
|
|
386
|
+
view: ContentDatabaseView,
|
|
387
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
388
|
+
) {
|
|
389
|
+
const hidden = new Set(view.hiddenPropertyIds ?? []);
|
|
390
|
+
return itemLayoutProperties(
|
|
391
|
+
viewOrderedDefinitions(view, definitions).filter(
|
|
392
|
+
(definition) =>
|
|
393
|
+
definition.visibility !== "always_hide" && !hidden.has(definition.id),
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Applies an agent's item layout patch to a list view. Returns undefined
|
|
400
|
+
* when the patch leaves the layout alone and null to restore default rows.
|
|
401
|
+
*/
|
|
402
|
+
function nextListItemLayout(
|
|
403
|
+
patch: z.infer<typeof updateViewPatchSchema>,
|
|
404
|
+
view: ContentDatabaseView,
|
|
405
|
+
definitions: readonly PropertyDefinitionRow[],
|
|
406
|
+
): ContentDatabaseItemLayout | null | undefined {
|
|
407
|
+
const { itemLayout, itemLayoutPreset } = patch;
|
|
408
|
+
if (itemLayout === undefined && itemLayoutPreset === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
if (itemLayout === null && itemLayoutPreset !== undefined) {
|
|
412
|
+
setupError(
|
|
413
|
+
"INVALID_VIEW_REFERENCE",
|
|
414
|
+
"Pass itemLayoutPreset or itemLayout: null, not both.",
|
|
415
|
+
400,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (itemLayout === null) return null;
|
|
419
|
+
const allProperties = itemLayoutProperties(
|
|
420
|
+
viewOrderedDefinitions(view, definitions),
|
|
421
|
+
);
|
|
422
|
+
// Properties hidden in this view can't be put in a slot, as in the UI.
|
|
423
|
+
const hidden = new Set(view.hiddenPropertyIds ?? []);
|
|
424
|
+
const properties = allProperties.filter(
|
|
425
|
+
(property) => !hidden.has(property.definition.id),
|
|
426
|
+
);
|
|
427
|
+
let base = view.itemLayout ?? null;
|
|
428
|
+
if (itemLayoutPreset !== undefined) {
|
|
429
|
+
const preset = contentDatabaseItemLayoutPresets(properties).find(
|
|
430
|
+
(candidate) => candidate.id === itemLayoutPreset,
|
|
431
|
+
)!;
|
|
432
|
+
if (preset.missingPropertyType) {
|
|
433
|
+
setupError(
|
|
434
|
+
"INVALID_VIEW_REFERENCE",
|
|
435
|
+
`The ${preset.id} preset needs a ${preset.missingPropertyType} property.`,
|
|
436
|
+
400,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
base = preset.layout;
|
|
440
|
+
}
|
|
441
|
+
if (!itemLayout) {
|
|
442
|
+
return (
|
|
443
|
+
base &&
|
|
444
|
+
dropDeletedContentDatabaseItemLayoutReferences(base, allProperties)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
base ??= startingContentDatabaseItemLayout(
|
|
448
|
+
defaultListRowProperties(view, definitions),
|
|
449
|
+
);
|
|
450
|
+
for (const slot of ["line3", "trailing"] as const) {
|
|
451
|
+
const ids = itemLayout[slot];
|
|
452
|
+
if (ids) uniqueReferences(ids, slot);
|
|
453
|
+
}
|
|
454
|
+
const leading = itemLayout.leading && {
|
|
455
|
+
kind: itemLayout.leading.kind,
|
|
456
|
+
propertyId:
|
|
457
|
+
itemLayout.leading.propertyId ??
|
|
458
|
+
itemLayoutLeadingCandidates(properties, itemLayout.leading.kind)[0]
|
|
459
|
+
?.definition.id ??
|
|
460
|
+
null,
|
|
461
|
+
};
|
|
462
|
+
const withPatch = (from: ContentDatabaseItemLayout) =>
|
|
463
|
+
normalizeContentDatabaseItemLayout({
|
|
464
|
+
leading: leading ?? from.leading,
|
|
465
|
+
titleWeight: itemLayout.titleWeight ?? from.titleWeight,
|
|
466
|
+
line2: {
|
|
467
|
+
propertyId:
|
|
468
|
+
itemLayout.line2?.propertyId !== undefined
|
|
469
|
+
? itemLayout.line2.propertyId
|
|
470
|
+
: from.line2.propertyId,
|
|
471
|
+
lines: itemLayout.line2?.lines ?? from.line2.lines,
|
|
472
|
+
},
|
|
473
|
+
line3: itemLayout.line3 ?? from.line3,
|
|
474
|
+
trailing: itemLayout.trailing ?? from.trailing,
|
|
475
|
+
})!;
|
|
476
|
+
// Only the slots this patch names are checked. Saved slots that name a
|
|
477
|
+
// hidden property keep it for when it's shown again, and saved slots that
|
|
478
|
+
// name a deleted one drop it; neither fails an unrelated edit.
|
|
479
|
+
const [error] = contentDatabaseItemLayoutReferenceErrors(
|
|
480
|
+
withPatch(pruneContentDatabaseItemLayout(base, properties)),
|
|
481
|
+
properties,
|
|
482
|
+
);
|
|
483
|
+
if (error) setupError("INVALID_VIEW_REFERENCE", error, 400);
|
|
484
|
+
return dropDeletedContentDatabaseItemLayoutReferences(
|
|
485
|
+
withPatch(base),
|
|
486
|
+
allProperties,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
231
490
|
function parseRawConfig(value: string | null) {
|
|
232
491
|
if (!value) return {} as Record<string, unknown>;
|
|
233
492
|
try {
|
|
@@ -373,14 +632,43 @@ export async function runUpdateContentDatabaseView(
|
|
|
373
632
|
404,
|
|
374
633
|
);
|
|
375
634
|
}
|
|
376
|
-
|
|
635
|
+
const patchKeys = Object.keys(input.patch);
|
|
636
|
+
let patch: Record<string, unknown> = input.patch;
|
|
637
|
+
if (existing.type === "list") {
|
|
638
|
+
const unsupported = patchKeys.find(
|
|
639
|
+
(key) => !LIST_VIEW_PATCH_KEYS.has(key),
|
|
640
|
+
);
|
|
641
|
+
if (unsupported) {
|
|
642
|
+
setupError(
|
|
643
|
+
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
644
|
+
`List views accept only name, icon, itemLayout, and itemLayoutPreset here, not ${unsupported}.`,
|
|
645
|
+
400,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const { itemLayoutPreset: _preset, ...rest } = input.patch;
|
|
649
|
+
const itemLayout = nextListItemLayout(
|
|
650
|
+
input.patch,
|
|
651
|
+
existing,
|
|
652
|
+
context.definitions,
|
|
653
|
+
);
|
|
654
|
+
patch = itemLayout === undefined ? rest : { ...rest, itemLayout };
|
|
655
|
+
} else if (existing.type === "table") {
|
|
656
|
+
const listKey = patchKeys.find((key) => key in listPatchFields);
|
|
657
|
+
if (listKey) {
|
|
658
|
+
setupError(
|
|
659
|
+
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
660
|
+
`${listKey} applies only to list views.`,
|
|
661
|
+
400,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
validateTablePatch(input.patch, context.definitions);
|
|
665
|
+
} else {
|
|
377
666
|
setupError(
|
|
378
667
|
"UNSUPPORTED_SCHEMA_CHANGE",
|
|
379
|
-
"Ordinary database setup can update only table views.",
|
|
668
|
+
"Ordinary database setup can update only table and list views.",
|
|
380
669
|
400,
|
|
381
670
|
);
|
|
382
671
|
}
|
|
383
|
-
validateTablePatch(input.patch, context.definitions);
|
|
384
672
|
viewId = existing.id;
|
|
385
673
|
const index = rawViews.findIndex(
|
|
386
674
|
(view) =>
|
|
@@ -398,7 +686,7 @@ export async function runUpdateContentDatabaseView(
|
|
|
398
686
|
nextViews = [...rawViews];
|
|
399
687
|
nextViews[index] = {
|
|
400
688
|
...(rawViews[index] as Record<string, unknown>),
|
|
401
|
-
...
|
|
689
|
+
...patch,
|
|
402
690
|
};
|
|
403
691
|
}
|
|
404
692
|
|
|
@@ -480,9 +768,15 @@ export async function runReplaceContentDatabaseViews(input: {
|
|
|
480
768
|
typeof candidate === "object" &&
|
|
481
769
|
(candidate as { id?: unknown }).id === view.id,
|
|
482
770
|
);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
771
|
+
const savedView =
|
|
772
|
+
saved && typeof saved === "object"
|
|
773
|
+
? (saved as Record<string, unknown>)
|
|
774
|
+
: null;
|
|
775
|
+
return withSavableItemLayout(
|
|
776
|
+
savedView ? { ...savedView, ...view } : view,
|
|
777
|
+
savedView?.itemLayout,
|
|
778
|
+
context.definitions,
|
|
779
|
+
);
|
|
486
780
|
});
|
|
487
781
|
const nextRaw: Record<string, unknown> = {
|
|
488
782
|
...rawConfig,
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
DocumentProperty,
|
|
31
31
|
} from "../shared/api.js";
|
|
32
32
|
import { blocksFieldId } from "../shared/blocks-field-identity.js";
|
|
33
|
+
import { contentDatabaseItemLayoutField } from "../shared/database-item-layout.js";
|
|
33
34
|
import {
|
|
34
35
|
DEFAULT_BLOCKS_FIELD_NAME,
|
|
35
36
|
defaultPropertyOptions,
|
|
@@ -398,6 +399,7 @@ function defaultDatabaseView(
|
|
|
398
399
|
rowDensity: normalizeDatabaseRowDensity(values.rowDensity),
|
|
399
400
|
openPagesIn: normalizeDatabaseOpenPagesIn(values.openPagesIn),
|
|
400
401
|
formQuestions: normalizeDatabaseFormQuestions(values.formQuestions),
|
|
402
|
+
...contentDatabaseItemLayoutField(values.itemLayout),
|
|
401
403
|
};
|
|
402
404
|
}
|
|
403
405
|
|
|
@@ -460,6 +462,7 @@ function normalizeDatabaseView(value: unknown): ContentDatabaseView | null {
|
|
|
460
462
|
rowDensity: normalizeDatabaseRowDensity(view.rowDensity),
|
|
461
463
|
openPagesIn: normalizeDatabaseOpenPagesIn(view.openPagesIn),
|
|
462
464
|
formQuestions: normalizeDatabaseFormQuestions(view.formQuestions),
|
|
465
|
+
...contentDatabaseItemLayoutField(view.itemLayout),
|
|
463
466
|
};
|
|
464
467
|
}
|
|
465
468
|
|
|
@@ -10,6 +10,7 @@ import { z } from "zod";
|
|
|
10
10
|
import { getDb, schema } from "../server/db/index.js";
|
|
11
11
|
import { syncPrivateViewIconReferences } from "../server/lib/private-icon-references.js";
|
|
12
12
|
import { CONTENT_DATABASE_FILTER_OPERATORS } from "../shared/api.js";
|
|
13
|
+
import { withSavedContentDatabaseItemLayout } from "../shared/database-item-layout.js";
|
|
13
14
|
import { withSavedTableColumnPresentation } from "../shared/database-table-columns.js";
|
|
14
15
|
import { lockContentDatabaseMutation } from "./_content-database-mutation-lock.js";
|
|
15
16
|
import {
|
|
@@ -19,9 +20,11 @@ import {
|
|
|
19
20
|
} from "./_database-setup-mutation.js";
|
|
20
21
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
21
22
|
import {
|
|
23
|
+
contentDatabaseItemLayoutSchema,
|
|
22
24
|
runReplaceContentDatabaseViews,
|
|
23
25
|
runUpdateContentDatabaseView,
|
|
24
26
|
updateContentDatabaseViewAgentSchema,
|
|
27
|
+
withSavableItemLayout,
|
|
25
28
|
} from "./_database-view-setup.js";
|
|
26
29
|
import {
|
|
27
30
|
parseDatabaseViewConfig,
|
|
@@ -114,6 +117,7 @@ const viewSchema = z
|
|
|
114
117
|
.default("default"),
|
|
115
118
|
openPagesIn: z.enum(["preview", "full_page"]).default("preview"),
|
|
116
119
|
formQuestions: z.array(formQuestionSchema).default([]),
|
|
120
|
+
itemLayout: contentDatabaseItemLayoutSchema.nullable().optional(),
|
|
117
121
|
})
|
|
118
122
|
.strict();
|
|
119
123
|
|
|
@@ -143,7 +147,7 @@ const guardedViewReplacementSchema = setupGuardSchema
|
|
|
143
147
|
|
|
144
148
|
export default defineAction({
|
|
145
149
|
description:
|
|
146
|
-
"Create one named ordinary table view or sparsely update one exact saved table view using stable property IDs, fresh schema and configuration revisions, and an idempotency key.",
|
|
150
|
+
"Create one named ordinary table view, or sparsely update one exact saved table view or a list view's name, icon, and row layout (itemLayout, itemLayoutPreset), using stable property IDs, fresh schema and configuration revisions, and an idempotency key.",
|
|
147
151
|
mcpTool: true,
|
|
148
152
|
mcpApp: { structuredContent: true },
|
|
149
153
|
agentInputSchema: updateContentDatabaseViewAgentSchema,
|
|
@@ -213,10 +217,22 @@ export default defineAction({
|
|
|
213
217
|
const currentViewConfig = parseDatabaseViewConfig(
|
|
214
218
|
lockedDatabase.viewConfigJson,
|
|
215
219
|
);
|
|
220
|
+
const definitions = await tx
|
|
221
|
+
.select()
|
|
222
|
+
.from(schema.documentPropertyDefinitions)
|
|
223
|
+
.where(eq(schema.documentPropertyDefinitions.databaseId, databaseId));
|
|
216
224
|
const nextViewConfig = {
|
|
217
225
|
...viewConfig,
|
|
218
226
|
views: viewConfig.views?.map((view) =>
|
|
219
|
-
|
|
227
|
+
withSavableItemLayout(
|
|
228
|
+
withSavedContentDatabaseItemLayout(
|
|
229
|
+
withSavedTableColumnPresentation(view, currentViewConfig.views),
|
|
230
|
+
currentViewConfig.views,
|
|
231
|
+
),
|
|
232
|
+
currentViewConfig.views.find((saved) => saved.id === view.id)
|
|
233
|
+
?.itemLayout,
|
|
234
|
+
definitions,
|
|
235
|
+
),
|
|
220
236
|
),
|
|
221
237
|
};
|
|
222
238
|
const nextViewConfigJson = serializeDatabaseViewConfig(nextViewConfig);
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
ContentDatabaseViewType,
|
|
25
25
|
DocumentProperty,
|
|
26
26
|
} from "../shared/api.js";
|
|
27
|
+
import { normalizeContentDatabaseItemLayout } from "../shared/database-item-layout.js";
|
|
27
28
|
import {
|
|
28
29
|
databaseColumnWraps,
|
|
29
30
|
databaseFrozenColumnIds,
|
|
@@ -782,6 +783,17 @@ export function databaseCurrentViewSnapshot(
|
|
|
782
783
|
"preview",
|
|
783
784
|
formQuestions:
|
|
784
785
|
arrayValue(nav.databaseFormQuestions) ?? activeView?.formQuestions ?? [],
|
|
786
|
+
...(viewType === "list"
|
|
787
|
+
? {
|
|
788
|
+
// An explicit null is a reset to default rows that may not be saved yet.
|
|
789
|
+
itemLayout:
|
|
790
|
+
nav.databaseItemLayout === null
|
|
791
|
+
? null
|
|
792
|
+
: (normalizeContentDatabaseItemLayout(nav.databaseItemLayout) ??
|
|
793
|
+
activeView?.itemLayout ??
|
|
794
|
+
null),
|
|
795
|
+
}
|
|
796
|
+
: {}),
|
|
785
797
|
visibleItemCount:
|
|
786
798
|
numberValue(nav.databaseVisibleItemCount) ??
|
|
787
799
|
response.pagination?.returnedItems ??
|