@contentful/experiences-components-react 1.42.4-prerelease-20250702T1207-37b3bfc.0 → 2.0.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
@@ -5,6 +5,7 @@ import { BLOCKS } from '@contentful/rich-text-types';
5
5
  import { z } from 'zod';
6
6
  import 'lodash-es';
7
7
  import 'md5';
8
+ import { create } from 'zustand';
8
9
 
9
10
  var css_248z$8 = "@import url(https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,400;0,500;0,600;1,400;1,600&display=swap);: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-font-family-sans:Archivo,Helvetica,Arial,sans-serif;--cf-font-family-serif:Georgia,Cambria,Times New Roman,Times,serif;--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}";
10
11
  styleInject(css_248z$8);
@@ -1408,7 +1409,13 @@ const ParameterDefinitionSchema = z.object({
1408
1409
  }),
1409
1410
  })
1410
1411
  .optional(),
1411
- contentTypes: z.array(z.string()),
1412
+ contentTypes: z.record(z.string(), z.object({
1413
+ sys: z.object({
1414
+ type: z.literal('Link'),
1415
+ id: z.string(),
1416
+ linkType: z.enum(['ContentType']),
1417
+ }),
1418
+ })),
1412
1419
  passToNodes: z.array(PassToNodeSchema).optional(),
1413
1420
  });
1414
1421
  const ParameterDefinitionsSchema = z.record(propertyKeySchema, ParameterDefinitionSchema);
@@ -1428,7 +1435,7 @@ const ComponentSettingsSchema = z
1428
1435
  variableDefinitions: ComponentVariablesSchema,
1429
1436
  thumbnailId: z.enum(THUMBNAIL_IDS).optional(),
1430
1437
  category: z.string().max(50, 'Category must contain at most 50 characters').optional(),
1431
- prebindingDefinitions: z.array(PrebindingDefinitionSchema).length(1).optional(),
1438
+ prebindingDefinitions: z.array(PrebindingDefinitionSchema).max(1).optional(),
1432
1439
  })
1433
1440
  .strict();
1434
1441
  z.object({
@@ -1634,6 +1641,419 @@ class DebugLogger {
1634
1641
  DebugLogger.instance = null;
1635
1642
  DebugLogger.getInstance();
1636
1643
 
1644
+ const isLink = (maybeLink) => {
1645
+ if (maybeLink === null)
1646
+ return false;
1647
+ if (typeof maybeLink !== 'object')
1648
+ return false;
1649
+ const link = maybeLink;
1650
+ return Boolean(link.sys?.id) && link.sys?.type === 'Link' && Boolean(link.sys?.linkType);
1651
+ };
1652
+
1653
+ /**
1654
+ * This module encapsulates format of the path to a deep reference.
1655
+ */
1656
+ const parseDataSourcePathIntoFieldset = (path) => {
1657
+ const parsedPath = parseDeepPath(path);
1658
+ if (null === parsedPath) {
1659
+ throw new Error(`Cannot parse path '${path}' as deep path`);
1660
+ }
1661
+ return parsedPath.fields.map((field) => [null, field, '~locale']);
1662
+ };
1663
+ /**
1664
+ * Detects if paths is valid deep-path, like:
1665
+ * - /gV6yKXp61hfYrR7rEyKxY/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1666
+ * or regular, like:
1667
+ * - /6J8eA60yXwdm5eyUh9fX6/fields/mainStory/~locale
1668
+ * @returns
1669
+ */
1670
+ const isDeepPath = (deepPathCandidate) => {
1671
+ const deepPathParsed = parseDeepPath(deepPathCandidate);
1672
+ if (!deepPathParsed) {
1673
+ return false;
1674
+ }
1675
+ return deepPathParsed.fields.length > 1;
1676
+ };
1677
+ const parseDeepPath = (deepPathCandidate) => {
1678
+ // ALGORITHM:
1679
+ // We start with deep path in form:
1680
+ // /uuid123/fields/mainStory/~locale/fields/cover/~locale/fields/title/~locale
1681
+ // First turn string into array of segments
1682
+ // ['', 'uuid123', 'fields', 'mainStory', '~locale', 'fields', 'cover', '~locale', 'fields', 'title', '~locale']
1683
+ // Then group segments into intermediate represenatation - chunks, where each non-initial chunk starts with 'fields'
1684
+ // [
1685
+ // [ "", "uuid123" ],
1686
+ // [ "fields", "mainStory", "~locale" ],
1687
+ // [ "fields", "cover", "~locale" ],
1688
+ // [ "fields", "title", "~locale" ]
1689
+ // ]
1690
+ // Then check "initial" chunk for corretness
1691
+ // Then check all "field-leading" chunks for correctness
1692
+ const isValidInitialChunk = (initialChunk) => {
1693
+ // must have start with '' and have at least 2 segments, second non-empty
1694
+ // eg. /-_432uuid123123
1695
+ return /^\/([^/^~]+)$/.test(initialChunk.join('/'));
1696
+ };
1697
+ const isValidFieldChunk = (fieldChunk) => {
1698
+ // must start with 'fields' and have at least 3 segments, second non-empty and last segment must be '~locale'
1699
+ // eg. fields/-32234mainStory/~locale
1700
+ return /^fields\/[^/^~]+\/~locale$/.test(fieldChunk.join('/'));
1701
+ };
1702
+ const deepPathSegments = deepPathCandidate.split('/');
1703
+ const chunks = chunkSegments(deepPathSegments, { startNextChunkOnElementEqualTo: 'fields' });
1704
+ if (chunks.length <= 1) {
1705
+ return null; // malformed path, even regular paths have at least 2 chunks
1706
+ }
1707
+ else if (chunks.length === 2) {
1708
+ return null; // deep paths have at least 3 chunks
1709
+ }
1710
+ // With 3+ chunks we can now check for deep path correctness
1711
+ const [initialChunk, ...fieldChunks] = chunks;
1712
+ if (!isValidInitialChunk(initialChunk)) {
1713
+ return null;
1714
+ }
1715
+ if (!fieldChunks.every(isValidFieldChunk)) {
1716
+ return null;
1717
+ }
1718
+ return {
1719
+ key: initialChunk[1], // pick uuid from initial chunk ['','uuid123'],
1720
+ fields: fieldChunks.map((fieldChunk) => fieldChunk[1]), // pick only fieldName eg. from ['fields','mainStory', '~locale'] we pick `mainStory`
1721
+ };
1722
+ };
1723
+ const chunkSegments = (segments, { startNextChunkOnElementEqualTo }) => {
1724
+ const chunks = [];
1725
+ let currentChunk = [];
1726
+ const isSegmentBeginningOfChunk = (segment) => segment === startNextChunkOnElementEqualTo;
1727
+ const excludeEmptyChunks = (chunk) => chunk.length > 0;
1728
+ for (let i = 0; i < segments.length; i++) {
1729
+ const isInitialElement = i === 0;
1730
+ const segment = segments[i];
1731
+ if (isInitialElement) {
1732
+ currentChunk = [segment];
1733
+ }
1734
+ else if (isSegmentBeginningOfChunk(segment)) {
1735
+ chunks.push(currentChunk);
1736
+ currentChunk = [segment];
1737
+ }
1738
+ else {
1739
+ currentChunk.push(segment);
1740
+ }
1741
+ }
1742
+ chunks.push(currentChunk);
1743
+ return chunks.filter(excludeEmptyChunks);
1744
+ };
1745
+
1746
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1747
+ function get(obj, path) {
1748
+ if (!path.length) {
1749
+ return obj;
1750
+ }
1751
+ try {
1752
+ const [currentPath, ...nextPath] = path;
1753
+ return get(obj[currentPath], nextPath);
1754
+ }
1755
+ catch (err) {
1756
+ return undefined;
1757
+ }
1758
+ }
1759
+ const isEntry = (value) => {
1760
+ return (null !== value &&
1761
+ typeof value === 'object' &&
1762
+ 'sys' in value &&
1763
+ value.sys?.type === 'Entry');
1764
+ };
1765
+ const isAsset = (value) => {
1766
+ return (null !== value &&
1767
+ typeof value === 'object' &&
1768
+ 'sys' in value &&
1769
+ value.sys?.type === 'Asset');
1770
+ };
1771
+
1772
+ function deepFreeze(obj) {
1773
+ const propNames = Object.getOwnPropertyNames(obj);
1774
+ for (const name of propNames) {
1775
+ const value = obj[name];
1776
+ if (value && typeof value === 'object') {
1777
+ deepFreeze(value);
1778
+ }
1779
+ }
1780
+ return Object.freeze(obj);
1781
+ }
1782
+
1783
+ /**
1784
+ * Base Store for entities
1785
+ * Can be extended for the different loading behaviours (editor, production, ..)
1786
+ */
1787
+ class EntityStoreBase {
1788
+ constructor({ entities, locale }) {
1789
+ /* serialized */ this.entryMap = new Map();
1790
+ /* serialized */ this.assetMap = new Map();
1791
+ this.locale = locale;
1792
+ for (const entity of entities) {
1793
+ this.addEntity(entity);
1794
+ }
1795
+ }
1796
+ get entities() {
1797
+ return [...this.entryMap.values(), ...this.assetMap.values()];
1798
+ }
1799
+ updateEntity(entity) {
1800
+ this.addEntity(entity);
1801
+ }
1802
+ getEntryOrAsset(linkOrEntryOrAsset, path) {
1803
+ if (isDeepPath(path)) {
1804
+ return this.getDeepEntry(linkOrEntryOrAsset, path);
1805
+ }
1806
+ let entity;
1807
+ if (isLink(linkOrEntryOrAsset)) {
1808
+ const resolvedEntity = linkOrEntryOrAsset.sys.linkType === 'Entry'
1809
+ ? this.entryMap.get(linkOrEntryOrAsset.sys.id)
1810
+ : this.assetMap.get(linkOrEntryOrAsset.sys.id);
1811
+ if (!resolvedEntity || resolvedEntity.sys.type !== linkOrEntryOrAsset.sys.linkType) {
1812
+ console.warn(`Experience references unresolved entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
1813
+ return;
1814
+ }
1815
+ entity = resolvedEntity;
1816
+ }
1817
+ else if (isAsset(linkOrEntryOrAsset) || isEntry(linkOrEntryOrAsset)) {
1818
+ // We already have the complete entity in preview & delivery (resolved by the CMA client)
1819
+ entity = linkOrEntryOrAsset;
1820
+ }
1821
+ else {
1822
+ throw new Error(`Unexpected object when resolving entity: ${JSON.stringify(linkOrEntryOrAsset)}`);
1823
+ }
1824
+ return entity;
1825
+ }
1826
+ /**
1827
+ * @deprecated in the base class this should be simply an abstract method
1828
+ * @param entityLink
1829
+ * @param path
1830
+ * @returns
1831
+ */
1832
+ getValue(entityLink, path) {
1833
+ const entity = this.getEntity(entityLink.sys.linkType, entityLink.sys.id);
1834
+ if (!entity) {
1835
+ // TODO: move to `debug` utils once it is extracted
1836
+ console.warn(`Unresolved entity reference: ${entityLink.sys.linkType} with ID ${entityLink.sys.id}`);
1837
+ return;
1838
+ }
1839
+ return get(entity, path);
1840
+ }
1841
+ getEntityFromLink(link) {
1842
+ const resolvedEntity = link.sys.linkType === 'Entry'
1843
+ ? this.entryMap.get(link.sys.id)
1844
+ : this.assetMap.get(link.sys.id);
1845
+ if (!resolvedEntity || resolvedEntity.sys.type !== link.sys.linkType) {
1846
+ console.warn(`Experience references unresolved entity: ${JSON.stringify(link)}`);
1847
+ return;
1848
+ }
1849
+ return resolvedEntity;
1850
+ }
1851
+ getAssetById(assetId) {
1852
+ const asset = this.assetMap.get(assetId);
1853
+ if (!asset) {
1854
+ console.warn(`Asset with ID "${assetId}" is not found in the store`);
1855
+ return;
1856
+ }
1857
+ return asset;
1858
+ }
1859
+ getEntryById(entryId) {
1860
+ const entry = this.entryMap.get(entryId);
1861
+ if (!entry) {
1862
+ console.warn(`Entry with ID "${entryId}" is not found in the store`);
1863
+ return;
1864
+ }
1865
+ return entry;
1866
+ }
1867
+ getEntitiesFromMap(type, ids) {
1868
+ const resolved = [];
1869
+ const missing = [];
1870
+ for (const id of ids) {
1871
+ const entity = this.getEntity(type, id);
1872
+ if (entity) {
1873
+ resolved.push(entity);
1874
+ }
1875
+ else {
1876
+ missing.push(id);
1877
+ }
1878
+ }
1879
+ return {
1880
+ resolved,
1881
+ missing,
1882
+ };
1883
+ }
1884
+ addEntity(entity) {
1885
+ if (isAsset(entity)) {
1886
+ // cloned and frozen
1887
+ this.assetMap.set(entity.sys.id, deepFreeze(structuredClone(entity)));
1888
+ }
1889
+ else if (isEntry(entity)) {
1890
+ // cloned and frozen
1891
+ this.entryMap.set(entity.sys.id, deepFreeze(structuredClone(entity)));
1892
+ }
1893
+ else {
1894
+ throw new Error(`Attempted to add an entity to the store that is neither Asset nor Entry: '${JSON.stringify(entity)}'`);
1895
+ }
1896
+ }
1897
+ async fetchAsset(id) {
1898
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', [id]);
1899
+ if (missing.length) {
1900
+ // TODO: move to `debug` utils once it is extracted
1901
+ console.warn(`Asset "${id}" is not in the store`);
1902
+ return;
1903
+ }
1904
+ return resolved[0];
1905
+ }
1906
+ async fetchAssets(ids) {
1907
+ const { resolved, missing } = this.getEntitiesFromMap('Asset', ids);
1908
+ if (missing.length) {
1909
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1910
+ }
1911
+ return resolved;
1912
+ }
1913
+ async fetchEntry(id) {
1914
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', [id]);
1915
+ if (missing.length) {
1916
+ // TODO: move to `debug` utils once it is extracted
1917
+ console.warn(`Entry "${id}" is not in the store`);
1918
+ return;
1919
+ }
1920
+ return resolved[0];
1921
+ }
1922
+ async fetchEntries(ids) {
1923
+ const { resolved, missing } = this.getEntitiesFromMap('Entry', ids);
1924
+ if (missing.length) {
1925
+ throw new Error(`Missing assets in the store (${missing.join(',')})`);
1926
+ }
1927
+ return resolved;
1928
+ }
1929
+ getDeepEntry(linkOrEntryOrAsset, path) {
1930
+ const resolveFieldset = (unresolvedFieldset, headEntry) => {
1931
+ const resolvedFieldset = [];
1932
+ let entityToResolveFieldsFrom = headEntry;
1933
+ for (let i = 0; i < unresolvedFieldset.length; i++) {
1934
+ const isLeaf = i === unresolvedFieldset.length - 1; // with last row, we are not expecting a link, but a value
1935
+ const row = unresolvedFieldset[i];
1936
+ const [, field, _localeQualifier] = row;
1937
+ if (!entityToResolveFieldsFrom) {
1938
+ throw new Error(`Logic Error: Cannot resolve field ${field} of a fieldset as there is no entity to resolve it from.`);
1939
+ }
1940
+ if (isLeaf) {
1941
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1942
+ break;
1943
+ }
1944
+ const fieldValue = get(entityToResolveFieldsFrom, ['fields', field]);
1945
+ if (undefined === fieldValue) {
1946
+ return {
1947
+ resolvedFieldset,
1948
+ isFullyResolved: false,
1949
+ reason: `Cannot resolve field Link<${entityToResolveFieldsFrom.sys.type}>(sys.id=${entityToResolveFieldsFrom.sys.id}).fields[${field}] as field value is not defined`,
1950
+ };
1951
+ }
1952
+ else if (isLink(fieldValue)) {
1953
+ const entity = this.getEntityFromLink(fieldValue);
1954
+ if (entity === undefined) {
1955
+ return {
1956
+ resolvedFieldset,
1957
+ isFullyResolved: false,
1958
+ reason: `Field reference Link (sys.id=${fieldValue.sys.id}) not found in the EntityStore, waiting...`,
1959
+ };
1960
+ }
1961
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1962
+ entityToResolveFieldsFrom = entity; // we move up
1963
+ }
1964
+ else if (isAsset(fieldValue) || isEntry(fieldValue)) {
1965
+ resolvedFieldset.push([entityToResolveFieldsFrom, field, _localeQualifier]);
1966
+ entityToResolveFieldsFrom = fieldValue; // we move up
1967
+ }
1968
+ else {
1969
+ return {
1970
+ resolvedFieldset,
1971
+ isFullyResolved: false,
1972
+ reason: `Deep path points to an invalid field value of type '${typeof fieldValue}' (value=${fieldValue})`,
1973
+ };
1974
+ }
1975
+ }
1976
+ return {
1977
+ resolvedFieldset,
1978
+ isFullyResolved: true,
1979
+ };
1980
+ };
1981
+ const headEntity = isLink(linkOrEntryOrAsset)
1982
+ ? this.getEntityFromLink(linkOrEntryOrAsset)
1983
+ : linkOrEntryOrAsset;
1984
+ if (undefined === headEntity) {
1985
+ return;
1986
+ }
1987
+ const unresolvedFieldset = parseDataSourcePathIntoFieldset(path);
1988
+ // The purpose here is to take this intermediate representation of the deep-path
1989
+ // and to follow the links to the leaf-entity and field
1990
+ // in case we can't follow till the end, we should signal that there was null-reference in the path
1991
+ const { resolvedFieldset, isFullyResolved, reason } = resolveFieldset(unresolvedFieldset, headEntity);
1992
+ if (!isFullyResolved) {
1993
+ reason &&
1994
+ console.debug(`[exp-builder.sdk::EntityStoreBased::getValueDeep()] Deep path wasn't resolved till leaf node, falling back to undefined, because: ${reason}`);
1995
+ return;
1996
+ }
1997
+ const [leafEntity] = resolvedFieldset[resolvedFieldset.length - 1];
1998
+ return leafEntity;
1999
+ }
2000
+ getEntity(type, id) {
2001
+ if (type === 'Asset') {
2002
+ return this.assetMap.get(id);
2003
+ }
2004
+ return this.entryMap.get(id);
2005
+ }
2006
+ toJSON() {
2007
+ return {
2008
+ entryMap: Object.fromEntries(this.entryMap),
2009
+ assetMap: Object.fromEntries(this.assetMap),
2010
+ locale: this.locale,
2011
+ };
2012
+ }
2013
+ }
2014
+
2015
+ class UninitializedEntityStore extends EntityStoreBase {
2016
+ constructor() {
2017
+ super({ entities: [], locale: 'uninitialized-locale-in-uninitialized-entity-store' });
2018
+ }
2019
+ }
2020
+
2021
+ create((set, get) => ({
2022
+ // The UninitializedEntityStore is a placeholder instance and is here to highlight the
2023
+ // // fact that it's not used by anything until during loading lifecycle it'sreplaced by real entity store:
2024
+ // - in Preview+Delivery mode: right after we fetch Expereince and it entities
2025
+ // - in EDITOR (VisualEditor) mode: right after the VisualEditor is async imported and initialize event happens
2026
+ entityStore: new UninitializedEntityStore(),
2027
+ areEntitiesFetched: false,
2028
+ setEntitiesFetched(fetched) {
2029
+ set({ areEntitiesFetched: fetched });
2030
+ },
2031
+ resolveAssetById(assetId) {
2032
+ if (!assetId)
2033
+ return undefined;
2034
+ const { entityStore } = get();
2035
+ return entityStore.getAssetById(assetId);
2036
+ },
2037
+ resolveEntryById(entryId) {
2038
+ if (!entryId)
2039
+ return undefined;
2040
+ const { entityStore } = get();
2041
+ return entityStore.getEntryById(entryId);
2042
+ },
2043
+ resolveEntity(link) {
2044
+ if (!link)
2045
+ return undefined;
2046
+ const { entityStore } = get();
2047
+ return entityStore.getEntityFromLink(link);
2048
+ },
2049
+ resetEntityStore(entityStore) {
2050
+ set({
2051
+ entityStore,
2052
+ areEntitiesFetched: false,
2053
+ });
2054
+ },
2055
+ }));
2056
+
1637
2057
  var VisualEditorMode;
1638
2058
  (function (VisualEditorMode) {
1639
2059
  VisualEditorMode["LazyLoad"] = "lazyLoad";