@elytracms/studio 0.0.72 → 0.0.74

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 (43) hide show
  1. package/dist/components/app-views/default-modules.js +1 -1
  2. package/dist/components/app-views/default-modules.js.map +1 -1
  3. package/dist/components/cms/cms-labels.d.ts +6 -0
  4. package/dist/components/cms/cms-labels.js +21 -1
  5. package/dist/components/cms/cms-labels.js.map +1 -1
  6. package/dist/components/cms/cms-store.d.ts +16 -1
  7. package/dist/components/cms/cms-store.js +114 -31
  8. package/dist/components/cms/cms-store.js.map +1 -1
  9. package/dist/components/cms/collection-table.js +71 -9
  10. package/dist/components/cms/collection-table.js.map +1 -1
  11. package/dist/components/cms/column-width.d.ts +8 -0
  12. package/dist/components/cms/column-width.js +18 -0
  13. package/dist/components/cms/column-width.js.map +1 -1
  14. package/dist/components/cms/versions-model.d.ts +13 -0
  15. package/dist/components/cms/versions-model.js +44 -0
  16. package/dist/components/cms/versions-model.js.map +1 -1
  17. package/dist/components/cms/versions-pane.js +27 -6
  18. package/dist/components/cms/versions-pane.js.map +1 -1
  19. package/dist/components/cms/view-bar.js +16 -6
  20. package/dist/components/cms/view-bar.js.map +1 -1
  21. package/dist/components/collection-view/cms-collection-view-source.js +7 -0
  22. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  23. package/dist/lib/cms-views/view-model.d.ts +114 -28
  24. package/dist/lib/cms-views/view-model.js +262 -71
  25. package/dist/lib/cms-views/view-model.js.map +1 -1
  26. package/dist/lib/collection-view/collection-view-source.d.ts +28 -0
  27. package/dist/lib/collection-view/collection-view-source.js +15 -0
  28. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  29. package/dist/lib/i18n/messages.de.js +5 -1
  30. package/dist/lib/i18n/messages.de.js.map +1 -1
  31. package/dist/lib/i18n/messages.en.d.ts +3 -0
  32. package/dist/lib/i18n/messages.en.js +5 -1
  33. package/dist/lib/i18n/messages.en.js.map +1 -1
  34. package/dist/lib/operations/session-hydration.d.ts +8 -0
  35. package/dist/lib/operations/session-hydration.js +3 -0
  36. package/dist/lib/operations/session-hydration.js.map +1 -1
  37. package/dist/ui/index.d.ts +1 -0
  38. package/dist/ui/index.js +1 -0
  39. package/dist/ui/index.js.map +1 -1
  40. package/dist/ui/status-dot.d.ts +39 -0
  41. package/dist/ui/status-dot.js +36 -0
  42. package/dist/ui/status-dot.js.map +1 -0
  43. package/package.json +2 -2
@@ -4,12 +4,12 @@ import { createCollectionRegistry, createDocumentStore, documentKey, documentSch
4
4
  import { translate } from '@elytracms/studio/lib/i18n';
5
5
  import { configureStudioLocales, useStudioLocale } from '@elytracms/studio/lib/locale';
6
6
  import { studioManifestLookup } from '@elytracms/studio/lib/registry';
7
- import { resolveDeploymentCollections } from '@elytracms/studio/lib/workspace';
7
+ import { resolveDeploymentCollections, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
8
8
  import { validateComposition, validateRichTextEmbeds } from './cms-composition-validators.js';
9
9
  import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils.js';
10
10
  import { createRequiredContext } from '@elytracms/studio/lib/required-context';
11
11
  import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
12
- import { ALL_VIEW_ID, canonicalViewState, configViewIssues, configViewsFor, defaultConfigViewId, defaultViewState, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
12
+ import { ALL_VIEW_ID, DEFAULT_VIEW_OVERRIDE_ID, canonicalViewState, configViewIssues, defaultConfigViewId, defaultViewState, deprecatedDefaultFlagViewIds, hasDefaultViewOverride, pseudoColumns, resolveCollectionViews, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
13
13
  import { useOptionalAssets } from '../assets/assets-store.js';
14
14
  import { consumeStagedDocuments } from './document-staging.js';
15
15
  import { applyCellValue, commitCellEdit } from './cell-edit.js';
@@ -65,11 +65,11 @@ export function hasDocumentFor(docs, collectionId) {
65
65
  return docs.some((d) => d.collection === collectionId);
66
66
  }
67
67
  /**
68
- * Surface a duplicate `default: true` across a collection's config views (EC-343) as
69
- * a visible, one-time console warning — never a silent swallow. This is a developer
70
- * config mistake (editors can't fix config-as-code), so it takes the same warn-once
71
- * console channel the i18n runtime uses for config problems, not an editor banner;
72
- * the first-declared default still seeds deterministically. Deduped per collection id.
68
+ * Surface a duplicate `initial: true` across a collection's config views (EC-343/
69
+ * EC-444) as a visible, one-time console warning — never a silent swallow. This is a
70
+ * developer config mistake (editors can't fix config-as-code), so it takes the same
71
+ * warn-once console channel the i18n runtime uses for config problems, not an editor
72
+ * banner; the first-declared still seeds deterministically. Deduped per collection id.
73
73
  */
74
74
  const warnedDuplicateDefaultCollections = new Set();
75
75
  function warnDuplicateDefaultViews(collection) {
@@ -82,13 +82,31 @@ function warnDuplicateDefaultViews(collection) {
82
82
  if (typeof console !== 'undefined')
83
83
  console.warn(`[views] ${issues[0].message}`);
84
84
  }
85
+ /**
86
+ * Surface the DEPRECATED `default: true` view flag (EC-444) as a one-time console
87
+ * warning per collection — never silent. `default` (the view flag) is renamed to
88
+ * `initial`; it is read as an alias for one release and removed next release. Same
89
+ * warn-once channel as the duplicate check; deduped per collection id.
90
+ */
91
+ const warnedDeprecatedDefaultCollections = new Set();
92
+ function warnDeprecatedDefaultFlag(collection) {
93
+ if (warnedDeprecatedDefaultCollections.has(collection.id))
94
+ return;
95
+ const ids = deprecatedDefaultFlagViewIds(collection);
96
+ if (ids.length === 0)
97
+ return;
98
+ warnedDeprecatedDefaultCollections.add(collection.id);
99
+ if (typeof console !== 'undefined') {
100
+ console.warn(`[views] Collection "${collection.id}" uses the deprecated view flag \`default\` on ${ids.join(', ')}; rename it to \`initial\` (EC-444 — \`default\` is removed next release).`);
101
+ }
102
+ }
85
103
  /** The unnamed built-in view: no filters, no sort, every field visible. */
86
- function defaultView(collection) {
104
+ function defaultView(collection, pseudo = []) {
87
105
  return {
88
106
  id: ALL_VIEW_ID,
89
107
  name: translate('cms.list.view.all'),
90
108
  state: collection
91
- ? defaultViewState(collection)
109
+ ? defaultViewState(collection, pseudo)
92
110
  : { filters: [], sort: null, columns: [], hidden: [] },
93
111
  };
94
112
  }
@@ -99,7 +117,7 @@ function defaultView(collection) {
99
117
  * layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
100
118
  * Failed/denied commands surface as {@link CmsStore.persistError}.
101
119
  */
102
- export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialLocaleConfig, }) {
120
+ export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialLocaleConfig, }) {
103
121
  const [documents, setDocuments] = useState(() => {
104
122
  const seeded = (initialDocuments ?? sampleDocuments).map((d) => structuredClone(d));
105
123
  // EC-320 (0.0.44 follow-up): merge any documents STAGED by the
@@ -121,6 +139,33 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
121
139
  return new Set(initialPublishedKeys);
122
140
  return new Set(initialDocuments ? [] : samplePublishedKeys);
123
141
  });
142
+ // EC-445: per-document last-write timestamps behind the `$updatedAt` column.
143
+ // Seeded from hydration (connected: the persisted `cmsDocuments.updatedAt`), then
144
+ // re-stamped on the common edit paths. A seeded document with no hydrated timestamp
145
+ // (the playground fixtures / a kit) gets a session-start stamp so the column is
146
+ // never a wall of blanks; a fresh draft is stamped when it is created.
147
+ const [updatedAtByKey, setUpdatedAtByKey] = useState(() => {
148
+ const now = new Date().toISOString();
149
+ const map = new Map();
150
+ if (initialTimestamps) {
151
+ for (const [key, value] of Object.entries(initialTimestamps))
152
+ map.set(key, value);
153
+ }
154
+ for (const doc of initialDocuments ?? sampleDocuments) {
155
+ const key = documentKey(doc);
156
+ if (!map.has(key))
157
+ map.set(key, now);
158
+ }
159
+ return map;
160
+ });
161
+ const stampUpdatedAt = useCallback((ref, iso = new Date().toISOString()) => {
162
+ setUpdatedAtByKey((prev) => {
163
+ const next = new Map(prev);
164
+ next.set(documentKey(ref), iso);
165
+ return next;
166
+ });
167
+ }, []);
168
+ const documentUpdatedAt = useCallback((ref) => updatedAtByKey.get(documentKey(ref)), [updatedAtByKey]);
124
169
  const [activeViews, setActiveViews] = useState({});
125
170
  // The live table state per collection where it diverges from the active
126
171
  // view; absent key = exactly the active view's state (not dirty).
@@ -211,8 +256,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
211
256
  // Replace-or-append (upsert): a brand-new document must still land even
212
257
  // though nothing in the session matches its ref yet.
213
258
  setDocuments((prev) => upsertDoc(prev, doc));
259
+ stampUpdatedAt(doc); // EC-445: a save bumps the $updatedAt column
214
260
  persisterRef.current?.upsertDocument(doc);
215
- }, []);
261
+ }, [stampUpdatedAt]);
216
262
  const commitFieldEdit = useCallback((input) => {
217
263
  // Pure map-replace: an edit always targets a document already open in
218
264
  // the session, so a miss (already gone) is a no-op, never an append.
@@ -222,7 +268,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
222
268
  registry,
223
269
  documents: documentsRef.current,
224
270
  localeConfig,
225
- applySession: replaceWith,
271
+ // EC-445: the optimistic apply bumps the $updatedAt column (a rollback
272
+ // leaves it — it self-corrects on the next hydration).
273
+ applySession: (doc) => {
274
+ replaceWith(doc);
275
+ stampUpdatedAt(doc);
276
+ },
226
277
  rollbackSession: replaceWith,
227
278
  ...(persister && projectId !== undefined
228
279
  ? {
@@ -236,7 +287,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
236
287
  }
237
288
  : {}),
238
289
  }, input);
239
- }, [registry, localeConfig, projectId]);
290
+ }, [registry, localeConfig, projectId, stampUpdatedAt]);
240
291
  // Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
241
292
  // `buildNewDocument`) + optimistic append + persist, consumed by both
242
293
  // `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
@@ -248,9 +299,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
248
299
  const insertNewDocument = useCallback((collectionId, options) => {
249
300
  const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current);
250
301
  setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
302
+ stampUpdatedAt(doc); // EC-445: a fresh draft's $updatedAt starts at creation time
251
303
  persisterRef.current?.upsertDocument(doc);
252
304
  return doc;
253
- }, [registry]);
305
+ }, [registry, stampUpdatedAt]);
254
306
  const createDocument = useCallback((collectionId) => {
255
307
  const doc = insertNewDocument(collectionId);
256
308
  return { collection: collectionId, id: doc.id };
@@ -321,9 +373,14 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
321
373
  return;
322
374
  const { operations } = await getStudioOperations();
323
375
  const record = await operations.documents.get({ projectId, ref });
324
- if (record.ok)
376
+ if (record.ok) {
325
377
  setPublished(ref, record.value.publishedVersion !== undefined);
326
- }, [projectId, setPublished]);
378
+ // EC-445: publish/unpublish patches `updatedAt` — adopt the authoritative
379
+ // value so the $updatedAt column reflects the actual write, not a guess.
380
+ if (record.value.updatedAt)
381
+ stampUpdatedAt(ref, record.value.updatedAt);
382
+ }
383
+ }, [projectId, setPublished, stampUpdatedAt]);
327
384
  const publishDocument = useCallback(async (ref) => {
328
385
  setPublished(ref, true); // optimistic
329
386
  // EC-273: no clearing here — the draft is already free of conditionally-hidden
@@ -447,36 +504,57 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
447
504
  }, [registry, documents, localeConfig, assetExists]);
448
505
  const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
449
506
  // --- Views (EC-246b: config-as-code) --------------------------------------
507
+ // EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
508
+ // `cmsDocuments` collection has a publish lifecycle + materialized timestamps
509
+ // ($state + $updatedAt always apply); $url applies only where the deployment
510
+ // resolves a delivery route for it (mirrors the live-URL column's own guard).
511
+ const pseudoColumnsFor = useCallback((collectionId) => {
512
+ const collection = registry.get(collectionId);
513
+ if (!collection)
514
+ return [];
515
+ const routable = resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId);
516
+ return pseudoColumns(collection, { publishing: true, timestamps: true, routable });
517
+ }, [registry]);
450
518
  const viewsOf = useCallback((collectionId) => {
451
- // The built-in default plus the collection's config-declared views (EC-246b).
519
+ // The baseline view plus the collection's config-declared views (EC-246b/EC-444).
452
520
  // Views are PRESENTATION, read live from elytra.config.ts (AD-11) — not the
453
521
  // persisted content schema the session booted from — so they reflect code
454
522
  // immediately without a re-seed (the persisted schema is only baselined on
455
523
  // first run). The EC-169 DB saved-view store is retired: shared views are
456
524
  // code, editors keep only transient in-session tweaks + Reset.
525
+ //
526
+ // EC-444: a config view with the reserved `id: 'default'` REPLACES the
527
+ // synthesized "All entries" baseline verbatim (first tab); absent one, the
528
+ // synthesized baseline leads. `resolveCollectionViews` owns that selection.
529
+ const pseudo = pseudoColumnsFor(collectionId);
530
+ const baseline = defaultView(registry.get(collectionId), pseudo);
457
531
  const configCollection = resolveDeploymentCollections().find((c) => c.id === collectionId);
458
- return [
459
- defaultView(registry.get(collectionId)),
460
- ...(configCollection ? configViewsFor(configCollection) : []),
461
- ];
462
- }, [registry]);
463
- // The INITIAL active view for a collection (EC-343): a config view flagged
464
- // `default: true` seeds the first-load lens; else the built-in "All" default. Only
532
+ if (!configCollection)
533
+ return [baseline];
534
+ return resolveCollectionViews(configCollection, baseline, pseudo);
535
+ }, [registry, pseudoColumnsFor]);
536
+ // The INITIAL active view for a collection (EC-343/EC-444): a config view flagged
537
+ // `initial: true` seeds the first-load lens; else the baseline — the `id: 'default'`
538
+ // override when present, else the synthesized "All entries" (`ALL_VIEW_ID`). Only
465
539
  // consulted before the editor has picked a view (`activeViews[id]` wins once set),
466
- // so transient switching + Reset are unaffected. A duplicate `default` is warned
467
- // once (never silent) and the first-declared default seeds deterministically.
540
+ // so transient switching + Reset are unaffected. A duplicate `initial` and any
541
+ // deprecated `default` flag are each warned once (never silent).
468
542
  const initialViewId = useCallback((collectionId) => {
469
543
  const configCollection = resolveDeploymentCollections().find((c) => c.id === collectionId);
470
544
  if (!configCollection)
471
545
  return ALL_VIEW_ID;
472
546
  warnDuplicateDefaultViews(configCollection);
473
- return defaultConfigViewId(configCollection) ?? ALL_VIEW_ID;
547
+ warnDeprecatedDefaultFlag(configCollection);
548
+ const initial = defaultConfigViewId(configCollection);
549
+ if (initial)
550
+ return initial;
551
+ return hasDefaultViewOverride(configCollection) ? DEFAULT_VIEW_OVERRIDE_ID : ALL_VIEW_ID;
474
552
  }, []);
475
553
  const activeViewOf = useCallback((collectionId) => {
476
554
  const id = activeViews[collectionId] ?? initialViewId(collectionId);
477
555
  return (viewsOf(collectionId).find((view) => view.id === id) ??
478
- defaultView(registry.get(collectionId)));
479
- }, [activeViews, initialViewId, viewsOf, registry]);
556
+ defaultView(registry.get(collectionId), pseudoColumnsFor(collectionId)));
557
+ }, [activeViews, initialViewId, viewsOf, registry, pseudoColumnsFor]);
480
558
  const tableStateOf = useCallback((collectionId) => tableStates[collectionId] ?? activeViewOf(collectionId).state, [tableStates, activeViewOf]);
481
559
  const dropTableState = useCallback((collectionId) => {
482
560
  setTableStates((prev) => {
@@ -606,6 +684,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
606
684
  ensureSingletonDocument,
607
685
  deleteDocument,
608
686
  isPublished,
687
+ documentUpdatedAt,
609
688
  publishDocument,
610
689
  unpublishDocument,
611
690
  loadDocumentVersions,
@@ -631,8 +710,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
631
710
  if (collection === undefined)
632
711
  return !viewStatesEqual(working, active);
633
712
  // Compare schema-resolved forms (EC-213): a column-order reconciliation
634
- // or a stale `hidden` entry must not read as an unsaved edit.
635
- return !viewStatesEqual(canonicalViewState(collection, working), canonicalViewState(collection, active));
713
+ // or a stale `hidden` entry must not read as an unsaved edit. EC-445: the
714
+ // pseudo-column universe must match so a reserved column never reads as a diff.
715
+ const pseudo = pseudoColumnsFor(collectionId);
716
+ return !viewStatesEqual(canonicalViewState(collection, working, pseudo), canonicalViewState(collection, active, pseudo));
636
717
  },
637
718
  resetTableState: dropTableState,
638
719
  runBulk,
@@ -656,6 +737,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
656
737
  ensureSingletonDocument,
657
738
  deleteDocument,
658
739
  isPublished,
740
+ documentUpdatedAt,
659
741
  publishDocument,
660
742
  unpublishDocument,
661
743
  loadDocumentVersions,
@@ -673,6 +755,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
673
755
  dropTableState,
674
756
  runBulk,
675
757
  registry,
758
+ pseudoColumnsFor,
676
759
  persistError,
677
760
  ]);
678
761
  return _jsx(CmsContext.Provider, { value: store$, children: children });