@pramen/cms-editor 0.0.28 → 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.28",
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,14 +179,31 @@ 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
- await api.call("addBlock", { pageId: page.id, blockTypeSlug: slug, region, fields: {} });
188
- await reload();
189
- flash("block added");
192
+ const { block, placement } = await api.call<{
193
+ block: { id: string; title?: string | null; fields?: Record<string, unknown> | null };
194
+ placement: { id: string; isShared?: boolean | number };
195
+ }>("addBlock", { pageId: page.id, blockTypeSlug: slug, region, fields: {} });
196
+ const rb: RenderedBlock = {
197
+ id: String(placement.id),
198
+ block_id: String(block.id),
199
+ block_type: slug,
200
+ title: block.title ?? null,
201
+ fields: block.fields ?? {},
202
+ is_shared: Boolean(placement.isShared),
203
+ };
204
+ patchRegion(region, (list) => list.map((b) => (b.id === tempId ? rb : b)));
190
205
  } catch (e) {
206
+ patchRegion(region, (list) => list.filter((b) => b.id !== tempId));
191
207
  setErr(errMsg(e));
192
208
  }
193
209
  };
@@ -255,7 +271,6 @@ export function PageEditor({ api, page, blockTypes, tab, onTab, onBack, onChange
255
271
 
256
272
  <div className="overflow-auto py-1.5">
257
273
  {err ? <Banner>{err}</Banner> : null}
258
- {msg ? <Banner ok>{msg}</Banner> : null}
259
274
  {regions.map((r) => {
260
275
  const blocks = assembled?.regions[r.name] ?? [];
261
276
  const allowed = r.allowedTypes && r.allowedTypes.length ? r.allowedTypes : blockTypes.map((b) => b.slug);
@@ -337,13 +352,17 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
337
352
  const placementId = block.id;
338
353
 
339
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.
340
357
  useEffect(() => {
358
+ if (block.pending) { setFields({}); return; }
341
359
  let alive = true;
342
360
  api.call<{ fields?: Record<string, unknown> }>("getBlock", { blockId }).then((b) => { if (alive) setFields(b?.fields ?? {}); }).catch((e) => onError(errMsg(e)));
343
361
  return () => { alive = false; };
344
- }, [api, blockId, onError]);
362
+ }, [api, blockId, onError, block.pending]);
345
363
 
346
364
  const flush = useCallback(async () => {
365
+ if (block.pending) return; // don't write against a placeholder's temp id
347
366
  if (timer.current) { clearTimeout(timer.current); timer.current = undefined; }
348
367
  const next = pending.current;
349
368
  if (!next) return;
@@ -358,7 +377,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
358
377
  onError(errMsg(e));
359
378
  setSaveState("idle");
360
379
  }
361
- }, [api, blockId, placementId, onPatch, onError]);
380
+ }, [api, blockId, placementId, onPatch, onError, block.pending]);
362
381
 
363
382
  // Flush a pending edit if the card unmounts (nav away, reorder remount) before the debounce.
364
383
  useEffect(() => () => { if (pending.current) void flush(); }, [flush]);
@@ -379,7 +398,7 @@ function BlockCard({ api, block, blockType, isFirst, isLast, onMove, onRemove, o
379
398
  // image is set to the whole card so you drag a preview of the block, not just the grip.
380
399
  <div
381
400
  ref={cardRef}
382
- 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" : ""}`}
383
402
  onDragOver={(e) => { e.preventDefault(); onDragOverBlock(); }}
384
403
  onDrop={(e) => { e.preventDefault(); onDropBlock(); }}
385
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 {