@elytracms/studio 0.0.74 → 0.0.76
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 +166 -5
- package/convex/schema.ts +21 -0
- package/dist/components/app-views/default-modules.js +4 -4
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +37 -2
- package/dist/components/cms/cms-store.js +121 -8
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +34 -17
- 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 +8 -0
- package/dist/components/cms/column-width.js.map +1 -1
- package/dist/components/cms/doc-state.d.ts +53 -0
- package/dist/components/cms/doc-state.js +58 -1
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +8 -4
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/project-list.js +6 -2
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/studio-project-screen.js +4 -2
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +29 -11
- package/dist/lib/cms-views/view-model.js +43 -16
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +16 -5
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +4 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +2 -0
- package/dist/lib/i18n/messages.en.js +7 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +8 -0
- package/dist/lib/operations/cms-persistence.js +9 -1
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/operations/session-hydration.d.ts +22 -0
- package/dist/lib/operations/session-hydration.js +12 -0
- package/dist/lib/operations/session-hydration.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +14 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.js +12 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/navigate-once.d.ts +77 -0
- package/dist/router/navigate-once.js +104 -0
- package/dist/router/navigate-once.js.map +1 -0
- package/dist/router/root.js +7 -3
- package/dist/router/root.js.map +1 -1
- package/dist/ui/status-dot.d.ts +17 -6
- package/dist/ui/status-dot.js +9 -7
- package/dist/ui/status-dot.js.map +1 -1
- package/package.json +2 -2
|
@@ -117,7 +117,7 @@ function defaultView(collection, pseudo = []) {
|
|
|
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, initialTimestamps, initialLocaleConfig, }) {
|
|
120
|
+
export function CmsProvider({ children, projectId, initialCollections, initialDocuments, initialPublishedKeys, initialTimestamps, initialPublishTimestamps, initialChangedKeys, 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
|
|
@@ -166,6 +166,71 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
166
166
|
});
|
|
167
167
|
}, []);
|
|
168
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]);
|
|
204
|
+
// EC-449: the documents whose draft carries unpublished changes — the amber
|
|
205
|
+
// "changed" `$state` dot. A SET like `publishedKeys` (membership = the
|
|
206
|
+
// materialized `hasUnpublishedChanges === true`); seeded from hydration, then
|
|
207
|
+
// re-stamped authoritatively from returned records — the verdict is computed
|
|
208
|
+
// ONCE at the write chokepoint (both adapters), never client-side per render.
|
|
209
|
+
const [changedKeys, setChangedKeys] = useState(() => new Set(initialChangedKeys ?? []));
|
|
210
|
+
// Adopt the authoritative flag: `true` adds the key; `false`/`undefined` (not
|
|
211
|
+
// changed / cleared at unpublish / not materialized) both remove it — the dot
|
|
212
|
+
// renders those identically, so the set stores no tri-state.
|
|
213
|
+
const stampHasUnpublishedChanges = useCallback((ref, changed) => {
|
|
214
|
+
setChangedKeys((prev) => {
|
|
215
|
+
const key = documentKey(ref);
|
|
216
|
+
if (prev.has(key) === (changed === true))
|
|
217
|
+
return prev;
|
|
218
|
+
const next = new Set(prev);
|
|
219
|
+
if (changed === true)
|
|
220
|
+
next.add(key);
|
|
221
|
+
else
|
|
222
|
+
next.delete(key);
|
|
223
|
+
return next;
|
|
224
|
+
});
|
|
225
|
+
}, []);
|
|
226
|
+
const documentHasUnpublishedChanges = useCallback((ref) => changedKeys.has(documentKey(ref)), [changedKeys]);
|
|
227
|
+
// The one adoption seam for a SAVED document record (persister queue upsert,
|
|
228
|
+
// inline cell edit, bulk set-field, restore): stamp the server-computed
|
|
229
|
+
// changed verdict for the record's ref. Referentially stable — the persister
|
|
230
|
+
// closes over it once.
|
|
231
|
+
const adoptSavedRecord = useCallback((record) => {
|
|
232
|
+
stampHasUnpublishedChanges({ collection: record.document.collection, id: record.document.id }, record.hasUnpublishedChanges);
|
|
233
|
+
}, [stampHasUnpublishedChanges]);
|
|
169
234
|
const [activeViews, setActiveViews] = useState({});
|
|
170
235
|
// The live table state per collection where it diverges from the active
|
|
171
236
|
// view; absent key = exactly the active view's state (not dirty).
|
|
@@ -203,6 +268,10 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
203
268
|
// the schema alone (above); the registry adds blocks/embed prop coverage.
|
|
204
269
|
lookupManifest: studioManifestLookup(),
|
|
205
270
|
onError: setPersistError,
|
|
271
|
+
// EC-449: adopt the server-computed `hasUnpublishedChanges` verdict from
|
|
272
|
+
// every queued upsert's authoritative record (saves, imports, bulk-cleaned
|
|
273
|
+
// drafts) — the amber list dot follows the chokepoint, never a client guess.
|
|
274
|
+
onDocumentSaved: adoptSavedRecord,
|
|
206
275
|
});
|
|
207
276
|
}
|
|
208
277
|
const registry = useMemo(() => createCollectionRegistry(collections), [collections]);
|
|
@@ -282,12 +351,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
282
351
|
// landed first, then issue the commit's single command.
|
|
283
352
|
await persister.ensureBaseline();
|
|
284
353
|
const { operations } = await getStudioOperations();
|
|
285
|
-
|
|
354
|
+
const result = await operations.documents.upsert({ projectId, document: next });
|
|
355
|
+
// EC-449: adopt the chokepoint's changed verdict (inline edits
|
|
356
|
+
// bypass the persister queue, so its hook doesn't see them).
|
|
357
|
+
if (result.ok)
|
|
358
|
+
adoptSavedRecord(result.value);
|
|
359
|
+
return result;
|
|
286
360
|
},
|
|
287
361
|
}
|
|
288
362
|
: {}),
|
|
289
363
|
}, input);
|
|
290
|
-
}, [registry, localeConfig, projectId, stampUpdatedAt]);
|
|
364
|
+
}, [registry, localeConfig, projectId, stampUpdatedAt, adoptSavedRecord]);
|
|
291
365
|
// Shared insert sequence (EC-326): existingIds/mint/seedDefaults (via
|
|
292
366
|
// `buildNewDocument`) + optimistic append + persist, consumed by both
|
|
293
367
|
// `createDocument` (always inserts) and `ensureSingletonDocument`'s empty
|
|
@@ -379,8 +453,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
379
453
|
// value so the $updatedAt column reflects the actual write, not a guess.
|
|
380
454
|
if (record.value.updatedAt)
|
|
381
455
|
stampUpdatedAt(ref, record.value.updatedAt);
|
|
456
|
+
// EC-447: adopt the authoritative `publishedAt` — set-once on the first
|
|
457
|
+
// publish, absent after unpublish. Passing `undefined` CLEARS the column, so
|
|
458
|
+
// an unpublish reverts $publishedAt to EmDash and a republish records fresh.
|
|
459
|
+
stampPublishedAt(ref, record.value.publishedAt);
|
|
460
|
+
// EC-449: adopt the authoritative changed flag — `false` after a publish
|
|
461
|
+
// (the draft became the baseline), absent after an unpublish (cleared).
|
|
462
|
+
stampHasUnpublishedChanges(ref, record.value.hasUnpublishedChanges);
|
|
382
463
|
}
|
|
383
|
-
}, [projectId, setPublished, stampUpdatedAt]);
|
|
464
|
+
}, [projectId, setPublished, stampUpdatedAt, stampPublishedAt, stampHasUnpublishedChanges]);
|
|
384
465
|
const publishDocument = useCallback(async (ref) => {
|
|
385
466
|
setPublished(ref, true); // optimistic
|
|
386
467
|
// EC-273: no clearing here — the draft is already free of conditionally-hidden
|
|
@@ -439,8 +520,12 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
439
520
|
// Pure map-replace: a restore always targets a document already open in
|
|
440
521
|
// the session.
|
|
441
522
|
setDocuments((prev) => replaceDoc(prev, restored));
|
|
523
|
+
// EC-449: a restore is a save-shaped write — adopt the chokepoint's fresh
|
|
524
|
+
// changed verdict from the returned record (e.g. restoring a published
|
|
525
|
+
// doc's draft to an older version flips the dot to amber immediately).
|
|
526
|
+
adoptSavedRecord(result.value);
|
|
442
527
|
return restored;
|
|
443
|
-
}, [projectId]);
|
|
528
|
+
}, [projectId, adoptSavedRecord]);
|
|
444
529
|
const validate = useCallback((doc) => validateDocument(registry, doc, {
|
|
445
530
|
store,
|
|
446
531
|
localeConfig,
|
|
@@ -620,7 +705,15 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
620
705
|
},
|
|
621
706
|
});
|
|
622
707
|
}
|
|
623
|
-
return operations.documents
|
|
708
|
+
return operations.documents
|
|
709
|
+
.upsert({ projectId, document: setFieldDoc(current) })
|
|
710
|
+
.then((result) => {
|
|
711
|
+
// EC-449: adopt the chokepoint's changed verdict per set-field item
|
|
712
|
+
// (bulk dispatch bypasses the persister queue and its hook).
|
|
713
|
+
if (result.ok)
|
|
714
|
+
adoptSavedRecord(result.value);
|
|
715
|
+
return result;
|
|
716
|
+
});
|
|
624
717
|
};
|
|
625
718
|
}
|
|
626
719
|
return runBulkAction({
|
|
@@ -659,7 +752,13 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
659
752
|
// (`publishedKeys`) via setPublished — there is no separate flat
|
|
660
753
|
// `state` mirror to keep in sync (published-ness is derived from the
|
|
661
754
|
// pointer set).
|
|
662
|
-
|
|
755
|
+
// EC-449: either transition resolves the changed flag — publish writes
|
|
756
|
+
// `false` (the draft became the baseline), unpublish clears it — and
|
|
757
|
+
// bulk has no per-item record reconcile, so mirror that here.
|
|
758
|
+
applyState: (ref, state) => {
|
|
759
|
+
setPublished(ref, state === 'published');
|
|
760
|
+
stampHasUnpublishedChanges(ref, false);
|
|
761
|
+
},
|
|
663
762
|
applyDelete: (ref) => setDocuments((prev) => removeDoc(prev, ref)),
|
|
664
763
|
// EC-247: fold the new value into the session document (same
|
|
665
764
|
// locale-aware path the dispatched upsert used). Not a plain
|
|
@@ -668,7 +767,17 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
668
767
|
applySetField: (ref) => setDocuments((prev) => prev.map((d) => (matchesRef(d, ref) ? setFieldDoc(d) : d))),
|
|
669
768
|
...(dispatch ? { dispatch } : {}),
|
|
670
769
|
}, collectionId, ids, action, setField);
|
|
671
|
-
}, [
|
|
770
|
+
}, [
|
|
771
|
+
projectId,
|
|
772
|
+
titleOf,
|
|
773
|
+
registry,
|
|
774
|
+
localeConfig,
|
|
775
|
+
activeLocale,
|
|
776
|
+
setPublished,
|
|
777
|
+
assetExists,
|
|
778
|
+
adoptSavedRecord,
|
|
779
|
+
stampHasUnpublishedChanges,
|
|
780
|
+
]);
|
|
672
781
|
const store$ = useMemo(() => ({
|
|
673
782
|
...(projectId !== undefined ? { projectId } : {}),
|
|
674
783
|
collections,
|
|
@@ -685,6 +794,8 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
685
794
|
deleteDocument,
|
|
686
795
|
isPublished,
|
|
687
796
|
documentUpdatedAt,
|
|
797
|
+
documentPublishedAt,
|
|
798
|
+
documentHasUnpublishedChanges,
|
|
688
799
|
publishDocument,
|
|
689
800
|
unpublishDocument,
|
|
690
801
|
loadDocumentVersions,
|
|
@@ -738,6 +849,8 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
738
849
|
deleteDocument,
|
|
739
850
|
isPublished,
|
|
740
851
|
documentUpdatedAt,
|
|
852
|
+
documentPublishedAt,
|
|
853
|
+
documentHasUnpublishedChanges,
|
|
741
854
|
publishDocument,
|
|
742
855
|
unpublishDocument,
|
|
743
856
|
loadDocumentVersions,
|