@pramen/cms-editor 0.0.30 → 0.0.31
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/app.css +1 -1
- package/dist/index.html +1 -1
- package/dist/{main.wjbaszqq.js → main.6dkmyshq.js} +39 -39
- package/package.json +1 -1
- package/src/components.tsx +39 -31
package/package.json
CHANGED
package/src/components.tsx
CHANGED
|
@@ -322,9 +322,9 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
// A single block, edited inline: the block IS its editor. Fields render in place (rich text
|
|
325
|
-
// as the WYSIWYG, media as a thumbnail picker)
|
|
326
|
-
//
|
|
327
|
-
//
|
|
325
|
+
// as the WYSIWYG, media as a thumbnail picker). Edits are held locally and committed only on
|
|
326
|
+
// an explicit Save — the header + footer show an "unsaved" state until you do, and nothing is
|
|
327
|
+
// written until you click Save. Collapse folds it to a one-line plain-text preview.
|
|
328
328
|
function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, onPatch, onError, dragging, isOver, onDragStartBlock, onDragOverBlock, onDropBlock, onDragEndBlock }: {
|
|
329
329
|
api: Api;
|
|
330
330
|
block: RenderedBlock;
|
|
@@ -345,8 +345,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
345
345
|
const [fields, setFields] = useState<Record<string, unknown> | null>(null);
|
|
346
346
|
const [collapsed, setCollapsed] = useState(false);
|
|
347
347
|
const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">("idle");
|
|
348
|
-
const
|
|
349
|
-
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
348
|
+
const saved = useRef<string>(""); // JSON of the last-persisted fields — the dirty baseline
|
|
350
349
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
351
350
|
const blockId = block.block_id;
|
|
352
351
|
const placementId = block.id;
|
|
@@ -355,39 +354,38 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
355
354
|
// A pending optimistic block has no persisted row yet — start empty and skip the fetch
|
|
356
355
|
// (its temp id would 404); when it reconciles to real ids the card remounts and fetches.
|
|
357
356
|
useEffect(() => {
|
|
358
|
-
if (block.pending) { setFields({}); return; }
|
|
357
|
+
if (block.pending) { setFields({}); saved.current = "{}"; return; }
|
|
359
358
|
let alive = true;
|
|
360
|
-
api.call<{ fields?: Record<string, unknown> }>("getBlock", { blockId }).then((b) => {
|
|
359
|
+
api.call<{ fields?: Record<string, unknown> }>("getBlock", { blockId }).then((b) => {
|
|
360
|
+
if (!alive) return;
|
|
361
|
+
const f = b?.fields ?? {};
|
|
362
|
+
setFields(f);
|
|
363
|
+
saved.current = JSON.stringify(f);
|
|
364
|
+
}).catch((e) => onError(errMsg(e)));
|
|
361
365
|
return () => { alive = false; };
|
|
362
366
|
}, [api, blockId, onError, block.pending]);
|
|
363
367
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
|
|
368
|
+
const dirty = fields != null && JSON.stringify(fields) !== saved.current;
|
|
369
|
+
|
|
370
|
+
// Explicit save — the ONLY thing that writes. No autosave, no save-on-blur, no save-on-unmount.
|
|
371
|
+
const save = useCallback(async () => {
|
|
372
|
+
if (block.pending || fields == null) return;
|
|
373
|
+
const snapshot = JSON.stringify(fields);
|
|
374
|
+
if (snapshot === saved.current) return;
|
|
370
375
|
setSaveState("saving");
|
|
371
376
|
try {
|
|
372
|
-
await api.call("updateBlock", { blockId, fields
|
|
373
|
-
|
|
377
|
+
await api.call("updateBlock", { blockId, fields });
|
|
378
|
+
saved.current = snapshot;
|
|
379
|
+
onPatch(placementId, fields);
|
|
374
380
|
setSaveState("saved");
|
|
375
|
-
setTimeout(() => setSaveState((s) => (s === "saved" ? "idle" : s)),
|
|
381
|
+
setTimeout(() => setSaveState((s) => (s === "saved" ? "idle" : s)), 1500);
|
|
376
382
|
} catch (e) {
|
|
377
383
|
onError(errMsg(e));
|
|
378
384
|
setSaveState("idle");
|
|
379
385
|
}
|
|
380
|
-
}, [api, blockId, placementId, onPatch, onError, block.pending]);
|
|
381
|
-
|
|
382
|
-
// Flush a pending edit if the card unmounts (nav away, reorder remount) before the debounce.
|
|
383
|
-
useEffect(() => () => { if (pending.current) void flush(); }, [flush]);
|
|
386
|
+
}, [api, blockId, placementId, fields, onPatch, onError, block.pending]);
|
|
384
387
|
|
|
385
|
-
const change = (next: Record<string, unknown>) =>
|
|
386
|
-
setFields(next);
|
|
387
|
-
pending.current = next;
|
|
388
|
-
if (timer.current) clearTimeout(timer.current);
|
|
389
|
-
timer.current = setTimeout(() => void flush(), 700);
|
|
390
|
-
};
|
|
388
|
+
const change = (next: Record<string, unknown>) => setFields(next);
|
|
391
389
|
|
|
392
390
|
const schema: FieldDefinition[] = blockType?.fieldsSchema ?? [];
|
|
393
391
|
const name = blockType?.name ?? block.block_type;
|
|
@@ -418,7 +416,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
418
416
|
<button type="button" className="w-4 shrink-0 text-fg-subtle hover:text-fg" title={collapsed ? "Expand" : "Collapse"} onClick={() => setCollapsed((c) => !c)}>{collapsed ? "▸" : "▾"}</button>
|
|
419
417
|
<span className="font-medium text-fg">{name}</span>
|
|
420
418
|
{block.is_shared ? <span className="text-[11px] text-accent-strong">shared</span> : null}
|
|
421
|
-
<span className=
|
|
419
|
+
<span className={`text-[11px] ${dirty && saveState !== "saving" ? "text-accent-strong" : "text-fg-subtle"}`}>{saveState === "saving" ? "saving…" : saveState === "saved" ? "saved ✓" : dirty ? "● unsaved" : ""}</span>
|
|
422
420
|
<span className="flex-1" />
|
|
423
421
|
<Button variant="ghost" size="sm" isDisabled={isFirst} onPress={() => onMove(-1)}>↑</Button>
|
|
424
422
|
<Button variant="ghost" size="sm" isDisabled={isLast} onPress={() => onMove(1)}>↓</Button>
|
|
@@ -429,15 +427,25 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
|
|
|
429
427
|
{fields == null ? "…" : blockPreview(fields) || <span className="italic">empty</span>}
|
|
430
428
|
</div>
|
|
431
429
|
) : (
|
|
432
|
-
|
|
433
|
-
// immediately (flush() no-ops when nothing is pending, so tabbing between fields is free).
|
|
434
|
-
<div className="border-t border-border px-3.5 py-3.5" onBlur={() => void flush()}>
|
|
430
|
+
<div className="border-t border-border px-3.5 py-3.5">
|
|
435
431
|
{fields == null ? (
|
|
436
432
|
<p className="text-sm text-fg-subtle">loading…</p>
|
|
437
433
|
) : schema.length === 0 ? (
|
|
438
434
|
<p className="text-sm text-fg-subtle">This block has no editable fields.</p>
|
|
439
435
|
) : (
|
|
440
|
-
|
|
436
|
+
<>
|
|
437
|
+
<FieldForm schema={schema} value={fields} onChange={change} api={api} />
|
|
438
|
+
<div className="mt-3.5 flex items-center gap-2 border-t border-border pt-3">
|
|
439
|
+
<Button size="sm" onPress={() => void save()} isDisabled={!dirty || saveState === "saving" || block.pending}>
|
|
440
|
+
{saveState === "saving" ? "Saving…" : "Save"}
|
|
441
|
+
</Button>
|
|
442
|
+
{dirty ? (
|
|
443
|
+
<span className="text-[11px] text-accent-strong">Unsaved changes</span>
|
|
444
|
+
) : saveState === "saved" ? (
|
|
445
|
+
<span className="text-[11px] text-fg-subtle">Saved ✓</span>
|
|
446
|
+
) : null}
|
|
447
|
+
</div>
|
|
448
|
+
</>
|
|
441
449
|
)}
|
|
442
450
|
</div>
|
|
443
451
|
)}
|