@pramen/cms-editor 0.0.29 → 0.0.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms-editor",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "Visual block/page editor for @pramen/cms — a standalone React SPA that talks to the CMS handlers over HTTP.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -164,7 +164,6 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
164
164
  const [ct, setCt] = useState<ContentType | null>(null);
165
165
  const [assembled, setAssembled] = useState<AssembledPage | null>(null);
166
166
  const [err, setErr] = useState("");
167
- const [msg, setMsg] = useState("");
168
167
 
169
168
  const btBySlug = useMemo(() => new Map(blockTypes.map((b) => [b.slug, b])), [blockTypes]);
170
169
 
@@ -180,12 +179,16 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
180
179
  useEffect(() => { reload(); }, [reload]);
181
180
 
182
181
  const regions: RegionDefinition[] = ct?.regions ?? [];
183
- const flash = (m: string) => { setMsg(m); setTimeout(() => setMsg(""), 1800); };
182
+
183
+ const patchRegion = (region: string, fn: (list: RenderedBlock[]) => RenderedBlock[]) =>
184
+ setAssembled((prev) => (prev ? { ...prev, regions: { ...prev.regions, [region]: fn(prev.regions[region] ?? []) } } : prev));
184
185
 
185
186
  const addBlock = async (region: string, slug: string) => {
187
+ // Optimistic: show a placeholder immediately (no wait for the round trip), then
188
+ // reconcile with the persisted row addBlock echoes — or roll it back on failure.
189
+ const tempId = `tmp:${crypto.randomUUID()}`;
190
+ patchRegion(region, (list) => [...list, { id: tempId, block_id: tempId, block_type: slug, title: null, fields: {}, is_shared: false, pending: true }]);
186
191
  try {
187
- // addBlock echoes the created block + placement, so append it locally instead of a
188
- // full getContentType+getPage reload — one round trip instead of three.
189
192
  const { block, placement } = await api.call<{
190
193
  block: { id: string; title?: string | null; fields?: Record<string, unknown> | null };
191
194
  placement: { id: string; isShared?: boolean | number };
@@ -198,9 +201,9 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
198
201
  fields: block.fields ?? {},
199
202
  is_shared: Boolean(placement.isShared),
200
203
  };
201
- setAssembled((prev) => (prev ? { ...prev, regions: { ...prev.regions, [region]: [...(prev.regions[region] ?? []), rb] } } : prev));
202
- flash("block added");
204
+ patchRegion(region, (list) => list.map((b) => (b.id === tempId ? rb : b)));
203
205
  } catch (e) {
206
+ patchRegion(region, (list) => list.filter((b) => b.id !== tempId));
204
207
  setErr(errMsg(e));
205
208
  }
206
209
  };
@@ -268,7 +271,6 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
268
271
 
269
272
  <div className="overflow-auto py-1.5">
270
273
  {err ? <Banner>{err}</Banner> : null}
271
- {msg ? <Banner ok>{msg}</Banner> : null}
272
274
  {regions.map((r) => {
273
275
  const blocks = assembled?.regions[r.name] ?? [];
274
276
  const allowed = r.allowedTypes && r.allowedTypes.length ? r.allowedTypes : blockTypes.map((b) => b.slug);
@@ -350,13 +352,17 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
350
352
  const placementId = block.id;
351
353
 
352
354
  // Load RAW fields (media as ids, richtext as HTML) so the value round-trips on save.
355
+ // A pending optimistic block has no persisted row yet — start empty and skip the fetch
356
+ // (its temp id would 404); when it reconciles to real ids the card remounts and fetches.
353
357
  useEffect(() => {
358
+ if (block.pending) { setFields({}); return; }
354
359
  let alive = true;
355
360
  api.call<{ fields?: Record<string, unknown> }>("getBlock", { blockId }).then((b) => { if (alive) setFields(b?.fields ?? {}); }).catch((e) => onError(errMsg(e)));
356
361
  return () => { alive = false; };
357
- }, [api, blockId, onError]);
362
+ }, [api, blockId, onError, block.pending]);
358
363
 
359
364
  const flush = useCallback(async () => {
365
+ if (block.pending) return; // don't write against a placeholder's temp id
360
366
  if (timer.current) { clearTimeout(timer.current); timer.current = undefined; }
361
367
  const next = pending.current;
362
368
  if (!next) return;
@@ -371,7 +377,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
371
377
  onError(errMsg(e));
372
378
  setSaveState("idle");
373
379
  }
374
- }, [api, blockId, placementId, onPatch, onError]);
380
+ }, [api, blockId, placementId, onPatch, onError, block.pending]);
375
381
 
376
382
  // Flush a pending edit if the card unmounts (nav away, reorder remount) before the debounce.
377
383
  useEffect(() => () => { if (pending.current) void flush(); }, [flush]);
@@ -392,7 +398,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
392
398
  // image is set to the whole card so you drag a preview of the block, not just the grip.
393
399
  <div
394
400
  ref={cardRef}
395
- className={`mb-2.5 rounded-panel border bg-surface-card transition-colors ${isOver ? "border-brand-green" : "border-border"} ${dragging ? "opacity-40" : ""}`}
401
+ className={`mb-2.5 rounded-panel border bg-surface-card transition-colors ${isOver ? "border-brand-green" : "border-border"} ${dragging ? "opacity-40" : ""} ${block.pending ? "pointer-events-none opacity-60" : ""}`}
396
402
  onDragOver={(e) => { e.preventDefault(); onDragOverBlock(); }}
397
403
  onDrop={(e) => { e.preventDefault(); onDropBlock(); }}
398
404
  >
package/src/types.ts CHANGED
@@ -95,6 +95,7 @@ export interface RenderedBlock {
95
95
  title: string | null;
96
96
  fields: Record<string, unknown>;
97
97
  is_shared: boolean;
98
+ pending?: boolean; // optimistic placeholder — not yet persisted (temp ids, no getBlock)
98
99
  }
99
100
 
100
101
  export interface AssembledPage {