@elytracms/studio 0.0.26 → 0.0.28

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 (138) hide show
  1. package/convex/assets.ts +19 -10
  2. package/convex/authority.ts +60 -0
  3. package/convex/cliTokens.ts +4 -4
  4. package/convex/cms.ts +12 -12
  5. package/convex/content.ts +10 -3
  6. package/convex/delivery.ts +22 -4
  7. package/convex/guard.ts +28 -11
  8. package/convex/members.ts +40 -7
  9. package/convex/redirects.ts +67 -0
  10. package/convex/references.ts +11 -11
  11. package/convex/schema.ts +30 -1
  12. package/convex/sync.ts +17 -10
  13. package/convex/users.ts +63 -0
  14. package/dist/components/app-views/default-modules.d.ts +33 -14
  15. package/dist/components/app-views/default-modules.js +59 -20
  16. package/dist/components/app-views/default-modules.js.map +1 -1
  17. package/dist/components/app-views/module.d.ts +32 -0
  18. package/dist/components/app-views/module.js +16 -0
  19. package/dist/components/app-views/module.js.map +1 -1
  20. package/dist/components/app-views/sidebar.d.ts +15 -1
  21. package/dist/components/app-views/sidebar.js +10 -0
  22. package/dist/components/app-views/sidebar.js.map +1 -1
  23. package/dist/components/cms/asset-picker.js +4 -4
  24. package/dist/components/cms/asset-picker.js.map +1 -1
  25. package/dist/components/cms/cell.js +16 -13
  26. package/dist/components/cms/cell.js.map +1 -1
  27. package/dist/components/cms/cms-store.d.ts +11 -26
  28. package/dist/components/cms/cms-store.js +25 -160
  29. package/dist/components/cms/cms-store.js.map +1 -1
  30. package/dist/components/cms/collection-table.d.ts +10 -0
  31. package/dist/components/cms/collection-table.js +72 -72
  32. package/dist/components/cms/collection-table.js.map +1 -1
  33. package/dist/components/cms/detail-editor.d.ts +14 -0
  34. package/dist/components/cms/detail-editor.js +224 -72
  35. package/dist/components/cms/detail-editor.js.map +1 -1
  36. package/dist/components/cms/document-editor-context.d.ts +26 -0
  37. package/dist/components/cms/document-editor-context.js.map +1 -1
  38. package/dist/components/cms/editable-cell.js +12 -7
  39. package/dist/components/cms/editable-cell.js.map +1 -1
  40. package/dist/components/cms/field-control.d.ts +2 -0
  41. package/dist/components/cms/field-control.js +40 -30
  42. package/dist/components/cms/field-control.js.map +1 -1
  43. package/dist/components/cms/filter-builder.d.ts +18 -0
  44. package/dist/components/cms/filter-builder.js +47 -6
  45. package/dist/components/cms/filter-builder.js.map +1 -1
  46. package/dist/components/cms/import-export.js +5 -3
  47. package/dist/components/cms/import-export.js.map +1 -1
  48. package/dist/components/cms/reference-confirm.js +8 -9
  49. package/dist/components/cms/reference-confirm.js.map +1 -1
  50. package/dist/components/cms/relation-picker.js +13 -10
  51. package/dist/components/cms/relation-picker.js.map +1 -1
  52. package/dist/components/cms/use-cell-edit.js +13 -7
  53. package/dist/components/cms/use-cell-edit.js.map +1 -1
  54. package/dist/components/cms/use-document-versions.d.ts +16 -0
  55. package/dist/components/cms/use-document-versions.js +31 -0
  56. package/dist/components/cms/use-document-versions.js.map +1 -0
  57. package/dist/components/cms/version-compare-text.d.ts +30 -0
  58. package/dist/components/cms/version-compare-text.js +113 -0
  59. package/dist/components/cms/version-compare-text.js.map +1 -0
  60. package/dist/components/cms/version-compare.d.ts +12 -0
  61. package/dist/components/cms/version-compare.js +271 -0
  62. package/dist/components/cms/version-compare.js.map +1 -0
  63. package/dist/components/cms/versions-pane.d.ts +3 -0
  64. package/dist/components/cms/versions-pane.js +47 -38
  65. package/dist/components/cms/versions-pane.js.map +1 -1
  66. package/dist/components/cms/view-bar.d.ts +21 -6
  67. package/dist/components/cms/view-bar.js +42 -57
  68. package/dist/components/cms/view-bar.js.map +1 -1
  69. package/dist/components/collection-view/cms-collection-view-source.d.ts +15 -0
  70. package/dist/components/collection-view/cms-collection-view-source.js +158 -0
  71. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -0
  72. package/dist/components/collection-view/collection-view-context.d.ts +10 -0
  73. package/dist/components/collection-view/collection-view-context.js +26 -0
  74. package/dist/components/collection-view/collection-view-context.js.map +1 -0
  75. package/dist/components/current-user-menu.js +8 -1
  76. package/dist/components/current-user-menu.js.map +1 -1
  77. package/dist/components/redirects/redirects-collection-view-source.d.ts +18 -0
  78. package/dist/components/redirects/redirects-collection-view-source.js +114 -0
  79. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -0
  80. package/dist/components/redirects/redirects-module.d.ts +18 -0
  81. package/dist/components/redirects/redirects-module.js +43 -0
  82. package/dist/components/redirects/redirects-module.js.map +1 -0
  83. package/dist/components/redirects/redirects-schema.d.ts +10 -0
  84. package/dist/components/redirects/redirects-schema.js +42 -0
  85. package/dist/components/redirects/redirects-schema.js.map +1 -0
  86. package/dist/components/redirects/redirects-store.d.ts +46 -0
  87. package/dist/components/redirects/redirects-store.js +124 -0
  88. package/dist/components/redirects/redirects-store.js.map +1 -0
  89. package/dist/components/references/references-panel.js +2 -2
  90. package/dist/components/references/references-panel.js.map +1 -1
  91. package/dist/components/section-nav.js +14 -5
  92. package/dist/components/section-nav.js.map +1 -1
  93. package/dist/components/users/user-search.d.ts +8 -0
  94. package/dist/components/users/user-search.js +13 -0
  95. package/dist/components/users/user-search.js.map +1 -0
  96. package/dist/components/users/users-collection-view-source.d.ts +28 -0
  97. package/dist/components/users/users-collection-view-source.js +157 -0
  98. package/dist/components/users/users-collection-view-source.js.map +1 -0
  99. package/dist/components/users/users-module.d.ts +23 -0
  100. package/dist/components/users/users-module.js +61 -0
  101. package/dist/components/users/users-module.js.map +1 -0
  102. package/dist/components/users/users-schema.d.ts +19 -0
  103. package/dist/components/users/users-schema.js +38 -0
  104. package/dist/components/users/users-schema.js.map +1 -0
  105. package/dist/components/users/users-store.d.ts +40 -0
  106. package/dist/components/users/users-store.js +87 -0
  107. package/dist/components/users/users-store.js.map +1 -0
  108. package/dist/lib/cms-views/index.d.ts +4 -5
  109. package/dist/lib/cms-views/index.js +4 -5
  110. package/dist/lib/cms-views/index.js.map +1 -1
  111. package/dist/lib/cms-views/view-model.d.ts +48 -1
  112. package/dist/lib/cms-views/view-model.js +77 -1
  113. package/dist/lib/cms-views/view-model.js.map +1 -1
  114. package/dist/lib/collection-view/collection-view-source.d.ts +251 -0
  115. package/dist/lib/collection-view/collection-view-source.js +10 -0
  116. package/dist/lib/collection-view/collection-view-source.js.map +1 -0
  117. package/dist/lib/operations/session-hydration.d.ts +7 -0
  118. package/dist/lib/operations/session-hydration.js +7 -1
  119. package/dist/lib/operations/session-hydration.js.map +1 -1
  120. package/dist/lib/persistence/convex-adapter.d.ts +5 -1
  121. package/dist/lib/persistence/convex-adapter.js +148 -1
  122. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  123. package/dist/lib/workspace/index.d.ts +1 -8
  124. package/dist/lib/workspace/index.js +3 -9
  125. package/dist/lib/workspace/index.js.map +1 -1
  126. package/dist/router/match.d.ts +4 -0
  127. package/dist/router/match.js +8 -0
  128. package/dist/router/match.js.map +1 -1
  129. package/dist/ui/index.d.ts +2 -0
  130. package/dist/ui/index.js +2 -0
  131. package/dist/ui/index.js.map +1 -1
  132. package/dist/ui/popover.d.ts +13 -0
  133. package/dist/ui/popover.js +17 -0
  134. package/dist/ui/popover.js.map +1 -0
  135. package/dist/ui/row-item.d.ts +16 -0
  136. package/dist/ui/row-item.js +18 -0
  137. package/dist/ui/row-item.js.map +1 -0
  138. package/package.json +7 -2
@@ -228,14 +228,90 @@ export function applyViewState(collection, docs, localeConfig, state, isPublishe
228
228
  /** The id of the unnamed built-in "All documents" view. */
229
229
  export const ALL_VIEW_ID = 'all';
230
230
  /** The default (unnamed) view: no filters, no sort, every field visible. */
231
+ /**
232
+ * Field types kept OFF the default table columns (EC-246b): the composite editing
233
+ * surfaces — a blocks canvas, rich text, a structured object/repeater — read as
234
+ * noise in a scalar list. `filterable` no longer forces (or governs) a column; the
235
+ * default lens is "all scalars". These stay in the column picker one toggle away,
236
+ * and any named view may list them in its `columns`; they're just off by default.
237
+ */
238
+ const NON_DEFAULT_COLUMN_TYPES = new Set(['blocks', 'richText', 'object']);
239
+ /** The collection's composite (non-scalar) field names — hidden in the default view. */
240
+ function nonScalarColumns(collection) {
241
+ return collection.fields.filter((f) => NON_DEFAULT_COLUMN_TYPES.has(f.type)).map((f) => f.name);
242
+ }
231
243
  export function defaultViewState(collection) {
232
244
  return {
233
245
  filters: [],
234
246
  sort: null,
247
+ // Every field keeps a slot (order + picker), but the composite surfaces start
248
+ // hidden so the default lens is the collection's scalars (EC-246b).
235
249
  columns: orderedColumnNames(collection),
236
- hidden: [],
250
+ hidden: nonScalarColumns(collection),
237
251
  };
238
252
  }
253
+ /**
254
+ * A config-declared view (EC-246b) — the flat authoring shape a collection
255
+ * declares in `cms/` config: `{ name, filters?, sort?, columns?, hidden? }`.
256
+ * Filters/sort/columns sit at the top level (no nested `state` wrapper). The
257
+ * studio validates + interprets these; `cms-core` stores them opaquely (it
258
+ * can't reference the filter vocabulary without a layering cycle).
259
+ */
260
+ export const configViewSchema = z.object({
261
+ /** Optional stable id; defaults to a slug of `name`. */
262
+ id: z.string().min(1).optional(),
263
+ name: z.string().min(1),
264
+ filters: z.array(filterClauseSchema).optional(),
265
+ sort: viewSortSchema.nullable().optional(),
266
+ /** Column order; when omitted, the view inherits the collection's default columns. */
267
+ columns: z.array(z.string().min(1)).optional(),
268
+ hidden: z.array(z.string().min(1)).optional(),
269
+ });
270
+ /** Slug used to derive a config view's stable id from its name. */
271
+ function configViewId(name, index) {
272
+ const slug = name
273
+ .toLowerCase()
274
+ .replace(/[^a-z0-9]+/g, '-')
275
+ .replace(/^-+|-+$/g, '');
276
+ return `config:${slug || index}`;
277
+ }
278
+ /**
279
+ * Resolve a collection's config-declared `views` into full {@link ResolvedView}s
280
+ * (EC-246b). Each is validated with {@link configViewSchema}; malformed entries
281
+ * and duplicate ids are dropped (defensive). Columns fall back to the collection
282
+ * default when a view doesn't pin them, so a filter-only view still renders every
283
+ * column. Config views are shared and change only in
284
+ * code — the runtime never mutates them.
285
+ */
286
+ export function configViewsFor(collection) {
287
+ const declared = collection.views;
288
+ if (!Array.isArray(declared))
289
+ return [];
290
+ const base = defaultViewState(collection);
291
+ const seen = new Set();
292
+ const resolved = [];
293
+ declared.forEach((raw, index) => {
294
+ const parsed = configViewSchema.safeParse(raw);
295
+ if (!parsed.success)
296
+ return;
297
+ const view = parsed.data;
298
+ const id = view.id ?? configViewId(view.name, index);
299
+ if (seen.has(id))
300
+ return;
301
+ seen.add(id);
302
+ resolved.push({
303
+ id,
304
+ name: view.name,
305
+ state: normalizeViewState({
306
+ filters: view.filters ?? [],
307
+ sort: view.sort ?? null,
308
+ columns: view.columns && view.columns.length > 0 ? view.columns : base.columns,
309
+ hidden: view.hidden ?? [],
310
+ }),
311
+ });
312
+ });
313
+ return resolved;
314
+ }
239
315
  /**
240
316
  * The default column order: the schema's title field first (it becomes the
241
317
  * default title/link column — EC-213 drops the separate primary-column
@@ -1 +1 @@
1
- {"version":3,"file":"view-model.js","sourceRoot":"","sources":["../../../src/lib/cms-views/view-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod'\nimport { documentDefaultLocale, resolveField, titleFieldOf } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef, LocaleConfig } from '@elytracms/core/cms-core'\nimport {\n FILTER_OPERATORS,\n compareListEntries,\n filterScalarOf,\n filterableFields,\n matchesCondition,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n FilterCondition,\n FilterOperator,\n FilterScalar,\n NormalizedCondition,\n WhereInput,\n} from '@elytracms/core/content'\n\n/**\n * The pure table-view model for the CMS collection table (EC-169).\n *\n * A view bundles three things: filters in the closed AD-3 vocabulary\n * (`eq`/`in`/`lt`/`lte`/`gt`/`gte`, AND-only — exactly `@elytracms/core/content`'s\n * query model, evaluated with the same matching/ordering primitives), a sort\n * (field + direction), and the visible/ordered columns. Everything here is\n * plain data + functions so filtering, dirtiness, and column edits are\n * testable without a DOM.\n *\n * Filters are offered ONLY on fields the schema declares `filterable: true`\n * (EC-143), plus one built-in option: the document's publication state. The\n * state is document metadata, not a schema field — it powers the\n * \"show me drafts → bulk publish\" working set and still speaks only the\n * closed vocabulary (`eq`/`in`).\n */\n\n// --- Schemas (single source of truth — persisted view records reuse these) ----\n\nexport const filterScalarSchema = z.union([z.string(), z.number(), z.boolean()])\n\nexport const filterClauseSchema = z.object({\n /** A schema field name, or {@link STATE_FIELD} for the publication state. */\n field: z.string().min(1),\n op: z.enum(FILTER_OPERATORS),\n /** `in` carries an array of scalars; every other operator one scalar. */\n value: z.union([filterScalarSchema, z.array(filterScalarSchema)]),\n})\nexport type FilterClause = z.infer<typeof filterClauseSchema>\n\nexport const viewSortSchema = z.object({\n field: z.string().min(1),\n dir: z.enum(['asc', 'desc']),\n})\nexport type ViewSort = z.infer<typeof viewSortSchema>\n\nexport const tableViewStateSchema = z.object({\n /** AND-only list of conditions (no OR, no nesting — vision AD-3). */\n filters: z.array(filterClauseSchema),\n sort: viewSortSchema.nullable(),\n /**\n * The canonical column order (field names) — EVERY column, visible or not\n * (EC-213). Order is independent of visibility: hiding then re-showing a\n * column keeps its place. Schema fields absent here resolve in at the end\n * (see {@link resolveColumns}).\n */\n columns: z.array(z.string().min(1)),\n /**\n * The subset of {@link TableViewState.columns} that is hidden. Defaults to\n * `[]` so views persisted before EC-213 (no `hidden` key) load unchanged.\n */\n hidden: z.array(z.string().min(1)).default([]),\n})\nexport type TableViewState = z.infer<typeof tableViewStateSchema>\n\n// --- The built-in publication-state filter column ------------------------------\n\n/**\n * Sentinel field key for the publication state. `$`-prefixed so it can never\n * collide with a schema field name (`fieldDefSchema` names are identifiers).\n */\nexport const STATE_FIELD = '$state'\n\nconst STATE_VALUES = ['draft', 'published'] as const\n\n/** One field the filter builder may offer. */\nexport interface FilterableFieldOption {\n /** Clause field key: a schema field name, or {@link STATE_FIELD}. */\n field: string\n label: string\n /** The schema field def, or `null` for the built-in publication state. */\n def: FieldDef | null\n}\n\nexport const STATE_OPTION: FilterableFieldOption = {\n field: STATE_FIELD,\n label: 'State',\n def: null,\n}\n\n/**\n * The closed list of fields the filter builder offers: exactly the schema's\n * `filterable: true` fields (EC-143) plus the publication state. Nothing else\n * is ever offered — the UI builds queries, it does not invent a language.\n */\nexport function filterableFieldOptions(collection: CollectionDef): FilterableFieldOption[] {\n const schemaOptions = filterableFields(collection).map((field) => ({\n field: field.name,\n label: field.form?.label ?? field.name,\n def: field,\n }))\n return [...schemaOptions, STATE_OPTION]\n}\n\nexport function filterOptionFor(\n collection: CollectionDef,\n field: string,\n): FilterableFieldOption | undefined {\n return filterableFieldOptions(collection).find((option) => option.field === field)\n}\n\n// --- Operators per field type ---------------------------------------------------\n\n/** Display order + labels for the closed vocabulary. */\nexport const OPERATOR_LABELS: Record<FilterOperator, string> = {\n eq: 'equals',\n in: 'is one of',\n lt: 'less than',\n lte: 'at most',\n gt: 'greater than',\n gte: 'at least',\n}\n\nconst ORDERED_OPERATORS: readonly FilterOperator[] = ['eq', 'in', 'lt', 'lte', 'gt', 'gte']\n\n/**\n * The vocabulary subset that makes sense for a field's type. Always a subset\n * of the closed `FILTER_OPERATORS` — never an extension.\n */\nexport function operatorsFor(option: FilterableFieldOption): FilterOperator[] {\n if (option.def === null) return ['eq', 'in'] // publication state\n switch (option.def.type) {\n case 'boolean':\n return ['eq']\n case 'select':\n case 'relation':\n case 'asset':\n return ['eq', 'in']\n default:\n return [...ORDERED_OPERATORS]\n }\n}\n\n/** A sensible starting clause when a field is added to the builder. */\nexport function defaultClause(option: FilterableFieldOption): FilterClause {\n if (option.def === null) return { field: STATE_FIELD, op: 'eq', value: 'published' }\n switch (option.def.type) {\n case 'boolean':\n return { field: option.field, op: 'eq', value: true }\n case 'number':\n return { field: option.field, op: 'eq', value: 0 }\n case 'select':\n return { field: option.field, op: 'eq', value: option.def.options[0]?.value ?? '' }\n default:\n return { field: option.field, op: 'eq', value: '' }\n }\n}\n\n// --- Text ⇄ scalar coercion (the value inputs of the builder) -------------------\n\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/** Coerce one text input into the scalar a clause compares against. */\nexport function scalarFromText(\n option: FilterableFieldOption | undefined,\n raw: string,\n): FilterScalar {\n const type = option?.def?.type\n if (type === 'number') {\n const trimmed = raw.trim()\n if (trimmed === '') return raw // an emptied input must not become eq 0\n const parsed = Number(trimmed)\n return Number.isNaN(parsed) ? raw : parsed\n }\n if (type === 'boolean') return raw === 'true'\n // Stored dates are full ISO strings; a date-input value compares correctly\n // once expanded to midnight UTC (ISO strings order by code units).\n if (type === 'date' && DATE_ONLY.test(raw)) return `${raw}T00:00:00.000Z`\n return raw\n}\n\n/** Parse a comma-separated list for the `in` operator. */\nexport function scalarListFromText(\n option: FilterableFieldOption | undefined,\n raw: string,\n): FilterScalar[] {\n return raw\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '')\n .map((part) => scalarFromText(option, part))\n}\n\nexport function scalarListToText(value: FilterScalar | FilterScalar[]): string {\n const list = Array.isArray(value) ? value : [value]\n return list.map(String).join(', ')\n}\n\n// --- Clause validation -----------------------------------------------------------\n\n/** A structured problem with one clause (by index in the filter list). */\nexport interface ClauseIssue {\n index: number\n message: string\n}\n\n/**\n * Validate clauses against the collection schema. Schema-field clauses run\n * through the canonical `validateWhere` (EC-143: `filterable` enforcement +\n * operator/value shape); state clauses check the `eq`/`in` subset and the\n * draft/published value set. Invalid clauses (e.g. a persisted view whose\n * field is no longer filterable) are reported — never silently applied.\n */\nexport function validateClauses(\n collection: CollectionDef,\n clauses: readonly FilterClause[],\n): ClauseIssue[] {\n const issues: ClauseIssue[] = []\n clauses.forEach((clause, index) => {\n if (clause.field === STATE_FIELD) {\n if (clause.op !== 'eq' && clause.op !== 'in') {\n issues.push({ index, message: 'State filters support only \"eq\" and \"in\".' })\n return\n }\n const values = Array.isArray(clause.value) ? clause.value : [clause.value]\n if (!values.every((value) => (STATE_VALUES as readonly unknown[]).includes(value))) {\n issues.push({ index, message: 'State must be \"draft\" or \"published\".' })\n }\n return\n }\n const where: WhereInput = {\n [clause.field]: { [clause.op]: clause.value } as FilterCondition,\n }\n const errors = validateWhere(collection, where)\n if (errors.length > 0) issues.push({ index, message: errors[0]!.message })\n })\n return issues\n}\n\n// --- Matching & sorting (delegates to @elytracms/core/content's primitives) -------------\n\n/**\n * Published-ness source for the `$state` pseudo-field (EC-224): the version\n * POINTER, supplied by the caller as `cms.isPublished`. Defaults to \"nothing\n * published\" — a bare row carries no publish signal now that the flat `state`\n * field is gone, so the table (which knows the pointer) passes the real\n * predicate; fixtures/tests that don't surface `$state` get the safe default.\n */\nexport type IsPublished = (doc: CmsDocument) => boolean\nconst defaultIsPublished: IsPublished = () => false\n\n/** The scalar a document exposes for a clause/sort field, locale-resolved. */\nfunction scalarOf(\n collection: CollectionDef,\n doc: CmsDocument,\n field: string,\n localeConfig: LocaleConfig,\n isPublished: IsPublished,\n): FilterScalar | null {\n if (field === STATE_FIELD) return isPublished(doc) ? 'published' : 'draft'\n if (!collection.fields.some((f) => f.name === field)) return null\n const locale = documentDefaultLocale(doc, localeConfig)\n return filterScalarOf(resolveField(collection, doc, field, locale, localeConfig).value)\n}\n\n/** Does a document satisfy one clause? Same semantics as the content accessors. */\nexport function clauseMatches(\n collection: CollectionDef,\n doc: CmsDocument,\n localeConfig: LocaleConfig,\n clause: FilterClause,\n isPublished: IsPublished = defaultIsPublished,\n): boolean {\n const value = scalarOf(collection, doc, clause.field, localeConfig, isPublished)\n const condition = { [clause.op]: clause.value } as NormalizedCondition\n return matchesCondition(value, condition)\n}\n\n/**\n * Sort documents by one column in the canonical deterministic order\n * (`compareListEntries`: typed value order, id-ascending tiebreak). Unlike the\n * content accessors, the *table* may sort any rendered column — sorting is a\n * presentation concern here; the `filterable` flag governs filters.\n */\nexport function sortViewDocuments(\n collection: CollectionDef,\n docs: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n sort: ViewSort | null,\n isPublished: IsPublished = defaultIsPublished,\n): CmsDocument[] {\n if (sort === null) return [...docs]\n const positions = new Map(\n docs.map((doc) => [doc.id, scalarOf(collection, doc, sort.field, localeConfig, isPublished)]),\n )\n return [...docs].sort((a, b) =>\n compareListEntries(\n { id: a.id, sortValue: positions.get(a.id) ?? null },\n { id: b.id, sortValue: positions.get(b.id) ?? null },\n sort.dir,\n ),\n )\n}\n\n/** The filtered + sorted documents of a view, plus any clause issues. */\nexport interface AppliedViewState {\n docs: CmsDocument[]\n /** Clauses that failed validation — reported AND excluded from matching. */\n issues: ClauseIssue[]\n}\n\n/**\n * Apply a view state to a document list: validate clauses, AND-match the valid\n * ones, sort. Invalid clauses never filter silently — they come back as\n * issues for the UI to surface.\n */\nexport function applyViewState(\n collection: CollectionDef,\n docs: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n state: TableViewState,\n isPublished: IsPublished = defaultIsPublished,\n): AppliedViewState {\n const issues = validateClauses(collection, state.filters)\n const invalid = new Set(issues.map((issue) => issue.index))\n const active = state.filters.filter((_, index) => !invalid.has(index))\n const matched = docs.filter((doc) =>\n active.every((clause) => clauseMatches(collection, doc, localeConfig, clause, isPublished)),\n )\n return {\n docs: sortViewDocuments(collection, matched, localeConfig, state.sort, isPublished),\n issues,\n }\n}\n\n// --- View state construction, equality, columns ----------------------------------\n\n/** The id of the unnamed built-in \"All documents\" view. */\nexport const ALL_VIEW_ID = 'all'\n\n/** The default (unnamed) view: no filters, no sort, every field visible. */\nexport function defaultViewState(collection: CollectionDef): TableViewState {\n return {\n filters: [],\n sort: null,\n columns: orderedColumnNames(collection),\n hidden: [],\n }\n}\n\n/**\n * The default column order: the schema's title field first (it becomes the\n * default title/link column — EC-213 drops the separate primary-column\n * selector, treating the first visible column as the title), then the\n * remaining fields in schema order.\n */\nfunction orderedColumnNames(collection: CollectionDef): string[] {\n const names = collection.fields.map((field) => field.name)\n const title = titleFieldOf(collection)?.name\n if (title === undefined || !names.includes(title)) return names\n return [title, ...names.filter((name) => name !== title)]\n}\n\n/** Deep-cloned canonical form (stable key order via the Zod shape). */\nexport function normalizeViewState(state: TableViewState): TableViewState {\n return tableViewStateSchema.parse(state)\n}\n\n/** Structural equality on normalized states — drives the \"unsaved changes\" affordance. */\nexport function viewStatesEqual(a: TableViewState, b: TableViewState): boolean {\n return JSON.stringify(normalizeViewState(a)) === JSON.stringify(normalizeViewState(b))\n}\n\n/** One column of a view resolved against the current schema (EC-213). */\nexport interface ResolvedColumn {\n /** The schema field name. */\n field: string\n /** Whether the column renders (`false` ⇒ in {@link TableViewState.hidden}). */\n visible: boolean\n}\n\n/**\n * Resolve a view's columns against the live schema (EC-213): every schema\n * field exactly once, in the view's saved order, with any fields the view\n * predates appended (visible) and any fields no longer in the schema dropped.\n * This is the single source of truth for \"which columns, in what order, shown\n * or hidden\" — order is preserved across hide/show because visibility lives in\n * `hidden`, never in the order.\n */\nexport function resolveColumns(\n collection: CollectionDef,\n state: TableViewState,\n): ResolvedColumn[] {\n const schemaNames = collection.fields.map((field) => field.name)\n const schema = new Set(schemaNames)\n const hidden = new Set(state.hidden)\n // Keep the saved order, restricted to live fields and de-duplicated (a\n // corrupted persisted state must never render one field as two columns).\n const ordered: string[] = []\n const seen = new Set<string>()\n for (const name of state.columns) {\n if (schema.has(name) && !seen.has(name)) {\n ordered.push(name)\n seen.add(name)\n }\n }\n const appended = schemaNames.filter((name) => !seen.has(name))\n return [...ordered, ...appended].map((field) => ({ field, visible: !hidden.has(field) }))\n}\n\n/** The visible columns of a view, in render order (EC-213). */\nexport function visibleColumns(collection: CollectionDef, state: TableViewState): string[] {\n return resolveColumns(collection, state)\n .filter((column) => column.visible)\n .map((column) => column.field)\n}\n\n/**\n * The title/link column — the document IDENTITY column (EC-242): the schema title\n * field, shown as the leftmost linked cell whose content is `resolveDocumentTitle`\n * (a string `titleField` or a function pattern). Always present regardless of\n * column visibility — it is the document's identity, not a toggleable data column —\n * and excluded from the data columns so the title never shows twice. Falls back to\n * the first visible column, then the first field, then `id`, when no title field\n * is resolvable (e.g. a function title with no backing `title` field).\n */\nexport function primaryColumn(collection: CollectionDef, state: TableViewState): string {\n return (\n titleFieldOf(collection)?.name ??\n visibleColumns(collection, state)[0] ??\n collection.fields[0]?.name ??\n 'id'\n )\n}\n\n/**\n * Reconcile `columns` to the full resolved order so column edits operate on a\n * complete, stable order (drops removed fields, pins appended ones), and prune\n * `hidden` to fields the live schema still has (a column removed from the schema\n * must not leave dead `hidden` cruft that trips dirty-detection / persists).\n */\nfunction withResolvedOrder(collection: CollectionDef, state: TableViewState): TableViewState {\n const columns = resolveColumns(collection, state).map((column) => column.field)\n const live = new Set(columns)\n const hidden = state.hidden.filter((name) => live.has(name))\n return { ...state, columns, hidden }\n}\n\n/**\n * The canonical, schema-resolved form of a view state — full column order with\n * `hidden` pruned to live fields (EC-213). Two states that render identically\n * share one canonical form, so dirty-detection never trips on a mere column\n * reconciliation or a stale `hidden` entry (only a real filter/sort/visibility/\n * order change counts as \"unsaved\").\n */\nexport function canonicalViewState(\n collection: CollectionDef,\n state: TableViewState,\n): TableViewState {\n return normalizeViewState(withResolvedOrder(collection, state))\n}\n\n/**\n * Toggle a column's visibility — position is preserved (EC-213): visibility\n * lives in `hidden`, the order in `columns` is untouched. Columns the saved\n * order predates are pinned into `columns` first so their place is stable.\n */\nexport function toggleColumn(\n collection: CollectionDef,\n state: TableViewState,\n field: string,\n): TableViewState {\n const base = withResolvedOrder(collection, state)\n const hidden = base.hidden.includes(field)\n ? base.hidden.filter((name) => name !== field)\n : [...base.hidden, field]\n return { ...base, hidden }\n}\n\n/** Move a column one position left (-1) or right (+1) in the full column order. */\nexport function moveColumn(\n collection: CollectionDef,\n state: TableViewState,\n field: string,\n delta: -1 | 1,\n): TableViewState {\n const base = withResolvedOrder(collection, state)\n const from = base.columns.indexOf(field)\n const to = from + delta\n if (from < 0 || to < 0 || to >= base.columns.length) return state\n const columns = [...base.columns]\n columns.splice(from, 1)\n columns.splice(to, 0, field)\n return { ...base, columns }\n}\n\n/** A fresh id for a saved view (uniqueness, not determinism, is the point). */\nexport function newViewId(): string {\n const random = Math.floor(Math.random() * 36 ** 6)\n .toString(36)\n .padStart(6, '0')\n return `view-${Date.now().toString(36)}-${random}`\n}\n"]}
1
+ {"version":3,"file":"view-model.js","sourceRoot":"","sources":["../../../src/lib/cms-views/view-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod'\nimport { documentDefaultLocale, resolveField, titleFieldOf } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef, LocaleConfig } from '@elytracms/core/cms-core'\nimport {\n FILTER_OPERATORS,\n compareListEntries,\n filterScalarOf,\n filterableFields,\n matchesCondition,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n FilterCondition,\n FilterOperator,\n FilterScalar,\n NormalizedCondition,\n WhereInput,\n} from '@elytracms/core/content'\n\n/**\n * The pure table-view model for the CMS collection table (EC-169).\n *\n * A view bundles three things: filters in the closed AD-3 vocabulary\n * (`eq`/`in`/`lt`/`lte`/`gt`/`gte`, AND-only — exactly `@elytracms/core/content`'s\n * query model, evaluated with the same matching/ordering primitives), a sort\n * (field + direction), and the visible/ordered columns. Everything here is\n * plain data + functions so filtering, dirtiness, and column edits are\n * testable without a DOM.\n *\n * Filters are offered ONLY on fields the schema declares `filterable: true`\n * (EC-143), plus one built-in option: the document's publication state. The\n * state is document metadata, not a schema field — it powers the\n * \"show me drafts → bulk publish\" working set and still speaks only the\n * closed vocabulary (`eq`/`in`).\n */\n\n// --- Schemas (single source of truth — persisted view records reuse these) ----\n\nexport const filterScalarSchema = z.union([z.string(), z.number(), z.boolean()])\n\nexport const filterClauseSchema = z.object({\n /** A schema field name, or {@link STATE_FIELD} for the publication state. */\n field: z.string().min(1),\n op: z.enum(FILTER_OPERATORS),\n /** `in` carries an array of scalars; every other operator one scalar. */\n value: z.union([filterScalarSchema, z.array(filterScalarSchema)]),\n})\nexport type FilterClause = z.infer<typeof filterClauseSchema>\n\nexport const viewSortSchema = z.object({\n field: z.string().min(1),\n dir: z.enum(['asc', 'desc']),\n})\nexport type ViewSort = z.infer<typeof viewSortSchema>\n\nexport const tableViewStateSchema = z.object({\n /** AND-only list of conditions (no OR, no nesting — vision AD-3). */\n filters: z.array(filterClauseSchema),\n sort: viewSortSchema.nullable(),\n /**\n * The canonical column order (field names) — EVERY column, visible or not\n * (EC-213). Order is independent of visibility: hiding then re-showing a\n * column keeps its place. Schema fields absent here resolve in at the end\n * (see {@link resolveColumns}).\n */\n columns: z.array(z.string().min(1)),\n /**\n * The subset of {@link TableViewState.columns} that is hidden. Defaults to\n * `[]` so views persisted before EC-213 (no `hidden` key) load unchanged.\n */\n hidden: z.array(z.string().min(1)).default([]),\n})\nexport type TableViewState = z.infer<typeof tableViewStateSchema>\n\n// --- The built-in publication-state filter column ------------------------------\n\n/**\n * Sentinel field key for the publication state. `$`-prefixed so it can never\n * collide with a schema field name (`fieldDefSchema` names are identifiers).\n */\nexport const STATE_FIELD = '$state'\n\nconst STATE_VALUES = ['draft', 'published'] as const\n\n/** One field the filter builder may offer. */\nexport interface FilterableFieldOption {\n /** Clause field key: a schema field name, or {@link STATE_FIELD}. */\n field: string\n label: string\n /** The schema field def, or `null` for the built-in publication state. */\n def: FieldDef | null\n}\n\nexport const STATE_OPTION: FilterableFieldOption = {\n field: STATE_FIELD,\n label: 'State',\n def: null,\n}\n\n/**\n * The closed list of fields the filter builder offers: exactly the schema's\n * `filterable: true` fields (EC-143) plus the publication state. Nothing else\n * is ever offered — the UI builds queries, it does not invent a language.\n */\nexport function filterableFieldOptions(collection: CollectionDef): FilterableFieldOption[] {\n const schemaOptions = filterableFields(collection).map((field) => ({\n field: field.name,\n label: field.form?.label ?? field.name,\n def: field,\n }))\n return [...schemaOptions, STATE_OPTION]\n}\n\nexport function filterOptionFor(\n collection: CollectionDef,\n field: string,\n): FilterableFieldOption | undefined {\n return filterableFieldOptions(collection).find((option) => option.field === field)\n}\n\n// --- Operators per field type ---------------------------------------------------\n\n/** Display order + labels for the closed vocabulary. */\nexport const OPERATOR_LABELS: Record<FilterOperator, string> = {\n eq: 'equals',\n in: 'is one of',\n lt: 'less than',\n lte: 'at most',\n gt: 'greater than',\n gte: 'at least',\n}\n\nconst ORDERED_OPERATORS: readonly FilterOperator[] = ['eq', 'in', 'lt', 'lte', 'gt', 'gte']\n\n/**\n * The vocabulary subset that makes sense for a field's type. Always a subset\n * of the closed `FILTER_OPERATORS` — never an extension.\n */\nexport function operatorsFor(option: FilterableFieldOption): FilterOperator[] {\n if (option.def === null) return ['eq', 'in'] // publication state\n switch (option.def.type) {\n case 'boolean':\n return ['eq']\n case 'select':\n case 'relation':\n case 'asset':\n return ['eq', 'in']\n default:\n return [...ORDERED_OPERATORS]\n }\n}\n\n/** A sensible starting clause when a field is added to the builder. */\nexport function defaultClause(option: FilterableFieldOption): FilterClause {\n if (option.def === null) return { field: STATE_FIELD, op: 'eq', value: 'published' }\n switch (option.def.type) {\n case 'boolean':\n return { field: option.field, op: 'eq', value: true }\n case 'number':\n return { field: option.field, op: 'eq', value: 0 }\n case 'select':\n return { field: option.field, op: 'eq', value: option.def.options[0]?.value ?? '' }\n default:\n return { field: option.field, op: 'eq', value: '' }\n }\n}\n\n// --- Text ⇄ scalar coercion (the value inputs of the builder) -------------------\n\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/\n\n/** Coerce one text input into the scalar a clause compares against. */\nexport function scalarFromText(\n option: FilterableFieldOption | undefined,\n raw: string,\n): FilterScalar {\n const type = option?.def?.type\n if (type === 'number') {\n const trimmed = raw.trim()\n if (trimmed === '') return raw // an emptied input must not become eq 0\n const parsed = Number(trimmed)\n return Number.isNaN(parsed) ? raw : parsed\n }\n if (type === 'boolean') return raw === 'true'\n // Stored dates are full ISO strings; a date-input value compares correctly\n // once expanded to midnight UTC (ISO strings order by code units).\n if (type === 'date' && DATE_ONLY.test(raw)) return `${raw}T00:00:00.000Z`\n return raw\n}\n\n/** Parse a comma-separated list for the `in` operator. */\nexport function scalarListFromText(\n option: FilterableFieldOption | undefined,\n raw: string,\n): FilterScalar[] {\n return raw\n .split(',')\n .map((part) => part.trim())\n .filter((part) => part !== '')\n .map((part) => scalarFromText(option, part))\n}\n\nexport function scalarListToText(value: FilterScalar | FilterScalar[]): string {\n const list = Array.isArray(value) ? value : [value]\n return list.map(String).join(', ')\n}\n\n// --- Clause validation -----------------------------------------------------------\n\n/** A structured problem with one clause (by index in the filter list). */\nexport interface ClauseIssue {\n index: number\n message: string\n}\n\n/**\n * Validate clauses against the collection schema. Schema-field clauses run\n * through the canonical `validateWhere` (EC-143: `filterable` enforcement +\n * operator/value shape); state clauses check the `eq`/`in` subset and the\n * draft/published value set. Invalid clauses (e.g. a persisted view whose\n * field is no longer filterable) are reported — never silently applied.\n */\nexport function validateClauses(\n collection: CollectionDef,\n clauses: readonly FilterClause[],\n): ClauseIssue[] {\n const issues: ClauseIssue[] = []\n clauses.forEach((clause, index) => {\n if (clause.field === STATE_FIELD) {\n if (clause.op !== 'eq' && clause.op !== 'in') {\n issues.push({ index, message: 'State filters support only \"eq\" and \"in\".' })\n return\n }\n const values = Array.isArray(clause.value) ? clause.value : [clause.value]\n if (!values.every((value) => (STATE_VALUES as readonly unknown[]).includes(value))) {\n issues.push({ index, message: 'State must be \"draft\" or \"published\".' })\n }\n return\n }\n const where: WhereInput = {\n [clause.field]: { [clause.op]: clause.value } as FilterCondition,\n }\n const errors = validateWhere(collection, where)\n if (errors.length > 0) issues.push({ index, message: errors[0]!.message })\n })\n return issues\n}\n\n// --- Matching & sorting (delegates to @elytracms/core/content's primitives) -------------\n\n/**\n * Published-ness source for the `$state` pseudo-field (EC-224): the version\n * POINTER, supplied by the caller as `cms.isPublished`. Defaults to \"nothing\n * published\" — a bare row carries no publish signal now that the flat `state`\n * field is gone, so the table (which knows the pointer) passes the real\n * predicate; fixtures/tests that don't surface `$state` get the safe default.\n */\nexport type IsPublished = (doc: CmsDocument) => boolean\nconst defaultIsPublished: IsPublished = () => false\n\n/** The scalar a document exposes for a clause/sort field, locale-resolved. */\nfunction scalarOf(\n collection: CollectionDef,\n doc: CmsDocument,\n field: string,\n localeConfig: LocaleConfig,\n isPublished: IsPublished,\n): FilterScalar | null {\n if (field === STATE_FIELD) return isPublished(doc) ? 'published' : 'draft'\n if (!collection.fields.some((f) => f.name === field)) return null\n const locale = documentDefaultLocale(doc, localeConfig)\n return filterScalarOf(resolveField(collection, doc, field, locale, localeConfig).value)\n}\n\n/** Does a document satisfy one clause? Same semantics as the content accessors. */\nexport function clauseMatches(\n collection: CollectionDef,\n doc: CmsDocument,\n localeConfig: LocaleConfig,\n clause: FilterClause,\n isPublished: IsPublished = defaultIsPublished,\n): boolean {\n const value = scalarOf(collection, doc, clause.field, localeConfig, isPublished)\n const condition = { [clause.op]: clause.value } as NormalizedCondition\n return matchesCondition(value, condition)\n}\n\n/**\n * Sort documents by one column in the canonical deterministic order\n * (`compareListEntries`: typed value order, id-ascending tiebreak). Unlike the\n * content accessors, the *table* may sort any rendered column — sorting is a\n * presentation concern here; the `filterable` flag governs filters.\n */\nexport function sortViewDocuments(\n collection: CollectionDef,\n docs: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n sort: ViewSort | null,\n isPublished: IsPublished = defaultIsPublished,\n): CmsDocument[] {\n if (sort === null) return [...docs]\n const positions = new Map(\n docs.map((doc) => [doc.id, scalarOf(collection, doc, sort.field, localeConfig, isPublished)]),\n )\n return [...docs].sort((a, b) =>\n compareListEntries(\n { id: a.id, sortValue: positions.get(a.id) ?? null },\n { id: b.id, sortValue: positions.get(b.id) ?? null },\n sort.dir,\n ),\n )\n}\n\n/** The filtered + sorted documents of a view, plus any clause issues. */\nexport interface AppliedViewState {\n docs: CmsDocument[]\n /** Clauses that failed validation — reported AND excluded from matching. */\n issues: ClauseIssue[]\n}\n\n/**\n * Apply a view state to a document list: validate clauses, AND-match the valid\n * ones, sort. Invalid clauses never filter silently — they come back as\n * issues for the UI to surface.\n */\nexport function applyViewState(\n collection: CollectionDef,\n docs: readonly CmsDocument[],\n localeConfig: LocaleConfig,\n state: TableViewState,\n isPublished: IsPublished = defaultIsPublished,\n): AppliedViewState {\n const issues = validateClauses(collection, state.filters)\n const invalid = new Set(issues.map((issue) => issue.index))\n const active = state.filters.filter((_, index) => !invalid.has(index))\n const matched = docs.filter((doc) =>\n active.every((clause) => clauseMatches(collection, doc, localeConfig, clause, isPublished)),\n )\n return {\n docs: sortViewDocuments(collection, matched, localeConfig, state.sort, isPublished),\n issues,\n }\n}\n\n// --- View state construction, equality, columns ----------------------------------\n\n/** The id of the unnamed built-in \"All documents\" view. */\nexport const ALL_VIEW_ID = 'all'\n\n/** The default (unnamed) view: no filters, no sort, every field visible. */\n/**\n * Field types kept OFF the default table columns (EC-246b): the composite editing\n * surfaces — a blocks canvas, rich text, a structured object/repeater — read as\n * noise in a scalar list. `filterable` no longer forces (or governs) a column; the\n * default lens is \"all scalars\". These stay in the column picker one toggle away,\n * and any named view may list them in its `columns`; they're just off by default.\n */\nconst NON_DEFAULT_COLUMN_TYPES = new Set<FieldDef['type']>(['blocks', 'richText', 'object'])\n\n/** The collection's composite (non-scalar) field names — hidden in the default view. */\nfunction nonScalarColumns(collection: CollectionDef): string[] {\n return collection.fields.filter((f) => NON_DEFAULT_COLUMN_TYPES.has(f.type)).map((f) => f.name)\n}\n\nexport function defaultViewState(collection: CollectionDef): TableViewState {\n return {\n filters: [],\n sort: null,\n // Every field keeps a slot (order + picker), but the composite surfaces start\n // hidden so the default lens is the collection's scalars (EC-246b).\n columns: orderedColumnNames(collection),\n hidden: nonScalarColumns(collection),\n }\n}\n\n/**\n * A config-declared view (EC-246b) — the flat authoring shape a collection\n * declares in `cms/` config: `{ name, filters?, sort?, columns?, hidden? }`.\n * Filters/sort/columns sit at the top level (no nested `state` wrapper). The\n * studio validates + interprets these; `cms-core` stores them opaquely (it\n * can't reference the filter vocabulary without a layering cycle).\n */\nexport const configViewSchema = z.object({\n /** Optional stable id; defaults to a slug of `name`. */\n id: z.string().min(1).optional(),\n name: z.string().min(1),\n filters: z.array(filterClauseSchema).optional(),\n sort: viewSortSchema.nullable().optional(),\n /** Column order; when omitted, the view inherits the collection's default columns. */\n columns: z.array(z.string().min(1)).optional(),\n hidden: z.array(z.string().min(1)).optional(),\n})\nexport type ConfigView = z.infer<typeof configViewSchema>\n\n/** A resolved view: identity + the table state it restores (matches `SavedView`). */\nexport interface ResolvedView {\n id: string\n name: string\n state: TableViewState\n}\n\n/** Slug used to derive a config view's stable id from its name. */\nfunction configViewId(name: string, index: number): string {\n const slug = name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n return `config:${slug || index}`\n}\n\n/**\n * Resolve a collection's config-declared `views` into full {@link ResolvedView}s\n * (EC-246b). Each is validated with {@link configViewSchema}; malformed entries\n * and duplicate ids are dropped (defensive). Columns fall back to the collection\n * default when a view doesn't pin them, so a filter-only view still renders every\n * column. Config views are shared and change only in\n * code — the runtime never mutates them.\n */\nexport function configViewsFor(collection: CollectionDef): ResolvedView[] {\n const declared = collection.views\n if (!Array.isArray(declared)) return []\n const base = defaultViewState(collection)\n const seen = new Set<string>()\n const resolved: ResolvedView[] = []\n declared.forEach((raw, index) => {\n const parsed = configViewSchema.safeParse(raw)\n if (!parsed.success) return\n const view = parsed.data\n const id = view.id ?? configViewId(view.name, index)\n if (seen.has(id)) return\n seen.add(id)\n resolved.push({\n id,\n name: view.name,\n state: normalizeViewState({\n filters: view.filters ?? [],\n sort: view.sort ?? null,\n columns: view.columns && view.columns.length > 0 ? view.columns : base.columns,\n hidden: view.hidden ?? [],\n }),\n })\n })\n return resolved\n}\n\n/**\n * The default column order: the schema's title field first (it becomes the\n * default title/link column — EC-213 drops the separate primary-column\n * selector, treating the first visible column as the title), then the\n * remaining fields in schema order.\n */\nfunction orderedColumnNames(collection: CollectionDef): string[] {\n const names = collection.fields.map((field) => field.name)\n const title = titleFieldOf(collection)?.name\n if (title === undefined || !names.includes(title)) return names\n return [title, ...names.filter((name) => name !== title)]\n}\n\n/** Deep-cloned canonical form (stable key order via the Zod shape). */\nexport function normalizeViewState(state: TableViewState): TableViewState {\n return tableViewStateSchema.parse(state)\n}\n\n/** Structural equality on normalized states — drives the \"unsaved changes\" affordance. */\nexport function viewStatesEqual(a: TableViewState, b: TableViewState): boolean {\n return JSON.stringify(normalizeViewState(a)) === JSON.stringify(normalizeViewState(b))\n}\n\n/** One column of a view resolved against the current schema (EC-213). */\nexport interface ResolvedColumn {\n /** The schema field name. */\n field: string\n /** Whether the column renders (`false` ⇒ in {@link TableViewState.hidden}). */\n visible: boolean\n}\n\n/**\n * Resolve a view's columns against the live schema (EC-213): every schema\n * field exactly once, in the view's saved order, with any fields the view\n * predates appended (visible) and any fields no longer in the schema dropped.\n * This is the single source of truth for \"which columns, in what order, shown\n * or hidden\" — order is preserved across hide/show because visibility lives in\n * `hidden`, never in the order.\n */\nexport function resolveColumns(\n collection: CollectionDef,\n state: TableViewState,\n): ResolvedColumn[] {\n const schemaNames = collection.fields.map((field) => field.name)\n const schema = new Set(schemaNames)\n const hidden = new Set(state.hidden)\n // Keep the saved order, restricted to live fields and de-duplicated (a\n // corrupted persisted state must never render one field as two columns).\n const ordered: string[] = []\n const seen = new Set<string>()\n for (const name of state.columns) {\n if (schema.has(name) && !seen.has(name)) {\n ordered.push(name)\n seen.add(name)\n }\n }\n const appended = schemaNames.filter((name) => !seen.has(name))\n return [...ordered, ...appended].map((field) => ({ field, visible: !hidden.has(field) }))\n}\n\n/** The visible columns of a view, in render order (EC-213). */\nexport function visibleColumns(collection: CollectionDef, state: TableViewState): string[] {\n return resolveColumns(collection, state)\n .filter((column) => column.visible)\n .map((column) => column.field)\n}\n\n/**\n * The title/link column — the document IDENTITY column (EC-242): the schema title\n * field, shown as the leftmost linked cell whose content is `resolveDocumentTitle`\n * (a string `titleField` or a function pattern). Always present regardless of\n * column visibility — it is the document's identity, not a toggleable data column —\n * and excluded from the data columns so the title never shows twice. Falls back to\n * the first visible column, then the first field, then `id`, when no title field\n * is resolvable (e.g. a function title with no backing `title` field).\n */\nexport function primaryColumn(collection: CollectionDef, state: TableViewState): string {\n return (\n titleFieldOf(collection)?.name ??\n visibleColumns(collection, state)[0] ??\n collection.fields[0]?.name ??\n 'id'\n )\n}\n\n/**\n * Reconcile `columns` to the full resolved order so column edits operate on a\n * complete, stable order (drops removed fields, pins appended ones), and prune\n * `hidden` to fields the live schema still has (a column removed from the schema\n * must not leave dead `hidden` cruft that trips dirty-detection / persists).\n */\nfunction withResolvedOrder(collection: CollectionDef, state: TableViewState): TableViewState {\n const columns = resolveColumns(collection, state).map((column) => column.field)\n const live = new Set(columns)\n const hidden = state.hidden.filter((name) => live.has(name))\n return { ...state, columns, hidden }\n}\n\n/**\n * The canonical, schema-resolved form of a view state — full column order with\n * `hidden` pruned to live fields (EC-213). Two states that render identically\n * share one canonical form, so dirty-detection never trips on a mere column\n * reconciliation or a stale `hidden` entry (only a real filter/sort/visibility/\n * order change counts as \"unsaved\").\n */\nexport function canonicalViewState(\n collection: CollectionDef,\n state: TableViewState,\n): TableViewState {\n return normalizeViewState(withResolvedOrder(collection, state))\n}\n\n/**\n * Toggle a column's visibility — position is preserved (EC-213): visibility\n * lives in `hidden`, the order in `columns` is untouched. Columns the saved\n * order predates are pinned into `columns` first so their place is stable.\n */\nexport function toggleColumn(\n collection: CollectionDef,\n state: TableViewState,\n field: string,\n): TableViewState {\n const base = withResolvedOrder(collection, state)\n const hidden = base.hidden.includes(field)\n ? base.hidden.filter((name) => name !== field)\n : [...base.hidden, field]\n return { ...base, hidden }\n}\n\n/** Move a column one position left (-1) or right (+1) in the full column order. */\nexport function moveColumn(\n collection: CollectionDef,\n state: TableViewState,\n field: string,\n delta: -1 | 1,\n): TableViewState {\n const base = withResolvedOrder(collection, state)\n const from = base.columns.indexOf(field)\n const to = from + delta\n if (from < 0 || to < 0 || to >= base.columns.length) return state\n const columns = [...base.columns]\n columns.splice(from, 1)\n columns.splice(to, 0, field)\n return { ...base, columns }\n}\n\n/** A fresh id for a saved view (uniqueness, not determinism, is the point). */\nexport function newViewId(): string {\n const random = Math.floor(Math.random() * 36 ** 6)\n .toString(36)\n .padStart(6, '0')\n return `view-${Date.now().toString(36)}-${random}`\n}\n"]}
@@ -0,0 +1,251 @@
1
+ import type { CmsDocument, CollectionDef, DocumentRef, DocumentUrl, FieldDef, Locale, LocaleConfig, RestoreScope } from '@elytracms/core/cms-core';
2
+ import type { CmsValidationIssue } from '@elytracms/core/cms-core';
3
+ import type { OperationError } from '@elytracms/core/operations';
4
+ import type { TableViewState } from '../cms-views/view-model';
5
+ import type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit';
6
+ import type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions';
7
+ import type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store';
8
+ /**
9
+ * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,
10
+ * vision AD-19).
11
+ *
12
+ * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are
13
+ * the studio's universal interaction surfaces. Historically they read the
14
+ * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.
15
+ * This interface is the data-agnostic seam they render over instead, so the SAME
16
+ * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app
17
+ * edit-shell, and any future rail — no hand-rolled UI per rail.
18
+ *
19
+ * **Shape:** the source is scoped to ONE collection (it carries `schema` +
20
+ * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces
21
+ * actually use — dropping the `collectionId` argument (the source already knows
22
+ * it). Keeping the names/signatures aligned makes the CMS adapter a
23
+ * behaviour-preserving pass-through and the migration near-mechanical.
24
+ *
25
+ * **Referential stability:** the surfaces put `documents`, `isPublished`,
26
+ * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST
27
+ * return stable references across renders (delegate to the underlying store's
28
+ * already-stable methods; never wrap in a fresh closure each render) or memos
29
+ * bust and the inline-edit order snapshot / perf regress.
30
+ *
31
+ * **Capabilities:** flags gate whole shell regions AND method presence — a source
32
+ * with `capabilities.versions === false` (the users rail) never mounts the
33
+ * Versions pane, so no `listVersions` read fires on credential records. Optional
34
+ * method groups are present iff the flag is set.
35
+ */
36
+ export interface CollectionViewSource {
37
+ /** The rail/collection this source serves. */
38
+ readonly collectionId: string;
39
+ /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */
40
+ readonly schema: CollectionDef;
41
+ /** Which regions/methods this source supports. */
42
+ readonly capabilities: CollectionViewCapabilities;
43
+ /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */
44
+ readonly projectId?: string;
45
+ /**
46
+ * The collection id assets live in (for a cell/field's `titleOf` label fallback
47
+ * when {@link resolveAsset} finds no record). Present only for sources that
48
+ * reference assets — a rail without assets (users) omits it.
49
+ */
50
+ readonly assetCollectionId?: string;
51
+ /** The ambient locale lens (only meaningful when `capabilities.locales`). */
52
+ readonly localeConfig: LocaleConfig;
53
+ readonly activeLocale: Locale;
54
+ /** The full row set (view mechanics filter/sort this client-side). */
55
+ documents(): CmsDocument[];
56
+ get(ref: DocumentRef): CmsDocument | undefined;
57
+ /** A relation/asset target's display title (hides the string-vs-function `titleField`). */
58
+ titleOf(ref: DocumentRef): string;
59
+ /**
60
+ * List the candidate records of a relation field's declared `target` collection
61
+ * — the pick-list for a relation picker (EC-072). References are first-class and
62
+ * collection-based: the field's schema declares which collection(s) it targets,
63
+ * and this resolves those rows for ANY target (cross-collection), not just this
64
+ * source's own collection. The caller passes `field.target`; a multi-target field
65
+ * (future) merges one call per declared target. An unknown/empty target yields `[]`.
66
+ */
67
+ referenceCandidates(collectionId: string): CmsDocument[];
68
+ /**
69
+ * The workspace schema a reference/asset can point at — the schema counterpart to
70
+ * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's
71
+ * fields (an asset field editing the asset collection's own metadata; the reuse
72
+ * capability correlating embed manifests against their target collection). A
73
+ * reference is collection-based, so resolving one sometimes needs the target's
74
+ * schema, not just its rows.
75
+ */
76
+ readonly collections: readonly CollectionDef[];
77
+ isFieldReadOnly(field: FieldDef): boolean;
78
+ /**
79
+ * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`
80
+ * (visible but read-only), or `hidden` (not rendered for this principal). This is
81
+ * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`
82
+ * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a
83
+ * sensitive field (`hidden`). A single tri-state (never two independent flags) so
84
+ * the nonsensical writable-but-invisible state can't be expressed. Optional:
85
+ * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open
86
+ * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the
87
+ * source; the starter's policy flows into its construction (engine = mechanism).
88
+ * Prefer {@link resolveFieldAccess} over calling this directly (it applies the
89
+ * default-open fallback).
90
+ */
91
+ fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess;
92
+ validate(doc: CmsDocument): CmsValidationIssue[];
93
+ /**
94
+ * Resolve an asset id to its display metadata for a cell/field, or `undefined`
95
+ * when there is no asset backend (a source without assets degrades to a label
96
+ * via {@link titleOf}, matching today's "no AssetsProvider" behaviour).
97
+ */
98
+ resolveAsset?(id: string): ResolvedAsset | undefined;
99
+ /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */
100
+ readonly lastError: OperationError | null;
101
+ clearLastError(): void;
102
+ /**
103
+ * Land any queued/optimistic writes so a subsequent pre-flight READ sees them
104
+ * (the reference index is maintained on write). A source with no async
105
+ * persistence omits it — its reads are already authoritative.
106
+ */
107
+ flush?(): Promise<void>;
108
+ /**
109
+ * The studio route splat for a record's detail surface (the row link + the
110
+ * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.
111
+ * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail
112
+ * has no detail surface, so rows render as plain text (no navigation).
113
+ */
114
+ detailPath?(id: string): string;
115
+ /**
116
+ * The studio route splat for this rail's LIST surface — the edit shell's
117
+ * "back to list" breadcrumb crumb and its post-delete navigation target, e.g.
118
+ * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never
119
+ * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`
120
+ * ⇒ the crumb is unlinked and delete falls back to the project index.
121
+ */
122
+ readonly listPath?: string;
123
+ viewState(): TableViewState;
124
+ setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void;
125
+ create(): DocumentRef;
126
+ save(doc: CmsDocument): void;
127
+ remove(ref: DocumentRef): void;
128
+ /**
129
+ * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one
130
+ * command PER item, per-item outcomes enumerated in the report (partial failure
131
+ * is never silent).
132
+ */
133
+ bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>;
134
+ /** `publish` — published-pool predicate + publish/unpublish. */
135
+ readonly publishing?: PublishingCapability;
136
+ /** `versions` — version history + restore. */
137
+ readonly versioning?: VersioningCapability;
138
+ /** `inlineEdit` — the list's single-command optimistic cell write. */
139
+ readonly inlineEditing?: InlineEditCapability;
140
+ /**
141
+ * Config-as-code named lenses (EC-246b) the header view selector switches
142
+ * between. Optional: a source that declares no named views (a non-CMS rail
143
+ * like users) omits it, and the selector renders nothing (single default).
144
+ */
145
+ readonly views?: NamedViewsCapability;
146
+ /**
147
+ * `importExport` — JSON round-trip of the collection's records. CMS-specific;
148
+ * a rail whose records are credential-bearing (users) omits it, so neither the
149
+ * import dialog nor the export action mounts (identities aren't dumped to JSON).
150
+ */
151
+ readonly imports?: ImportCapability;
152
+ }
153
+ /**
154
+ * The capability matrix. Each flag gates a shell region AND the presence of its
155
+ * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`
156
+ * (credential caution), `create:false` (identity minted by sign-up), and no
157
+ * `liveUrl`/`hierarchy`.
158
+ */
159
+ export interface CollectionViewCapabilities {
160
+ readonly publish: boolean;
161
+ readonly versions: boolean;
162
+ readonly locales: boolean;
163
+ readonly inlineEdit: boolean;
164
+ readonly create: boolean;
165
+ readonly remove: boolean;
166
+ readonly bulk: boolean;
167
+ /** The derived live-URL column (published pool × deployment routes). */
168
+ readonly liveUrl: boolean;
169
+ /** Parent-self-relation tree mode (hierarchical collections only). */
170
+ readonly hierarchy: boolean;
171
+ /** JSON import/export of the collection's records (off for credential rails). */
172
+ readonly importExport: boolean;
173
+ /**
174
+ * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The
175
+ * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.
176
+ */
177
+ readonly adminOnly?: boolean;
178
+ }
179
+ /**
180
+ * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility
181
+ * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not
182
+ * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;
183
+ * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is
184
+ * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).
185
+ */
186
+ export type FieldAccess = 'edit' | 'read' | 'hidden';
187
+ /**
188
+ * Resolve a field's effective principal access for a source — its {@link
189
+ * CollectionViewSource.fieldAccess} policy when present, else derived from the
190
+ * boolean `isFieldReadOnly` (default-open). The one place the shell asks "may this
191
+ * principal edit / see this field?", so the default-open fallback lives in one spot.
192
+ */
193
+ export declare function resolveFieldAccess(source: CollectionViewSource, field: FieldDef, record?: CmsDocument): FieldAccess;
194
+ /** Asset display metadata a cell/field renders (thumbnail or labelled badge). */
195
+ export interface ResolvedAsset {
196
+ readonly url?: string;
197
+ readonly contentType?: string;
198
+ readonly title?: string;
199
+ readonly filename?: string;
200
+ readonly alt?: string;
201
+ }
202
+ /** `publish` capability. */
203
+ export interface PublishingCapability {
204
+ /** Published-ness from the version pointer (NOT a row field). */
205
+ isPublished(ref: DocumentRef): boolean;
206
+ publish(ref: DocumentRef): Promise<void>;
207
+ unpublish(ref: DocumentRef): Promise<void>;
208
+ /**
209
+ * Live URLs for the routable documents, keyed by id — derived from the
210
+ * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't
211
+ * leak into a live link (EC-205/EC-216). `null` when the collection is not
212
+ * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.
213
+ */
214
+ liveUrls?(): Map<string, DocumentUrl> | null;
215
+ }
216
+ /** `versions` capability. */
217
+ export interface VersioningCapability {
218
+ restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>;
219
+ /**
220
+ * The document's recorded version history + live pin. Folds the flush-before-read
221
+ * ordering INTO the loader (the adapter's concern), so every fetch lists an
222
+ * authoritative history regardless of which surface triggered the change (EC-224).
223
+ */
224
+ load(ref: DocumentRef): Promise<DocumentVersionsView>;
225
+ }
226
+ /** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */
227
+ export interface InlineEditCapability {
228
+ commitField(input: CellCommitInput): Promise<CellCommitOutcome>;
229
+ }
230
+ /** A named lens the header selector offers — id + display name only (the header never needs its state). */
231
+ export interface NamedView {
232
+ readonly id: string;
233
+ readonly name: string;
234
+ }
235
+ /** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */
236
+ export interface ImportCapability {
237
+ importDocuments(items: unknown[]): ImportReport;
238
+ }
239
+ /**
240
+ * `views` capability: config-as-code named lenses (EC-246b). The header selector
241
+ * only *picks* a lens — it never creates/edits/deletes one (shared views are code).
242
+ */
243
+ export interface NamedViewsCapability {
244
+ /** The built-in default + config-declared named views. */
245
+ list(): readonly NamedView[];
246
+ /** The currently-selected view id. */
247
+ activeId(): string;
248
+ /** Switch views (resets the working table state to the view's baseline). */
249
+ setActive(id: string): void;
250
+ }
251
+ export type { CellCommitInput, CellCommitOutcome, ImportReport };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Resolve a field's effective principal access for a source — its {@link
3
+ * CollectionViewSource.fieldAccess} policy when present, else derived from the
4
+ * boolean `isFieldReadOnly` (default-open). The one place the shell asks "may this
5
+ * principal edit / see this field?", so the default-open fallback lives in one spot.
6
+ */
7
+ export function resolveFieldAccess(source, field, record) {
8
+ return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit');
9
+ }
10
+ //# sourceMappingURL=collection-view-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-view-source.js","sourceRoot":"","sources":["../../../src/lib/collection-view/collection-view-source.ts"],"names":[],"mappings":"AAkNA;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA4B,EAC5B,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACjG,CAAC","sourcesContent":["import type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n DocumentUrl,\n FieldDef,\n Locale,\n LocaleConfig,\n RestoreScope,\n} from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { TableViewState } from '../cms-views/view-model'\n// The concrete inline-edit + bulk vocabulary (Stream A relocates it into `lib` as it settles).\nimport type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit'\nimport type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions'\nimport type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store'\n\n/**\n * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,\n * vision AD-19).\n *\n * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are\n * the studio's universal interaction surfaces. Historically they read the\n * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.\n * This interface is the data-agnostic seam they render over instead, so the SAME\n * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app\n * edit-shell, and any future rail — no hand-rolled UI per rail.\n *\n * **Shape:** the source is scoped to ONE collection (it carries `schema` +\n * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces\n * actually use — dropping the `collectionId` argument (the source already knows\n * it). Keeping the names/signatures aligned makes the CMS adapter a\n * behaviour-preserving pass-through and the migration near-mechanical.\n *\n * **Referential stability:** the surfaces put `documents`, `isPublished`,\n * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST\n * return stable references across renders (delegate to the underlying store's\n * already-stable methods; never wrap in a fresh closure each render) or memos\n * bust and the inline-edit order snapshot / perf regress.\n *\n * **Capabilities:** flags gate whole shell regions AND method presence — a source\n * with `capabilities.versions === false` (the users rail) never mounts the\n * Versions pane, so no `listVersions` read fires on credential records. Optional\n * method groups are present iff the flag is set.\n */\nexport interface CollectionViewSource {\n /** The rail/collection this source serves. */\n readonly collectionId: string\n /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */\n readonly schema: CollectionDef\n /** Which regions/methods this source supports. */\n readonly capabilities: CollectionViewCapabilities\n\n /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */\n readonly projectId?: string\n /**\n * The collection id assets live in (for a cell/field's `titleOf` label fallback\n * when {@link resolveAsset} finds no record). Present only for sources that\n * reference assets — a rail without assets (users) omits it.\n */\n readonly assetCollectionId?: string\n /** The ambient locale lens (only meaningful when `capabilities.locales`). */\n readonly localeConfig: LocaleConfig\n readonly activeLocale: Locale\n\n // --- Reads -----------------------------------------------------------------\n /** The full row set (view mechanics filter/sort this client-side). */\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n /** A relation/asset target's display title (hides the string-vs-function `titleField`). */\n titleOf(ref: DocumentRef): string\n /**\n * List the candidate records of a relation field's declared `target` collection\n * — the pick-list for a relation picker (EC-072). References are first-class and\n * collection-based: the field's schema declares which collection(s) it targets,\n * and this resolves those rows for ANY target (cross-collection), not just this\n * source's own collection. The caller passes `field.target`; a multi-target field\n * (future) merges one call per declared target. An unknown/empty target yields `[]`.\n */\n referenceCandidates(collectionId: string): CmsDocument[]\n /**\n * The workspace schema a reference/asset can point at — the schema counterpart to\n * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's\n * fields (an asset field editing the asset collection's own metadata; the reuse\n * capability correlating embed manifests against their target collection). A\n * reference is collection-based, so resolving one sometimes needs the target's\n * schema, not just its rows.\n */\n readonly collections: readonly CollectionDef[]\n isFieldReadOnly(field: FieldDef): boolean\n /**\n * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`\n * (visible but read-only), or `hidden` (not rendered for this principal). This is\n * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`\n * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a\n * sensitive field (`hidden`). A single tri-state (never two independent flags) so\n * the nonsensical writable-but-invisible state can't be expressed. Optional:\n * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open\n * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the\n * source; the starter's policy flows into its construction (engine = mechanism).\n * Prefer {@link resolveFieldAccess} over calling this directly (it applies the\n * default-open fallback).\n */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Resolve an asset id to its display metadata for a cell/field, or `undefined`\n * when there is no asset backend (a source without assets degrades to a label\n * via {@link titleOf}, matching today's \"no AssetsProvider\" behaviour).\n */\n resolveAsset?(id: string): ResolvedAsset | undefined\n /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */\n readonly lastError: OperationError | null\n clearLastError(): void\n /**\n * Land any queued/optimistic writes so a subsequent pre-flight READ sees them\n * (the reference index is maintained on write). A source with no async\n * persistence omits it — its reads are already authoritative.\n */\n flush?(): Promise<void>\n\n /**\n * The studio route splat for a record's detail surface (the row link + the\n * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.\n * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail\n * has no detail surface, so rows render as plain text (no navigation).\n */\n detailPath?(id: string): string\n /**\n * The studio route splat for this rail's LIST surface — the edit shell's\n * \"back to list\" breadcrumb crumb and its post-delete navigation target, e.g.\n * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never\n * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`\n * ⇒ the crumb is unlinked and delete falls back to the project index.\n */\n readonly listPath?: string\n\n // --- View state (list) -----------------------------------------------------\n viewState(): TableViewState\n setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void\n\n // --- Writes ----------------------------------------------------------------\n create(): DocumentRef\n save(doc: CmsDocument): void\n remove(ref: DocumentRef): void\n /**\n * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one\n * command PER item, per-item outcomes enumerated in the report (partial failure\n * is never silent).\n */\n bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>\n\n // --- Capability method groups (present iff the matching flag is set) --------\n /** `publish` — published-pool predicate + publish/unpublish. */\n readonly publishing?: PublishingCapability\n /** `versions` — version history + restore. */\n readonly versioning?: VersioningCapability\n /** `inlineEdit` — the list's single-command optimistic cell write. */\n readonly inlineEditing?: InlineEditCapability\n /**\n * Config-as-code named lenses (EC-246b) the header view selector switches\n * between. Optional: a source that declares no named views (a non-CMS rail\n * like users) omits it, and the selector renders nothing (single default).\n */\n readonly views?: NamedViewsCapability\n /**\n * `importExport` — JSON round-trip of the collection's records. CMS-specific;\n * a rail whose records are credential-bearing (users) omits it, so neither the\n * import dialog nor the export action mounts (identities aren't dumped to JSON).\n */\n readonly imports?: ImportCapability\n}\n\n/**\n * The capability matrix. Each flag gates a shell region AND the presence of its\n * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`\n * (credential caution), `create:false` (identity minted by sign-up), and no\n * `liveUrl`/`hierarchy`.\n */\nexport interface CollectionViewCapabilities {\n readonly publish: boolean\n readonly versions: boolean\n readonly locales: boolean\n readonly inlineEdit: boolean\n readonly create: boolean\n readonly remove: boolean\n readonly bulk: boolean\n /** The derived live-URL column (published pool × deployment routes). */\n readonly liveUrl: boolean\n /** Parent-self-relation tree mode (hierarchical collections only). */\n readonly hierarchy: boolean\n /** JSON import/export of the collection's records (off for credential rails). */\n readonly importExport: boolean\n /**\n * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The\n * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.\n */\n readonly adminOnly?: boolean\n}\n\n/**\n * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility\n * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not\n * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;\n * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is\n * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).\n */\nexport type FieldAccess = 'edit' | 'read' | 'hidden'\n\n/**\n * Resolve a field's effective principal access for a source — its {@link\n * CollectionViewSource.fieldAccess} policy when present, else derived from the\n * boolean `isFieldReadOnly` (default-open). The one place the shell asks \"may this\n * principal edit / see this field?\", so the default-open fallback lives in one spot.\n */\nexport function resolveFieldAccess(\n source: CollectionViewSource,\n field: FieldDef,\n record?: CmsDocument,\n): FieldAccess {\n return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit')\n}\n\n/** Asset display metadata a cell/field renders (thumbnail or labelled badge). */\nexport interface ResolvedAsset {\n readonly url?: string\n readonly contentType?: string\n readonly title?: string\n readonly filename?: string\n readonly alt?: string\n}\n\n/** `publish` capability. */\nexport interface PublishingCapability {\n /** Published-ness from the version pointer (NOT a row field). */\n isPublished(ref: DocumentRef): boolean\n publish(ref: DocumentRef): Promise<void>\n unpublish(ref: DocumentRef): Promise<void>\n /**\n * Live URLs for the routable documents, keyed by id — derived from the\n * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't\n * leak into a live link (EC-205/EC-216). `null` when the collection is not\n * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.\n */\n liveUrls?(): Map<string, DocumentUrl> | null\n}\n\n/** `versions` capability. */\nexport interface VersioningCapability {\n restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>\n /**\n * The document's recorded version history + live pin. Folds the flush-before-read\n * ordering INTO the loader (the adapter's concern), so every fetch lists an\n * authoritative history regardless of which surface triggered the change (EC-224).\n */\n load(ref: DocumentRef): Promise<DocumentVersionsView>\n}\n\n/** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */\nexport interface InlineEditCapability {\n commitField(input: CellCommitInput): Promise<CellCommitOutcome>\n}\n\n/** A named lens the header selector offers — id + display name only (the header never needs its state). */\nexport interface NamedView {\n readonly id: string\n readonly name: string\n}\n\n/** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */\nexport interface ImportCapability {\n importDocuments(items: unknown[]): ImportReport\n}\n\n/**\n * `views` capability: config-as-code named lenses (EC-246b). The header selector\n * only *picks* a lens — it never creates/edits/deletes one (shared views are code).\n */\nexport interface NamedViewsCapability {\n /** The built-in default + config-declared named views. */\n list(): readonly NamedView[]\n /** The currently-selected view id. */\n activeId(): string\n /** Switch views (resets the working table state to the view's baseline). */\n setActive(id: string): void\n}\n\nexport type { CellCommitInput, CellCommitOutcome, ImportReport }\n"]}
@@ -1,6 +1,13 @@
1
1
  import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
2
  import type { AssetRecord } from '@elytracms/core/persistence';
3
3
  import type { OperationResult, OperationsClient } from './operations-package';
4
+ /**
5
+ * Legacy marker (EC-246b): editor-owned saved views are retired — list
6
+ * presentation is config-as-code now (named views live in `cms/`). The id is kept
7
+ * only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b
8
+ * deployment persisted, so they never surface as content.
9
+ */
10
+ export declare const VIEWS_COLLECTION = "cms-saved-views";
4
11
  /**
5
12
  * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).
6
13
  *
@@ -1,6 +1,12 @@
1
1
  import { documentKey } from '@elytracms/core/cms-core';
2
2
  import { DATA_SOURCES_COLLECTION } from '../data/sources-store';
3
- import { VIEWS_COLLECTION } from '../cms-views/views-store';
3
+ /**
4
+ * Legacy marker (EC-246b): editor-owned saved views are retired — list
5
+ * presentation is config-as-code now (named views live in `cms/`). The id is kept
6
+ * only so hydration keeps excluding any `cms-saved-views` documents a pre-EC-246b
7
+ * deployment persisted, so they never surface as content.
8
+ */
9
+ export const VIEWS_COLLECTION = 'cms-saved-views';
4
10
  /**
5
11
  * Persisted-session hydration (Milestone B sign-off fix, EC-177 gap 1).
6
12
  *