@contentful/experiences-visual-editor-react 3.7.0-prerelease-20250917T1034-42f0486.0 → 3.7.1-beta.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,15 +1,16 @@
1
1
  import styleInject from 'style-inject';
2
2
  import React, { useState, useEffect, useCallback, forwardRef, useMemo, useLayoutEffect, useRef } from 'react';
3
3
  import { z } from 'zod';
4
- import { cloneDeep, omit, isArray, isEqual, get as get$2, debounce } from 'lodash-es';
4
+ import cloneDeep from 'lodash.clonedeep';
5
5
  import md5 from 'md5';
6
6
  import { BLOCKS } from '@contentful/rich-text-types';
7
7
  import { create, useStore } from 'zustand';
8
8
  import { produce } from 'immer';
9
+ import { isEqual, get as get$2, debounce } from 'lodash-es';
9
10
  import '@contentful/rich-text-react-renderer';
10
11
 
11
- var css_248z$a = "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\n/*\n * All of these variables are tokens from Forma-36 and should not be adjusted as these\n * are global variables that may affect multiple places.\n * As our customers may use other design libraries, we try to avoid overlapping global\n * variables by always using the prefix `--exp-builder-` inside this SDK.\n */\n\n:root {\n /* Color tokens from Forma 36: https://f36.contentful.com/tokens/color-system */\n --exp-builder-blue100: #e8f5ff;\n --exp-builder-blue200: #ceecff;\n --exp-builder-blue300: #98cbff;\n --exp-builder-blue400: #40a0ff;\n --exp-builder-blue500: #036fe3;\n --exp-builder-blue600: #0059c8;\n --exp-builder-blue700: #0041ab;\n --exp-builder-blue800: #003298;\n --exp-builder-blue900: #002a8e;\n --exp-builder-gray100: #f7f9fa;\n --exp-builder-gray200: #e7ebee;\n --exp-builder-gray300: #cfd9e0;\n --exp-builder-gray400: #aec1cc;\n --exp-builder-gray500: #67728a;\n --exp-builder-gray600: #5a657c;\n --exp-builder-gray700: #414d63;\n --exp-builder-gray800: #1b273a;\n --exp-builder-gray900: #111b2b;\n --exp-builder-purple600: #6c3ecf;\n --exp-builder-red200: #ffe0e0;\n --exp-builder-red800: #7f0010;\n --exp-builder-color-white: #ffffff;\n --exp-builder-glow-primary: 0px 0px 0px 3px #e8f5ff;\n\n /* RGB colors for applying opacity */\n --exp-builder-blue100-rgb: 232, 245, 255;\n --exp-builder-blue300-rgb: 152, 203, 255;\n\n /* Spacing tokens from Forma 36: https://f36.contentful.com/tokens/spacing */\n --exp-builder-spacing-s: 0.75rem;\n --exp-builder-spacing-2xs: 0.25rem;\n\n /* Typography tokens from Forma 36: https://f36.contentful.com/tokens/typography */\n --exp-builder-font-size-l: 1rem;\n --exp-builder-font-size-m: 0.875rem;\n --exp-builder-font-stack-primary: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n --exp-builder-line-height-condensed: 1.25;\n}\n";
12
- styleInject(css_248z$a);
12
+ var css_248z$b = "html,\nbody {\n margin: 0;\n padding: 0;\n}\n\n/*\n * All of these variables are tokens from Forma-36 and should not be adjusted as these\n * are global variables that may affect multiple places.\n * As our customers may use other design libraries, we try to avoid overlapping global\n * variables by always using the prefix `--exp-builder-` inside this SDK.\n */\n\n:root {\n /* Color tokens from Forma 36: https://f36.contentful.com/tokens/color-system */\n --exp-builder-blue100: #e8f5ff;\n --exp-builder-blue200: #ceecff;\n --exp-builder-blue300: #98cbff;\n --exp-builder-blue400: #40a0ff;\n --exp-builder-blue500: #036fe3;\n --exp-builder-blue600: #0059c8;\n --exp-builder-blue700: #0041ab;\n --exp-builder-blue800: #003298;\n --exp-builder-blue900: #002a8e;\n --exp-builder-gray100: #f7f9fa;\n --exp-builder-gray200: #e7ebee;\n --exp-builder-gray300: #cfd9e0;\n --exp-builder-gray400: #aec1cc;\n --exp-builder-gray500: #67728a;\n --exp-builder-gray600: #5a657c;\n --exp-builder-gray700: #414d63;\n --exp-builder-gray800: #1b273a;\n --exp-builder-gray900: #111b2b;\n --exp-builder-purple600: #6c3ecf;\n --exp-builder-red200: #ffe0e0;\n --exp-builder-red800: #7f0010;\n --exp-builder-color-white: #ffffff;\n --exp-builder-glow-primary: 0px 0px 0px 3px #e8f5ff;\n\n /* RGB colors for applying opacity */\n --exp-builder-blue100-rgb: 232, 245, 255;\n --exp-builder-blue300-rgb: 152, 203, 255;\n\n /* Spacing tokens from Forma 36: https://f36.contentful.com/tokens/spacing */\n --exp-builder-spacing-s: 0.75rem;\n --exp-builder-spacing-2xs: 0.25rem;\n\n /* Typography tokens from Forma 36: https://f36.contentful.com/tokens/typography */\n --exp-builder-font-size-l: 1rem;\n --exp-builder-font-size-m: 0.875rem;\n --exp-builder-font-stack-primary: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,\n sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;\n --exp-builder-line-height-condensed: 1.25;\n}\n";
13
+ styleInject(css_248z$b);
13
14
 
14
15
  /** @deprecated will be removed when dropping backward compatibility for old DND */
15
16
  const INCOMING_EVENTS$1 = {
@@ -33,7 +34,9 @@ const INCOMING_EVENTS$1 = {
33
34
  /** @deprecated will be removed when dropping backward compatibility for old DND */
34
35
  HoverComponent: 'hoverComponent',
35
36
  UpdatedEntity: 'updatedEntity',
37
+ /** @deprecated not needed after `patternResolution` was introduced. Will be removed in the next major version. */
36
38
  AssembliesAdded: 'assembliesAdded',
39
+ /** @deprecated not needed after `patternResolution` was introduced. Will be removed in the next major version. */
37
40
  AssembliesRegistered: 'assembliesRegistered',
38
41
  /** @deprecated will be removed when dropping backward compatibility for old DND */
39
42
  MouseMove: 'mouseMove',
@@ -96,6 +99,7 @@ const CONTENTFUL_COMPONENTS$1 = {
96
99
  name: 'Carousel',
97
100
  },
98
101
  };
102
+ const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
99
103
  const CF_STYLE_ATTRIBUTES = [
100
104
  'cfVisibility',
101
105
  'cfHorizontalAlignment',
@@ -754,7 +758,7 @@ const BreakpointSchema$1 = z
754
758
  id: propertyKeySchema$1,
755
759
  // Can be replace with z.templateLiteral when upgrading to zod v4
756
760
  query: z.string().refine((s) => BREAKPOINT_QUERY_REGEX$1.test(s)),
757
- previewSize: z.string(),
761
+ previewSize: z.string().optional(),
758
762
  displayName: z.string(),
759
763
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
760
764
  })
@@ -821,6 +825,25 @@ z.object({
821
825
  usedComponents: localeWrapper$1(UsedComponentsSchema$1).optional(),
822
826
  });
823
827
 
828
+ function treeVisit$1$1(initialNode, onNode) {
829
+ const _treeVisit = (currentNode) => {
830
+ const children = [...currentNode.children];
831
+ onNode(currentNode);
832
+ for (const child of children) {
833
+ _treeVisit(child);
834
+ }
835
+ };
836
+ if (Array.isArray(initialNode)) {
837
+ for (const node of initialNode) {
838
+ _treeVisit(node);
839
+ }
840
+ }
841
+ else {
842
+ _treeVisit(initialNode);
843
+ }
844
+ }
845
+
846
+ const MAX_ALLOWED_PATHS$1 = 200;
824
847
  const THUMBNAIL_IDS$1 = [
825
848
  'columns',
826
849
  'columnsPlusRight',
@@ -851,7 +874,17 @@ const THUMBNAIL_IDS$1 = [
851
874
  const VariableMappingSchema$1 = z.object({
852
875
  parameterId: propertyKeySchema$1,
853
876
  type: z.literal('ContentTypeMapping'),
854
- pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
877
+ pathsByContentType: z
878
+ .record(z.string(), z.object({ path: z.string() }))
879
+ .superRefine((paths, ctx) => {
880
+ const variableId = ctx.path[ctx.path.length - 2];
881
+ if (Object.keys(paths).length > MAX_ALLOWED_PATHS$1) {
882
+ ctx.addIssue({
883
+ code: z.ZodIssueCode.custom,
884
+ message: `Too many paths defined for variable mapping with id "${variableId}", maximum allowed is ${MAX_ALLOWED_PATHS$1}`,
885
+ });
886
+ }
887
+ }),
855
888
  });
856
889
  const PassToNodeSchema$1 = z
857
890
  .object({
@@ -875,7 +908,10 @@ const ParameterDefinitionSchema$1 = z.object({
875
908
  })
876
909
  .optional(),
877
910
  contentTypes: z.array(z.string()).min(1),
878
- passToNodes: z.array(PassToNodeSchema$1).optional(),
911
+ passToNodes: z
912
+ .array(PassToNodeSchema$1)
913
+ .max(1, 'At most one "passToNodes" element is allowed per parameter definition.')
914
+ .optional(), // we might change this to be empty array for native parameter definitions, that's why we don't use .length(1)
879
915
  });
880
916
  const ParameterDefinitionsSchema$1 = z.record(propertyKeySchema$1, ParameterDefinitionSchema$1);
881
917
  const VariableMappingsSchema$1 = z.record(propertyKeySchema$1, VariableMappingSchema$1);
@@ -896,14 +932,108 @@ const ComponentSettingsSchema$1 = z
896
932
  category: z.string().max(50, 'Category must contain at most 50 characters').optional(),
897
933
  prebindingDefinitions: z.array(PrebindingDefinitionSchema$1).length(1).optional(),
898
934
  })
899
- .strict();
900
- z.object({
935
+ .strict()
936
+ .superRefine((componentSettings, ctx) => {
937
+ const { variableDefinitions, prebindingDefinitions } = componentSettings;
938
+ if (!prebindingDefinitions || prebindingDefinitions.length === 0) {
939
+ return;
940
+ }
941
+ const { parameterDefinitions, variableMappings, allowedVariableOverrides } = prebindingDefinitions[0];
942
+ validateAtMostOneNativeParameterDefinition$1(parameterDefinitions, ctx);
943
+ validateNoOverlapBetweenMappingAndOverrides$1(variableMappings, allowedVariableOverrides, ctx);
944
+ validateMappingsAgainstVariableDefinitions$1(variableMappings, allowedVariableOverrides, variableDefinitions, ctx);
945
+ validateMappingsAgainstParameterDefinitions$1(variableMappings, parameterDefinitions, ctx);
946
+ });
947
+ z
948
+ .object({
901
949
  componentTree: localeWrapper$1(ComponentTreeSchema$1),
902
950
  dataSource: localeWrapper$1(DataSourceSchema$1),
903
951
  unboundValues: localeWrapper$1(UnboundValuesSchema$1),
904
952
  usedComponents: localeWrapper$1(UsedComponentsSchema$1).optional(),
905
953
  componentSettings: localeWrapper$1(ComponentSettingsSchema$1),
954
+ })
955
+ .superRefine((patternFields, ctx) => {
956
+ const { componentTree, componentSettings } = patternFields;
957
+ // values at this point are wrapped under locale code
958
+ const nonLocalisedComponentTree = Object.values(componentTree)[0];
959
+ const nonLocalisedComponentSettings = Object.values(componentSettings)[0];
960
+ if (!nonLocalisedComponentSettings || !nonLocalisedComponentTree) {
961
+ return;
962
+ }
963
+ validatePassToNodes$1(nonLocalisedComponentTree.children || [], nonLocalisedComponentSettings || {}, ctx);
906
964
  });
965
+ const validateAtMostOneNativeParameterDefinition$1 = (parameterDefinitions, ctx) => {
966
+ const nativeParamDefinitions = Object.values(parameterDefinitions).filter((paramDefinition) => !(paramDefinition.passToNodes && paramDefinition.passToNodes.length > 0));
967
+ if (nativeParamDefinitions.length > 1) {
968
+ ctx.addIssue({
969
+ code: z.ZodIssueCode.custom,
970
+ message: `Only one native parameter definition (parameter definition without passToNodes) is allowed per prebinding definition.`,
971
+ });
972
+ }
973
+ };
974
+ const validateNoOverlapBetweenMappingAndOverrides$1 = (variableMappings, allowedVariableOverrides, ctx) => {
975
+ const variableMappingKeys = Object.keys(variableMappings || {});
976
+ const overridesSet = new Set(allowedVariableOverrides || []);
977
+ const overlap = variableMappingKeys.filter((key) => overridesSet.has(key));
978
+ if (overlap.length > 0) {
979
+ ctx.addIssue({
980
+ code: z.ZodIssueCode.custom,
981
+ message: `Found both variable mapping and allowed override for the following keys: ${overlap.map((key) => `"${key}"`).join(', ')}.`,
982
+ });
983
+ }
984
+ };
985
+ const validateMappingsAgainstVariableDefinitions$1 = (variableMappings, allowedVariableOverrides, variableDefinitions, ctx) => {
986
+ const nonDesignVariableDefinitionKeys = Object.entries(variableDefinitions)
987
+ .filter(([_, def]) => def.group !== 'style')
988
+ .map(([key]) => key);
989
+ const variableMappingKeys = Object.keys(variableMappings || {});
990
+ const allKeys = [...variableMappingKeys, ...(allowedVariableOverrides || [])];
991
+ const invalidMappings = allKeys.filter((key) => !nonDesignVariableDefinitionKeys.includes(key));
992
+ if (invalidMappings.length > 0) {
993
+ ctx.addIssue({
994
+ code: z.ZodIssueCode.custom,
995
+ message: `The following variable mappings or overrides are missing from the variable definitions: ${invalidMappings.map((key) => `"${key}"`).join(', ')}.`,
996
+ });
997
+ }
998
+ };
999
+ const validateMappingsAgainstParameterDefinitions$1 = (variableMappings, parameterDefinitions, ctx) => {
1000
+ const parameterDefinitionKeys = Object.keys(parameterDefinitions || {});
1001
+ for (const [mappingKey, mappingValue] of Object.entries(variableMappings || {})) {
1002
+ if (!parameterDefinitionKeys.includes(mappingValue.parameterId)) {
1003
+ ctx.addIssue({
1004
+ code: z.ZodIssueCode.custom,
1005
+ message: `The variable mapping with id "${mappingKey}" references a non-existing parameterId "${mappingValue.parameterId}".`,
1006
+ });
1007
+ }
1008
+ }
1009
+ };
1010
+ const validatePassToNodes$1 = (rootChildren, componentSettings, ctx) => {
1011
+ if (!componentSettings.prebindingDefinitions ||
1012
+ componentSettings.prebindingDefinitions.length === 0) {
1013
+ return;
1014
+ }
1015
+ const { parameterDefinitions } = componentSettings.prebindingDefinitions[0];
1016
+ let nodeIds = new Set();
1017
+ for (const paramDef of Object.values(parameterDefinitions || {})) {
1018
+ paramDef.passToNodes?.forEach((n) => nodeIds.add(n.nodeId));
1019
+ }
1020
+ treeVisit$1$1(rootChildren, (node) => {
1021
+ if (!node.id)
1022
+ return;
1023
+ if (nodeIds.has(node.id)) {
1024
+ nodeIds.delete(node.id);
1025
+ }
1026
+ });
1027
+ if (nodeIds.size > 0) {
1028
+ const stringifiedNodeIds = Array.from(nodeIds)
1029
+ .map((id) => `"${id}"`)
1030
+ .join(', ');
1031
+ ctx.addIssue({
1032
+ code: z.ZodIssueCode.custom,
1033
+ message: `The following node IDs referenced in passToNodes are not present in the component tree: ${stringifiedNodeIds}.`,
1034
+ });
1035
+ }
1036
+ };
907
1037
 
908
1038
  z
909
1039
  .object({
@@ -1570,7 +1700,9 @@ const stylesToRemove = CF_STYLE_ATTRIBUTES.filter((style) => !stylesToKeep.inclu
1570
1700
  // cfWrapColumns & cfWrapColumnsCount are no real style attributes as they are handled on the editor side
1571
1701
  const propsToRemove = ['cfSsrClassName', 'cfWrapColumns', 'cfWrapColumnsCount'];
1572
1702
  const sanitizeNodeProps = (nodeProps) => {
1573
- return omit(nodeProps, stylesToRemove, propsToRemove);
1703
+ const keysToRemove = [...stylesToRemove, ...propsToRemove];
1704
+ const sanitizedProps = Object.fromEntries(Object.entries(nodeProps).filter(([key]) => !keysToRemove.includes(key)));
1705
+ return sanitizedProps;
1574
1706
  };
1575
1707
 
1576
1708
  /** Turn the visibility value into a style object that can be used for inline styles in React */
@@ -2049,7 +2181,7 @@ function getArrayValue(entryOrAsset, path, entityStore) {
2049
2181
  }
2050
2182
  const fieldName = path.split('/').slice(2, -1);
2051
2183
  const arrayValue = get$1(entryOrAsset, fieldName);
2052
- if (!isArray(arrayValue)) {
2184
+ if (!Array.isArray(arrayValue)) {
2053
2185
  debug$1.warn(`[experiences-core::getArrayValue] A field '${fieldName}' of an entity was bound to an Array variable. Expected value of that field to be an array, but got: ${JSON.stringify(arrayValue)}`, { entity: entryOrAsset });
2054
2186
  return;
2055
2187
  }
@@ -2166,6 +2298,19 @@ function getTargetValueInPixels(targetWidthObject) {
2166
2298
  return targetWidthObject.value;
2167
2299
  }
2168
2300
  }
2301
+ /**
2302
+ * Creates a component definition for an assembly. As all assemblies use the same definition in the SDK,
2303
+ * all should be registered via this function.
2304
+ */
2305
+ const createAssemblyDefinition = (definitionId) => {
2306
+ return {
2307
+ id: definitionId,
2308
+ name: 'Component',
2309
+ variables: {},
2310
+ children: true,
2311
+ category: ASSEMBLY_DEFAULT_CATEGORY,
2312
+ };
2313
+ };
2169
2314
 
2170
2315
  class ParseError extends Error {
2171
2316
  constructor(message) {
@@ -2801,6 +2946,9 @@ class DeepReference {
2801
2946
  return new DeepReference(opt);
2802
2947
  }
2803
2948
  }
2949
+ /**
2950
+ * used in editor mode. for delivery mode see `gatherDeepReferencesFromExperienceEntry`
2951
+ */
2804
2952
  function gatherDeepReferencesFromTree(startingNode, dataSource, getEntityFromLink) {
2805
2953
  const deepReferences = [];
2806
2954
  treeVisit(startingNode, (node) => {
@@ -3071,7 +3219,9 @@ const INCOMING_EVENTS = {
3071
3219
  /** @deprecated will be removed when dropping backward compatibility for old DND */
3072
3220
  HoverComponent: 'hoverComponent',
3073
3221
  UpdatedEntity: 'updatedEntity',
3222
+ /** @deprecated not needed after `patternResolution` was introduced. Will be removed in the next major version. */
3074
3223
  AssembliesAdded: 'assembliesAdded',
3224
+ /** @deprecated not needed after `patternResolution` was introduced. Will be removed in the next major version. */
3075
3225
  AssembliesRegistered: 'assembliesRegistered',
3076
3226
  /** @deprecated will be removed when dropping backward compatibility for old DND */
3077
3227
  MouseMove: 'mouseMove',
@@ -3096,7 +3246,6 @@ var StudioCanvasMode$2;
3096
3246
  StudioCanvasMode["NONE"] = "none";
3097
3247
  })(StudioCanvasMode$2 || (StudioCanvasMode$2 = {}));
3098
3248
  const ASSEMBLY_NODE_TYPE = 'assembly';
3099
- const ASSEMBLY_DEFAULT_CATEGORY = 'Assemblies';
3100
3249
  const ASSEMBLY_BLOCK_NODE_TYPE = 'assemblyBlock';
3101
3250
  const EMPTY_CONTAINER_SIZE = '80px';
3102
3251
  const HYPERLINK_DEFAULT_PATTERN = `/{locale}/{entry.fields.slug}/`;
@@ -3255,34 +3404,7 @@ const useBreakpoints = (breakpoints) => {
3255
3404
  return { resolveDesignValue };
3256
3405
  };
3257
3406
 
3258
- // Note: During development, the hot reloading might empty this and it
3259
- // stays empty leading to not rendering assemblies. Ideally, this is
3260
- // integrated into the state machine to keep track of its state.
3261
- const assembliesRegistry = new Map([]);
3262
- const setAssemblies = (assemblies) => {
3263
- for (const assembly of assemblies) {
3264
- assembliesRegistry.set(assembly.sys.id, assembly);
3265
- }
3266
- };
3267
3407
  const componentRegistry = new Map();
3268
- const addComponentRegistration = (componentRegistration) => {
3269
- componentRegistry.set(componentRegistration.definition.id, componentRegistration);
3270
- };
3271
- const createAssemblyRegistration = ({ definitionId, definitionName, component, }) => {
3272
- const componentRegistration = componentRegistry.get(definitionId);
3273
- if (componentRegistration) {
3274
- return componentRegistration;
3275
- }
3276
- const definition = {
3277
- id: definitionId,
3278
- name: definitionName || 'Component',
3279
- variables: {},
3280
- children: true,
3281
- category: ASSEMBLY_DEFAULT_CATEGORY,
3282
- };
3283
- addComponentRegistration({ component, definition });
3284
- return componentRegistry.get(definitionId);
3285
- };
3286
3408
 
3287
3409
  const useEditorStore = create((set, get) => ({
3288
3410
  dataSource: {},
@@ -3322,181 +3444,453 @@ const useEditorStore = create((set, get) => ({
3322
3444
  },
3323
3445
  }));
3324
3446
 
3325
- var css_248z$8 = ":root{--cf-color-white:#fff;--cf-color-black:#000;--cf-color-gray100:#f7f9fa;--cf-color-gray400:#aec1cc;--cf-color-gray400-rgb:174,193,204;--cf-spacing-0:0rem;--cf-spacing-1:0.125rem;--cf-spacing-2:0.25rem;--cf-spacing-3:0.375rem;--cf-spacing-4:0.5rem;--cf-spacing-5:0.625rem;--cf-spacing-6:0.75rem;--cf-spacing-7:0.875rem;--cf-spacing-8:1rem;--cf-spacing-9:1.25rem;--cf-spacing-10:1.5rem;--cf-spacing-11:1.75rem;--cf-spacing-12:2rem;--cf-spacing-13:2.25rem;--cf-text-xs:0.75rem;--cf-text-sm:0.875rem;--cf-text-base:1rem;--cf-text-lg:1.125rem;--cf-text-xl:1.25rem;--cf-text-2xl:1.5rem;--cf-text-3xl:2rem;--cf-text-4xl:2.75rem;--cf-font-light:300;--cf-font-normal:400;--cf-font-medium:500;--cf-font-semibold:600;--cf-font-bold:700;--cf-font-extra-bold:800;--cf-font-black:900;--cf-border-radius-none:0px;--cf-border-radius-sm:0.125rem;--cf-border-radius:0.25rem;--cf-border-radius-md:0.375rem;--cf-border-radius-lg:0.5rem;--cf-border-radius-xl:0.75rem;--cf-border-radius-2xl:1rem;--cf-border-radius-3xl:1.5rem;--cf-border-radius-full:9999px;--cf-max-width-full:100%;--cf-button-bg:var(--cf-color-black);--cf-button-color:var(--cf-color-white);--cf-text-color:var(--cf-color-black)}*{box-sizing:border-box}";
3326
- styleInject(css_248z$8);
3327
-
3328
- /** @deprecated will be removed when dropping backward compatibility for old DND */
3329
- /**
3330
- * These modes are ONLY intended to be internally used within the context of
3331
- * editing an experience inside of Contentful Studio. i.e. these modes
3332
- * intentionally do not include preview/delivery modes.
3333
- */
3334
- var StudioCanvasMode$1;
3335
- (function (StudioCanvasMode) {
3336
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
3337
- StudioCanvasMode["EDITOR"] = "editorMode";
3338
- StudioCanvasMode["NONE"] = "none";
3339
- })(StudioCanvasMode$1 || (StudioCanvasMode$1 = {}));
3340
- var PostMessageMethods$1;
3341
- (function (PostMessageMethods) {
3342
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
3343
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
3344
- })(PostMessageMethods$1 || (PostMessageMethods$1 = {}));
3345
-
3346
- var css_248z$7 = ".cf-button:empty:before{content:\"\";display:inline-block}";
3347
- styleInject(css_248z$7);
3348
-
3349
- var css_248z$6 = ".cf-heading{white-space:pre-line}";
3350
- styleInject(css_248z$6);
3351
-
3352
- var css_248z$5 = ".cf-richtext{white-space:pre-line}.cf-richtext>:first-child{margin-top:0}.cf-richtext>:last-child{margin-bottom:0}";
3353
- styleInject(css_248z$5);
3354
-
3355
- var css_248z$4 = ".cf-text{white-space:pre-line}.cf-text-link .cf-text{margin:0}";
3356
- styleInject(css_248z$4);
3357
-
3358
- var css_248z$3 = "div.cf-placeholder-wrapper{outline:2px solid rgba(var(--cf-color-gray400-rgb),.5);outline-offset:-2px;overflow:hidden;position:relative}img.cf-placeholder-image{background-color:var(--cf-color-gray100);height:100%;width:100%}svg.cf-placeholder-icon{height:var(--cf-text-3xl);left:50%;max-height:100%;max-width:100%;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--cf-text-3xl)}svg.cf-placeholder-icon path{fill:var(--cf-color-gray400)}";
3359
- styleInject(css_248z$3);
3360
-
3361
- /**
3362
- * These modes are ONLY intended to be internally used within the context of
3363
- * editing an experience inside of Contentful Studio. i.e. these modes
3364
- * intentionally do not include preview/delivery modes.
3365
- */
3366
- var StudioCanvasMode;
3367
- (function (StudioCanvasMode) {
3368
- StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
3369
- StudioCanvasMode["EDITOR"] = "editorMode";
3370
- StudioCanvasMode["NONE"] = "none";
3371
- })(StudioCanvasMode || (StudioCanvasMode = {}));
3372
- const CONTENTFUL_COMPONENTS = {
3373
- section: {
3374
- id: 'contentful-section',
3375
- name: 'Section',
3376
- },
3377
- container: {
3378
- id: 'contentful-container',
3379
- name: 'Container',
3380
- },
3381
- columns: {
3382
- id: 'contentful-columns',
3383
- name: 'Columns',
3384
- },
3385
- singleColumn: {
3386
- id: 'contentful-single-column',
3387
- name: 'Column',
3388
- },
3389
- button: {
3390
- id: 'contentful-button',
3391
- name: 'Button',
3392
- },
3393
- heading: {
3394
- id: 'contentful-heading',
3395
- name: 'Heading',
3396
- },
3397
- image: {
3398
- id: 'contentful-image',
3399
- name: 'Image',
3400
- },
3401
- richText: {
3402
- id: 'contentful-richText',
3403
- name: 'Rich Text',
3404
- },
3405
- text: {
3406
- id: 'contentful-text',
3407
- name: 'Text',
3408
- },
3409
- divider: {
3410
- id: 'contentful-divider',
3411
- name: 'Divider',
3412
- },
3413
- carousel: {
3414
- id: 'contentful-carousel',
3415
- name: 'Carousel',
3416
- },
3417
- };
3418
- var PostMessageMethods;
3419
- (function (PostMessageMethods) {
3420
- PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
3421
- PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
3422
- })(PostMessageMethods || (PostMessageMethods = {}));
3423
- new Set(Object.values(CONTENTFUL_COMPONENTS).map((component) => component.id));
3424
-
3425
- // If more than one version is supported, use z.union
3426
- const SchemaVersions = z.literal('2023-09-28');
3427
- // Keep deprecated versions here just for reference
3428
- z.union([
3429
- z.literal('2023-08-23'),
3430
- z.literal('2023-07-26'),
3431
- z.literal('2023-06-27'),
3432
- ]);
3433
-
3434
- const DefinitionPropertyTypeSchema = z.enum([
3435
- 'Text',
3436
- 'RichText',
3437
- 'Number',
3438
- 'Date',
3439
- 'Boolean',
3440
- 'Location',
3441
- 'Media',
3442
- 'Object',
3443
- 'Hyperlink',
3444
- 'Array',
3445
- 'Link',
3446
- ]);
3447
- const DefinitionPropertyKeySchema = z
3448
- .string()
3449
- .regex(/^[a-zA-Z0-9-_]{1,32}$/, { message: 'Property needs to match: /^[a-zA-Z0-9-_]{1,32}$/' });
3450
- const PrimitiveValueSchema = z.union([
3451
- z.string(),
3452
- z.boolean(),
3453
- z.number(),
3454
- z.record(z.any(), z.any()),
3455
- z.undefined(),
3456
- ]);
3457
- const UsedComponentsSchema = z.array(z.object({
3458
- sys: z.object({
3459
- type: z.literal('Link'),
3460
- id: z.string(),
3461
- linkType: z.literal('Entry'),
3462
- }),
3463
- }));
3464
- const uuidKeySchema = z
3465
- .string()
3466
- .regex(/^[a-zA-Z0-9-_]{1,21}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,21}$/' });
3467
- const DataSourceSchema = z.record(uuidKeySchema, z.object({
3468
- sys: z.object({
3469
- type: z.literal('Link'),
3470
- id: z.string(),
3471
- linkType: z.enum(['Entry', 'Asset']),
3472
- }),
3473
- }));
3474
- const UnboundValuesSchema = z.record(uuidKeySchema, z.object({
3475
- value: PrimitiveValueSchema,
3476
- }));
3477
- /**
3478
- * Property keys for imported components have a limit of 32 characters (to be implemented) while
3479
- * property keys for patterns have a limit of 54 characters (<32-char-variable-name>_<21-char-nanoid-id>).
3480
- * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
3481
- */
3482
- const propertyKeySchema = z
3483
- .string()
3484
- .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
3485
- const ComponentTreeNodeIdSchema = z
3486
- .string()
3487
- .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
3488
- const breakpointsRefinement = (value, ctx) => {
3489
- if (!value.length || value[0].query !== '*') {
3490
- ctx.addIssue({
3491
- code: z.ZodIssueCode.custom,
3492
- message: `The first breakpoint should include the following attributes: { "query": "*" }`,
3493
- });
3494
- return;
3495
- }
3496
- // Return early if there's only one generic breakpoint
3497
- const hasNoBreakpointsStrategy = value.length === 1;
3498
- if (hasNoBreakpointsStrategy) {
3499
- return;
3447
+ function useEditorSubscriber(inMemoryEntitiesStore) {
3448
+ const entityStore = inMemoryEntitiesStore((state) => state.entityStore);
3449
+ const areEntitiesFetched = inMemoryEntitiesStore((state) => state.areEntitiesFetched);
3450
+ const setEntitiesFetched = inMemoryEntitiesStore((state) => state.setEntitiesFetched);
3451
+ const resetEntityStore = inMemoryEntitiesStore((state) => state.resetEntityStore);
3452
+ const { updateTree, updateNodesByUpdatedEntity } = useTreeStore((state) => ({
3453
+ updateTree: state.updateTree,
3454
+ updateNodesByUpdatedEntity: state.updateNodesByUpdatedEntity,
3455
+ }));
3456
+ const unboundValues = useEditorStore((state) => state.unboundValues);
3457
+ const dataSource = useEditorStore((state) => state.dataSource);
3458
+ const setLocale = useEditorStore((state) => state.setLocale);
3459
+ const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
3460
+ const setDataSource = useEditorStore((state) => state.setDataSource);
3461
+ const reloadApp = () => {
3462
+ sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
3463
+ // Wait a moment to ensure that the message was sent
3464
+ setTimeout(() => {
3465
+ // Received a hot reload message from webpack dev server -> reload the canvas
3466
+ window.location.reload();
3467
+ }, 50);
3468
+ };
3469
+ useEffect(() => {
3470
+ sendMessage(OUTGOING_EVENTS.RequestComponentTreeUpdate, undefined);
3471
+ }, []);
3472
+ /**
3473
+ * Fills up entityStore with entities from newDataSource and from the tree.
3474
+ * Also manages "entity status" variables (areEntitiesFetched, isFetchingEntities)
3475
+ */
3476
+ const fetchMissingEntities = useCallback(async (entityStore, newDataSource, tree) => {
3477
+ // if we realize that there's nothing missing and nothing to fill-fetch before we do any async call,
3478
+ // then we can simply return and not lock the EntityStore at all.
3479
+ const startFetching = () => {
3480
+ setEntitiesFetched(false);
3481
+ };
3482
+ const endFetching = () => {
3483
+ setEntitiesFetched(true);
3484
+ };
3485
+ // Prepare L1 entities and deepReferences
3486
+ const entityLinksL1 = Object.values(newDataSource);
3487
+ /**
3488
+ * Checks only for _missing_ L1 entities
3489
+ * WARNING: Does NOT check for entity staleness/versions. If an entity is stale, it will NOT be considered missing.
3490
+ * If ExperienceBuilder wants to update stale entities, it should post `▼UPDATED_ENTITY` message to SDK.
3491
+ */
3492
+ const isMissingL1Entities = (entityLinks) => {
3493
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinks);
3494
+ return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
3495
+ };
3496
+ /**
3497
+ * PRECONDITION: all L1 entities are fetched
3498
+ */
3499
+ const isMissingL2Entities = (deepReferences) => {
3500
+ const referentLinks = deepReferences
3501
+ .map((deepReference) => deepReference.extractReferent(entityStore))
3502
+ .filter(isLink$1);
3503
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
3504
+ return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
3505
+ };
3506
+ /**
3507
+ * POST_CONDITION: entityStore is has all L1 entities (aka headEntities)
3508
+ */
3509
+ const fillupL1 = async ({ entityLinksL1, }) => {
3510
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinksL1);
3511
+ await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
3512
+ };
3513
+ /**
3514
+ * PRECONDITION: all L1 entites are fetched
3515
+ */
3516
+ const fillupL2 = async ({ deepReferences }) => {
3517
+ const referentLinks = deepReferences
3518
+ .map((deepReference) => deepReference.extractReferent(entityStore))
3519
+ .filter(isLink$1);
3520
+ const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
3521
+ await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
3522
+ };
3523
+ try {
3524
+ if (isMissingL1Entities(entityLinksL1)) {
3525
+ startFetching();
3526
+ await fillupL1({ entityLinksL1 });
3527
+ }
3528
+ const deepReferences = gatherDeepReferencesFromTree(tree.root, newDataSource, entityStore.getEntityFromLink.bind(entityStore));
3529
+ if (isMissingL2Entities(deepReferences)) {
3530
+ startFetching();
3531
+ await fillupL2({ deepReferences });
3532
+ }
3533
+ }
3534
+ catch (error) {
3535
+ debug$1.error('[experiences-visual-editor-react::useEditorSubscriber] Failed fetching entities', { error });
3536
+ throw error; // TODO: The original catch didn't let's rethrow; for the moment throw to see if we have any errors
3537
+ }
3538
+ finally {
3539
+ endFetching();
3540
+ }
3541
+ }, [setEntitiesFetched]);
3542
+ useEffect(() => {
3543
+ const onMessage = async (event) => {
3544
+ let reason;
3545
+ if ((reason = doesMismatchMessageSchema(event))) {
3546
+ if (event.origin.startsWith('http://localhost') &&
3547
+ `${event.data}`.includes('webpackHotUpdate')) {
3548
+ reloadApp();
3549
+ }
3550
+ else {
3551
+ debug$1.warn(`[experiences-visual-editor-react::onMessage] Ignoring alien incoming message from origin [${event.origin}], due to: [${reason}]`, event);
3552
+ }
3553
+ return;
3554
+ }
3555
+ const eventData = tryParseMessage(event);
3556
+ debug$1.debug(`[experiences-visual-editor-react::onMessage] Received message [${eventData.eventType}]`, eventData);
3557
+ if (eventData.eventType === PostMessageMethods$2.REQUESTED_ENTITIES) {
3558
+ // Expected message: This message is handled in the EntityStore to store fetched entities
3559
+ return;
3560
+ }
3561
+ switch (eventData.eventType) {
3562
+ case INCOMING_EVENTS.ExperienceUpdated: {
3563
+ const { tree, locale, changedNode, changedValueType } = eventData.payload;
3564
+ let newEntityStore = entityStore;
3565
+ if (entityStore.locale !== locale) {
3566
+ newEntityStore = new EditorModeEntityStore({ locale, entities: [] });
3567
+ setLocale(locale);
3568
+ resetEntityStore(newEntityStore);
3569
+ }
3570
+ // Below are mutually exclusive cases
3571
+ if (changedNode) {
3572
+ /**
3573
+ * On single node updates, we want to skip the process of getting the data (datasource and unbound values)
3574
+ * from tree. Since we know the updated node, we can skip that recursion everytime the tree updates and
3575
+ * just update the relevant data we need from the relevant node.
3576
+ *
3577
+ * We still update the tree here so we don't have a stale "tree"
3578
+ */
3579
+ if (changedValueType === 'BoundValue') {
3580
+ const newDataSource = { ...dataSource, ...changedNode.data.dataSource };
3581
+ setDataSource(newDataSource);
3582
+ await fetchMissingEntities(newEntityStore, newDataSource, tree);
3583
+ }
3584
+ else if (changedValueType === 'UnboundValue') {
3585
+ setUnboundValues({
3586
+ ...unboundValues,
3587
+ ...changedNode.data.unboundValues,
3588
+ });
3589
+ }
3590
+ }
3591
+ else {
3592
+ const { dataSource, unboundValues } = getDataFromTree(tree);
3593
+ setDataSource(dataSource);
3594
+ setUnboundValues(unboundValues);
3595
+ await fetchMissingEntities(newEntityStore, dataSource, tree);
3596
+ }
3597
+ // Update the tree when all necessary data is fetched and ready for rendering.
3598
+ updateTree(tree);
3599
+ break;
3600
+ }
3601
+ case INCOMING_EVENTS.AssembliesRegistered: {
3602
+ // Not necessary anymore since `patternResolution` which was introduced in 2024.
3603
+ break;
3604
+ }
3605
+ case INCOMING_EVENTS.AssembliesAdded: {
3606
+ // Not necessary anymore since `patternResolution` which was introduced in 2024.
3607
+ break;
3608
+ }
3609
+ case INCOMING_EVENTS.UpdatedEntity: {
3610
+ const { entity: updatedEntity, shouldRerender } = eventData.payload;
3611
+ if (updatedEntity) {
3612
+ const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
3613
+ const didEntityChange = storedEntity?.sys.version !== updatedEntity.sys.version;
3614
+ entityStore.updateEntity(updatedEntity);
3615
+ // We traverse the whole tree, so this is a opt-in feature to only use it when required.
3616
+ if (shouldRerender && didEntityChange) {
3617
+ updateNodesByUpdatedEntity(updatedEntity.sys.id);
3618
+ }
3619
+ }
3620
+ break;
3621
+ }
3622
+ case INCOMING_EVENTS.RequestEditorMode: {
3623
+ break;
3624
+ }
3625
+ default: {
3626
+ const knownEvents = Object.values(INCOMING_EVENTS);
3627
+ const isDeprecatedMessage = knownEvents.includes(eventData.eventType);
3628
+ if (!isDeprecatedMessage) {
3629
+ debug$1.error(`[experiences-visual-editor-react::onMessage] Logic error, unsupported eventType: [${eventData.eventType}]`);
3630
+ }
3631
+ }
3632
+ }
3633
+ };
3634
+ window.addEventListener('message', onMessage);
3635
+ return () => {
3636
+ window.removeEventListener('message', onMessage);
3637
+ };
3638
+ }, [
3639
+ entityStore,
3640
+ setDataSource,
3641
+ setLocale,
3642
+ dataSource,
3643
+ areEntitiesFetched,
3644
+ fetchMissingEntities,
3645
+ setUnboundValues,
3646
+ unboundValues,
3647
+ updateTree,
3648
+ updateNodesByUpdatedEntity,
3649
+ resetEntityStore,
3650
+ ]);
3651
+ }
3652
+
3653
+ const CircularDependencyErrorPlaceholder = ({ wrappingPatternIds, ...props }) => {
3654
+ return (React.createElement("div", { ...props, "data-cf-node-error": "circular-pattern-dependency", style: {
3655
+ border: '1px solid red',
3656
+ background: 'rgba(255, 0, 0, 0.1)',
3657
+ padding: '1rem 1rem 0 1rem',
3658
+ width: '100%',
3659
+ height: '100%',
3660
+ } },
3661
+ "Circular usage of patterns detected:",
3662
+ React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
3663
+ const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
3664
+ const entry = inMemoryEntities.maybeResolveLink(entryLink);
3665
+ const entryTitle = entry?.fields?.title;
3666
+ const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
3667
+ return React.createElement("li", { key: patternId }, text);
3668
+ }))));
3669
+ };
3670
+
3671
+ class ImportedComponentError extends Error {
3672
+ constructor(message) {
3673
+ super(message);
3674
+ this.name = 'ImportedComponentError';
3675
+ }
3676
+ }
3677
+ class ExperienceSDKError extends Error {
3678
+ constructor(message) {
3679
+ super(message);
3680
+ this.name = 'ExperienceSDKError';
3681
+ }
3682
+ }
3683
+ class ImportedComponentErrorBoundary extends React.Component {
3684
+ componentDidCatch(error, _errorInfo) {
3685
+ if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
3686
+ // This error was already handled by a nested error boundary and should be passed upwards
3687
+ // We have to do this as we wrap every component on every layer with this error boundary and
3688
+ // thus an error deep in the tree bubbles through many layers of error boundaries.
3689
+ throw error;
3690
+ }
3691
+ // Differentiate between custom and SDK-provided components for error tracking
3692
+ const ErrorClass = isContentfulComponent(this.props.componentId)
3693
+ ? ExperienceSDKError
3694
+ : ImportedComponentError;
3695
+ const err = new ErrorClass(error.message);
3696
+ err.stack = error.stack;
3697
+ throw err;
3698
+ }
3699
+ render() {
3700
+ return this.props.children;
3701
+ }
3702
+ }
3703
+
3704
+ const MissingComponentPlaceholder = ({ blockId }) => {
3705
+ return (React.createElement("div", { style: {
3706
+ border: '1px solid red',
3707
+ width: '100%',
3708
+ height: '100%',
3709
+ } },
3710
+ "Missing component '",
3711
+ blockId,
3712
+ "'"));
3713
+ };
3714
+
3715
+ var css_248z$a = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
3716
+ var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
3717
+ styleInject(css_248z$a);
3718
+
3719
+ var css_248z$8 = ":root{--cf-color-white:#fff;--cf-color-black:#000;--cf-color-gray100:#f7f9fa;--cf-color-gray400:#aec1cc;--cf-color-gray400-rgb:174,193,204;--cf-spacing-0:0rem;--cf-spacing-1:0.125rem;--cf-spacing-2:0.25rem;--cf-spacing-3:0.375rem;--cf-spacing-4:0.5rem;--cf-spacing-5:0.625rem;--cf-spacing-6:0.75rem;--cf-spacing-7:0.875rem;--cf-spacing-8:1rem;--cf-spacing-9:1.25rem;--cf-spacing-10:1.5rem;--cf-spacing-11:1.75rem;--cf-spacing-12:2rem;--cf-spacing-13:2.25rem;--cf-text-xs:0.75rem;--cf-text-sm:0.875rem;--cf-text-base:1rem;--cf-text-lg:1.125rem;--cf-text-xl:1.25rem;--cf-text-2xl:1.5rem;--cf-text-3xl:2rem;--cf-text-4xl:2.75rem;--cf-font-light:300;--cf-font-normal:400;--cf-font-medium:500;--cf-font-semibold:600;--cf-font-bold:700;--cf-font-extra-bold:800;--cf-font-black:900;--cf-border-radius-none:0px;--cf-border-radius-sm:0.125rem;--cf-border-radius:0.25rem;--cf-border-radius-md:0.375rem;--cf-border-radius-lg:0.5rem;--cf-border-radius-xl:0.75rem;--cf-border-radius-2xl:1rem;--cf-border-radius-3xl:1.5rem;--cf-border-radius-full:9999px;--cf-max-width-full:100%;--cf-button-bg:var(--cf-color-black);--cf-button-color:var(--cf-color-white);--cf-text-color:var(--cf-color-black)}*{box-sizing:border-box}";
3720
+ styleInject(css_248z$8);
3721
+
3722
+ /** @deprecated will be removed when dropping backward compatibility for old DND */
3723
+ /**
3724
+ * These modes are ONLY intended to be internally used within the context of
3725
+ * editing an experience inside of Contentful Studio. i.e. these modes
3726
+ * intentionally do not include preview/delivery modes.
3727
+ */
3728
+ var StudioCanvasMode$1;
3729
+ (function (StudioCanvasMode) {
3730
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
3731
+ StudioCanvasMode["EDITOR"] = "editorMode";
3732
+ StudioCanvasMode["NONE"] = "none";
3733
+ })(StudioCanvasMode$1 || (StudioCanvasMode$1 = {}));
3734
+ var PostMessageMethods$1;
3735
+ (function (PostMessageMethods) {
3736
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
3737
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
3738
+ })(PostMessageMethods$1 || (PostMessageMethods$1 = {}));
3739
+
3740
+ var css_248z$7 = ".cf-button:empty:before{content:\"\";display:inline-block}";
3741
+ styleInject(css_248z$7);
3742
+
3743
+ var css_248z$6 = ".cf-heading{white-space:pre-line}";
3744
+ styleInject(css_248z$6);
3745
+
3746
+ var css_248z$5 = ".cf-richtext{white-space:pre-line}.cf-richtext>:first-child{margin-top:0}.cf-richtext>:last-child{margin-bottom:0}";
3747
+ styleInject(css_248z$5);
3748
+
3749
+ var css_248z$4 = ".cf-text{white-space:pre-line}.cf-text-link .cf-text{margin:0}";
3750
+ styleInject(css_248z$4);
3751
+
3752
+ var css_248z$3 = "div.cf-placeholder-wrapper{outline:2px solid rgba(var(--cf-color-gray400-rgb),.5);outline-offset:-2px;overflow:hidden;position:relative}img.cf-placeholder-image{background-color:var(--cf-color-gray100);height:100%;width:100%}svg.cf-placeholder-icon{height:var(--cf-text-3xl);left:50%;max-height:100%;max-width:100%;position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--cf-text-3xl)}svg.cf-placeholder-icon path{fill:var(--cf-color-gray400)}";
3753
+ styleInject(css_248z$3);
3754
+
3755
+ /**
3756
+ * These modes are ONLY intended to be internally used within the context of
3757
+ * editing an experience inside of Contentful Studio. i.e. these modes
3758
+ * intentionally do not include preview/delivery modes.
3759
+ */
3760
+ var StudioCanvasMode;
3761
+ (function (StudioCanvasMode) {
3762
+ StudioCanvasMode["READ_ONLY"] = "readOnlyMode";
3763
+ StudioCanvasMode["EDITOR"] = "editorMode";
3764
+ StudioCanvasMode["NONE"] = "none";
3765
+ })(StudioCanvasMode || (StudioCanvasMode = {}));
3766
+ const CONTENTFUL_COMPONENTS = {
3767
+ section: {
3768
+ id: 'contentful-section',
3769
+ name: 'Section',
3770
+ },
3771
+ container: {
3772
+ id: 'contentful-container',
3773
+ name: 'Container',
3774
+ },
3775
+ columns: {
3776
+ id: 'contentful-columns',
3777
+ name: 'Columns',
3778
+ },
3779
+ singleColumn: {
3780
+ id: 'contentful-single-column',
3781
+ name: 'Column',
3782
+ },
3783
+ button: {
3784
+ id: 'contentful-button',
3785
+ name: 'Button',
3786
+ },
3787
+ heading: {
3788
+ id: 'contentful-heading',
3789
+ name: 'Heading',
3790
+ },
3791
+ image: {
3792
+ id: 'contentful-image',
3793
+ name: 'Image',
3794
+ },
3795
+ richText: {
3796
+ id: 'contentful-richText',
3797
+ name: 'Rich Text',
3798
+ },
3799
+ text: {
3800
+ id: 'contentful-text',
3801
+ name: 'Text',
3802
+ },
3803
+ divider: {
3804
+ id: 'contentful-divider',
3805
+ name: 'Divider',
3806
+ },
3807
+ carousel: {
3808
+ id: 'contentful-carousel',
3809
+ name: 'Carousel',
3810
+ },
3811
+ };
3812
+ var PostMessageMethods;
3813
+ (function (PostMessageMethods) {
3814
+ PostMessageMethods["REQUEST_ENTITIES"] = "REQUEST_ENTITIES";
3815
+ PostMessageMethods["REQUESTED_ENTITIES"] = "REQUESTED_ENTITIES";
3816
+ })(PostMessageMethods || (PostMessageMethods = {}));
3817
+ new Set(Object.values(CONTENTFUL_COMPONENTS).map((component) => component.id));
3818
+
3819
+ // If more than one version is supported, use z.union
3820
+ const SchemaVersions = z.literal('2023-09-28');
3821
+ // Keep deprecated versions here just for reference
3822
+ z.union([
3823
+ z.literal('2023-08-23'),
3824
+ z.literal('2023-07-26'),
3825
+ z.literal('2023-06-27'),
3826
+ ]);
3827
+
3828
+ const DefinitionPropertyTypeSchema = z.enum([
3829
+ 'Text',
3830
+ 'RichText',
3831
+ 'Number',
3832
+ 'Date',
3833
+ 'Boolean',
3834
+ 'Location',
3835
+ 'Media',
3836
+ 'Object',
3837
+ 'Hyperlink',
3838
+ 'Array',
3839
+ 'Link',
3840
+ ]);
3841
+ const DefinitionPropertyKeySchema = z
3842
+ .string()
3843
+ .regex(/^[a-zA-Z0-9-_]{1,32}$/, { message: 'Property needs to match: /^[a-zA-Z0-9-_]{1,32}$/' });
3844
+ const PrimitiveValueSchema = z.union([
3845
+ z.string(),
3846
+ z.boolean(),
3847
+ z.number(),
3848
+ z.record(z.any(), z.any()),
3849
+ z.undefined(),
3850
+ ]);
3851
+ const UsedComponentsSchema = z.array(z.object({
3852
+ sys: z.object({
3853
+ type: z.literal('Link'),
3854
+ id: z.string(),
3855
+ linkType: z.literal('Entry'),
3856
+ }),
3857
+ }));
3858
+ const uuidKeySchema = z
3859
+ .string()
3860
+ .regex(/^[a-zA-Z0-9-_]{1,21}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,21}$/' });
3861
+ const DataSourceSchema = z.record(uuidKeySchema, z.object({
3862
+ sys: z.object({
3863
+ type: z.literal('Link'),
3864
+ id: z.string(),
3865
+ linkType: z.enum(['Entry', 'Asset']),
3866
+ }),
3867
+ }));
3868
+ const UnboundValuesSchema = z.record(uuidKeySchema, z.object({
3869
+ value: PrimitiveValueSchema,
3870
+ }));
3871
+ /**
3872
+ * Property keys for imported components have a limit of 32 characters (to be implemented) while
3873
+ * property keys for patterns have a limit of 54 characters (<32-char-variable-name>_<21-char-nanoid-id>).
3874
+ * Because we cannot distinguish between the two in the componentTree, we will use the larger limit for both.
3875
+ */
3876
+ const propertyKeySchema = z
3877
+ .string()
3878
+ .regex(/^[a-zA-Z0-9-_]{1,54}$/, { message: 'Does not match /^[a-zA-Z0-9-_]{1,54}$/' });
3879
+ const ComponentTreeNodeIdSchema = z
3880
+ .string()
3881
+ .regex(/^[a-zA-Z0-9]{1,8}$/, { message: 'Does not match /^[a-zA-Z0-9]{1,8}$/' });
3882
+ const breakpointsRefinement = (value, ctx) => {
3883
+ if (!value.length || value[0].query !== '*') {
3884
+ ctx.addIssue({
3885
+ code: z.ZodIssueCode.custom,
3886
+ message: `The first breakpoint should include the following attributes: { "query": "*" }`,
3887
+ });
3888
+ return;
3889
+ }
3890
+ // Return early if there's only one generic breakpoint
3891
+ const hasNoBreakpointsStrategy = value.length === 1;
3892
+ if (hasNoBreakpointsStrategy) {
3893
+ return;
3500
3894
  }
3501
3895
  // Check if any breakpoint id occurs twice
3502
3896
  const ids = value.map((breakpoint) => breakpoint.id);
@@ -3615,7 +4009,7 @@ const BreakpointSchema = z
3615
4009
  id: propertyKeySchema,
3616
4010
  // Can be replace with z.templateLiteral when upgrading to zod v4
3617
4011
  query: z.string().refine((s) => BREAKPOINT_QUERY_REGEX.test(s)),
3618
- previewSize: z.string(),
4012
+ previewSize: z.string().optional(),
3619
4013
  displayName: z.string(),
3620
4014
  displayIcon: z.enum(['desktop', 'tablet', 'mobile']).optional(),
3621
4015
  })
@@ -3682,6 +4076,25 @@ z.object({
3682
4076
  usedComponents: localeWrapper(UsedComponentsSchema).optional(),
3683
4077
  });
3684
4078
 
4079
+ function treeVisit$1(initialNode, onNode) {
4080
+ const _treeVisit = (currentNode) => {
4081
+ const children = [...currentNode.children];
4082
+ onNode(currentNode);
4083
+ for (const child of children) {
4084
+ _treeVisit(child);
4085
+ }
4086
+ };
4087
+ if (Array.isArray(initialNode)) {
4088
+ for (const node of initialNode) {
4089
+ _treeVisit(node);
4090
+ }
4091
+ }
4092
+ else {
4093
+ _treeVisit(initialNode);
4094
+ }
4095
+ }
4096
+
4097
+ const MAX_ALLOWED_PATHS = 200;
3685
4098
  const THUMBNAIL_IDS = [
3686
4099
  'columns',
3687
4100
  'columnsPlusRight',
@@ -3712,7 +4125,17 @@ const THUMBNAIL_IDS = [
3712
4125
  const VariableMappingSchema = z.object({
3713
4126
  parameterId: propertyKeySchema,
3714
4127
  type: z.literal('ContentTypeMapping'),
3715
- pathsByContentType: z.record(z.string(), z.object({ path: z.string() })),
4128
+ pathsByContentType: z
4129
+ .record(z.string(), z.object({ path: z.string() }))
4130
+ .superRefine((paths, ctx) => {
4131
+ const variableId = ctx.path[ctx.path.length - 2];
4132
+ if (Object.keys(paths).length > MAX_ALLOWED_PATHS) {
4133
+ ctx.addIssue({
4134
+ code: z.ZodIssueCode.custom,
4135
+ message: `Too many paths defined for variable mapping with id "${variableId}", maximum allowed is ${MAX_ALLOWED_PATHS}`,
4136
+ });
4137
+ }
4138
+ }),
3716
4139
  });
3717
4140
  const PassToNodeSchema = z
3718
4141
  .object({
@@ -3736,7 +4159,10 @@ const ParameterDefinitionSchema = z.object({
3736
4159
  })
3737
4160
  .optional(),
3738
4161
  contentTypes: z.array(z.string()).min(1),
3739
- passToNodes: z.array(PassToNodeSchema).optional(),
4162
+ passToNodes: z
4163
+ .array(PassToNodeSchema)
4164
+ .max(1, 'At most one "passToNodes" element is allowed per parameter definition.')
4165
+ .optional(), // we might change this to be empty array for native parameter definitions, that's why we don't use .length(1)
3740
4166
  });
3741
4167
  const ParameterDefinitionsSchema = z.record(propertyKeySchema, ParameterDefinitionSchema);
3742
4168
  const VariableMappingsSchema = z.record(propertyKeySchema, VariableMappingSchema);
@@ -3757,14 +4183,108 @@ const ComponentSettingsSchema = z
3757
4183
  category: z.string().max(50, 'Category must contain at most 50 characters').optional(),
3758
4184
  prebindingDefinitions: z.array(PrebindingDefinitionSchema).length(1).optional(),
3759
4185
  })
3760
- .strict();
3761
- z.object({
4186
+ .strict()
4187
+ .superRefine((componentSettings, ctx) => {
4188
+ const { variableDefinitions, prebindingDefinitions } = componentSettings;
4189
+ if (!prebindingDefinitions || prebindingDefinitions.length === 0) {
4190
+ return;
4191
+ }
4192
+ const { parameterDefinitions, variableMappings, allowedVariableOverrides } = prebindingDefinitions[0];
4193
+ validateAtMostOneNativeParameterDefinition(parameterDefinitions, ctx);
4194
+ validateNoOverlapBetweenMappingAndOverrides(variableMappings, allowedVariableOverrides, ctx);
4195
+ validateMappingsAgainstVariableDefinitions(variableMappings, allowedVariableOverrides, variableDefinitions, ctx);
4196
+ validateMappingsAgainstParameterDefinitions(variableMappings, parameterDefinitions, ctx);
4197
+ });
4198
+ z
4199
+ .object({
3762
4200
  componentTree: localeWrapper(ComponentTreeSchema),
3763
4201
  dataSource: localeWrapper(DataSourceSchema),
3764
4202
  unboundValues: localeWrapper(UnboundValuesSchema),
3765
4203
  usedComponents: localeWrapper(UsedComponentsSchema).optional(),
3766
4204
  componentSettings: localeWrapper(ComponentSettingsSchema),
4205
+ })
4206
+ .superRefine((patternFields, ctx) => {
4207
+ const { componentTree, componentSettings } = patternFields;
4208
+ // values at this point are wrapped under locale code
4209
+ const nonLocalisedComponentTree = Object.values(componentTree)[0];
4210
+ const nonLocalisedComponentSettings = Object.values(componentSettings)[0];
4211
+ if (!nonLocalisedComponentSettings || !nonLocalisedComponentTree) {
4212
+ return;
4213
+ }
4214
+ validatePassToNodes(nonLocalisedComponentTree.children || [], nonLocalisedComponentSettings || {}, ctx);
3767
4215
  });
4216
+ const validateAtMostOneNativeParameterDefinition = (parameterDefinitions, ctx) => {
4217
+ const nativeParamDefinitions = Object.values(parameterDefinitions).filter((paramDefinition) => !(paramDefinition.passToNodes && paramDefinition.passToNodes.length > 0));
4218
+ if (nativeParamDefinitions.length > 1) {
4219
+ ctx.addIssue({
4220
+ code: z.ZodIssueCode.custom,
4221
+ message: `Only one native parameter definition (parameter definition without passToNodes) is allowed per prebinding definition.`,
4222
+ });
4223
+ }
4224
+ };
4225
+ const validateNoOverlapBetweenMappingAndOverrides = (variableMappings, allowedVariableOverrides, ctx) => {
4226
+ const variableMappingKeys = Object.keys(variableMappings || {});
4227
+ const overridesSet = new Set(allowedVariableOverrides || []);
4228
+ const overlap = variableMappingKeys.filter((key) => overridesSet.has(key));
4229
+ if (overlap.length > 0) {
4230
+ ctx.addIssue({
4231
+ code: z.ZodIssueCode.custom,
4232
+ message: `Found both variable mapping and allowed override for the following keys: ${overlap.map((key) => `"${key}"`).join(', ')}.`,
4233
+ });
4234
+ }
4235
+ };
4236
+ const validateMappingsAgainstVariableDefinitions = (variableMappings, allowedVariableOverrides, variableDefinitions, ctx) => {
4237
+ const nonDesignVariableDefinitionKeys = Object.entries(variableDefinitions)
4238
+ .filter(([_, def]) => def.group !== 'style')
4239
+ .map(([key]) => key);
4240
+ const variableMappingKeys = Object.keys(variableMappings || {});
4241
+ const allKeys = [...variableMappingKeys, ...(allowedVariableOverrides || [])];
4242
+ const invalidMappings = allKeys.filter((key) => !nonDesignVariableDefinitionKeys.includes(key));
4243
+ if (invalidMappings.length > 0) {
4244
+ ctx.addIssue({
4245
+ code: z.ZodIssueCode.custom,
4246
+ message: `The following variable mappings or overrides are missing from the variable definitions: ${invalidMappings.map((key) => `"${key}"`).join(', ')}.`,
4247
+ });
4248
+ }
4249
+ };
4250
+ const validateMappingsAgainstParameterDefinitions = (variableMappings, parameterDefinitions, ctx) => {
4251
+ const parameterDefinitionKeys = Object.keys(parameterDefinitions || {});
4252
+ for (const [mappingKey, mappingValue] of Object.entries(variableMappings || {})) {
4253
+ if (!parameterDefinitionKeys.includes(mappingValue.parameterId)) {
4254
+ ctx.addIssue({
4255
+ code: z.ZodIssueCode.custom,
4256
+ message: `The variable mapping with id "${mappingKey}" references a non-existing parameterId "${mappingValue.parameterId}".`,
4257
+ });
4258
+ }
4259
+ }
4260
+ };
4261
+ const validatePassToNodes = (rootChildren, componentSettings, ctx) => {
4262
+ if (!componentSettings.prebindingDefinitions ||
4263
+ componentSettings.prebindingDefinitions.length === 0) {
4264
+ return;
4265
+ }
4266
+ const { parameterDefinitions } = componentSettings.prebindingDefinitions[0];
4267
+ let nodeIds = new Set();
4268
+ for (const paramDef of Object.values(parameterDefinitions || {})) {
4269
+ paramDef.passToNodes?.forEach((n) => nodeIds.add(n.nodeId));
4270
+ }
4271
+ treeVisit$1(rootChildren, (node) => {
4272
+ if (!node.id)
4273
+ return;
4274
+ if (nodeIds.has(node.id)) {
4275
+ nodeIds.delete(node.id);
4276
+ }
4277
+ });
4278
+ if (nodeIds.size > 0) {
4279
+ const stringifiedNodeIds = Array.from(nodeIds)
4280
+ .map((id) => `"${id}"`)
4281
+ .join(', ');
4282
+ ctx.addIssue({
4283
+ code: z.ZodIssueCode.custom,
4284
+ message: `The following node IDs referenced in passToNodes are not present in the component tree: ${stringifiedNodeIds}.`,
4285
+ });
4286
+ }
4287
+ };
3768
4288
 
3769
4289
  z
3770
4290
  .object({
@@ -4062,707 +4582,408 @@ const parseDeepPath = (deepPathCandidate) => {
4062
4582
  else if (chunks.length === 2) {
4063
4583
  return null; // deep paths have at least 3 chunks
4064
4584
  }
4065
- // With 3+ chunks we can now check for deep path correctness
4066
- const [initialChunk, ...fieldChunks] = chunks;
4067
- if (!isValidInitialChunk(initialChunk)) {
4068
- return null;
4069
- }
4070
- if (!fieldChunks.every(isValidFieldChunk)) {
4071
- return null;
4072
- }
4073
- return {
4074
- key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
4075
- fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
4076
- };
4077
- };
4078
- const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
4079
- const chunks = [];
4080
- let currentChunk = [];
4081
- const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
4082
- const excludeEmptyChunks = (chunk) => chunk.length > 0;
4083
- for (let i = 0; i < segments.length; i++) {
4084
- const isInitialElement = i === 0;
4085
- const segment = segments[i];
4086
- if (isInitialElement) {
4087
- currentChunk = [segment];
4088
- }
4089
- else if (isSegmentBeginningOfChunk(segment)) {
4090
- chunks.push(currentChunk);
4091
- currentChunk = [segment];
4092
- }
4093
- else {
4094
- currentChunk.push(segment);
4095
- }
4096
- }
4097
- chunks.push(currentChunk);
4098
- return chunks.filter(excludeEmptyChunks);
4099
- };
4100
-
4101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
4102
- function get(obj, path) {
4103
- if (!path.length) {
4104
- return obj;
4105
- }
4106
- try {
4107
- const [currentPath, ...nextPath] = path;
4108
- return get(obj[currentPath], nextPath);
4109
- }
4110
- catch (err) {
4111
- return undefined;
4112
- }
4113
- }
4114
- const isEntry = (value) => {
4115
- return (null !== value &&
4116
- typeof value === 'object' &&
4117
- 'sys' in value &&
4118
- value.sys?.type === 'Entry');
4119
- };
4120
- const isAsset = (value) => {
4121
- return (null !== value &&
4122
- typeof value === 'object' &&
4123
- 'sys' in value &&
4124
- value.sys?.type === 'Asset');
4125
- };
4126
-
4127
- function deepFreeze(obj) {
4128
- const propNames = Object.getOwnPropertyNames(obj);
4129
- for (const name of propNames) {
4130
- const value = obj[name];
4131
- if (value && typeof value === 'object') {
4132
- deepFreeze(value);
4133
- }
4134
- }
4135
- return Object.freeze(obj);
4136
- }
4137
-
4138
- /**
4139
- * Base Store for entities
4140
- * Can be extended for the different loading behaviours (editor, production, ..)
4141
- */
4142
- class EntityStoreBase {
4143
- constructor({ entities, locale }) {
4144
- /* serialized */ this.entryMap = new Map();
4145
- /* serialized */ this.assetMap = new Map();
4146
- this.locale = locale;
4147
- for (const entity of entities) {
4148
- this.addEntity(entity);
4149
- }
4150
- }
4151
- get entities() {
4152
- return [...this.entryMap.values(), ...this.assetMap.values()];
4153
- }
4154
- updateEntity(entity) {
4155
- this.addEntity(entity);
4156
- }
4157
- getEntryOrAsset(linkOrEntryOrAsset, path) {
4158
- if (isDeepPath(path)) {
4159
- return this.getDeepEntry(linkOrEntryOrAsset, path);
4160
- }
4161
- let entity;
4162
- if (isLink(linkOrEntryOrAsset)) {
4163
- const resolvedEntity = linkOrEntryOrAsset.sys.linkType === 'Entry'
4164
- ? this.entryMap.get(linkOrEntryOrAsset.sys.id)
4165
- : this.assetMap.get(linkOrEntryOrAsset.sys.id);
4166
- if (!resolvedEntity || resolvedEntity.sys.type !== linkOrEntryOrAsset.sys.linkType) {
4167
- debug.warn(`[experiences-core::EntityStoreBase] Experience references unresolved entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
4168
- return;
4169
- }
4170
- entity = resolvedEntity;
4171
- }
4172
- else if (isAsset(linkOrEntryOrAsset) || isEntry(linkOrEntryOrAsset)) {
4173
- // We already have the complete entity in preview & delivery (resolved by the CMA client)
4174
- entity = linkOrEntryOrAsset;
4175
- }
4176
- else {
4177
- throw new Error(`[experiences-core::EntityStoreBase] Unexpected object when resolving entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
4178
- }
4179
- return entity;
4180
- }
4181
- /**
4182
- * @deprecated in the base class this should be simply an abstract method
4183
- * @param entityLink
4184
- * @param path
4185
- * @returns
4186
- */
4187
- getValue(entityLink, path) {
4188
- const entity = this.getEntity(entityLink.sys.linkType, entityLink.sys.id);
4189
- if (!entity) {
4190
- // TODO: move to `debug` utils once it is extracted
4191
- debug.warn(`Unresolved entity reference: ${entityLink.sys.linkType} with ID ${entityLink.sys.id}`);
4192
- return;
4193
- }
4194
- return get(entity, path);
4195
- }
4196
- getEntityFromLink(link) {
4197
- const resolvedEntity = link.sys.linkType === 'Entry'
4198
- ? this.entryMap.get(link.sys.id)
4199
- : this.assetMap.get(link.sys.id);
4200
- if (!resolvedEntity || resolvedEntity.sys.type !== link.sys.linkType) {
4201
- debug.warn(`[experiences-core::EntityStoreBase] Experience references unresolved entity: ${JSON.stringify(link)}`);
4202
- return;
4203
- }
4204
- return resolvedEntity;
4205
- }
4206
- getAssetById(assetId) {
4207
- const asset = this.assetMap.get(assetId);
4208
- if (!asset) {
4209
- debug.warn(`[experiences-core::EntityStoreBase] Asset with ID "${assetId}" is not found in the store`);
4210
- return;
4211
- }
4212
- return asset;
4213
- }
4214
- getEntryById(entryId) {
4215
- const entry = this.entryMap.get(entryId);
4216
- if (!entry) {
4217
- debug.warn(`[experiences-core::EntityStoreBase] Entry with ID "${entryId}" is not found in the store`);
4218
- return;
4219
- }
4220
- return entry;
4221
- }
4222
- getEntitiesFromMap(type, ids) {
4223
- const resolved = [];
4224
- const missing = [];
4225
- for (const id of ids) {
4226
- const entity = this.getEntity(type, id);
4227
- if (entity) {
4228
- resolved.push(entity);
4229
- }
4230
- else {
4231
- missing.push(id);
4232
- }
4233
- }
4234
- return {
4235
- resolved,
4236
- missing,
4237
- };
4238
- }
4239
- addEntity(entity) {
4240
- if (isAsset(entity)) {
4241
- // cloned and frozen
4242
- this.assetMap.set(entity.sys.id, deepFreeze(cloneDeep(entity)));
4243
- }
4244
- else if (isEntry(entity)) {
4245
- // cloned and frozen
4246
- this.entryMap.set(entity.sys.id, deepFreeze(cloneDeep(entity)));
4247
- }
4248
- else {
4249
- throw new Error(`Attempted to add an entity to the store that is neither Asset nor Entry: '${JSON.stringify(entity)}'`);
4250
- }
4251
- }
4252
- async fetchAsset(id) {
4253
- const { resolved, missing } = this.getEntitiesFromMap('Asset', [id]);
4254
- if (missing.length) {
4255
- // TODO: move to `debug` utils once it is extracted
4256
- debug.warn(`[experiences-core::EntityStoreBase] Asset "${id}" is not in the store`);
4257
- return;
4258
- }
4259
- return resolved[0];
4260
- }
4261
- async fetchAssets(ids) {
4262
- const { resolved, missing } = this.getEntitiesFromMap('Asset', ids);
4263
- if (missing.length) {
4264
- throw new Error(`Missing assets in the store (${missing.join(',')})`);
4265
- }
4266
- return resolved;
4267
- }
4268
- async fetchEntry(id) {
4269
- const { resolved, missing } = this.getEntitiesFromMap('Entry', [id]);
4270
- if (missing.length) {
4271
- // TODO: move to `debug` utils once it is extracted
4272
- debug.warn(`[experiences-core::EntityStoreBase] Entry "${id}" is not in the store`);
4273
- return;
4274
- }
4275
- return resolved[0];
4585
+ // With 3+ chunks we can now check for deep path correctness
4586
+ const [initialChunk, ...fieldChunks] = chunks;
4587
+ if (!isValidInitialChunk(initialChunk)) {
4588
+ return null;
4276
4589
  }
4277
- async fetchEntries(ids) {
4278
- const { resolved, missing } = this.getEntitiesFromMap('Entry', ids);
4279
- if (missing.length) {
4280
- throw new Error(`Missing assets in the store (${missing.join(',')})`);
4281
- }
4282
- return resolved;
4590
+ if (!fieldChunks.every(isValidFieldChunk)) {
4591
+ return null;
4283
4592
  }
4284
- getDeepEntry(linkOrEntryOrAsset, path) {
4285
- const resolveFieldset = (unresolvedFieldset, headEntry) => {
4286
- const resolvedFieldset = [];
4287
- let entityToResolveFieldsFrom = headEntry;
4288
- for (let i = 0; i < unresolvedFieldset.length; i++) {
4289
- const isLeaf = i === unresolvedFieldset.length - 1; // with last row, we are not expecting a link, but a value
4290
- const row = unresolvedFieldset[i];
4291
- const [, field, _localeQualifier] = row;
4292
- if (!entityToResolveFieldsFrom) {
4293
- throw new Error(`Logic Error: Cannot resolve field ${field} of a fieldset as there is no entity to resolve it from.`);
4294
- }
4295
- if (isLeaf) {
4296
- resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4297
- break;
4298
- }
4299
- const fieldValue = get(entityToResolveFieldsFrom, ['fields', field]);
4300
- if (undefined === fieldValue) {
4301
- return {
4302
- resolvedFieldset,
4303
- isFullyResolved: false,
4304
- reason: `Cannot resolve field Link<${entityToResolveFieldsFrom.sys.type}>(sys.id=${entityToResolveFieldsFrom.sys.id}).fields[${field}] as field value is not defined`,
4305
- };
4306
- }
4307
- else if (isLink(fieldValue)) {
4308
- const entity = this.getEntityFromLink(fieldValue);
4309
- if (entity === undefined) {
4310
- return {
4311
- resolvedFieldset,
4312
- isFullyResolved: false,
4313
- reason: `Field reference Link (sys.id=${fieldValue.sys.id}) not found in the EntityStore, waiting...`,
4314
- };
4315
- }
4316
- resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4317
- entityToResolveFieldsFrom = entity; // we move up
4318
- }
4319
- else if (isAsset(fieldValue) || isEntry(fieldValue)) {
4320
- resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4321
- entityToResolveFieldsFrom = fieldValue; // we move up
4322
- }
4323
- else {
4324
- return {
4325
- resolvedFieldset,
4326
- isFullyResolved: false,
4327
- reason: `Deep path points to an invalid field value of type '${typeof fieldValue}' (value=${fieldValue})`,
4328
- };
4329
- }
4330
- }
4331
- return {
4332
- resolvedFieldset,
4333
- isFullyResolved: true,
4334
- };
4335
- };
4336
- const headEntity = isLink(linkOrEntryOrAsset)
4337
- ? this.getEntityFromLink(linkOrEntryOrAsset)
4338
- : linkOrEntryOrAsset;
4339
- if (undefined === headEntity) {
4340
- return;
4593
+ return {
4594
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
4595
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
4596
+ };
4597
+ };
4598
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
4599
+ const chunks = [];
4600
+ let currentChunk = [];
4601
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
4602
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
4603
+ for (let i = 0; i < segments.length; i++) {
4604
+ const isInitialElement = i === 0;
4605
+ const segment = segments[i];
4606
+ if (isInitialElement) {
4607
+ currentChunk = [segment];
4341
4608
  }
4342
- const unresolvedFieldset = parseDataSourcePathIntoFieldset(path);
4343
- // The purpose here is to take this intermediate representation of the deep-path
4344
- // and to follow the links to the leaf-entity and field
4345
- // in case we can't follow till the end, we should signal that there was null-reference in the path
4346
- const { resolvedFieldset, isFullyResolved, reason } = resolveFieldset(unresolvedFieldset, headEntity);
4347
- if (!isFullyResolved) {
4348
- if (reason) {
4349
- debug.log(`[experiences-core::EntityStoreBase] Deep path wasn't resolved till leaf node, falling back to undefined, because: ${reason}`);
4350
- }
4351
- return;
4609
+ else if (isSegmentBeginningOfChunk(segment)) {
4610
+ chunks.push(currentChunk);
4611
+ currentChunk = [segment];
4352
4612
  }
4353
- const [leafEntity] = resolvedFieldset[resolvedFieldset.length - 1];
4354
- return leafEntity;
4355
- }
4356
- getEntity(type, id) {
4357
- if (type === 'Asset') {
4358
- return this.assetMap.get(id);
4613
+ else {
4614
+ currentChunk.push(segment);
4359
4615
  }
4360
- return this.entryMap.get(id);
4361
4616
  }
4362
- toJSON() {
4363
- return {
4364
- entryMap: Object.fromEntries(this.entryMap),
4365
- assetMap: Object.fromEntries(this.assetMap),
4366
- locale: this.locale,
4367
- };
4617
+ chunks.push(currentChunk);
4618
+ return chunks.filter(excludeEmptyChunks);
4619
+ };
4620
+
4621
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4622
+ function get(obj, path) {
4623
+ if (!path.length) {
4624
+ return obj;
4625
+ }
4626
+ try {
4627
+ const [currentPath, ...nextPath] = path;
4628
+ return get(obj[currentPath], nextPath);
4629
+ }
4630
+ catch (err) {
4631
+ return undefined;
4368
4632
  }
4369
4633
  }
4634
+ const isEntry = (value) => {
4635
+ return (null !== value &&
4636
+ typeof value === 'object' &&
4637
+ 'sys' in value &&
4638
+ value.sys?.type === 'Entry');
4639
+ };
4640
+ const isAsset = (value) => {
4641
+ return (null !== value &&
4642
+ typeof value === 'object' &&
4643
+ 'sys' in value &&
4644
+ value.sys?.type === 'Asset');
4645
+ };
4370
4646
 
4371
- class UninitializedEntityStore extends EntityStoreBase {
4372
- constructor() {
4373
- super({ entities: [], locale: 'uninitialized-locale-in-uninitialized-entity-store' });
4647
+ function deepFreeze(obj) {
4648
+ const propNames = Object.getOwnPropertyNames(obj);
4649
+ for (const name of propNames) {
4650
+ const value = obj[name];
4651
+ if (value && typeof value === 'object') {
4652
+ deepFreeze(value);
4653
+ }
4374
4654
  }
4655
+ return Object.freeze(obj);
4375
4656
  }
4376
4657
 
4377
- create((set, get) => ({
4378
- // The UninitializedEntityStore is a placeholder instance and is here to highlight the
4379
- // // fact that it's not used by anything until during loading lifecycle it'sreplaced by real entity store:
4380
- // - in Preview+Delivery mode: right after we fetch Expereince and it entities
4381
- // - in EDITOR (VisualEditor) mode: right after the VisualEditor is async imported and initialize event happens
4382
- entityStore: new UninitializedEntityStore(),
4383
- areEntitiesFetched: false,
4384
- setEntitiesFetched(fetched) {
4385
- set({ areEntitiesFetched: fetched });
4386
- },
4387
- resolveAssetById(assetId) {
4388
- if (!assetId)
4389
- return undefined;
4390
- const { entityStore } = get();
4391
- return entityStore.getAssetById(assetId);
4392
- },
4393
- resolveEntryById(entryId) {
4394
- if (!entryId)
4395
- return undefined;
4396
- const { entityStore } = get();
4397
- return entityStore.getEntryById(entryId);
4398
- },
4399
- resolveEntity(link) {
4400
- if (!link)
4401
- return undefined;
4402
- const { entityStore } = get();
4403
- return entityStore.getEntityFromLink(link);
4404
- },
4405
- resetEntityStore(entityStore) {
4406
- set({
4407
- entityStore,
4408
- areEntitiesFetched: false,
4409
- });
4410
- },
4411
- }));
4412
-
4413
- var VisualEditorMode;
4414
- (function (VisualEditorMode) {
4415
- VisualEditorMode["LazyLoad"] = "lazyLoad";
4416
- VisualEditorMode["InjectScript"] = "injectScript";
4417
- })(VisualEditorMode || (VisualEditorMode = {}));
4418
-
4419
- var css_248z$2$1 = ".contentful-container{display:flex;pointer-events:all;position:relative}.contentful-container::-webkit-scrollbar{display:none}.cf-container-wrapper{position:relative;width:100%}.contentful-container:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.contentful-section-label:after{content:\"Section\"}.contentful-container-label:after{content:\"Container\"}.contentful-container-link,.contentful-container-link:active,.contentful-container-link:focus-visible,.contentful-container-link:hover,.contentful-container-link:read-write,.contentful-container-link:visited{color:inherit;outline:unset;text-decoration:unset}";
4420
- styleInject(css_248z$2$1);
4421
-
4422
- const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave, onMouseDown, onClick, flex, flexBasis, flexShrink, flexDirection, gap, justifyContent, justifyItems, justifySelf, alignItems, alignSelf, alignContent, order, flexWrap, flexGrow, className, cssStyles, ...props }, ref) => {
4423
- return (React.createElement("div", { id: id, ref: ref, style: {
4424
- display: 'flex',
4425
- flex,
4426
- flexBasis,
4427
- flexShrink,
4428
- flexDirection,
4429
- gap,
4430
- justifyContent,
4431
- justifyItems,
4432
- justifySelf,
4433
- alignItems,
4434
- alignSelf,
4435
- alignContent,
4436
- order,
4437
- flexWrap,
4438
- flexGrow,
4439
- ...cssStyles,
4440
- }, className: className, onMouseEnter: onMouseEnter, onMouseUp: onMouseUp, onMouseDown: onMouseDown, onMouseLeave: onMouseLeave, onClick: onClick, ...props }, children));
4441
- });
4442
- Flex.displayName = 'Flex';
4443
-
4444
- var css_248z$1$1 = ".cf-divider{display:contents;height:100%;position:relative;width:100%}.cf-divider hr{border:none}";
4445
- styleInject(css_248z$1$1);
4446
-
4447
- var css_248z$9 = ".cf-columns{display:flex;flex-direction:column;gap:24px;grid-template-columns:repeat(12,1fr);min-height:0;min-width:0}@media (min-width:768px){.cf-columns{display:grid}}.cf-single-column-wrapper{position:relative}.cf-single-column-wrapper:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.cf-single-column-label:after{content:\"Column\"}";
4448
- styleInject(css_248z$9);
4449
-
4450
- const assemblyStyle = { display: 'contents' };
4451
- const Assembly = (props) => {
4452
- // Using a display contents so assembly content/children
4453
- // can appear as if they are direct children of the div wrapper's parent
4454
- return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
4455
- };
4456
-
4457
- function useEditorSubscriber(inMemoryEntitiesStore) {
4458
- const entityStore = inMemoryEntitiesStore((state) => state.entityStore);
4459
- const areEntitiesFetched = inMemoryEntitiesStore((state) => state.areEntitiesFetched);
4460
- const setEntitiesFetched = inMemoryEntitiesStore((state) => state.setEntitiesFetched);
4461
- const resetEntityStore = inMemoryEntitiesStore((state) => state.resetEntityStore);
4462
- const { updateTree, updateNodesByUpdatedEntity } = useTreeStore((state) => ({
4463
- updateTree: state.updateTree,
4464
- updateNodesByUpdatedEntity: state.updateNodesByUpdatedEntity,
4465
- }));
4466
- const unboundValues = useEditorStore((state) => state.unboundValues);
4467
- const dataSource = useEditorStore((state) => state.dataSource);
4468
- const setLocale = useEditorStore((state) => state.setLocale);
4469
- const setUnboundValues = useEditorStore((state) => state.setUnboundValues);
4470
- const setDataSource = useEditorStore((state) => state.setDataSource);
4471
- const reloadApp = () => {
4472
- sendMessage(OUTGOING_EVENTS.CanvasReload, undefined);
4473
- // Wait a moment to ensure that the message was sent
4474
- setTimeout(() => {
4475
- // Received a hot reload message from webpack dev server -> reload the canvas
4476
- window.location.reload();
4477
- }, 50);
4478
- };
4479
- useEffect(() => {
4480
- sendMessage(OUTGOING_EVENTS.RequestComponentTreeUpdate, undefined);
4481
- }, []);
4658
+ /**
4659
+ * Base Store for entities
4660
+ * Can be extended for the different loading behaviours (editor, production, ..)
4661
+ */
4662
+ class EntityStoreBase {
4663
+ constructor({ entities, locale }) {
4664
+ /* serialized */ this.entryMap = new Map();
4665
+ /* serialized */ this.assetMap = new Map();
4666
+ this.locale = locale;
4667
+ for (const entity of entities) {
4668
+ this.addEntity(entity);
4669
+ }
4670
+ }
4671
+ get entities() {
4672
+ return [...this.entryMap.values(), ...this.assetMap.values()];
4673
+ }
4674
+ updateEntity(entity) {
4675
+ this.addEntity(entity);
4676
+ }
4677
+ getEntryOrAsset(linkOrEntryOrAsset, path) {
4678
+ if (isDeepPath(path)) {
4679
+ return this.getDeepEntry(linkOrEntryOrAsset, path);
4680
+ }
4681
+ let entity;
4682
+ if (isLink(linkOrEntryOrAsset)) {
4683
+ const resolvedEntity = linkOrEntryOrAsset.sys.linkType === 'Entry'
4684
+ ? this.entryMap.get(linkOrEntryOrAsset.sys.id)
4685
+ : this.assetMap.get(linkOrEntryOrAsset.sys.id);
4686
+ if (!resolvedEntity || resolvedEntity.sys.type !== linkOrEntryOrAsset.sys.linkType) {
4687
+ debug.warn(`[experiences-core::EntityStoreBase] Experience references unresolved entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
4688
+ return;
4689
+ }
4690
+ entity = resolvedEntity;
4691
+ }
4692
+ else if (isAsset(linkOrEntryOrAsset) || isEntry(linkOrEntryOrAsset)) {
4693
+ // We already have the complete entity in preview & delivery (resolved by the CMA client)
4694
+ entity = linkOrEntryOrAsset;
4695
+ }
4696
+ else {
4697
+ throw new Error(`[experiences-core::EntityStoreBase] Unexpected object when resolving entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
4698
+ }
4699
+ return entity;
4700
+ }
4482
4701
  /**
4483
- * Fills up entityStore with entities from newDataSource and from the tree.
4484
- * Also manages "entity status" variables (areEntitiesFetched, isFetchingEntities)
4702
+ * @deprecated in the base class this should be simply an abstract method
4703
+ * @param entityLink
4704
+ * @param path
4705
+ * @returns
4485
4706
  */
4486
- const fetchMissingEntities = useCallback(async (entityStore, newDataSource, tree) => {
4487
- // if we realize that there's nothing missing and nothing to fill-fetch before we do any async call,
4488
- // then we can simply return and not lock the EntityStore at all.
4489
- const startFetching = () => {
4490
- setEntitiesFetched(false);
4491
- };
4492
- const endFetching = () => {
4493
- setEntitiesFetched(true);
4494
- };
4495
- // Prepare L1 entities and deepReferences
4496
- const entityLinksL1 = [
4497
- ...Object.values(newDataSource),
4498
- ...assembliesRegistry.values(), // we count assemblies here as "L1 entities", for convenience. Even though they're not headEntities.
4499
- ];
4500
- /**
4501
- * Checks only for _missing_ L1 entities
4502
- * WARNING: Does NOT check for entity staleness/versions. If an entity is stale, it will NOT be considered missing.
4503
- * If ExperienceBuilder wants to update stale entities, it should post `▼UPDATED_ENTITY` message to SDK.
4504
- */
4505
- const isMissingL1Entities = (entityLinks) => {
4506
- const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinks);
4507
- return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
4508
- };
4509
- /**
4510
- * PRECONDITION: all L1 entities are fetched
4511
- */
4512
- const isMissingL2Entities = (deepReferences) => {
4513
- const referentLinks = deepReferences
4514
- .map((deepReference) => deepReference.extractReferent(entityStore))
4515
- .filter(isLink$1);
4516
- const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
4517
- return Boolean(missingAssetIds.length) || Boolean(missingEntryIds.length);
4518
- };
4519
- /**
4520
- * POST_CONDITION: entityStore is has all L1 entities (aka headEntities)
4521
- */
4522
- const fillupL1 = async ({ entityLinksL1, }) => {
4523
- const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(entityLinksL1);
4524
- await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
4525
- };
4526
- /**
4527
- * PRECONDITION: all L1 entites are fetched
4528
- */
4529
- const fillupL2 = async ({ deepReferences }) => {
4530
- const referentLinks = deepReferences
4531
- .map((deepReference) => deepReference.extractReferent(entityStore))
4532
- .filter(isLink$1);
4533
- const { missingAssetIds, missingEntryIds } = entityStore.getMissingEntityIds(referentLinks);
4534
- await entityStore.fetchEntities({ missingAssetIds, missingEntryIds });
4535
- };
4536
- try {
4537
- if (isMissingL1Entities(entityLinksL1)) {
4538
- startFetching();
4539
- await fillupL1({ entityLinksL1 });
4540
- }
4541
- const deepReferences = gatherDeepReferencesFromTree(tree.root, newDataSource, entityStore.getEntityFromLink.bind(entityStore));
4542
- if (isMissingL2Entities(deepReferences)) {
4543
- startFetching();
4544
- await fillupL2({ deepReferences });
4545
- }
4707
+ getValue(entityLink, path) {
4708
+ const entity = this.getEntity(entityLink.sys.linkType, entityLink.sys.id);
4709
+ if (!entity) {
4710
+ // TODO: move to `debug` utils once it is extracted
4711
+ debug.warn(`Unresolved entity reference: ${entityLink.sys.linkType} with ID ${entityLink.sys.id}`);
4712
+ return;
4546
4713
  }
4547
- catch (error) {
4548
- debug$1.error('[experiences-visual-editor-react::useEditorSubscriber] Failed fetching entities', { error });
4549
- throw error; // TODO: The original catch didn't let's rethrow; for the moment throw to see if we have any errors
4714
+ return get(entity, path);
4715
+ }
4716
+ getEntityFromLink(link) {
4717
+ const resolvedEntity = link.sys.linkType === 'Entry'
4718
+ ? this.entryMap.get(link.sys.id)
4719
+ : this.assetMap.get(link.sys.id);
4720
+ if (!resolvedEntity || resolvedEntity.sys.type !== link.sys.linkType) {
4721
+ debug.warn(`[experiences-core::EntityStoreBase] Experience references unresolved entity: ${JSON.stringify(link)}`);
4722
+ return;
4550
4723
  }
4551
- finally {
4552
- endFetching();
4724
+ return resolvedEntity;
4725
+ }
4726
+ getAssetById(assetId) {
4727
+ const asset = this.assetMap.get(assetId);
4728
+ if (!asset) {
4729
+ debug.warn(`[experiences-core::EntityStoreBase] Asset with ID "${assetId}" is not found in the store`);
4730
+ return;
4553
4731
  }
4554
- }, [setEntitiesFetched /* setFetchingEntities, assembliesRegistry */]);
4555
- useEffect(() => {
4556
- const onMessage = async (event) => {
4557
- let reason;
4558
- if ((reason = doesMismatchMessageSchema(event))) {
4559
- if (event.origin.startsWith('http://localhost') &&
4560
- `${event.data}`.includes('webpackHotUpdate')) {
4561
- reloadApp();
4562
- }
4563
- else {
4564
- debug$1.warn(`[experiences-visual-editor-react::onMessage] Ignoring alien incoming message from origin [${event.origin}], due to: [${reason}]`, event);
4565
- }
4566
- return;
4732
+ return asset;
4733
+ }
4734
+ getEntryById(entryId) {
4735
+ const entry = this.entryMap.get(entryId);
4736
+ if (!entry) {
4737
+ debug.warn(`[experiences-core::EntityStoreBase] Entry with ID "${entryId}" is not found in the store`);
4738
+ return;
4739
+ }
4740
+ return entry;
4741
+ }
4742
+ getEntitiesFromMap(type, ids) {
4743
+ const resolved = [];
4744
+ const missing = [];
4745
+ for (const id of ids) {
4746
+ const entity = this.getEntity(type, id);
4747
+ if (entity) {
4748
+ resolved.push(entity);
4567
4749
  }
4568
- const eventData = tryParseMessage(event);
4569
- debug$1.debug(`[experiences-visual-editor-react::onMessage] Received message [${eventData.eventType}]`, eventData);
4570
- if (eventData.eventType === PostMessageMethods$2.REQUESTED_ENTITIES) {
4571
- // Expected message: This message is handled in the EntityStore to store fetched entities
4572
- return;
4750
+ else {
4751
+ missing.push(id);
4573
4752
  }
4574
- switch (eventData.eventType) {
4575
- case INCOMING_EVENTS.ExperienceUpdated: {
4576
- const { tree, locale, changedNode, changedValueType, assemblies } = eventData.payload;
4577
- // Make sure to first store the assemblies before setting the tree and thus triggering a rerender
4578
- if (assemblies) {
4579
- setAssemblies(assemblies);
4580
- // If the assemblyEntry is not yet fetched, this will be done below by
4581
- // the imperative calls to fetchMissingEntities.
4582
- }
4583
- let newEntityStore = entityStore;
4584
- if (entityStore.locale !== locale) {
4585
- newEntityStore = new EditorModeEntityStore({ locale, entities: [] });
4586
- setLocale(locale);
4587
- resetEntityStore(newEntityStore);
4588
- }
4589
- // Below are mutually exclusive cases
4590
- if (changedNode) {
4591
- /**
4592
- * On single node updates, we want to skip the process of getting the data (datasource and unbound values)
4593
- * from tree. Since we know the updated node, we can skip that recursion everytime the tree updates and
4594
- * just update the relevant data we need from the relevant node.
4595
- *
4596
- * We still update the tree here so we don't have a stale "tree"
4597
- */
4598
- if (changedValueType === 'BoundValue') {
4599
- const newDataSource = { ...dataSource, ...changedNode.data.dataSource };
4600
- setDataSource(newDataSource);
4601
- await fetchMissingEntities(newEntityStore, newDataSource, tree);
4602
- }
4603
- else if (changedValueType === 'UnboundValue') {
4604
- setUnboundValues({
4605
- ...unboundValues,
4606
- ...changedNode.data.unboundValues,
4607
- });
4608
- }
4609
- }
4610
- else {
4611
- const { dataSource, unboundValues } = getDataFromTree(tree);
4612
- setDataSource(dataSource);
4613
- setUnboundValues(unboundValues);
4614
- await fetchMissingEntities(newEntityStore, dataSource, tree);
4615
- }
4616
- // Update the tree when all necessary data is fetched and ready for rendering.
4617
- updateTree(tree);
4618
- break;
4753
+ }
4754
+ return {
4755
+ resolved,
4756
+ missing,
4757
+ };
4758
+ }
4759
+ addEntity(entity) {
4760
+ if (isAsset(entity)) {
4761
+ // cloned and frozen
4762
+ this.assetMap.set(entity.sys.id, deepFreeze(cloneDeep(entity)));
4763
+ }
4764
+ else if (isEntry(entity)) {
4765
+ // cloned and frozen
4766
+ this.entryMap.set(entity.sys.id, deepFreeze(cloneDeep(entity)));
4767
+ }
4768
+ else {
4769
+ throw new Error(`Attempted to add an entity to the store that is neither Asset nor Entry: '${JSON.stringify(entity)}'`);
4770
+ }
4771
+ }
4772
+ async fetchAsset(id) {
4773
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', [id]);
4774
+ if (missing.length) {
4775
+ // TODO: move to `debug` utils once it is extracted
4776
+ debug.warn(`[experiences-core::EntityStoreBase] Asset "${id}" is not in the store`);
4777
+ return;
4778
+ }
4779
+ return resolved[0];
4780
+ }
4781
+ async fetchAssets(ids) {
4782
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', ids);
4783
+ if (missing.length) {
4784
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
4785
+ }
4786
+ return resolved;
4787
+ }
4788
+ async fetchEntry(id) {
4789
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', [id]);
4790
+ if (missing.length) {
4791
+ // TODO: move to `debug` utils once it is extracted
4792
+ debug.warn(`[experiences-core::EntityStoreBase] Entry "${id}" is not in the store`);
4793
+ return;
4794
+ }
4795
+ return resolved[0];
4796
+ }
4797
+ async fetchEntries(ids) {
4798
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', ids);
4799
+ if (missing.length) {
4800
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
4801
+ }
4802
+ return resolved;
4803
+ }
4804
+ getDeepEntry(linkOrEntryOrAsset, path) {
4805
+ const resolveFieldset = (unresolvedFieldset, headEntry) => {
4806
+ const resolvedFieldset = [];
4807
+ let entityToResolveFieldsFrom = headEntry;
4808
+ for (let i = 0; i < unresolvedFieldset.length; i++) {
4809
+ const isLeaf = i === unresolvedFieldset.length - 1; // with last row, we are not expecting a link, but a value
4810
+ const row = unresolvedFieldset[i];
4811
+ const [, field, _localeQualifier] = row;
4812
+ if (!entityToResolveFieldsFrom) {
4813
+ throw new Error(`Logic Error: Cannot resolve field ${field} of a fieldset as there is no entity to resolve it from.`);
4619
4814
  }
4620
- case INCOMING_EVENTS.AssembliesRegistered: {
4621
- const { assemblies } = eventData.payload;
4622
- assemblies.forEach((definition) => {
4623
- addComponentRegistration({
4624
- component: Assembly,
4625
- definition,
4626
- });
4627
- });
4815
+ if (isLeaf) {
4816
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4628
4817
  break;
4629
4818
  }
4630
- case INCOMING_EVENTS.AssembliesAdded: {
4631
- const { assembly, assemblyDefinition, } = eventData.payload;
4632
- entityStore.updateEntity(assembly);
4633
- // Using a Map here to avoid setting state and rerending all existing assemblies when a new assembly is added
4634
- // TODO: Figure out if we can extend this love to data source and unbound values. Maybe that'll solve the blink
4635
- // of all bound and unbound values when new values are added
4636
- assembliesRegistry.set(assembly.sys.id, {
4637
- sys: { id: assembly.sys.id, linkType: 'Entry', type: 'Link' },
4638
- });
4639
- if (assemblyDefinition) {
4640
- addComponentRegistration({
4641
- component: Assembly,
4642
- definition: assemblyDefinition,
4643
- });
4644
- }
4645
- break;
4819
+ const fieldValue = get(entityToResolveFieldsFrom, ['fields', field]);
4820
+ if (undefined === fieldValue) {
4821
+ return {
4822
+ resolvedFieldset,
4823
+ isFullyResolved: false,
4824
+ reason: `Cannot resolve field Link<${entityToResolveFieldsFrom.sys.type}>(sys.id=${entityToResolveFieldsFrom.sys.id}).fields[${field}] as field value is not defined`,
4825
+ };
4646
4826
  }
4647
- case INCOMING_EVENTS.UpdatedEntity: {
4648
- const { entity: updatedEntity, shouldRerender } = eventData.payload;
4649
- if (updatedEntity) {
4650
- const storedEntity = entityStore.entities.find((entity) => entity.sys.id === updatedEntity.sys.id);
4651
- const didEntityChange = storedEntity?.sys.version !== updatedEntity.sys.version;
4652
- entityStore.updateEntity(updatedEntity);
4653
- // We traverse the whole tree, so this is a opt-in feature to only use it when required.
4654
- if (shouldRerender && didEntityChange) {
4655
- updateNodesByUpdatedEntity(updatedEntity.sys.id);
4656
- }
4827
+ else if (isLink(fieldValue)) {
4828
+ const entity = this.getEntityFromLink(fieldValue);
4829
+ if (entity === undefined) {
4830
+ return {
4831
+ resolvedFieldset,
4832
+ isFullyResolved: false,
4833
+ reason: `Field reference Link (sys.id=${fieldValue.sys.id}) not found in the EntityStore, waiting...`,
4834
+ };
4657
4835
  }
4658
- break;
4836
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4837
+ entityToResolveFieldsFrom = entity; // we move up
4659
4838
  }
4660
- case INCOMING_EVENTS.RequestEditorMode: {
4661
- break;
4839
+ else if (isAsset(fieldValue) || isEntry(fieldValue)) {
4840
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
4841
+ entityToResolveFieldsFrom = fieldValue; // we move up
4662
4842
  }
4663
- default: {
4664
- const knownEvents = Object.values(INCOMING_EVENTS);
4665
- const isDeprecatedMessage = knownEvents.includes(eventData.eventType);
4666
- if (!isDeprecatedMessage) {
4667
- debug$1.error(`[experiences-visual-editor-react::onMessage] Logic error, unsupported eventType: [${eventData.eventType}]`);
4668
- }
4843
+ else {
4844
+ return {
4845
+ resolvedFieldset,
4846
+ isFullyResolved: false,
4847
+ reason: `Deep path points to an invalid field value of type '${typeof fieldValue}' (value=${fieldValue})`,
4848
+ };
4669
4849
  }
4670
4850
  }
4851
+ return {
4852
+ resolvedFieldset,
4853
+ isFullyResolved: true,
4854
+ };
4671
4855
  };
4672
- window.addEventListener('message', onMessage);
4673
- return () => {
4674
- window.removeEventListener('message', onMessage);
4675
- };
4676
- }, [
4677
- entityStore,
4678
- setDataSource,
4679
- setLocale,
4680
- dataSource,
4681
- areEntitiesFetched,
4682
- fetchMissingEntities,
4683
- setUnboundValues,
4684
- unboundValues,
4685
- updateTree,
4686
- updateNodesByUpdatedEntity,
4687
- resetEntityStore,
4688
- ]);
4689
- }
4690
-
4691
- const CircularDependencyErrorPlaceholder = ({ wrappingPatternIds, ...props }) => {
4692
- return (React.createElement("div", { ...props, "data-cf-node-error": "circular-pattern-dependency", style: {
4693
- border: '1px solid red',
4694
- background: 'rgba(255, 0, 0, 0.1)',
4695
- padding: '1rem 1rem 0 1rem',
4696
- width: '100%',
4697
- height: '100%',
4698
- } },
4699
- "Circular usage of patterns detected:",
4700
- React.createElement("ul", null, Array.from(wrappingPatternIds).map((patternId) => {
4701
- const entryLink = { sys: { type: 'Link', linkType: 'Entry', id: patternId } };
4702
- const entry = inMemoryEntities.maybeResolveLink(entryLink);
4703
- const entryTitle = entry?.fields?.title;
4704
- const text = entryTitle ? `${entryTitle} (${patternId})` : patternId;
4705
- return React.createElement("li", { key: patternId }, text);
4706
- }))));
4707
- };
4708
-
4709
- class ImportedComponentError extends Error {
4710
- constructor(message) {
4711
- super(message);
4712
- this.name = 'ImportedComponentError';
4713
- }
4714
- }
4715
- class ExperienceSDKError extends Error {
4716
- constructor(message) {
4717
- super(message);
4718
- this.name = 'ExperienceSDKError';
4856
+ const headEntity = isLink(linkOrEntryOrAsset)
4857
+ ? this.getEntityFromLink(linkOrEntryOrAsset)
4858
+ : linkOrEntryOrAsset;
4859
+ if (undefined === headEntity) {
4860
+ return;
4861
+ }
4862
+ const unresolvedFieldset = parseDataSourcePathIntoFieldset(path);
4863
+ // The purpose here is to take this intermediate representation of the deep-path
4864
+ // and to follow the links to the leaf-entity and field
4865
+ // in case we can't follow till the end, we should signal that there was null-reference in the path
4866
+ const { resolvedFieldset, isFullyResolved, reason } = resolveFieldset(unresolvedFieldset, headEntity);
4867
+ if (!isFullyResolved) {
4868
+ if (reason) {
4869
+ debug.log(`[experiences-core::EntityStoreBase] Deep path wasn't resolved till leaf node, falling back to undefined, because: ${reason}`);
4870
+ }
4871
+ return;
4872
+ }
4873
+ const [leafEntity] = resolvedFieldset[resolvedFieldset.length - 1];
4874
+ return leafEntity;
4719
4875
  }
4720
- }
4721
- class ImportedComponentErrorBoundary extends React.Component {
4722
- componentDidCatch(error, _errorInfo) {
4723
- if (error.name === 'ImportedComponentError' || error.name === 'ExperienceSDKError') {
4724
- // This error was already handled by a nested error boundary and should be passed upwards
4725
- // We have to do this as we wrap every component on every layer with this error boundary and
4726
- // thus an error deep in the tree bubbles through many layers of error boundaries.
4727
- throw error;
4876
+ getEntity(type, id) {
4877
+ if (type === 'Asset') {
4878
+ return this.assetMap.get(id);
4728
4879
  }
4729
- // Differentiate between custom and SDK-provided components for error tracking
4730
- const ErrorClass = isContentfulComponent(this.props.componentId)
4731
- ? ExperienceSDKError
4732
- : ImportedComponentError;
4733
- const err = new ErrorClass(error.message);
4734
- err.stack = error.stack;
4735
- throw err;
4880
+ return this.entryMap.get(id);
4736
4881
  }
4737
- render() {
4738
- return this.props.children;
4882
+ toJSON() {
4883
+ return {
4884
+ entryMap: Object.fromEntries(this.entryMap),
4885
+ assetMap: Object.fromEntries(this.assetMap),
4886
+ locale: this.locale,
4887
+ };
4739
4888
  }
4740
4889
  }
4741
4890
 
4742
- const MissingComponentPlaceholder = ({ blockId }) => {
4743
- return (React.createElement("div", { style: {
4744
- border: '1px solid red',
4745
- width: '100%',
4746
- height: '100%',
4747
- } },
4748
- "Missing component '",
4749
- blockId,
4750
- "'"));
4751
- };
4891
+ class UninitializedEntityStore extends EntityStoreBase {
4892
+ constructor() {
4893
+ super({ entities: [], locale: 'uninitialized-locale-in-uninitialized-entity-store' });
4894
+ }
4895
+ }
4752
4896
 
4753
- var css_248z$2 = ".EditorBlock-module_emptySlot__za-Bi {\n min-height: 80px;\n min-width: 80px;\n}\n";
4754
- var styles$1 = {"emptySlot":"EditorBlock-module_emptySlot__za-Bi"};
4897
+ create((set, get) => ({
4898
+ // The UninitializedEntityStore is a placeholder instance and is here to highlight the
4899
+ // // fact that it's not used by anything until during loading lifecycle it'sreplaced by real entity store:
4900
+ // - in Preview+Delivery mode: right after we fetch Expereince and it entities
4901
+ // - in EDITOR (VisualEditor) mode: right after the VisualEditor is async imported and initialize event happens
4902
+ entityStore: new UninitializedEntityStore(),
4903
+ areEntitiesFetched: false,
4904
+ setEntitiesFetched(fetched) {
4905
+ set({ areEntitiesFetched: fetched });
4906
+ },
4907
+ resolveAssetById(assetId) {
4908
+ if (!assetId)
4909
+ return undefined;
4910
+ const { entityStore } = get();
4911
+ return entityStore.getAssetById(assetId);
4912
+ },
4913
+ resolveEntryById(entryId) {
4914
+ if (!entryId)
4915
+ return undefined;
4916
+ const { entityStore } = get();
4917
+ return entityStore.getEntryById(entryId);
4918
+ },
4919
+ resolveEntity(link) {
4920
+ if (!link)
4921
+ return undefined;
4922
+ const { entityStore } = get();
4923
+ return entityStore.getEntityFromLink(link);
4924
+ },
4925
+ resetEntityStore(entityStore) {
4926
+ set({
4927
+ entityStore,
4928
+ areEntitiesFetched: false,
4929
+ });
4930
+ },
4931
+ }));
4932
+
4933
+ var VisualEditorMode;
4934
+ (function (VisualEditorMode) {
4935
+ VisualEditorMode["LazyLoad"] = "lazyLoad";
4936
+ VisualEditorMode["InjectScript"] = "injectScript";
4937
+ })(VisualEditorMode || (VisualEditorMode = {}));
4938
+
4939
+ var css_248z$2 = ".contentful-container{display:flex;pointer-events:all;position:relative}.contentful-container::-webkit-scrollbar{display:none}.cf-container-wrapper{position:relative;width:100%}.contentful-container:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.contentful-section-label:after{content:\"Section\"}.contentful-container-label:after{content:\"Container\"}.contentful-container-link,.contentful-container-link:active,.contentful-container-link:focus-visible,.contentful-container-link:hover,.contentful-container-link:read-write,.contentful-container-link:visited{color:inherit;outline:unset;text-decoration:unset}";
4755
4940
  styleInject(css_248z$2);
4756
4941
 
4942
+ const Flex = forwardRef(({ id, children, onMouseEnter, onMouseUp, onMouseLeave, onMouseDown, onClick, flex, flexBasis, flexShrink, flexDirection, gap, justifyContent, justifyItems, justifySelf, alignItems, alignSelf, alignContent, order, flexWrap, flexGrow, className, cssStyles, ...props }, ref) => {
4943
+ return (React.createElement("div", { id: id, ref: ref, style: {
4944
+ display: 'flex',
4945
+ flex,
4946
+ flexBasis,
4947
+ flexShrink,
4948
+ flexDirection,
4949
+ gap,
4950
+ justifyContent,
4951
+ justifyItems,
4952
+ justifySelf,
4953
+ alignItems,
4954
+ alignSelf,
4955
+ alignContent,
4956
+ order,
4957
+ flexWrap,
4958
+ flexGrow,
4959
+ ...cssStyles,
4960
+ }, className: className, onMouseEnter: onMouseEnter, onMouseUp: onMouseUp, onMouseDown: onMouseDown, onMouseLeave: onMouseLeave, onClick: onClick, ...props }, children));
4961
+ });
4962
+ Flex.displayName = 'Flex';
4963
+
4964
+ var css_248z$1$1 = ".cf-divider{display:contents;height:100%;position:relative;width:100%}.cf-divider hr{border:none}";
4965
+ styleInject(css_248z$1$1);
4966
+
4967
+ var css_248z$9 = ".cf-columns{display:flex;flex-direction:column;gap:24px;grid-template-columns:repeat(12,1fr);min-height:0;min-width:0}@media (min-width:768px){.cf-columns{display:grid}}.cf-single-column-wrapper{position:relative}.cf-single-column-wrapper:after{align-items:center;bottom:0;color:var(--exp-builder-gray400);content:\"\";display:block;display:flex;font-family:var(--exp-builder-font-stack-primary);font-size:12px;justify-content:center;left:0;overflow-x:clip;pointer-events:none;position:absolute;right:0;top:0;z-index:1}.cf-single-column-label:after{content:\"Column\"}";
4968
+ styleInject(css_248z$9);
4969
+
4970
+ const assemblyStyle = { display: 'contents' };
4971
+ const Assembly = (props) => {
4972
+ // Using a display contents so assembly content/children
4973
+ // can appear as if they are direct children of the div wrapper's parent
4974
+ return React.createElement("div", { "data-test-id": "assembly", ...props, style: assemblyStyle });
4975
+ };
4976
+
4757
4977
  const useComponentRegistration = (node) => {
4758
4978
  return useMemo(() => {
4759
- let registration = componentRegistry.get(node.data.blockId);
4760
- if (node.type === ASSEMBLY_NODE_TYPE && !registration) {
4761
- registration = createAssemblyRegistration({
4762
- definitionId: node.data.blockId,
4979
+ if (node.type === ASSEMBLY_NODE_TYPE) {
4980
+ // The definition and component are the same for all assemblies
4981
+ return {
4763
4982
  component: Assembly,
4764
- });
4983
+ definition: createAssemblyDefinition(node.data.blockId),
4984
+ };
4765
4985
  }
4986
+ const registration = componentRegistry.get(node.data.blockId);
4766
4987
  if (!registration) {
4767
4988
  debug$1.warn(`[experiences-visual-editor-react::useComponentRegistration] Component registration not found for component with id: "${node.data.blockId}". The registered component might have been removed from the code. To proceed, remove the component manually from the layers tab.`);
4768
4989
  return undefined;
@@ -4854,7 +5075,9 @@ const checkIsNodeVisible = (node, resolveDesignValue) => {
4854
5075
  return node.children.some((childNode) => checkIsNodeVisible(childNode, resolveDesignValue));
4855
5076
  }
4856
5077
  // Check if the current node is visible (`cfVisibility` is enforced on all nodes)
4857
- return !!resolveDesignValue(node.data.props['cfVisibility'].valuesByBreakpoint);
5078
+ // Check explicitly for false, as `undefined` is treated as `true`. It could become undefined when the breakpoint IDs changed.
5079
+ return (resolveDesignValue(node.data.props['cfVisibility'].valuesByBreakpoint) !==
5080
+ false);
4858
5081
  };
4859
5082
 
4860
5083
  const useComponentProps = ({ node, entityStore, areEntitiesFetched, resolveDesignValue, definition, options, }) => {