@cmssy/react 9.2.0 → 9.3.0

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.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var core = require('@cmssy/core');
4
4
  var react = require('react');
5
+ var blockErrorBoundary = require('@cmssy/react/block-error-boundary');
5
6
  var jsxRuntime = require('react/jsx-runtime');
6
7
  var types = require('@cmssy/types');
7
8
 
@@ -48,41 +49,155 @@ function blocksToMeta(blocks, defaults = {}) {
48
49
  }
49
50
  return out;
50
51
  }
52
+
53
+ // src/components/block-error.ts
54
+ var BLOCK_ERROR_KEY = "__cmssyBlockError";
55
+ var SOURCES = [
56
+ "loader",
57
+ "render",
58
+ "unregistered"
59
+ ];
60
+ function blockErrorMessage(err) {
61
+ return err instanceof Error ? err.message : String(err);
62
+ }
63
+ function unregisteredBlockError(type) {
64
+ return {
65
+ source: "unregistered",
66
+ message: `Block type "${type}" is not registered in this site's blocks array.`
67
+ };
68
+ }
69
+ function markBlockError(error) {
70
+ return { [BLOCK_ERROR_KEY]: error };
71
+ }
72
+ function readBlockError(value) {
73
+ if (typeof value !== "object" || value === null) return void 0;
74
+ const marked = value[BLOCK_ERROR_KEY];
75
+ if (typeof marked !== "object" || marked === null) return void 0;
76
+ const { message, source } = marked;
77
+ if (typeof message !== "string") return void 0;
78
+ if (!SOURCES.includes(source)) return void 0;
79
+ return { message, source };
80
+ }
81
+ var SOURCE_LABELS = {
82
+ loader: "loader failed",
83
+ render: "render failed",
84
+ unregistered: "type not registered"
85
+ };
86
+ var cardStyle = {
87
+ boxSizing: "border-box",
88
+ margin: "8px 0",
89
+ padding: "12px 16px",
90
+ border: "1px dashed #ef4444",
91
+ borderRadius: "8px",
92
+ background: "rgba(239, 68, 68, 0.08)",
93
+ color: "#ef4444",
94
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
95
+ fontSize: "13px",
96
+ lineHeight: 1.5,
97
+ textAlign: "left"
98
+ };
99
+ var headingStyle = {
100
+ fontWeight: 700,
101
+ marginBottom: "4px"
102
+ };
103
+ var metaStyle = {
104
+ opacity: 0.8,
105
+ marginBottom: "4px"
106
+ };
107
+ var messageStyle = {
108
+ whiteSpace: "pre-wrap",
109
+ overflowWrap: "anywhere"
110
+ };
111
+ function BlockErrorCard({
112
+ blockType,
113
+ blockId,
114
+ error
115
+ }) {
116
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", "data-cmssy-block-error": error.source, style: cardStyle, children: [
117
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: headingStyle, children: [
118
+ 'Block "',
119
+ blockType,
120
+ '" - ',
121
+ SOURCE_LABELS[error.source]
122
+ ] }),
123
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: metaStyle, children: [
124
+ "id: ",
125
+ blockId
126
+ ] }),
127
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: messageStyle, children: error.message })
128
+ ] });
129
+ }
51
130
  var WARN_CAP = 256;
52
131
  var warned = /* @__PURE__ */ new Set();
53
132
  function UnknownBlock({ type }) {
54
133
  if (typeof window !== "undefined" && !warned.has(type)) {
55
134
  if (warned.size >= WARN_CAP) warned.clear();
56
135
  warned.add(type);
57
- console.warn(`[cmssy] no component registered for block type "${type}"`);
136
+ console.error(`[cmssy] no component registered for block type "${type}"`);
58
137
  }
59
138
  return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-cmssy-unknown-block": type });
60
139
  }
61
140
  function renderResolvedBlock(block, map, locale, defaultLocale, options = {}) {
62
- const { context, data, resolvedContent, enabledLocales } = options;
141
+ const { context, data, resolvedContent, enabledLocales, error, editMode } = options;
63
142
  const Component = Object.hasOwn(map, block.type) ? map[block.type] : void 0;
64
- const content = resolvedContent ?? core.getBlockContentForLanguage(
65
- block.content,
66
- locale,
67
- defaultLocale,
68
- enabledLocales?.length ? enabledLocales : void 0
69
- );
70
- return /* @__PURE__ */ jsxRuntime.jsx(
143
+ const wrap = (children, hidden = false) => /* @__PURE__ */ jsxRuntime.jsx(
71
144
  "div",
72
145
  {
73
146
  "data-block-id": block.id,
74
147
  "data-block-type": block.type,
75
- style: Component ? void 0 : { display: "none" },
76
- children: Component ? react.createElement(Component, {
77
- content,
78
- style: core.asBucket(block.style),
79
- advanced: core.asBucket(block.advanced),
80
- context,
81
- data
82
- }) : /* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type })
148
+ style: hidden ? { display: "none" } : void 0,
149
+ children
83
150
  },
84
151
  block.id
85
152
  );
153
+ if (!Component) {
154
+ return editMode ? wrap(
155
+ /* @__PURE__ */ jsxRuntime.jsx(
156
+ BlockErrorCard,
157
+ {
158
+ blockType: block.type,
159
+ blockId: block.id,
160
+ error: unregisteredBlockError(block.type)
161
+ }
162
+ )
163
+ ) : wrap(/* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type }), true);
164
+ }
165
+ if (error) {
166
+ if (!editMode) return null;
167
+ return wrap(
168
+ /* @__PURE__ */ jsxRuntime.jsx(
169
+ BlockErrorCard,
170
+ {
171
+ blockType: block.type,
172
+ blockId: block.id,
173
+ error
174
+ }
175
+ )
176
+ );
177
+ }
178
+ const content = resolvedContent ?? core.getBlockContentForLanguage(
179
+ block.content,
180
+ locale,
181
+ defaultLocale,
182
+ enabledLocales?.length ? enabledLocales : void 0
183
+ );
184
+ return wrap(
185
+ /* @__PURE__ */ jsxRuntime.jsx(
186
+ blockErrorBoundary.BlockErrorBoundary,
187
+ {
188
+ blockType: block.type,
189
+ blockId: block.id,
190
+ editMode,
191
+ children: react.createElement(Component, {
192
+ content,
193
+ style: core.asBucket(block.style),
194
+ advanced: core.asBucket(block.advanced),
195
+ context,
196
+ data
197
+ })
198
+ }
199
+ )
200
+ );
86
201
  }
87
202
  async function resolveBlocks(blocks, loaderMap, locale, defaultLocale, context, enabledLocales) {
88
203
  return Promise.all(
@@ -95,19 +210,21 @@ async function resolveBlocks(blocks, loaderMap, locale, defaultLocale, context,
95
210
  );
96
211
  const loader = loaderMap[block.type];
97
212
  let data;
213
+ let error;
98
214
  if (loader) {
99
215
  try {
100
216
  data = await loader({ content, context });
101
217
  } catch (err) {
102
218
  if (typeof console !== "undefined") {
103
- console.warn(
219
+ console.error(
104
220
  `[cmssy] loader for block "${block.type}" (${block.id}) failed`,
105
221
  err
106
222
  );
107
223
  }
224
+ error = { message: blockErrorMessage(err), source: "loader" };
108
225
  }
109
226
  }
110
- return { content, data };
227
+ return { content, data, error };
111
228
  })
112
229
  );
113
230
  }
@@ -148,7 +265,8 @@ async function CmssyServerPage({
148
265
  config,
149
266
  forms,
150
267
  auth,
151
- workspace
268
+ workspace,
269
+ editMode
152
270
  }) {
153
271
  if (!page) return null;
154
272
  const { locale, defaultLocale, enabledLocales } = await resolveRenderLocale({
@@ -180,10 +298,25 @@ async function CmssyServerPage({
180
298
  context,
181
299
  data: resolved[i]?.data,
182
300
  resolvedContent: resolved[i]?.content,
183
- enabledLocales
301
+ enabledLocales,
302
+ error: resolved[i]?.error,
303
+ editMode
184
304
  })
185
305
  ) });
186
306
  }
307
+ function collectBlockData(blocks, resolved, isPreview) {
308
+ const data = {};
309
+ blocks.forEach((block, index) => {
310
+ const entry = resolved[index];
311
+ if (!entry) return;
312
+ if (entry.error && isPreview) {
313
+ data[block.id] = markBlockError(entry.error);
314
+ } else if (entry.data !== void 0) {
315
+ data[block.id] = entry.data;
316
+ }
317
+ });
318
+ return data;
319
+ }
187
320
  async function resolveBlockData({
188
321
  page,
189
322
  blocks,
@@ -210,12 +343,7 @@ async function resolveBlockData({
210
343
  context,
211
344
  enabledLocales
212
345
  );
213
- const data = {};
214
- page.blocks.forEach((block, index) => {
215
- const value = resolved[index]?.data;
216
- if (value !== void 0) data[block.id] = value;
217
- });
218
- return data;
346
+ return collectBlockData(page.blocks, resolved, isPreview);
219
347
  }
220
348
  async function resolveLayoutBlockData({
221
349
  groups,
@@ -246,12 +374,7 @@ async function resolveLayoutBlockData({
246
374
  context,
247
375
  enabledLocales
248
376
  );
249
- const data = {};
250
- layoutBlocks.forEach((block, index) => {
251
- const value = resolved[index]?.data;
252
- if (value !== void 0) data[block.id] = value;
253
- });
254
- return data;
377
+ return collectBlockData(layoutBlocks, resolved, isPreview);
255
378
  }
256
379
  async function CmssyServerLayout({
257
380
  groups,
@@ -260,7 +383,8 @@ async function CmssyServerLayout({
260
383
  locale: localeProp,
261
384
  defaultLocale: defaultLocaleProp,
262
385
  enabledLocales: enabledLocalesProp,
263
- config
386
+ config,
387
+ editMode
264
388
  }) {
265
389
  const { locale, defaultLocale, enabledLocales } = await resolveRenderLocale({
266
390
  locale: localeProp,
@@ -287,7 +411,9 @@ async function CmssyServerLayout({
287
411
  context,
288
412
  data: resolved[i]?.data,
289
413
  resolvedContent: resolved[i]?.content,
290
- enabledLocales
414
+ enabledLocales,
415
+ error: resolved[i]?.error,
416
+ editMode
291
417
  })
292
418
  ) });
293
419
  }
@@ -300,32 +426,71 @@ function CmssyBlock({
300
426
  patchedStyle,
301
427
  patchedAdvanced,
302
428
  editable,
429
+ editMode,
303
430
  layoutPosition,
304
431
  context,
305
432
  data
306
433
  }) {
434
+ const inEditMode = editMode ?? editable ?? false;
307
435
  const Component = Object.hasOwn(blockMap, block.type) ? blockMap[block.type] : void 0;
308
- const base = core.getBlockContentForLanguage(block.content, locale, defaultLocale);
309
- const content = patchedContent ? { ...base, ...patchedContent } : base;
310
- const style = patchedStyle ? { ...core.asBucket(block.style), ...patchedStyle } : core.asBucket(block.style);
311
- const advanced = patchedAdvanced ? { ...core.asBucket(block.advanced), ...patchedAdvanced } : core.asBucket(block.advanced);
312
- return /* @__PURE__ */ jsxRuntime.jsx(
436
+ const blockError = readBlockError(data);
437
+ const wrap = (children, hidden = false) => /* @__PURE__ */ jsxRuntime.jsx(
313
438
  "div",
314
439
  {
315
440
  "data-block-id": block.id,
316
441
  "data-block-type": block.type,
317
442
  "data-layout-position": layoutPosition,
318
443
  draggable: editable || void 0,
319
- style: Component ? void 0 : { display: "none" },
320
- children: Component ? react.createElement(Component, {
321
- content,
322
- style,
323
- advanced,
324
- context,
325
- data
326
- }) : /* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type })
444
+ style: hidden ? { display: "none" } : void 0,
445
+ children
327
446
  }
328
447
  );
448
+ if (!Component) {
449
+ return inEditMode ? wrap(
450
+ /* @__PURE__ */ jsxRuntime.jsx(
451
+ BlockErrorCard,
452
+ {
453
+ blockType: block.type,
454
+ blockId: block.id,
455
+ error: unregisteredBlockError(block.type)
456
+ }
457
+ )
458
+ ) : wrap(/* @__PURE__ */ jsxRuntime.jsx(UnknownBlock, { type: block.type }), true);
459
+ }
460
+ if (blockError) {
461
+ if (!inEditMode) return null;
462
+ return wrap(
463
+ /* @__PURE__ */ jsxRuntime.jsx(
464
+ BlockErrorCard,
465
+ {
466
+ blockType: block.type,
467
+ blockId: block.id,
468
+ error: blockError
469
+ }
470
+ )
471
+ );
472
+ }
473
+ const base = core.getBlockContentForLanguage(block.content, locale, defaultLocale);
474
+ const content = patchedContent ? { ...base, ...patchedContent } : base;
475
+ const style = patchedStyle ? { ...core.asBucket(block.style), ...patchedStyle } : core.asBucket(block.style);
476
+ const advanced = patchedAdvanced ? { ...core.asBucket(block.advanced), ...patchedAdvanced } : core.asBucket(block.advanced);
477
+ return wrap(
478
+ /* @__PURE__ */ jsxRuntime.jsx(
479
+ blockErrorBoundary.BlockErrorBoundary,
480
+ {
481
+ blockType: block.type,
482
+ blockId: block.id,
483
+ editMode: inEditMode,
484
+ children: react.createElement(Component, {
485
+ content,
486
+ style,
487
+ advanced,
488
+ context,
489
+ data
490
+ })
491
+ }
492
+ )
493
+ );
329
494
  }
330
495
 
331
496
  Object.defineProperty(exports, "CmssyRequestError", {
package/dist/index.d.cts CHANGED
@@ -24,13 +24,14 @@ interface CmssyServerPageProps {
24
24
  auth?: CmssyBlockAuthContext;
25
25
  /** Workspace identity, exposed via context.workspace. Resolved by createCmssyPage. */
26
26
  workspace?: CmssyBlockWorkspace;
27
+ editMode?: boolean;
27
28
  }
28
29
  /**
29
30
  * Async React Server Component (Next.js App Router / RSC). It runs each block's
30
31
  * loader server-side before rendering, so it must be rendered in a server
31
32
  * component tree (as `createCmssyPage` does) - not in a client component.
32
33
  */
33
- declare function CmssyServerPage({ page, blocks, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, forms, auth, workspace, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
34
+ declare function CmssyServerPage({ page, blocks, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, forms, auth, workspace, editMode, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
34
35
 
35
36
  interface ResolveBlockDataOptions {
36
37
  page: CmssyPageData | null;
@@ -68,13 +69,14 @@ interface CmssyServerLayoutProps {
68
69
  * Without it the SDK has to guess, and its guess is "en".
69
70
  */
70
71
  config?: CmssyClientConfig;
72
+ editMode?: boolean;
71
73
  }
72
74
  /**
73
75
  * Async React Server Component. Like CmssyServerPage it runs each block's
74
76
  * loader server-side before rendering, so a header block can list categories
75
77
  * the same way a page block can. Must be rendered in a server component tree.
76
78
  */
77
- declare function CmssyServerLayout({ groups, blocks, position, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, }: CmssyServerLayoutProps): Promise<react_jsx_runtime.JSX.Element | null>;
79
+ declare function CmssyServerLayout({ groups, blocks, position, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, editMode, }: CmssyServerLayoutProps): Promise<react_jsx_runtime.JSX.Element | null>;
78
80
 
79
81
  interface CmssyBlockProps {
80
82
  block: RawBlock;
@@ -85,11 +87,12 @@ interface CmssyBlockProps {
85
87
  patchedStyle?: Record<string, unknown>;
86
88
  patchedAdvanced?: Record<string, unknown>;
87
89
  editable?: boolean;
90
+ editMode?: boolean;
88
91
  layoutPosition?: string;
89
92
  context?: CmssyBlockContext;
90
93
  data?: unknown;
91
94
  }
92
- declare function CmssyBlock({ block, locale, defaultLocale, blockMap, patchedContent, patchedStyle, patchedAdvanced, editable, layoutPosition, context, data, }: CmssyBlockProps): react_jsx_runtime.JSX.Element;
95
+ declare function CmssyBlock({ block, locale, defaultLocale, blockMap, patchedContent, patchedStyle, patchedAdvanced, editable, editMode, layoutPosition, context, data, }: CmssyBlockProps): react_jsx_runtime.JSX.Element | null;
93
96
 
94
97
  interface UnknownBlockProps {
95
98
  type: string;
package/dist/index.d.ts CHANGED
@@ -24,13 +24,14 @@ interface CmssyServerPageProps {
24
24
  auth?: CmssyBlockAuthContext;
25
25
  /** Workspace identity, exposed via context.workspace. Resolved by createCmssyPage. */
26
26
  workspace?: CmssyBlockWorkspace;
27
+ editMode?: boolean;
27
28
  }
28
29
  /**
29
30
  * Async React Server Component (Next.js App Router / RSC). It runs each block's
30
31
  * loader server-side before rendering, so it must be rendered in a server
31
32
  * component tree (as `createCmssyPage` does) - not in a client component.
32
33
  */
33
- declare function CmssyServerPage({ page, blocks, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, forms, auth, workspace, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
34
+ declare function CmssyServerPage({ page, blocks, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, forms, auth, workspace, editMode, }: CmssyServerPageProps): Promise<react_jsx_runtime.JSX.Element | null>;
34
35
 
35
36
  interface ResolveBlockDataOptions {
36
37
  page: CmssyPageData | null;
@@ -68,13 +69,14 @@ interface CmssyServerLayoutProps {
68
69
  * Without it the SDK has to guess, and its guess is "en".
69
70
  */
70
71
  config?: CmssyClientConfig;
72
+ editMode?: boolean;
71
73
  }
72
74
  /**
73
75
  * Async React Server Component. Like CmssyServerPage it runs each block's
74
76
  * loader server-side before rendering, so a header block can list categories
75
77
  * the same way a page block can. Must be rendered in a server component tree.
76
78
  */
77
- declare function CmssyServerLayout({ groups, blocks, position, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, }: CmssyServerLayoutProps): Promise<react_jsx_runtime.JSX.Element | null>;
79
+ declare function CmssyServerLayout({ groups, blocks, position, locale: localeProp, defaultLocale: defaultLocaleProp, enabledLocales: enabledLocalesProp, config, editMode, }: CmssyServerLayoutProps): Promise<react_jsx_runtime.JSX.Element | null>;
78
80
 
79
81
  interface CmssyBlockProps {
80
82
  block: RawBlock;
@@ -85,11 +87,12 @@ interface CmssyBlockProps {
85
87
  patchedStyle?: Record<string, unknown>;
86
88
  patchedAdvanced?: Record<string, unknown>;
87
89
  editable?: boolean;
90
+ editMode?: boolean;
88
91
  layoutPosition?: string;
89
92
  context?: CmssyBlockContext;
90
93
  data?: unknown;
91
94
  }
92
- declare function CmssyBlock({ block, locale, defaultLocale, blockMap, patchedContent, patchedStyle, patchedAdvanced, editable, layoutPosition, context, data, }: CmssyBlockProps): react_jsx_runtime.JSX.Element;
95
+ declare function CmssyBlock({ block, locale, defaultLocale, blockMap, patchedContent, patchedStyle, patchedAdvanced, editable, editMode, layoutPosition, context, data, }: CmssyBlockProps): react_jsx_runtime.JSX.Element | null;
93
96
 
94
97
  interface UnknownBlockProps {
95
98
  type: string;