@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.
- 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 +16 -1
- package/dist/components/cms/cms-store.js +114 -31
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +71 -9
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/column-width.d.ts +8 -0
- package/dist/components/cms/column-width.js +18 -0
- package/dist/components/cms/column-width.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 +7 -0
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +114 -28
- package/dist/lib/cms-views/view-model.js +262 -71
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +28 -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 +5 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +3 -0
- package/dist/lib/i18n/messages.en.js +5 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +8 -0
- package/dist/lib/operations/session-hydration.js +3 -0
- package/dist/lib/operations/session-hydration.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 +39 -0
- package/dist/ui/status-dot.js +36 -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, canonicalViewState, configViewIssues,
|
|
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 `
|
|
69
|
-
* a visible, one-time console warning — never a silent swallow. This is a
|
|
70
|
-
* config mistake (editors can't fix config-as-code), so it takes the same
|
|
71
|
-
* console channel the i18n runtime uses for config problems, not an editor
|
|
72
|
-
* the first-declared
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
//
|
|
464
|
-
//
|
|
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 `
|
|
467
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 });
|