@elytracms/studio 0.0.73 → 0.0.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/cms.ts +67 -3
- package/convex/schema.ts +9 -0
- package/dist/components/app-views/default-modules.js +1 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/cms/cms-labels.d.ts +6 -0
- package/dist/components/cms/cms-labels.js +21 -1
- package/dist/components/cms/cms-labels.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +33 -1
- package/dist/components/cms/cms-store.js +115 -18
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +89 -11
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +16 -0
- package/dist/components/cms/column-width.js +26 -0
- package/dist/components/cms/column-width.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +46 -0
- package/dist/components/cms/doc-state.js +47 -1
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/versions-model.d.ts +13 -0
- package/dist/components/cms/versions-model.js +44 -0
- package/dist/components/cms/versions-model.js.map +1 -1
- package/dist/components/cms/versions-pane.js +27 -6
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +16 -6
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +8 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +80 -12
- package/dist/lib/cms-views/view-model.js +170 -44
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +29 -0
- package/dist/lib/collection-view/collection-view-source.js +15 -0
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +8 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +5 -0
- package/dist/lib/i18n/messages.en.js +11 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +19 -0
- package/dist/lib/operations/session-hydration.js +9 -0
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +9 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/status-dot.d.ts +50 -0
- package/dist/ui/status-dot.js +38 -0
- package/dist/ui/status-dot.js.map +1 -0
- package/package.json +2 -2
|
@@ -76,6 +76,74 @@ export type TableViewState = z.infer<typeof tableViewStateSchema>;
|
|
|
76
76
|
* collide with a schema field name (`fieldDefSchema` names are identifiers).
|
|
77
77
|
*/
|
|
78
78
|
export declare const STATE_FIELD = "$state";
|
|
79
|
+
/**
|
|
80
|
+
* Reserved pseudo-COLUMN keys — the columns-axis siblings of the filter-axis
|
|
81
|
+
* {@link STATE_FIELD}. `$`-prefixed so they can never collide with a schema field
|
|
82
|
+
* name; they participate in a view's `columns` list AND the Display picker exactly
|
|
83
|
+
* like a schema field (orderable / hideable / includable), but they are NOT drawn
|
|
84
|
+
* from `collection.fields` — the column UNIVERSE is `[...schemaFields, ...pseudoColumns]`.
|
|
85
|
+
*
|
|
86
|
+
* - `$state` — publication status. A reserved `$`-key, but NO LONGER a reorderable
|
|
87
|
+
* column: EC-448 promoted it to a FIXED structural leading column (checkbox →
|
|
88
|
+
* status-dot → title), so it is NOT emitted by {@link pseudoColumns} and never
|
|
89
|
+
* participates in the reorderable columns/hidden/order model. It stays a reserved
|
|
90
|
+
* key (filter + sort) and {@link isPseudoColumn} still recognizes it, so an authored
|
|
91
|
+
* view listing `$state` is a harmless no-op (see {@link resolveConfigColumns}).
|
|
92
|
+
* - `$url` — the derived delivery URL (routable collections only; NON-sortable).
|
|
93
|
+
* - `$updatedAt` — the record's last-write timestamp.
|
|
94
|
+
* - `$publishedAt` — the record's FIRST-publish instant (EC-447; opt-in, HIDDEN by
|
|
95
|
+
* default). Now materialized end-to-end (`cmsDocuments.publishedAt` → source
|
|
96
|
+
* `timestamps.publishedAt`) and emitted by {@link pseudoColumns} when the source
|
|
97
|
+
* both materializes timestamps and has a publish lifecycle.
|
|
98
|
+
*/
|
|
99
|
+
export declare const URL_FIELD = "$url";
|
|
100
|
+
export declare const PUBLISHED_AT_FIELD = "$publishedAt";
|
|
101
|
+
export declare const UPDATED_AT_FIELD = "$updatedAt";
|
|
102
|
+
/** Is `name` a reserved pseudo-column (a `$`-key, not a schema field)? */
|
|
103
|
+
export declare function isPseudoColumn(name: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Capability signals that make a pseudo-column applicable to a collection/source
|
|
106
|
+
* (EC-445). Supplied by the caller from the portable source — the model stays pure.
|
|
107
|
+
*/
|
|
108
|
+
export interface PseudoColumnOptions {
|
|
109
|
+
/** The source materializes per-document timestamps → `$updatedAt` applies. */
|
|
110
|
+
timestamps?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* The collection has a publish lifecycle. Gates `$publishedAt` (with `timestamps`).
|
|
113
|
+
* EC-448: it no longer gates a reorderable `$state` column — `$state` is the FIXED
|
|
114
|
+
* structural status column, gated by the same signal at the render site, not here.
|
|
115
|
+
*/
|
|
116
|
+
publishing?: boolean;
|
|
117
|
+
/** The collection resolves delivery URLs → `$url` applies (mirrors `urlByDocId`). */
|
|
118
|
+
routable?: boolean;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The reserved REORDERABLE pseudo-columns that APPLY to a collection, in their default
|
|
122
|
+
* appended order (EC-445/EC-447): `$updatedAt`, `$publishedAt`, then `$url`. `$url` is
|
|
123
|
+
* gated on `routable` (a non-routable collection has no URL column, not an empty one),
|
|
124
|
+
* `$updatedAt` on `timestamps`. `$publishedAt` (EC-447) needs BOTH `timestamps` (its
|
|
125
|
+
* materialized value) AND `publishing` (a publish lifecycle to have a publish date) —
|
|
126
|
+
* it is OPT-IN, HIDDEN by default (`DEFAULT_HIDDEN_PSEUDO_COLUMNS`), so it joins the
|
|
127
|
+
* universe/picker without appearing in the baseline lens.
|
|
128
|
+
*
|
|
129
|
+
* NOTE (EC-448): `$state` is NO LONGER emitted here. It became a FIXED structural
|
|
130
|
+
* leading column (checkbox → status-dot → title), rendered by `collection-table.tsx`
|
|
131
|
+
* regardless of the reorderable model — so it is never in the order/hide/picker set,
|
|
132
|
+
* even though `publishing` is what gates BOTH it and the status column's presence.
|
|
133
|
+
* `opts.publishing` therefore only affects `$publishedAt` here. Pure — a caller
|
|
134
|
+
* (`pseudoColumnsForSource`) supplies the signals from the source.
|
|
135
|
+
*/
|
|
136
|
+
export declare function pseudoColumns(collection: CollectionDef, opts?: PseudoColumnOptions): string[];
|
|
137
|
+
/**
|
|
138
|
+
* The per-document metadata accessors the pseudo-DATE columns sort by (EC-445) — the
|
|
139
|
+
* columns-axis analogue of {@link IsPublished}. Threaded into the sort machinery so a
|
|
140
|
+
* `$updatedAt`/`$publishedAt` sort orders by the source's materialized timestamp; a
|
|
141
|
+
* missing accessor (fixtures/tests) sorts those rows last. `$url` is derived → never sorted.
|
|
142
|
+
*/
|
|
143
|
+
export interface PseudoColumnMeta {
|
|
144
|
+
updatedAt?(doc: CmsDocument): string | undefined;
|
|
145
|
+
publishedAt?(doc: CmsDocument): string | undefined;
|
|
146
|
+
}
|
|
79
147
|
/** One field the filter builder may offer. */
|
|
80
148
|
export interface FilterableFieldOption {
|
|
81
149
|
/** Clause field key: a schema field name, or {@link STATE_FIELD}. */
|
|
@@ -173,7 +241,7 @@ export declare function clauseMatches(collection: CollectionDef, doc: CmsDocumen
|
|
|
173
241
|
* content accessors, the *table* may sort any rendered column — sorting is a
|
|
174
242
|
* presentation concern here; the `filterable` flag governs filters.
|
|
175
243
|
*/
|
|
176
|
-
export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, sort: ViewSort | null, isPublished?: IsPublished): CmsDocument[];
|
|
244
|
+
export declare function sortViewDocuments(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, sort: ViewSort | null, isPublished?: IsPublished, meta?: PseudoColumnMeta): CmsDocument[];
|
|
177
245
|
/** The filtered + sorted documents of a view, plus any clause issues. */
|
|
178
246
|
export interface AppliedViewState {
|
|
179
247
|
docs: CmsDocument[];
|
|
@@ -185,10 +253,10 @@ export interface AppliedViewState {
|
|
|
185
253
|
* ones, sort. Invalid clauses never filter silently — they come back as
|
|
186
254
|
* issues for the UI to surface.
|
|
187
255
|
*/
|
|
188
|
-
export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, state: TableViewState, isPublished?: IsPublished): AppliedViewState;
|
|
256
|
+
export declare function applyViewState(collection: CollectionDef, docs: readonly CmsDocument[], localeConfig: LocaleConfig, state: TableViewState, isPublished?: IsPublished, meta?: PseudoColumnMeta): AppliedViewState;
|
|
189
257
|
/** The id of the synthesized built-in "All entries" baseline view (EC-444). */
|
|
190
258
|
export declare const ALL_VIEW_ID = "all";
|
|
191
|
-
export declare function defaultViewState(collection: CollectionDef): TableViewState;
|
|
259
|
+
export declare function defaultViewState(collection: CollectionDef, pseudo?: readonly string[]): TableViewState;
|
|
192
260
|
/**
|
|
193
261
|
* Resolve an authored VISIBLE column list (EC-444) into the internal two-axis
|
|
194
262
|
* `{columns, hidden}` table state. The authored list is exactly the columns shown,
|
|
@@ -198,7 +266,7 @@ export declare function defaultViewState(collection: CollectionDef): TableViewSt
|
|
|
198
266
|
* inherits the collection's default lens (all scalars visible, composites hidden).
|
|
199
267
|
* Unknown names in the authored list (a field the schema no longer has) are dropped.
|
|
200
268
|
*/
|
|
201
|
-
export declare function resolveConfigColumns(collection: CollectionDef, visible: readonly string[] | undefined): Pick<TableViewState, 'columns' | 'hidden'>;
|
|
269
|
+
export declare function resolveConfigColumns(collection: CollectionDef, visible: readonly string[] | undefined, pseudo?: readonly string[]): Pick<TableViewState, 'columns' | 'hidden'>;
|
|
202
270
|
/** The reserved `views[]` id whose entry OVERRIDES the synthesized baseline view (EC-444). */
|
|
203
271
|
export declare const DEFAULT_VIEW_OVERRIDE_ID = "default";
|
|
204
272
|
/**
|
|
@@ -259,7 +327,7 @@ export interface ResolvedView {
|
|
|
259
327
|
* column. Config views are shared and change only in
|
|
260
328
|
* code — the runtime never mutates them.
|
|
261
329
|
*/
|
|
262
|
-
export declare function configViewsFor(collection: CollectionDef): ResolvedView[];
|
|
330
|
+
export declare function configViewsFor(collection: CollectionDef, pseudo?: readonly string[]): ResolvedView[];
|
|
263
331
|
/**
|
|
264
332
|
* Does the collection declare a baseline-override view (a config view whose id is
|
|
265
333
|
* {@link DEFAULT_VIEW_OVERRIDE_ID})? When true the synthesized "All entries" lens is
|
|
@@ -274,7 +342,7 @@ export declare function hasDefaultViewOverride(collection: CollectionDef): boole
|
|
|
274
342
|
* `synthesizedBaseline` the caller passes (the studio's translated "All entries"
|
|
275
343
|
* lens). Pure — the store supplies the synthesized baseline (i18n-bound `name`).
|
|
276
344
|
*/
|
|
277
|
-
export declare function resolveCollectionViews(collection: CollectionDef, synthesizedBaseline: ResolvedView): ResolvedView[];
|
|
345
|
+
export declare function resolveCollectionViews(collection: CollectionDef, synthesizedBaseline: ResolvedView, pseudo?: readonly string[]): ResolvedView[];
|
|
278
346
|
/**
|
|
279
347
|
* The ids of config views that still use the DEPRECATED `default` flag without the
|
|
280
348
|
* new `initial` (EC-444). Drives the one-release deprecation warning (mirrors the
|
|
@@ -325,9 +393,9 @@ export interface ResolvedColumn {
|
|
|
325
393
|
* or hidden" — order is preserved across hide/show because visibility lives in
|
|
326
394
|
* `hidden`, never in the order.
|
|
327
395
|
*/
|
|
328
|
-
export declare function resolveColumns(collection: CollectionDef, state: TableViewState): ResolvedColumn[];
|
|
396
|
+
export declare function resolveColumns(collection: CollectionDef, state: TableViewState, pseudo?: readonly string[]): ResolvedColumn[];
|
|
329
397
|
/** The visible columns of a view, in render order (EC-213). */
|
|
330
|
-
export declare function visibleColumns(collection: CollectionDef, state: TableViewState): string[];
|
|
398
|
+
export declare function visibleColumns(collection: CollectionDef, state: TableViewState, pseudo?: readonly string[]): string[];
|
|
331
399
|
/**
|
|
332
400
|
* The title/link column — the document IDENTITY column (EC-242): the schema title
|
|
333
401
|
* field, shown as the leftmost linked cell whose content is `resolveDocumentTitle`
|
|
@@ -337,7 +405,7 @@ export declare function visibleColumns(collection: CollectionDef, state: TableVi
|
|
|
337
405
|
* the first visible column, then the first field, then `id`, when no title field
|
|
338
406
|
* is resolvable (e.g. a function title with no backing `title` field).
|
|
339
407
|
*/
|
|
340
|
-
export declare function primaryColumn(collection: CollectionDef, state: TableViewState): string;
|
|
408
|
+
export declare function primaryColumn(collection: CollectionDef, state: TableViewState, pseudo?: readonly string[]): string;
|
|
341
409
|
/**
|
|
342
410
|
* The canonical, schema-resolved form of a view state — full column order with
|
|
343
411
|
* `hidden` pruned to live fields (EC-213). Two states that render identically
|
|
@@ -345,12 +413,12 @@ export declare function primaryColumn(collection: CollectionDef, state: TableVie
|
|
|
345
413
|
* reconciliation or a stale `hidden` entry (only a real filter/sort/visibility/
|
|
346
414
|
* order change counts as "unsaved").
|
|
347
415
|
*/
|
|
348
|
-
export declare function canonicalViewState(collection: CollectionDef, state: TableViewState): TableViewState;
|
|
416
|
+
export declare function canonicalViewState(collection: CollectionDef, state: TableViewState, pseudo?: readonly string[]): TableViewState;
|
|
349
417
|
/**
|
|
350
418
|
* Toggle a column's visibility — position is preserved (EC-213): visibility
|
|
351
419
|
* lives in `hidden`, the order in `columns` is untouched. Columns the saved
|
|
352
420
|
* order predates are pinned into `columns` first so their place is stable.
|
|
353
421
|
*/
|
|
354
|
-
export declare function toggleColumn(collection: CollectionDef, state: TableViewState, field: string): TableViewState;
|
|
422
|
+
export declare function toggleColumn(collection: CollectionDef, state: TableViewState, field: string, pseudo?: readonly string[]): TableViewState;
|
|
355
423
|
/** Move a column one position left (-1) or right (+1) in the full column order. */
|
|
356
|
-
export declare function moveColumn(collection: CollectionDef, state: TableViewState, field: string, delta: -1 | 1): TableViewState;
|
|
424
|
+
export declare function moveColumn(collection: CollectionDef, state: TableViewState, field: string, delta: -1 | 1, pseudo?: readonly string[]): TableViewState;
|
|
@@ -55,6 +55,83 @@ export const tableViewStateSchema = z.object({
|
|
|
55
55
|
*/
|
|
56
56
|
export const STATE_FIELD = '$state';
|
|
57
57
|
const STATE_VALUES = ['draft', 'published'];
|
|
58
|
+
// --- Reserved pseudo-COLUMNS (EC-445) ------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* Reserved pseudo-COLUMN keys — the columns-axis siblings of the filter-axis
|
|
61
|
+
* {@link STATE_FIELD}. `$`-prefixed so they can never collide with a schema field
|
|
62
|
+
* name; they participate in a view's `columns` list AND the Display picker exactly
|
|
63
|
+
* like a schema field (orderable / hideable / includable), but they are NOT drawn
|
|
64
|
+
* from `collection.fields` — the column UNIVERSE is `[...schemaFields, ...pseudoColumns]`.
|
|
65
|
+
*
|
|
66
|
+
* - `$state` — publication status. A reserved `$`-key, but NO LONGER a reorderable
|
|
67
|
+
* column: EC-448 promoted it to a FIXED structural leading column (checkbox →
|
|
68
|
+
* status-dot → title), so it is NOT emitted by {@link pseudoColumns} and never
|
|
69
|
+
* participates in the reorderable columns/hidden/order model. It stays a reserved
|
|
70
|
+
* key (filter + sort) and {@link isPseudoColumn} still recognizes it, so an authored
|
|
71
|
+
* view listing `$state` is a harmless no-op (see {@link resolveConfigColumns}).
|
|
72
|
+
* - `$url` — the derived delivery URL (routable collections only; NON-sortable).
|
|
73
|
+
* - `$updatedAt` — the record's last-write timestamp.
|
|
74
|
+
* - `$publishedAt` — the record's FIRST-publish instant (EC-447; opt-in, HIDDEN by
|
|
75
|
+
* default). Now materialized end-to-end (`cmsDocuments.publishedAt` → source
|
|
76
|
+
* `timestamps.publishedAt`) and emitted by {@link pseudoColumns} when the source
|
|
77
|
+
* both materializes timestamps and has a publish lifecycle.
|
|
78
|
+
*/
|
|
79
|
+
export const URL_FIELD = '$url';
|
|
80
|
+
export const PUBLISHED_AT_FIELD = '$publishedAt';
|
|
81
|
+
export const UPDATED_AT_FIELD = '$updatedAt';
|
|
82
|
+
/**
|
|
83
|
+
* Every reserved pseudo-column key. `$state` is included (it is still a reserved
|
|
84
|
+
* `$`-key: the filter/sort axis + the FIXED structural status column, EC-448) so
|
|
85
|
+
* {@link isPseudoColumn} never mistakes it for a schema field — but it is NOT a
|
|
86
|
+
* reorderable column, so it is absent from the `pseudo` sets {@link pseudoColumns}
|
|
87
|
+
* emits into the order/hide/picker model.
|
|
88
|
+
*/
|
|
89
|
+
const PSEUDO_COLUMN_FIELDS = [
|
|
90
|
+
STATE_FIELD,
|
|
91
|
+
URL_FIELD,
|
|
92
|
+
PUBLISHED_AT_FIELD,
|
|
93
|
+
UPDATED_AT_FIELD,
|
|
94
|
+
];
|
|
95
|
+
/** Is `name` a reserved pseudo-column (a `$`-key, not a schema field)? */
|
|
96
|
+
export function isPseudoColumn(name) {
|
|
97
|
+
return PSEUDO_COLUMN_FIELDS.includes(name);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Pseudo-columns HIDDEN by default (opt-in): the extra publish timestamp. Everything
|
|
101
|
+
* else applicable (`$updatedAt`/`$state`/`$url`) is on by default (owner, 2026-07-17).
|
|
102
|
+
*/
|
|
103
|
+
const DEFAULT_HIDDEN_PSEUDO_COLUMNS = new Set([PUBLISHED_AT_FIELD]);
|
|
104
|
+
/**
|
|
105
|
+
* The reserved REORDERABLE pseudo-columns that APPLY to a collection, in their default
|
|
106
|
+
* appended order (EC-445/EC-447): `$updatedAt`, `$publishedAt`, then `$url`. `$url` is
|
|
107
|
+
* gated on `routable` (a non-routable collection has no URL column, not an empty one),
|
|
108
|
+
* `$updatedAt` on `timestamps`. `$publishedAt` (EC-447) needs BOTH `timestamps` (its
|
|
109
|
+
* materialized value) AND `publishing` (a publish lifecycle to have a publish date) —
|
|
110
|
+
* it is OPT-IN, HIDDEN by default (`DEFAULT_HIDDEN_PSEUDO_COLUMNS`), so it joins the
|
|
111
|
+
* universe/picker without appearing in the baseline lens.
|
|
112
|
+
*
|
|
113
|
+
* NOTE (EC-448): `$state` is NO LONGER emitted here. It became a FIXED structural
|
|
114
|
+
* leading column (checkbox → status-dot → title), rendered by `collection-table.tsx`
|
|
115
|
+
* regardless of the reorderable model — so it is never in the order/hide/picker set,
|
|
116
|
+
* even though `publishing` is what gates BOTH it and the status column's presence.
|
|
117
|
+
* `opts.publishing` therefore only affects `$publishedAt` here. Pure — a caller
|
|
118
|
+
* (`pseudoColumnsForSource`) supplies the signals from the source.
|
|
119
|
+
*/
|
|
120
|
+
export function pseudoColumns(collection, opts = {}) {
|
|
121
|
+
void collection; // reserved for future per-collection gating; kept for API symmetry
|
|
122
|
+
const cols = [];
|
|
123
|
+
if (opts.timestamps)
|
|
124
|
+
cols.push(UPDATED_AT_FIELD);
|
|
125
|
+
// EC-447: a publish date needs both a timestamps source (the value) AND a publish
|
|
126
|
+
// lifecycle (the meaning). Hidden by default → surfaced via the Display picker / a
|
|
127
|
+
// config view's `columns`, never a blank column on a rail that can't publish.
|
|
128
|
+
if (opts.timestamps && opts.publishing)
|
|
129
|
+
cols.push(PUBLISHED_AT_FIELD);
|
|
130
|
+
// EC-448: $state is structural (fixed leading column), not a reorderable pseudo-column.
|
|
131
|
+
if (opts.routable)
|
|
132
|
+
cols.push(URL_FIELD);
|
|
133
|
+
return cols;
|
|
134
|
+
}
|
|
58
135
|
export const STATE_OPTION = {
|
|
59
136
|
field: STATE_FIELD,
|
|
60
137
|
label: 'State',
|
|
@@ -267,9 +344,17 @@ function rawValueOf(collection, doc, field, localeConfig, isPublished) {
|
|
|
267
344
|
return resolveField(collection, doc, field, locale, localeConfig).value;
|
|
268
345
|
}
|
|
269
346
|
/** The scalar a document exposes for a SORT field, locale-resolved. */
|
|
270
|
-
function scalarOf(collection, doc, field, localeConfig, isPublished) {
|
|
347
|
+
function scalarOf(collection, doc, field, localeConfig, isPublished, meta = {}) {
|
|
271
348
|
if (field === STATE_FIELD)
|
|
272
349
|
return isPublished(doc) ? 'published' : 'draft';
|
|
350
|
+
// EC-445: the pseudo-DATE columns sort by the source's materialized ISO timestamp
|
|
351
|
+
// (ISO strings order lexicographically = chronologically); `$url` is derived → not sortable.
|
|
352
|
+
if (field === UPDATED_AT_FIELD)
|
|
353
|
+
return meta.updatedAt?.(doc) ?? null;
|
|
354
|
+
if (field === PUBLISHED_AT_FIELD)
|
|
355
|
+
return meta.publishedAt?.(doc) ?? null;
|
|
356
|
+
if (field === URL_FIELD)
|
|
357
|
+
return null;
|
|
273
358
|
if (!collection.fields.some((f) => f.name === field))
|
|
274
359
|
return null;
|
|
275
360
|
const locale = documentDefaultLocale(doc, localeConfig);
|
|
@@ -292,10 +377,13 @@ export function clauseMatches(collection, doc, localeConfig, clause, isPublished
|
|
|
292
377
|
* content accessors, the *table* may sort any rendered column — sorting is a
|
|
293
378
|
* presentation concern here; the `filterable` flag governs filters.
|
|
294
379
|
*/
|
|
295
|
-
export function sortViewDocuments(collection, docs, localeConfig, sort, isPublished = defaultIsPublished) {
|
|
380
|
+
export function sortViewDocuments(collection, docs, localeConfig, sort, isPublished = defaultIsPublished, meta = {}) {
|
|
296
381
|
if (sort === null)
|
|
297
382
|
return [...docs];
|
|
298
|
-
const positions = new Map(docs.map((doc) => [
|
|
383
|
+
const positions = new Map(docs.map((doc) => [
|
|
384
|
+
doc.id,
|
|
385
|
+
scalarOf(collection, doc, sort.field, localeConfig, isPublished, meta),
|
|
386
|
+
]));
|
|
299
387
|
return [...docs].sort((a, b) => compareListEntries({ id: a.id, sortValue: positions.get(a.id) ?? null }, { id: b.id, sortValue: positions.get(b.id) ?? null }, sort.dir));
|
|
300
388
|
}
|
|
301
389
|
/**
|
|
@@ -303,13 +391,13 @@ export function sortViewDocuments(collection, docs, localeConfig, sort, isPublis
|
|
|
303
391
|
* ones, sort. Invalid clauses never filter silently — they come back as
|
|
304
392
|
* issues for the UI to surface.
|
|
305
393
|
*/
|
|
306
|
-
export function applyViewState(collection, docs, localeConfig, state, isPublished = defaultIsPublished) {
|
|
394
|
+
export function applyViewState(collection, docs, localeConfig, state, isPublished = defaultIsPublished, meta = {}) {
|
|
307
395
|
const issues = validateClauses(collection, state.filters);
|
|
308
396
|
const invalid = new Set(issues.map((issue) => issue.index));
|
|
309
397
|
const active = state.filters.filter((_, index) => !invalid.has(index));
|
|
310
398
|
const matched = docs.filter((doc) => active.every((clause) => clauseMatches(collection, doc, localeConfig, clause, isPublished)));
|
|
311
399
|
return {
|
|
312
|
-
docs: sortViewDocuments(collection, matched, localeConfig, state.sort, isPublished),
|
|
400
|
+
docs: sortViewDocuments(collection, matched, localeConfig, state.sort, isPublished, meta),
|
|
313
401
|
issues,
|
|
314
402
|
};
|
|
315
403
|
}
|
|
@@ -333,17 +421,24 @@ const NON_DEFAULT_COLUMN_TYPES = new Set(['blocks', 'richText', 'object']);
|
|
|
333
421
|
* and a view curates columns by declaring an explicit `columns` list (or the
|
|
334
422
|
* `id: 'default'` baseline override).
|
|
335
423
|
*/
|
|
336
|
-
function defaultHiddenColumns(collection) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
424
|
+
function defaultHiddenColumns(collection, pseudo = []) {
|
|
425
|
+
const composites = collection.fields
|
|
426
|
+
.filter((f) => NON_DEFAULT_COLUMN_TYPES.has(f.type))
|
|
427
|
+
.map((f) => f.name);
|
|
428
|
+
// EC-445: applicable opt-in pseudo-columns ($publishedAt) start hidden; the rest
|
|
429
|
+
// ($updatedAt/$state/$url) are visible by default (owner: keep them on).
|
|
430
|
+
const hiddenPseudo = pseudo.filter((name) => DEFAULT_HIDDEN_PSEUDO_COLUMNS.has(name));
|
|
431
|
+
return [...composites, ...hiddenPseudo];
|
|
432
|
+
}
|
|
433
|
+
export function defaultViewState(collection, pseudo = []) {
|
|
340
434
|
return {
|
|
341
435
|
filters: [],
|
|
342
436
|
sort: null,
|
|
343
437
|
// Every field keeps a slot (order + picker), but the composite surfaces start
|
|
344
438
|
// hidden so the default lens is the collection's scalar columns (EC-246b/EC-444).
|
|
345
|
-
|
|
346
|
-
|
|
439
|
+
// EC-445: the applicable reserved pseudo-columns append after the data columns.
|
|
440
|
+
columns: orderedColumnNames(collection, pseudo),
|
|
441
|
+
hidden: defaultHiddenColumns(collection, pseudo),
|
|
347
442
|
};
|
|
348
443
|
}
|
|
349
444
|
/**
|
|
@@ -355,23 +450,35 @@ export function defaultViewState(collection) {
|
|
|
355
450
|
* inherits the collection's default lens (all scalars visible, composites hidden).
|
|
356
451
|
* Unknown names in the authored list (a field the schema no longer has) are dropped.
|
|
357
452
|
*/
|
|
358
|
-
export function resolveConfigColumns(collection, visible) {
|
|
453
|
+
export function resolveConfigColumns(collection, visible, pseudo = []) {
|
|
359
454
|
if (visible === undefined || visible.length === 0) {
|
|
360
|
-
const base = defaultViewState(collection);
|
|
455
|
+
const base = defaultViewState(collection, pseudo);
|
|
361
456
|
return { columns: base.columns, hidden: base.hidden };
|
|
362
457
|
}
|
|
363
|
-
const
|
|
364
|
-
const
|
|
458
|
+
const universe = orderedColumnNames(collection, pseudo);
|
|
459
|
+
const inUniverse = new Set(universe);
|
|
365
460
|
const seen = new Set();
|
|
366
461
|
const shown = [];
|
|
367
462
|
for (const name of visible) {
|
|
368
|
-
if (
|
|
463
|
+
if (inUniverse.has(name) && !seen.has(name)) {
|
|
369
464
|
shown.push(name);
|
|
370
465
|
seen.add(name);
|
|
371
466
|
}
|
|
372
467
|
}
|
|
373
|
-
const rest =
|
|
374
|
-
|
|
468
|
+
const rest = universe.filter((name) => !seen.has(name));
|
|
469
|
+
// EC-445: a schema-ONLY authored list (mentioning no pseudo-column) keeps the
|
|
470
|
+
// applicable pseudo-columns at their DEFAULT visibility — so a consumer's
|
|
471
|
+
// pre-EC-445 `columns:[…schema…]` list still shows today's url (+ updatedAt).
|
|
472
|
+
// Mentioning ANY reorderable pseudo-column switches them to the visible-list rule
|
|
473
|
+
// (only listed shows). EC-448: `$state` is EXCLUDED here — it is a fixed structural
|
|
474
|
+
// column, not a reorderable one, so an authored `$state` is an inert no-op: it must
|
|
475
|
+
// not flip the visible-list rule (nor is it in the universe, so it is simply dropped).
|
|
476
|
+
const mentionsPseudo = visible.some((name) => isPseudoColumn(name) && name !== STATE_FIELD);
|
|
477
|
+
const keepVisible = mentionsPseudo
|
|
478
|
+
? new Set()
|
|
479
|
+
: new Set(pseudo.filter((name) => !DEFAULT_HIDDEN_PSEUDO_COLUMNS.has(name)));
|
|
480
|
+
const hidden = rest.filter((name) => !keepVisible.has(name));
|
|
481
|
+
return { columns: [...shown, ...rest], hidden };
|
|
375
482
|
}
|
|
376
483
|
/** The reserved `views[]` id whose entry OVERRIDES the synthesized baseline view (EC-444). */
|
|
377
484
|
export const DEFAULT_VIEW_OVERRIDE_ID = 'default';
|
|
@@ -436,7 +543,7 @@ function configViewId(name, index) {
|
|
|
436
543
|
* column. Config views are shared and change only in
|
|
437
544
|
* code — the runtime never mutates them.
|
|
438
545
|
*/
|
|
439
|
-
export function configViewsFor(collection) {
|
|
546
|
+
export function configViewsFor(collection, pseudo = []) {
|
|
440
547
|
const declared = collection.views;
|
|
441
548
|
if (!Array.isArray(declared))
|
|
442
549
|
return [];
|
|
@@ -453,7 +560,7 @@ export function configViewsFor(collection) {
|
|
|
453
560
|
seen.add(id);
|
|
454
561
|
// EC-444: `initial` supersedes the deprecated `default` alias (one-release window).
|
|
455
562
|
const initial = view.initial ?? view.default;
|
|
456
|
-
const { columns, hidden } = resolveConfigColumns(collection, view.columns);
|
|
563
|
+
const { columns, hidden } = resolveConfigColumns(collection, view.columns, pseudo);
|
|
457
564
|
resolved.push({
|
|
458
565
|
id,
|
|
459
566
|
name: view.name,
|
|
@@ -484,8 +591,8 @@ export function hasDefaultViewOverride(collection) {
|
|
|
484
591
|
* `synthesizedBaseline` the caller passes (the studio's translated "All entries"
|
|
485
592
|
* lens). Pure — the store supplies the synthesized baseline (i18n-bound `name`).
|
|
486
593
|
*/
|
|
487
|
-
export function resolveCollectionViews(collection, synthesizedBaseline) {
|
|
488
|
-
const configViews = configViewsFor(collection);
|
|
594
|
+
export function resolveCollectionViews(collection, synthesizedBaseline, pseudo = []) {
|
|
595
|
+
const configViews = configViewsFor(collection, pseudo);
|
|
489
596
|
const overrideIndex = configViews.findIndex((view) => view.id === DEFAULT_VIEW_OVERRIDE_ID);
|
|
490
597
|
if (overrideIndex < 0)
|
|
491
598
|
return [synthesizedBaseline, ...configViews];
|
|
@@ -555,12 +662,15 @@ export function configViewIssues(collection) {
|
|
|
555
662
|
* selector, treating the first visible column as the title), then the
|
|
556
663
|
* remaining fields in schema order.
|
|
557
664
|
*/
|
|
558
|
-
function orderedColumnNames(collection) {
|
|
665
|
+
function orderedColumnNames(collection, pseudo = []) {
|
|
559
666
|
const names = collection.fields.map((field) => field.name);
|
|
560
667
|
const title = titleFieldOf(collection)?.name;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
668
|
+
const schemaOrdered = title === undefined || !names.includes(title)
|
|
669
|
+
? names
|
|
670
|
+
: [title, ...names.filter((name) => name !== title)];
|
|
671
|
+
// EC-445: the column UNIVERSE = schema fields, then the applicable reserved
|
|
672
|
+
// pseudo-columns (they are NOT in `collection.fields`).
|
|
673
|
+
return [...schemaOrdered, ...pseudo];
|
|
564
674
|
}
|
|
565
675
|
/** Deep-cloned canonical form (stable key order via the Zod shape). */
|
|
566
676
|
export function normalizeViewState(state) {
|
|
@@ -578,26 +688,40 @@ export function viewStatesEqual(a, b) {
|
|
|
578
688
|
* or hidden" — order is preserved across hide/show because visibility lives in
|
|
579
689
|
* `hidden`, never in the order.
|
|
580
690
|
*/
|
|
581
|
-
export function resolveColumns(collection, state) {
|
|
691
|
+
export function resolveColumns(collection, state, pseudo = []) {
|
|
582
692
|
const schemaNames = collection.fields.map((field) => field.name);
|
|
583
|
-
|
|
693
|
+
// EC-445: the valid universe to KEEP from the saved order is schema fields PLUS the
|
|
694
|
+
// applicable pseudo-columns (which are not in `collection.fields`) — so a seeded
|
|
695
|
+
// `$state`/`$url`/`$updatedAt` is preserved + ordered, never dropped as "unknown".
|
|
696
|
+
const keep = new Set([...schemaNames, ...pseudo]);
|
|
584
697
|
const hidden = new Set(state.hidden);
|
|
585
|
-
|
|
698
|
+
const inState = new Set(state.columns);
|
|
699
|
+
// Keep the saved order, restricted to the live universe and de-duplicated (a
|
|
586
700
|
// corrupted persisted state must never render one field as two columns).
|
|
587
701
|
const ordered = [];
|
|
588
702
|
const seen = new Set();
|
|
589
703
|
for (const name of state.columns) {
|
|
590
|
-
if (
|
|
704
|
+
if (keep.has(name) && !seen.has(name)) {
|
|
591
705
|
ordered.push(name);
|
|
592
706
|
seen.add(name);
|
|
593
707
|
}
|
|
594
708
|
}
|
|
595
|
-
|
|
596
|
-
|
|
709
|
+
// Append universe members the saved order predates: new schema fields (EC-213
|
|
710
|
+
// discovery), then applicable pseudo-columns not yet placed (EC-445).
|
|
711
|
+
const appended = [
|
|
712
|
+
...schemaNames.filter((name) => !seen.has(name)),
|
|
713
|
+
...pseudo.filter((name) => !seen.has(name)),
|
|
714
|
+
];
|
|
715
|
+
return [...ordered, ...appended].map((field) => {
|
|
716
|
+
// A freshly-appended opt-in pseudo-column ($publishedAt) starts hidden; once it
|
|
717
|
+
// is in the saved order (an editor un-hid it) its visibility follows `hidden`.
|
|
718
|
+
const defaultHiddenAppend = DEFAULT_HIDDEN_PSEUDO_COLUMNS.has(field) && !inState.has(field);
|
|
719
|
+
return { field, visible: !hidden.has(field) && !defaultHiddenAppend };
|
|
720
|
+
});
|
|
597
721
|
}
|
|
598
722
|
/** The visible columns of a view, in render order (EC-213). */
|
|
599
|
-
export function visibleColumns(collection, state) {
|
|
600
|
-
return resolveColumns(collection, state)
|
|
723
|
+
export function visibleColumns(collection, state, pseudo = []) {
|
|
724
|
+
return resolveColumns(collection, state, pseudo)
|
|
601
725
|
.filter((column) => column.visible)
|
|
602
726
|
.map((column) => column.field);
|
|
603
727
|
}
|
|
@@ -610,9 +734,11 @@ export function visibleColumns(collection, state) {
|
|
|
610
734
|
* the first visible column, then the first field, then `id`, when no title field
|
|
611
735
|
* is resolvable (e.g. a function title with no backing `title` field).
|
|
612
736
|
*/
|
|
613
|
-
export function primaryColumn(collection, state) {
|
|
737
|
+
export function primaryColumn(collection, state, pseudo = []) {
|
|
614
738
|
return (titleFieldOf(collection)?.name ??
|
|
615
|
-
|
|
739
|
+
// EC-445: a pseudo-column is never the identity column (it has no title value) —
|
|
740
|
+
// fall back to the first visible SCHEMA column.
|
|
741
|
+
visibleColumns(collection, state, pseudo).find((name) => !isPseudoColumn(name)) ??
|
|
616
742
|
collection.fields[0]?.name ??
|
|
617
743
|
'id');
|
|
618
744
|
}
|
|
@@ -622,8 +748,8 @@ export function primaryColumn(collection, state) {
|
|
|
622
748
|
* `hidden` to fields the live schema still has (a column removed from the schema
|
|
623
749
|
* must not leave dead `hidden` cruft that trips dirty-detection / persists).
|
|
624
750
|
*/
|
|
625
|
-
function withResolvedOrder(collection, state) {
|
|
626
|
-
const columns = resolveColumns(collection, state).map((column) => column.field);
|
|
751
|
+
function withResolvedOrder(collection, state, pseudo = []) {
|
|
752
|
+
const columns = resolveColumns(collection, state, pseudo).map((column) => column.field);
|
|
627
753
|
const live = new Set(columns);
|
|
628
754
|
const hidden = state.hidden.filter((name) => live.has(name));
|
|
629
755
|
return { ...state, columns, hidden };
|
|
@@ -635,24 +761,24 @@ function withResolvedOrder(collection, state) {
|
|
|
635
761
|
* reconciliation or a stale `hidden` entry (only a real filter/sort/visibility/
|
|
636
762
|
* order change counts as "unsaved").
|
|
637
763
|
*/
|
|
638
|
-
export function canonicalViewState(collection, state) {
|
|
639
|
-
return normalizeViewState(withResolvedOrder(collection, state));
|
|
764
|
+
export function canonicalViewState(collection, state, pseudo = []) {
|
|
765
|
+
return normalizeViewState(withResolvedOrder(collection, state, pseudo));
|
|
640
766
|
}
|
|
641
767
|
/**
|
|
642
768
|
* Toggle a column's visibility — position is preserved (EC-213): visibility
|
|
643
769
|
* lives in `hidden`, the order in `columns` is untouched. Columns the saved
|
|
644
770
|
* order predates are pinned into `columns` first so their place is stable.
|
|
645
771
|
*/
|
|
646
|
-
export function toggleColumn(collection, state, field) {
|
|
647
|
-
const base = withResolvedOrder(collection, state);
|
|
772
|
+
export function toggleColumn(collection, state, field, pseudo = []) {
|
|
773
|
+
const base = withResolvedOrder(collection, state, pseudo);
|
|
648
774
|
const hidden = base.hidden.includes(field)
|
|
649
775
|
? base.hidden.filter((name) => name !== field)
|
|
650
776
|
: [...base.hidden, field];
|
|
651
777
|
return { ...base, hidden };
|
|
652
778
|
}
|
|
653
779
|
/** Move a column one position left (-1) or right (+1) in the full column order. */
|
|
654
|
-
export function moveColumn(collection, state, field, delta) {
|
|
655
|
-
const base = withResolvedOrder(collection, state);
|
|
780
|
+
export function moveColumn(collection, state, field, delta, pseudo = []) {
|
|
781
|
+
const base = withResolvedOrder(collection, state, pseudo);
|
|
656
782
|
const from = base.columns.indexOf(field);
|
|
657
783
|
const to = from + delta;
|
|
658
784
|
if (from < 0 || to < 0 || to >= base.columns.length)
|