Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove 'use client' directive from ExperienceRoot [ALT-1428] #834

Draft
wants to merge 7 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions packages/core/src/utils/getNodeProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { EntityStore } from '@/entity';
import { ComponentRegistration, ResolveDesignValueType } from '@/types';
import { ComponentTreeNode, DesignValue, PrimitiveValue } from '@contentful/experiences-validators';
import { transformBoundContentValue } from './transformers';
import { Entry, UnresolvedLink } from 'contentful';
import { resolveHyperlinkPattern } from './resolveHyperlinkPattern';
import { HYPERLINK_DEFAULT_PATTERN } from '@/constants';

interface GetNodePropsParams {
node: ComponentTreeNode;
componentRegistration: ComponentRegistration | undefined;
isAssembly: boolean;
getPatternChildNodeClassName?: (childNodeId: string) => string | undefined;
entityStore: EntityStore;
locale: string;
hyperlinkPattern: string | undefined;
resolveDesignValue: ResolveDesignValueType;
}

export const getNodeProps = ({
node,
componentRegistration,
isAssembly,
getPatternChildNodeClassName,
entityStore,
locale,
hyperlinkPattern,
resolveDesignValue,
}: GetNodePropsParams) => {
// Don't enrich the assembly wrapper node with props
if (!componentRegistration || isAssembly) {
return {
cfSsrClassName: node.variables.cfSsrClassName
? resolveDesignValue(
(node.variables.cfSsrClassName as DesignValue).valuesByBreakpoint,
'cfSsrClassName',
)
: undefined,
};
}

const propMap: Record<string, PrimitiveValue> = {
// @ts-expect-error -- node id is being generated in ssrStyles.ts, currently missing ComponentTreeNode type
cfSsrClassName: node.id
? // @ts-expect-error -- node id is being generated in ssrStyles.ts, currently missing ComponentTreeNode type
getPatternChildNodeClassName?.(node.id)
: node.variables.cfSsrClassName
? resolveDesignValue(
(node.variables.cfSsrClassName as DesignValue).valuesByBreakpoint,
'cfSsrClassName',
)
: undefined,
};

const props = Object.entries(componentRegistration.definition.variables).reduce(
(acc, [variableName, variableDefinition]) => {
const variable = node.variables[variableName];
if (!variable) return acc;

switch (variable.type) {
case 'DesignValue':
acc[variableName] = resolveDesignValue(variable.valuesByBreakpoint, variableName);
break;
case 'BoundValue': {
const [, uuid] = variable.path.split('/');
const binding = entityStore.dataSource[uuid] as UnresolvedLink<'Entry' | 'Asset'>;

const value = transformBoundContentValue(
node.variables,
entityStore,
binding,
resolveDesignValue,
variableName,
variableDefinition,
variable.path,
);
acc[variableName] = value ?? variableDefinition.defaultValue;
break;
}

case 'HyperlinkValue': {
const binding = entityStore.dataSource[variable.linkTargetKey];
const hyperlinkEntry = entityStore.getEntryOrAsset(binding, variable.linkTargetKey);

const value = resolveHyperlinkPattern(
componentRegistration.definition.hyperlinkPattern ||
hyperlinkPattern ||
HYPERLINK_DEFAULT_PATTERN,
hyperlinkEntry as Entry,
locale,
);
if (value) {
acc[variableName] = value;
}
break;
}
case 'UnboundValue': {
const uuid = variable.key;
acc[variableName] =
entityStore.unboundValues[uuid]?.value ?? variableDefinition.defaultValue;
break;
}
case 'ComponentValue':
// We're rendering a pattern entry. Content cannot be set for ComponentValue type properties
// directly in the pattern so we can safely use the default value
// This can either a design (style) or a content variable
acc[variableName] = variableDefinition.defaultValue;
break;
default:
break;
}
return acc;
},
propMap,
);

return props;
};
1 change: 1 addition & 0 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './isLink';
export * from './pathSchema';
export * from './resolveHyperlinkPattern';
export * from './sanitizeNodeProps';
export * from './getNodeProps';
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use client';
import React from 'react';
import { validateExperienceBuilderConfig } from '@contentful/experiences-core';
import { EntityStore } from '@contentful/experiences-core';
import type { Experience } from '@contentful/experiences-core/types';
import { useDetectCanvasMode } from './hooks/useDetectCanvasMode';
import { StudioCanvasMode } from '@contentful/experiences-core/constants';

type ExperienceRootProps = {
experience?: Experience<EntityStore> | string | null;
locale: string;
editorRoot: JSX.Element | null;
deliveryRoot: JSX.Element | null;
};

export const ExperienceCanvasDetection: React.FC<ExperienceRootProps> = ({
locale,
editorRoot,
deliveryRoot,
}) => {
const mode = useDetectCanvasMode();

validateExperienceBuilderConfig({
locale,
mode,
});

if (mode === StudioCanvasMode.EDITOR) {
return editorRoot;
}

if (mode === StudioCanvasMode.READ_ONLY) {
return editorRoot;
}

return deliveryRoot;
};
49 changes: 18 additions & 31 deletions packages/experience-builder-sdk/src/ExperienceRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,37 @@
'use client';
import React from 'react';
import {
VisualEditorMode,
createExperience,
validateExperienceBuilderConfig,
} from '@contentful/experiences-core';
import { createExperience, VisualEditorMode } from '@contentful/experiences-core';
import { EntityStore } from '@contentful/experiences-core';
import type { Experience } from '@contentful/experiences-core/types';
import { PreviewDeliveryRoot } from './blocks/preview/PreviewDeliveryRoot';
import { ExperienceCanvasDetection } from './ExperienceCanvasDetection';
import VisualEditorClientInitialization from './blocks/editor/VisualEditorClientInitialization';
import VisualEditorRoot from './blocks/editor/VisualEditorRoot';
import { useDetectCanvasMode } from './hooks/useDetectCanvasMode';
import { StudioCanvasMode } from '@contentful/experiences-core/constants';

type ExperienceRootProps = {
experience?: Experience<EntityStore> | string | null;
locale: string;
visualEditorMode?: VisualEditorMode;
};

export const ExperienceRoot = ({
locale,
experience,
visualEditorMode = VisualEditorMode.LazyLoad,
}: ExperienceRootProps) => {
const mode = useDetectCanvasMode();

//If experience is passed in as a JSON string, recreate it to an experience object
export const ExperienceRoot = ({ locale, experience }: ExperienceRootProps) => {
const experienceObject =
typeof experience === 'string' ? createExperience(experience) : experience;

validateExperienceBuilderConfig({
locale,
mode,
});
const initialEntities = experienceObject?.entityStore?.entities || [];

if (mode === StudioCanvasMode.EDITOR || mode === StudioCanvasMode.READ_ONLY) {
return (
<VisualEditorRoot
experience={experienceObject as Experience<EntityStore> | undefined}
visualEditorMode={visualEditorMode}
initialLocale={locale}
/>
);
}
const editorRoot = (
<VisualEditorClientInitialization initialEntities={initialEntities} initialLocale={locale}>
<VisualEditorRoot experience={experience} />
</VisualEditorClientInitialization>
);

if (!experienceObject) return null;
const deliveryRoot = <PreviewDeliveryRoot locale={locale} experience={experience} />;

return <PreviewDeliveryRoot locale={locale} experience={experienceObject} />;
return (
<ExperienceCanvasDetection
locale={locale}
editorRoot={editorRoot}
deliveryRoot={deliveryRoot}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';
import React, { Suspense } from 'react';
import { useInitializeVisualEditor } from '../../hooks/useInitializeVisualEditor';
import { Entry, Asset, ChainModifiers } from 'contentful';
import { ErrorBoundary } from '../../components/ErrorBoundary';

type VisualEditorClientInitializationProps = {
initialLocale: string;
initialEntities: (Entry | Asset<ChainModifiers, string>)[];
children: JSX.Element;
};

export const VisualEditorClientInitialization: React.FC<VisualEditorClientInitializationProps> = ({
initialEntities,
initialLocale,
children,
}) => {
useInitializeVisualEditor({
initialLocale,
initialEntities,
});

return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</ErrorBoundary>
);
};

export default VisualEditorClientInitialization;

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,33 +1,24 @@
'use client';
// Need 'use client' for now since bundling of visual editor pkg and lazy loading
// it in results in the bundle having client code mixed into it
// todo: update build config of visual editor to work with server components

import React, { Suspense } from 'react';
import { EntityStore, VisualEditorMode } from '@contentful/experiences-core';
import { EntityStore } from '@contentful/experiences-core';
import { ErrorBoundary } from '../../components/ErrorBoundary';
import { useInitializeVisualEditor } from '../../hooks/useInitializeVisualEditor';
import type { Experience } from '@contentful/experiences-core/types';

const VisualEditorLoader = React.lazy(() => import('./VisualEditorLoader'));
const VisualEditorLoader = React.lazy(() => import('@contentful/experiences-visual-editor-react'));

type VisualEditorRootProps = {
visualEditorMode: VisualEditorMode;
experience?: Experience<EntityStore>;
initialLocale: string;
experience?: Experience<EntityStore> | string | null;
};

export const VisualEditorRoot: React.FC<VisualEditorRootProps> = ({
visualEditorMode,
experience,
initialLocale,
}) => {
const initialEntities = experience?.entityStore?.entities || [];

useInitializeVisualEditor({
initialLocale,
initialEntities,
});

export const VisualEditorRoot: React.FC<VisualEditorRootProps> = ({ experience }) => {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<VisualEditorLoader experience={experience} visualEditorMode={visualEditorMode} />
<VisualEditorLoader experienceObject={experience} />
</Suspense>
</ErrorBoundary>
);
Expand Down
Loading