@pramen/cms-editor 0.0.31 → 0.0.32
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/index.html +1 -1
- package/dist/{main.6dkmyshq.js → main.kr19xkv0.js} +54 -54
- package/package.json +1 -1
- package/src/components.tsx +35 -2
package/package.json
CHANGED
package/src/components.tsx
CHANGED
|
@@ -165,6 +165,23 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
165
165
|
const [assembled, setAssembled] = useState<AssembledPage | null>(null);
|
|
166
166
|
const [err, setErr] = useState("");
|
|
167
167
|
|
|
168
|
+
// Unsaved-changes guard. Each BlockCard reports its dirty state up; while any block has
|
|
169
|
+
// unsaved edits, warn before leaving the editor (← all pages) and before a browser unload
|
|
170
|
+
// (refresh / close / navigating off-site). Reorder/add/remove keep block state — React
|
|
171
|
+
// reuses BlockCard instances by key — so those don't lose edits and need no guard; only
|
|
172
|
+
// unmounting the whole editor (leaving) or a page unload discards the local edits.
|
|
173
|
+
const dirtyRef = useRef<Set<string>>(new Set());
|
|
174
|
+
const [dirtyCount, setDirtyCount] = useState(0);
|
|
175
|
+
const reportDirty = useCallback((id: string, isDirty: boolean) => {
|
|
176
|
+
const s = dirtyRef.current;
|
|
177
|
+
if (isDirty ? s.has(id) : !s.has(id)) return;
|
|
178
|
+
if (isDirty) s.add(id);
|
|
179
|
+
else s.delete(id);
|
|
180
|
+
setDirtyCount(s.size);
|
|
181
|
+
}, []);
|
|
182
|
+
const confirmLeave = () =>
|
|
183
|
+
dirtyRef.current.size === 0 || window.confirm(`You have unsaved changes in ${dirtyRef.current.size} block${dirtyRef.current.size === 1 ? "" : "s"}. Leave without saving?`);
|
|
184
|
+
|
|
168
185
|
const btBySlug = useMemo(() => new Map(blockTypes.map((b) => [b.slug, b])), [blockTypes]);
|
|
169
186
|
|
|
170
187
|
const reload = useCallback(async () => {
|
|
@@ -178,6 +195,14 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
178
195
|
}, [api, page.typeId, page.slug, page.locale]);
|
|
179
196
|
useEffect(() => { reload(); }, [reload]);
|
|
180
197
|
|
|
198
|
+
// Native prompt on refresh/close/off-site nav while there are unsaved edits.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (dirtyCount === 0) return;
|
|
201
|
+
const onBeforeUnload = (e: BeforeUnloadEvent) => { e.preventDefault(); e.returnValue = ""; };
|
|
202
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
203
|
+
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
|
204
|
+
}, [dirtyCount]);
|
|
205
|
+
|
|
181
206
|
const regions: RegionDefinition[] = ct?.regions ?? [];
|
|
182
207
|
|
|
183
208
|
const patchRegion = (region: string, fn: (list: RenderedBlock[]) => RenderedBlock[]) =>
|
|
@@ -254,7 +279,8 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
254
279
|
return (
|
|
255
280
|
<div className="grid min-h-[calc(100vh-68px)] grid-cols-[260px_1fr_400px] gap-5 px-7 pb-7 pt-2 max-[820px]:grid-cols-1">
|
|
256
281
|
<div className="overflow-auto rounded-panel bg-surface-muted p-[18px]">
|
|
257
|
-
<Button variant="ghost" size="sm" onPress={onBack}>← all pages</Button>
|
|
282
|
+
<Button variant="ghost" size="sm" onPress={() => { if (confirmLeave()) onBack(); }}>← all pages</Button>
|
|
283
|
+
{dirtyCount > 0 ? <p className="mt-2 text-[11px] text-accent-strong">● {dirtyCount} unsaved block{dirtyCount === 1 ? "" : "s"}</p> : null}
|
|
258
284
|
<Section>Regions</Section>
|
|
259
285
|
{regions.map((r) => (
|
|
260
286
|
<div key={r.name} className={`${ROW} mb-2`}>
|
|
@@ -289,6 +315,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
289
315
|
onMove={(d) => reorderMove(blocks, r.name, i, d, reorder)}
|
|
290
316
|
onRemove={() => removeBlock(b)}
|
|
291
317
|
onPatch={patchBlockFields}
|
|
318
|
+
onDirtyChange={reportDirty}
|
|
292
319
|
onError={setErr}
|
|
293
320
|
dragging={drag?.region === r.name && drag.from === i}
|
|
294
321
|
isOver={!!drag && drag.region === r.name && drag.over === i && drag.from !== i}
|
|
@@ -325,7 +352,7 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
325
352
|
// as the WYSIWYG, media as a thumbnail picker). Edits are held locally and committed only on
|
|
326
353
|
// an explicit Save — the header + footer show an "unsaved" state until you do, and nothing is
|
|
327
354
|
// written until you click Save. Collapse folds it to a one-line plain-text preview.
|
|
328
|
-
function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, onPatch, onError, dragging, isOver, onDragStartBlock, onDragOverBlock, onDropBlock, onDragEndBlock }: {
|
|
355
|
+
function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, onPatch, onDirtyChange, onError, dragging, isOver, onDragStartBlock, onDragOverBlock, onDropBlock, onDragEndBlock }: {
|
|
329
356
|
api: Api;
|
|
330
357
|
block: RenderedBlock;
|
|
331
358
|
blockType: BlockType | undefined;
|
|
@@ -334,6 +361,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
334
361
|
onMove: (dir: number) => void;
|
|
335
362
|
onRemove: () => void;
|
|
336
363
|
onPatch: (placementId: string, fields: Record<string, unknown>) => void;
|
|
364
|
+
onDirtyChange: (placementId: string, dirty: boolean) => void;
|
|
337
365
|
onError: (s: string) => void;
|
|
338
366
|
dragging: boolean;
|
|
339
367
|
isOver: boolean;
|
|
@@ -387,6 +415,11 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
387
415
|
|
|
388
416
|
const change = (next: Record<string, unknown>) => setFields(next);
|
|
389
417
|
|
|
418
|
+
// Report dirty state up (for the editor's leave/unload guard); clear it on unmount so a
|
|
419
|
+
// removed block never leaves a stale "unsaved" flag behind.
|
|
420
|
+
useEffect(() => { onDirtyChange(placementId, dirty); }, [dirty, placementId, onDirtyChange]);
|
|
421
|
+
useEffect(() => () => { onDirtyChange(placementId, false); }, [placementId, onDirtyChange]);
|
|
422
|
+
|
|
390
423
|
const schema: FieldDefinition[] = blockType?.fieldsSchema ?? [];
|
|
391
424
|
const name = blockType?.name ?? block.block_type;
|
|
392
425
|
|