@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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { buildBlockContext, getBlockContentForLanguage, asBucket, resolveSiteLocales } from '@cmssy/core';
2
2
  export { CmssyRequestError, DEFAULT_CMSSY_API_URL, FORM_QUERY, MODEL_DEFINITIONS_QUERY, MODEL_RECORDS_QUERY, PROTOCOL_VERSION, SITE_CONFIG_QUERY, SUBMIT_FORM_MUTATION, buildBlockContext, buildLocaleSwitchHref, collectFormIds, createCmssyClient, fetchLayouts, fetchPage, fetchPageById, fetchPageMeta, fetchPages, fetchSiteConfig, fields, getBlockContentForLanguage, graphqlRequest, isProtocolCompatible, localizeHref, localizeHtmlLinks, normalizeOrigin, normalizeSlug, parseEditorMessage, postToEditor, resolveApiUrl, resolveForms, resolvePublicUrl, resolveSiteLocales, resolveWorkspaceId, splitLocaleFromPath } from '@cmssy/core';
3
3
  import { createElement } from 'react';
4
- import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { BlockErrorBoundary } from '@cmssy/react/block-error-boundary';
5
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
6
  export { evaluateFieldConditionGroup } from '@cmssy/types';
6
7
 
7
8
  // src/index.ts
@@ -47,41 +48,155 @@ function blocksToMeta(blocks, defaults = {}) {
47
48
  }
48
49
  return out;
49
50
  }
51
+
52
+ // src/components/block-error.ts
53
+ var BLOCK_ERROR_KEY = "__cmssyBlockError";
54
+ var SOURCES = [
55
+ "loader",
56
+ "render",
57
+ "unregistered"
58
+ ];
59
+ function blockErrorMessage(err) {
60
+ return err instanceof Error ? err.message : String(err);
61
+ }
62
+ function unregisteredBlockError(type) {
63
+ return {
64
+ source: "unregistered",
65
+ message: `Block type "${type}" is not registered in this site's blocks array.`
66
+ };
67
+ }
68
+ function markBlockError(error) {
69
+ return { [BLOCK_ERROR_KEY]: error };
70
+ }
71
+ function readBlockError(value) {
72
+ if (typeof value !== "object" || value === null) return void 0;
73
+ const marked = value[BLOCK_ERROR_KEY];
74
+ if (typeof marked !== "object" || marked === null) return void 0;
75
+ const { message, source } = marked;
76
+ if (typeof message !== "string") return void 0;
77
+ if (!SOURCES.includes(source)) return void 0;
78
+ return { message, source };
79
+ }
80
+ var SOURCE_LABELS = {
81
+ loader: "loader failed",
82
+ render: "render failed",
83
+ unregistered: "type not registered"
84
+ };
85
+ var cardStyle = {
86
+ boxSizing: "border-box",
87
+ margin: "8px 0",
88
+ padding: "12px 16px",
89
+ border: "1px dashed #ef4444",
90
+ borderRadius: "8px",
91
+ background: "rgba(239, 68, 68, 0.08)",
92
+ color: "#ef4444",
93
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
94
+ fontSize: "13px",
95
+ lineHeight: 1.5,
96
+ textAlign: "left"
97
+ };
98
+ var headingStyle = {
99
+ fontWeight: 700,
100
+ marginBottom: "4px"
101
+ };
102
+ var metaStyle = {
103
+ opacity: 0.8,
104
+ marginBottom: "4px"
105
+ };
106
+ var messageStyle = {
107
+ whiteSpace: "pre-wrap",
108
+ overflowWrap: "anywhere"
109
+ };
110
+ function BlockErrorCard({
111
+ blockType,
112
+ blockId,
113
+ error
114
+ }) {
115
+ return /* @__PURE__ */ jsxs("div", { role: "alert", "data-cmssy-block-error": error.source, style: cardStyle, children: [
116
+ /* @__PURE__ */ jsxs("div", { style: headingStyle, children: [
117
+ 'Block "',
118
+ blockType,
119
+ '" - ',
120
+ SOURCE_LABELS[error.source]
121
+ ] }),
122
+ /* @__PURE__ */ jsxs("div", { style: metaStyle, children: [
123
+ "id: ",
124
+ blockId
125
+ ] }),
126
+ /* @__PURE__ */ jsx("div", { style: messageStyle, children: error.message })
127
+ ] });
128
+ }
50
129
  var WARN_CAP = 256;
51
130
  var warned = /* @__PURE__ */ new Set();
52
131
  function UnknownBlock({ type }) {
53
132
  if (typeof window !== "undefined" && !warned.has(type)) {
54
133
  if (warned.size >= WARN_CAP) warned.clear();
55
134
  warned.add(type);
56
- console.warn(`[cmssy] no component registered for block type "${type}"`);
135
+ console.error(`[cmssy] no component registered for block type "${type}"`);
57
136
  }
58
137
  return /* @__PURE__ */ jsx("div", { "data-cmssy-unknown-block": type });
59
138
  }
60
139
  function renderResolvedBlock(block, map, locale, defaultLocale, options = {}) {
61
- const { context, data, resolvedContent, enabledLocales } = options;
140
+ const { context, data, resolvedContent, enabledLocales, error, editMode } = options;
62
141
  const Component = Object.hasOwn(map, block.type) ? map[block.type] : void 0;
63
- const content = resolvedContent ?? getBlockContentForLanguage(
64
- block.content,
65
- locale,
66
- defaultLocale,
67
- enabledLocales?.length ? enabledLocales : void 0
68
- );
69
- return /* @__PURE__ */ jsx(
142
+ const wrap = (children, hidden = false) => /* @__PURE__ */ jsx(
70
143
  "div",
71
144
  {
72
145
  "data-block-id": block.id,
73
146
  "data-block-type": block.type,
74
- style: Component ? void 0 : { display: "none" },
75
- children: Component ? createElement(Component, {
76
- content,
77
- style: asBucket(block.style),
78
- advanced: asBucket(block.advanced),
79
- context,
80
- data
81
- }) : /* @__PURE__ */ jsx(UnknownBlock, { type: block.type })
147
+ style: hidden ? { display: "none" } : void 0,
148
+ children
82
149
  },
83
150
  block.id
84
151
  );
152
+ if (!Component) {
153
+ return editMode ? wrap(
154
+ /* @__PURE__ */ jsx(
155
+ BlockErrorCard,
156
+ {
157
+ blockType: block.type,
158
+ blockId: block.id,
159
+ error: unregisteredBlockError(block.type)
160
+ }
161
+ )
162
+ ) : wrap(/* @__PURE__ */ jsx(UnknownBlock, { type: block.type }), true);
163
+ }
164
+ if (error) {
165
+ if (!editMode) return null;
166
+ return wrap(
167
+ /* @__PURE__ */ jsx(
168
+ BlockErrorCard,
169
+ {
170
+ blockType: block.type,
171
+ blockId: block.id,
172
+ error
173
+ }
174
+ )
175
+ );
176
+ }
177
+ const content = resolvedContent ?? getBlockContentForLanguage(
178
+ block.content,
179
+ locale,
180
+ defaultLocale,
181
+ enabledLocales?.length ? enabledLocales : void 0
182
+ );
183
+ return wrap(
184
+ /* @__PURE__ */ jsx(
185
+ BlockErrorBoundary,
186
+ {
187
+ blockType: block.type,
188
+ blockId: block.id,
189
+ editMode,
190
+ children: createElement(Component, {
191
+ content,
192
+ style: asBucket(block.style),
193
+ advanced: asBucket(block.advanced),
194
+ context,
195
+ data
196
+ })
197
+ }
198
+ )
199
+ );
85
200
  }
86
201
  async function resolveBlocks(blocks, loaderMap, locale, defaultLocale, context, enabledLocales) {
87
202
  return Promise.all(
@@ -94,19 +209,21 @@ async function resolveBlocks(blocks, loaderMap, locale, defaultLocale, context,
94
209
  );
95
210
  const loader = loaderMap[block.type];
96
211
  let data;
212
+ let error;
97
213
  if (loader) {
98
214
  try {
99
215
  data = await loader({ content, context });
100
216
  } catch (err) {
101
217
  if (typeof console !== "undefined") {
102
- console.warn(
218
+ console.error(
103
219
  `[cmssy] loader for block "${block.type}" (${block.id}) failed`,
104
220
  err
105
221
  );
106
222
  }
223
+ error = { message: blockErrorMessage(err), source: "loader" };
107
224
  }
108
225
  }
109
- return { content, data };
226
+ return { content, data, error };
110
227
  })
111
228
  );
112
229
  }
@@ -147,7 +264,8 @@ async function CmssyServerPage({
147
264
  config,
148
265
  forms,
149
266
  auth,
150
- workspace
267
+ workspace,
268
+ editMode
151
269
  }) {
152
270
  if (!page) return null;
153
271
  const { locale, defaultLocale, enabledLocales } = await resolveRenderLocale({
@@ -179,10 +297,25 @@ async function CmssyServerPage({
179
297
  context,
180
298
  data: resolved[i]?.data,
181
299
  resolvedContent: resolved[i]?.content,
182
- enabledLocales
300
+ enabledLocales,
301
+ error: resolved[i]?.error,
302
+ editMode
183
303
  })
184
304
  ) });
185
305
  }
306
+ function collectBlockData(blocks, resolved, isPreview) {
307
+ const data = {};
308
+ blocks.forEach((block, index) => {
309
+ const entry = resolved[index];
310
+ if (!entry) return;
311
+ if (entry.error && isPreview) {
312
+ data[block.id] = markBlockError(entry.error);
313
+ } else if (entry.data !== void 0) {
314
+ data[block.id] = entry.data;
315
+ }
316
+ });
317
+ return data;
318
+ }
186
319
  async function resolveBlockData({
187
320
  page,
188
321
  blocks,
@@ -209,12 +342,7 @@ async function resolveBlockData({
209
342
  context,
210
343
  enabledLocales
211
344
  );
212
- const data = {};
213
- page.blocks.forEach((block, index) => {
214
- const value = resolved[index]?.data;
215
- if (value !== void 0) data[block.id] = value;
216
- });
217
- return data;
345
+ return collectBlockData(page.blocks, resolved, isPreview);
218
346
  }
219
347
  async function resolveLayoutBlockData({
220
348
  groups,
@@ -245,12 +373,7 @@ async function resolveLayoutBlockData({
245
373
  context,
246
374
  enabledLocales
247
375
  );
248
- const data = {};
249
- layoutBlocks.forEach((block, index) => {
250
- const value = resolved[index]?.data;
251
- if (value !== void 0) data[block.id] = value;
252
- });
253
- return data;
376
+ return collectBlockData(layoutBlocks, resolved, isPreview);
254
377
  }
255
378
  async function CmssyServerLayout({
256
379
  groups,
@@ -259,7 +382,8 @@ async function CmssyServerLayout({
259
382
  locale: localeProp,
260
383
  defaultLocale: defaultLocaleProp,
261
384
  enabledLocales: enabledLocalesProp,
262
- config
385
+ config,
386
+ editMode
263
387
  }) {
264
388
  const { locale, defaultLocale, enabledLocales } = await resolveRenderLocale({
265
389
  locale: localeProp,
@@ -286,7 +410,9 @@ async function CmssyServerLayout({
286
410
  context,
287
411
  data: resolved[i]?.data,
288
412
  resolvedContent: resolved[i]?.content,
289
- enabledLocales
413
+ enabledLocales,
414
+ error: resolved[i]?.error,
415
+ editMode
290
416
  })
291
417
  ) });
292
418
  }
@@ -299,32 +425,71 @@ function CmssyBlock({
299
425
  patchedStyle,
300
426
  patchedAdvanced,
301
427
  editable,
428
+ editMode,
302
429
  layoutPosition,
303
430
  context,
304
431
  data
305
432
  }) {
433
+ const inEditMode = editMode ?? editable ?? false;
306
434
  const Component = Object.hasOwn(blockMap, block.type) ? blockMap[block.type] : void 0;
307
- const base = getBlockContentForLanguage(block.content, locale, defaultLocale);
308
- const content = patchedContent ? { ...base, ...patchedContent } : base;
309
- const style = patchedStyle ? { ...asBucket(block.style), ...patchedStyle } : asBucket(block.style);
310
- const advanced = patchedAdvanced ? { ...asBucket(block.advanced), ...patchedAdvanced } : asBucket(block.advanced);
311
- return /* @__PURE__ */ jsx(
435
+ const blockError = readBlockError(data);
436
+ const wrap = (children, hidden = false) => /* @__PURE__ */ jsx(
312
437
  "div",
313
438
  {
314
439
  "data-block-id": block.id,
315
440
  "data-block-type": block.type,
316
441
  "data-layout-position": layoutPosition,
317
442
  draggable: editable || void 0,
318
- style: Component ? void 0 : { display: "none" },
319
- children: Component ? createElement(Component, {
320
- content,
321
- style,
322
- advanced,
323
- context,
324
- data
325
- }) : /* @__PURE__ */ jsx(UnknownBlock, { type: block.type })
443
+ style: hidden ? { display: "none" } : void 0,
444
+ children
326
445
  }
327
446
  );
447
+ if (!Component) {
448
+ return inEditMode ? wrap(
449
+ /* @__PURE__ */ jsx(
450
+ BlockErrorCard,
451
+ {
452
+ blockType: block.type,
453
+ blockId: block.id,
454
+ error: unregisteredBlockError(block.type)
455
+ }
456
+ )
457
+ ) : wrap(/* @__PURE__ */ jsx(UnknownBlock, { type: block.type }), true);
458
+ }
459
+ if (blockError) {
460
+ if (!inEditMode) return null;
461
+ return wrap(
462
+ /* @__PURE__ */ jsx(
463
+ BlockErrorCard,
464
+ {
465
+ blockType: block.type,
466
+ blockId: block.id,
467
+ error: blockError
468
+ }
469
+ )
470
+ );
471
+ }
472
+ const base = getBlockContentForLanguage(block.content, locale, defaultLocale);
473
+ const content = patchedContent ? { ...base, ...patchedContent } : base;
474
+ const style = patchedStyle ? { ...asBucket(block.style), ...patchedStyle } : asBucket(block.style);
475
+ const advanced = patchedAdvanced ? { ...asBucket(block.advanced), ...patchedAdvanced } : asBucket(block.advanced);
476
+ return wrap(
477
+ /* @__PURE__ */ jsx(
478
+ BlockErrorBoundary,
479
+ {
480
+ blockType: block.type,
481
+ blockId: block.id,
482
+ editMode: inEditMode,
483
+ children: createElement(Component, {
484
+ content,
485
+ style,
486
+ advanced,
487
+ context,
488
+ data
489
+ })
490
+ }
491
+ )
492
+ );
328
493
  }
329
494
 
330
495
  export { CmssyBlock, CmssyServerLayout, CmssyServerPage, UnknownBlock, blocksToMeta, blocksToSchemas, buildBlockMap, defineBlock, resolveBlockData, resolveLayoutBlockData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/react",
3
- "version": "9.2.0",
3
+ "version": "9.3.0",
4
4
  "description": "React blocks, renderers, data client and editor bridge for cmssy headless sites",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -32,6 +32,11 @@
32
32
  "types": "./dist/client.d.ts",
33
33
  "import": "./dist/client.js",
34
34
  "require": "./dist/client.cjs"
35
+ },
36
+ "./block-error-boundary": {
37
+ "types": "./dist/block-error-boundary.d.ts",
38
+ "import": "./dist/block-error-boundary.js",
39
+ "require": "./dist/block-error-boundary.cjs"
35
40
  }
36
41
  },
37
42
  "main": "./dist/index.cjs",
@@ -57,7 +62,7 @@
57
62
  },
58
63
  "dependencies": {
59
64
  "@cmssy/types": "0.28.0",
60
- "@cmssy/core": "9.2.0"
65
+ "@cmssy/core": "9.3.0"
61
66
  },
62
67
  "scripts": {
63
68
  "build": "tsup",