@contentful/experiences-components-react 1.42.4-prerelease-20250702T1207-37b3bfc.0 → 2.0.0-beta.1

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