@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.
Files changed (22) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/community/qr/package.json +1 -1
  3. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +15 -1
  4. package/corpus/templates/content/CHANGELOG.md +5 -0
  5. package/corpus/templates/content/actions/_database-view-setup.ts +305 -11
  6. package/corpus/templates/content/actions/_property-utils.ts +3 -0
  7. package/corpus/templates/content/actions/update-content-database-view.ts +18 -2
  8. package/corpus/templates/content/actions/view-screen.ts +12 -0
  9. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +91 -16
  10. package/corpus/templates/content/app/components/editor/database/ListItemLayout.tsx +410 -0
  11. package/corpus/templates/content/app/components/editor/database/ListItemRow.tsx +426 -0
  12. package/corpus/templates/content/app/components/editor/database/ListView.tsx +136 -18
  13. package/corpus/templates/content/app/components/editor/database/navigation-state.ts +3 -0
  14. package/corpus/templates/content/app/components/editor/database/settings.tsx +3 -3
  15. package/corpus/templates/content/app/components/editor/database/view-config.ts +4 -0
  16. package/corpus/templates/content/app/global.css +13 -0
  17. package/corpus/templates/content/app/i18n-data.ts +32 -0
  18. package/corpus/templates/content/changelog/2026-10-19-list-rows-can-be-customized.md +6 -0
  19. package/corpus/templates/content/changelog/2026-10-19-list-views-fit-phone-screens.md +6 -0
  20. package/corpus/templates/content/shared/api.ts +18 -0
  21. package/corpus/templates/content/shared/database-item-layout.ts +591 -0
  22. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 13662
35
+ - template files: 13667
@@ -11,7 +11,7 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@agent-native/core": "^0.213.1",
14
+ "@agent-native/core": "^0.214.0",
15
15
  "postgres": "^3.4.9",
16
16
  "qrcode": "^1.5.4",
17
17
  "zod": "^4.6.7"
@@ -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 table presentation. It refuses destructive
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 type { DocumentPropertyType } from "../shared/properties.js";
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 updateTablePatchSchema = z
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: updateTablePatchSchema,
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 updateTablePatchSchema>
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
- if (existing.type !== "table") {
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
- ...input.patch,
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
- return saved && typeof saved === "object"
484
- ? { ...(saved as Record<string, unknown>), ...view }
485
- : view;
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
- withSavedTableColumnPresentation(view, currentViewConfig.views),
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 ??