@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
|
@@ -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, DEFAULT_VIEW_OVERRIDE_ID, canonicalViewState, configViewIssues, defaultConfigViewId, defaultViewState, deprecatedDefaultFlagViewIds, hasDefaultViewOverride, resolveCollectionViews, 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';
|
|
@@ -101,12 +101,12 @@ function warnDeprecatedDefaultFlag(collection) {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
/** The unnamed built-in view: no filters, no sort, every field visible. */
|
|
104
|
-
function defaultView(collection) {
|
|
104
|
+
function defaultView(collection, pseudo = []) {
|
|
105
105
|
return {
|
|
106
106
|
id: ALL_VIEW_ID,
|
|
107
107
|
name: translate('cms.list.view.all'),
|
|
108
108
|
state: collection
|
|
109
|
-
? defaultViewState(collection)
|
|
109
|
+
? defaultViewState(collection, pseudo)
|
|
110
110
|
: { filters: [], sort: null, columns: [], hidden: [] },
|
|
111
111
|
};
|
|
112
112
|
}
|
|
@@ -117,7 +117,7 @@ function defaultView(collection) {
|
|
|
117
117
|
* layer (`documents.*` / `schema.*` commands) through {@link createCmsPersister}.
|
|
118
118
|
* Failed/denied commands surface as {@link CmsStore.persistError}.
|
|
119
119
|
*/
|
|
120
|
-
export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialLocaleConfig, }) {
|
|
120
|
+
export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialLocaleConfig, }) {
|
|
121
121
|
const [documents, setDocuments] = useState(() => {
|
|
122
122
|
const seeded = (initialDocuments ?? sampleDocuments).map((d) => structuredClone(d));
|
|
123
123
|
// EC-320 (0.0.44 follow-up): merge any documents STAGED by the
|
|
@@ -139,6 +139,68 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
139
139
|
return new Set(initialPublishedKeys);
|
|
140
140
|
return new Set(initialDocuments ? [] : samplePublishedKeys);
|
|
141
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]);
|
|
169
|
+
// EC-447: per-document FIRST-publish instants behind the `$publishedAt` column.
|
|
170
|
+
// Seeded from hydration (the persisted `cmsDocuments.publishedAt`); UNLIKE
|
|
171
|
+
// `$updatedAt` there is NO session-start fallback — an absent entry is a real "no
|
|
172
|
+
// publish date" (EmDash). The store re-stamps/clears it authoritatively on
|
|
173
|
+
// publish/unpublish (the set-once + clear semantics live in the persistence spine).
|
|
174
|
+
const [publishedAtByKey, setPublishedAtByKey] = useState(() => {
|
|
175
|
+
const map = new Map();
|
|
176
|
+
if (initialPublishTimestamps) {
|
|
177
|
+
for (const [key, value] of Object.entries(initialPublishTimestamps))
|
|
178
|
+
map.set(key, value);
|
|
179
|
+
}
|
|
180
|
+
return map;
|
|
181
|
+
});
|
|
182
|
+
// Adopt the AUTHORITATIVE publish instant from the reconciled record: an ISO value
|
|
183
|
+
// stamps it (set-once is enforced server-side, so this only ever adopts the pinned
|
|
184
|
+
// first-publish value); `undefined` CLEARS it (unpublish). Mirrors `stampUpdatedAt`
|
|
185
|
+
// but supports clearing — the column shows a date iff the doc is currently published.
|
|
186
|
+
const stampPublishedAt = useCallback((ref, iso) => {
|
|
187
|
+
setPublishedAtByKey((prev) => {
|
|
188
|
+
const key = documentKey(ref);
|
|
189
|
+
if (iso === undefined) {
|
|
190
|
+
if (!prev.has(key))
|
|
191
|
+
return prev;
|
|
192
|
+
const next = new Map(prev);
|
|
193
|
+
next.delete(key);
|
|
194
|
+
return next;
|
|
195
|
+
}
|
|
196
|
+
if (prev.get(key) === iso)
|
|
197
|
+
return prev;
|
|
198
|
+
const next = new Map(prev);
|
|
199
|
+
next.set(key, iso);
|
|
200
|
+
return next;
|
|
201
|
+
});
|
|
202
|
+
}, []);
|
|
203
|
+
const documentPublishedAt = useCallback((ref) => publishedAtByKey.get(documentKey(ref)), [publishedAtByKey]);
|
|
142
204
|
const [activeViews, setActiveViews] = useState({});
|
|
143
205
|
// The live table state per collection where it diverges from the active
|
|
144
206
|
// view; absent key = exactly the active view's state (not dirty).
|
|
@@ -229,8 +291,9 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
229
291
|
// Replace-or-append (upsert): a brand-new document must still land even
|
|
230
292
|
// though nothing in the session matches its ref yet.
|
|
231
293
|
setDocuments((prev) => upsertDoc(prev, doc));
|
|
294
|
+
stampUpdatedAt(doc); // EC-445: a save bumps the $updatedAt column
|
|
232
295
|
persisterRef.current?.upsertDocument(doc);
|
|
233
|
-
}, []);
|
|
296
|
+
}, [stampUpdatedAt]);
|
|
234
297
|
const commitFieldEdit = useCallback((input) => {
|
|
235
298
|
// Pure map-replace: an edit always targets a document already open in
|
|
236
299
|
// the session, so a miss (already gone) is a no-op, never an append.
|
|
@@ -240,7 +303,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
240
303
|
registry,
|
|
241
304
|
documents: documentsRef.current,
|
|
242
305
|
localeConfig,
|
|
243
|
-
|
|
306
|
+
// EC-445: the optimistic apply bumps the $updatedAt column (a rollback
|
|
307
|
+
// leaves it — it self-corrects on the next hydration).
|
|
308
|
+
applySession: (doc) => {
|
|
309
|
+
replaceWith(doc);
|
|
310
|
+
stampUpdatedAt(doc);
|
|
311
|
+
},
|
|
244
312
|
rollbackSession: replaceWith,
|
|
245
313
|
...(persister && projectId !== undefined
|
|
246
314
|
? {
|
|
@@ -254,7 +322,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
254
322
|
}
|
|
255
323
|
: {}),
|
|
256
324
|
}, input);
|
|
257
|
-
}, [registry, localeConfig, projectId]);
|
|
325
|
+
}, [registry, localeConfig, projectId, stampUpdatedAt]);
|
|
258
326
|
// Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
|
|
259
327
|
// `buildNewDocument`) + optimistic append + persist, consumed by both
|
|
260
328
|
// `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
|
|
@@ -266,9 +334,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
266
334
|
const insertNewDocument = useCallback((collectionId, options) => {
|
|
267
335
|
const doc = buildNewDocument(collectionId, registry, seq, documentsRef.current);
|
|
268
336
|
setDocuments((prev) => options?.guardExisting && hasDocumentFor(prev, collectionId) ? prev : [...prev, doc]);
|
|
337
|
+
stampUpdatedAt(doc); // EC-445: a fresh draft's $updatedAt starts at creation time
|
|
269
338
|
persisterRef.current?.upsertDocument(doc);
|
|
270
339
|
return doc;
|
|
271
|
-
}, [registry]);
|
|
340
|
+
}, [registry, stampUpdatedAt]);
|
|
272
341
|
const createDocument = useCallback((collectionId) => {
|
|
273
342
|
const doc = insertNewDocument(collectionId);
|
|
274
343
|
return { collection: collectionId, id: doc.id };
|
|
@@ -339,9 +408,18 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
339
408
|
return;
|
|
340
409
|
const { operations } = await getStudioOperations();
|
|
341
410
|
const record = await operations.documents.get({ projectId, ref });
|
|
342
|
-
if (record.ok)
|
|
411
|
+
if (record.ok) {
|
|
343
412
|
setPublished(ref, record.value.publishedVersion !== undefined);
|
|
344
|
-
|
|
413
|
+
// EC-445: publish/unpublish patches `updatedAt` — adopt the authoritative
|
|
414
|
+
// value so the $updatedAt column reflects the actual write, not a guess.
|
|
415
|
+
if (record.value.updatedAt)
|
|
416
|
+
stampUpdatedAt(ref, record.value.updatedAt);
|
|
417
|
+
// EC-447: adopt the authoritative `publishedAt` — set-once on the first
|
|
418
|
+
// publish, absent after unpublish. Passing `undefined` CLEARS the column, so
|
|
419
|
+
// an unpublish reverts $publishedAt to EmDash and a republish records fresh.
|
|
420
|
+
stampPublishedAt(ref, record.value.publishedAt);
|
|
421
|
+
}
|
|
422
|
+
}, [projectId, setPublished, stampUpdatedAt, stampPublishedAt]);
|
|
345
423
|
const publishDocument = useCallback(async (ref) => {
|
|
346
424
|
setPublished(ref, true); // optimistic
|
|
347
425
|
// EC-273: no clearing here — the draft is already free of conditionally-hidden
|
|
@@ -465,6 +543,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
465
543
|
}, [registry, documents, localeConfig, assetExists]);
|
|
466
544
|
const isFieldReadOnly = useCallback((field) => field.form?.readOnly === true, []);
|
|
467
545
|
// --- Views (EC-246b: config-as-code) --------------------------------------
|
|
546
|
+
// EC-445: the reserved pseudo-columns a CMS collection's list surfaces. Every
|
|
547
|
+
// `cmsDocuments` collection has a publish lifecycle + materialized timestamps
|
|
548
|
+
// ($state + $updatedAt always apply); $url applies only where the deployment
|
|
549
|
+
// resolves a delivery route for it (mirrors the live-URL column's own guard).
|
|
550
|
+
const pseudoColumnsFor = useCallback((collectionId) => {
|
|
551
|
+
const collection = registry.get(collectionId);
|
|
552
|
+
if (!collection)
|
|
553
|
+
return [];
|
|
554
|
+
const routable = resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId);
|
|
555
|
+
return pseudoColumns(collection, { publishing: true, timestamps: true, routable });
|
|
556
|
+
}, [registry]);
|
|
468
557
|
const viewsOf = useCallback((collectionId) => {
|
|
469
558
|
// The baseline view plus the collection's config-declared views (EC-246b/EC-444).
|
|
470
559
|
// Views are PRESENTATION, read live from elytra.config.ts (AD-11) — not the
|
|
@@ -476,12 +565,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
476
565
|
// EC-444: a config view with the reserved `id: 'default'` REPLACES the
|
|
477
566
|
// synthesized "All entries" baseline verbatim (first tab); absent one, the
|
|
478
567
|
// synthesized baseline leads. `resolveCollectionViews` owns that selection.
|
|
479
|
-
const
|
|
568
|
+
const pseudo = pseudoColumnsFor(collectionId);
|
|
569
|
+
const baseline = defaultView(registry.get(collectionId), pseudo);
|
|
480
570
|
const configCollection = resolveDeploymentCollections().find((c) => c.id === collectionId);
|
|
481
571
|
if (!configCollection)
|
|
482
572
|
return [baseline];
|
|
483
|
-
return resolveCollectionViews(configCollection, baseline);
|
|
484
|
-
}, [registry]);
|
|
573
|
+
return resolveCollectionViews(configCollection, baseline, pseudo);
|
|
574
|
+
}, [registry, pseudoColumnsFor]);
|
|
485
575
|
// The INITIAL active view for a collection (EC-343/EC-444): a config view flagged
|
|
486
576
|
// `initial: true` seeds the first-load lens; else the baseline — the `id: 'default'`
|
|
487
577
|
// override when present, else the synthesized "All entries" (`ALL_VIEW_ID`). Only
|
|
@@ -502,8 +592,8 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
502
592
|
const activeViewOf = useCallback((collectionId) => {
|
|
503
593
|
const id = activeViews[collectionId] ?? initialViewId(collectionId);
|
|
504
594
|
return (viewsOf(collectionId).find((view) => view.id === id) ??
|
|
505
|
-
defaultView(registry.get(collectionId)));
|
|
506
|
-
}, [activeViews, initialViewId, viewsOf, registry]);
|
|
595
|
+
defaultView(registry.get(collectionId), pseudoColumnsFor(collectionId)));
|
|
596
|
+
}, [activeViews, initialViewId, viewsOf, registry, pseudoColumnsFor]);
|
|
507
597
|
const tableStateOf = useCallback((collectionId) => tableStates[collectionId] ?? activeViewOf(collectionId).state, [tableStates, activeViewOf]);
|
|
508
598
|
const dropTableState = useCallback((collectionId) => {
|
|
509
599
|
setTableStates((prev) => {
|
|
@@ -633,6 +723,8 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
633
723
|
ensureSingletonDocument,
|
|
634
724
|
deleteDocument,
|
|
635
725
|
isPublished,
|
|
726
|
+
documentUpdatedAt,
|
|
727
|
+
documentPublishedAt,
|
|
636
728
|
publishDocument,
|
|
637
729
|
unpublishDocument,
|
|
638
730
|
loadDocumentVersions,
|
|
@@ -658,8 +750,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
658
750
|
if (collection === undefined)
|
|
659
751
|
return !viewStatesEqual(working, active);
|
|
660
752
|
// Compare schema-resolved forms (EC-213): a column-order reconciliation
|
|
661
|
-
// or a stale `hidden` entry must not read as an unsaved edit.
|
|
662
|
-
|
|
753
|
+
// or a stale `hidden` entry must not read as an unsaved edit. EC-445: the
|
|
754
|
+
// pseudo-column universe must match so a reserved column never reads as a diff.
|
|
755
|
+
const pseudo = pseudoColumnsFor(collectionId);
|
|
756
|
+
return !viewStatesEqual(canonicalViewState(collection, working, pseudo), canonicalViewState(collection, active, pseudo));
|
|
663
757
|
},
|
|
664
758
|
resetTableState: dropTableState,
|
|
665
759
|
runBulk,
|
|
@@ -683,6 +777,8 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
683
777
|
ensureSingletonDocument,
|
|
684
778
|
deleteDocument,
|
|
685
779
|
isPublished,
|
|
780
|
+
documentUpdatedAt,
|
|
781
|
+
documentPublishedAt,
|
|
686
782
|
publishDocument,
|
|
687
783
|
unpublishDocument,
|
|
688
784
|
loadDocumentVersions,
|
|
@@ -700,6 +796,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
700
796
|
dropTableState,
|
|
701
797
|
runBulk,
|
|
702
798
|
registry,
|
|
799
|
+
pseudoColumnsFor,
|
|
703
800
|
persistError,
|
|
704
801
|
]);
|
|
705
802
|
return _jsx(CmsContext.Provider, { value: store$, children: children });
|