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