Skip to content

Commit

Permalink
fix(app-headless-cms): add parent field context (#4048)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pavel910 authored Mar 19, 2024
1 parent 8b8bc42 commit 764c931
Show file tree
Hide file tree
Showing 31 changed files with 532 additions and 286 deletions.
2 changes: 1 addition & 1 deletion apps/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Admin } from "@webiny/app-serverless-cms";
import { Cognito } from "@webiny/app-admin-users-cognito";
import "./App.scss";

export const App = () => {
export const App: React.FC = () => {
return (
<Admin>
<Cognito />
Expand Down
1 change: 1 addition & 0 deletions packages/api-headless-cms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ export interface CmsDynamicZoneTemplate {
fields: CmsModelField[];
layout: string[][];
validation: CmsModelFieldValidation[];
tags?: string[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { RenderFieldElement, ModelProvider } from "@webiny/app-headless-cms";
import { FieldElement, ModelProvider } from "@webiny/app-headless-cms";
import { Bind, BindPrefix } from "@webiny/form";
import { Cell } from "@webiny/ui/Grid";
import { FieldDTO, OperatorType } from "~/components/BulkActions/ActionEdit/domain";
Expand Down Expand Up @@ -28,11 +28,7 @@ export const FieldRenderer = (props: FieldRendererProps) => {
<BindPrefix name={props.name}>{customFieldRenderer.element}</BindPrefix>
) : (
<BindPrefix name={props.name + ".extensions"}>
<RenderFieldElement
field={props.field.raw}
Bind={Bind as any}
contentModel={fileModel}
/>
<FieldElement field={props.field.raw} Bind={Bind as any} contentModel={fileModel} />
</BindPrefix>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
GenericComponent,
Decorator
} from "@webiny/app-admin";
import { RenderFieldElement } from "@webiny/app-headless-cms";
import { FieldElement } from "@webiny/app-headless-cms";

export type FieldProps = React.ComponentProps<typeof RenderFieldElement>;
export type FieldProps = React.ComponentProps<typeof FieldElement>;

const shouldDecorate = (decoratorProps: FieldDecoratorProps, componentProps: FieldProps) => {
const { id } = decoratorProps;
Expand Down Expand Up @@ -36,7 +36,7 @@ export const createScopedFieldDecorator =

return (
<CompositionScope name={scope}>
<Compose component={RenderFieldElement} with={conditionalDecorator} />
<Compose component={FieldElement} with={conditionalDecorator} />
</CompositionScope>
);
};
Expand Down
3 changes: 2 additions & 1 deletion packages/app-headless-cms-common/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export interface CmsDynamicZoneTemplate {
fields: CmsModelField[];
layout: string[][];
validation: CmsModelFieldValidator[];
tags?: string[];
}

export type CmsContentEntryStatusType = "draft" | "published" | "unpublished";
Expand Down Expand Up @@ -562,7 +563,7 @@ interface BindComponentProps<T = any, F = any>
}

export type BindComponent<T = any, F = any> = React.ComponentType<BindComponentProps<T, F>> & {
parentName?: string;
parentName: string;
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef } from "react";
import { RenderFieldElement } from "./RenderFieldElement";
import { FieldElement } from "./FieldElement";
import styled from "@emotion/styled";
import { Form } from "@webiny/form";
import { FormAPI, FormRenderPropParams } from "@webiny/form/types";
Expand Down Expand Up @@ -85,7 +85,7 @@ export const ContentEntryForm = ({ onForm, ...props }: ContentEntryFormProps) =>
(formRenderProps: FormRenderPropParams) => {
const fields = model.fields.reduce((acc, field) => {
acc[field.fieldId] = (
<RenderFieldElement
<FieldElement
field={field}
/**
* TODO @ts-refactor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React, { useCallback } from "react";
import styled from "@emotion/styled";
import { Form, FormRenderPropParams } from "@webiny/form";
import { plugins } from "@webiny/plugins";
import { RenderFieldElement } from "./RenderFieldElement";
import { FieldElement } from "./FieldElement";
import { CmsContentFormRendererPlugin, CmsEditorContentModel } from "~/types";
import { Fields } from "~/admin/components/ContentEntryForm/Fields";
import { ModelProvider } from "~/admin/components/ModelProvider";

const FormWrapper = styled("div")({
height: "calc(100vh - 260px)",
Expand All @@ -26,7 +27,7 @@ export const ContentEntryFormPreview = (props: ContentEntryFormPreviewProps) =>
(formRenderProps: FormRenderPropParams) => {
const fields = contentModel.fields.reduce((acc, field) => {
acc[field.fieldId] = (
<RenderFieldElement
<FieldElement
field={field}
/**
* TODO @ts-refactor
Expand Down Expand Up @@ -61,24 +62,26 @@ export const ContentEntryFormPreview = (props: ContentEntryFormPreviewProps) =>
return (
<Form>
{formProps => (
<FormWrapper data-testid={"cms-content-form"}>
{formRenderer ? (
renderCustomLayout(formProps)
) : (
<Fields
contentModel={contentModel}
fields={contentModel.fields}
layout={contentModel.layout || []}
{...formProps}
/**
* TODO @ts-refactor
* Figure out type for Bind.
*/
// @ts-expect-error
Bind={formProps.Bind}
/>
)}
</FormWrapper>
<ModelProvider model={contentModel}>
<FormWrapper data-testid={"cms-content-form"}>
{formRenderer ? (
renderCustomLayout(formProps)
) : (
<Fields
contentModel={contentModel}
fields={contentModel.fields}
layout={contentModel.layout || []}
{...formProps}
/**
* TODO @ts-refactor
* Figure out type for Bind.
*/
// @ts-expect-error
Bind={formProps.Bind}
/>
)}
</FormWrapper>
</ModelProvider>
)}
</Form>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react";
import get from "lodash/get";
import { makeDecoratable } from "@webiny/app-admin";
import { i18n } from "@webiny/app/i18n";
import { CmsModelField, CmsEditorContentModel, BindComponent } from "~/types";
import Label from "./Label";
import { useBind } from "./useBind";
import { useRenderPlugins } from "./useRenderPlugins";
import { ModelFieldProvider } from "../ModelFieldProvider";

const t = i18n.ns("app-headless-cms/admin/components/content-form");

export interface FieldElementProps {
field: CmsModelField;
Bind: BindComponent;
contentModel: CmsEditorContentModel;
}

export const FieldElement = makeDecoratable("FieldElement", (props: FieldElementProps) => {
const renderPlugins = useRenderPlugins();
const { field, Bind, contentModel } = props;
const getBind = useBind({ Bind, field });

if (typeof field.renderer === "function") {
return (
<ModelFieldProvider field={field}>
{field.renderer({ field, getBind, Label, contentModel })}
</ModelFieldProvider>
);
}

const renderPlugin = renderPlugins.find(
plugin => plugin.renderer.rendererName === get(field, "renderer.name")
);

if (!renderPlugin) {
return t`Cannot render "{fieldName}" field - field renderer missing.`({
fieldName: <strong>{field.fieldId}</strong>
});
}

return (
<ModelFieldProvider field={field}>
{renderPlugin.renderer.render({ field, getBind, Label, contentModel })}
</ModelFieldProvider>
);
});

/**
* @deprecated Use `FieldElement` instead.
*/
export const RenderFieldElement = FieldElement;

/**
* @deprecated Use `FieldElementProps` instead.
*/
export type RenderFieldElementProps = FieldElementProps;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import { Cell, Grid } from "@webiny/ui/Grid";
import { RenderFieldElement } from "./RenderFieldElement";
import { FieldElement } from "./FieldElement";
import {
CmsEditorContentModel,
CmsModelField,
Expand All @@ -27,7 +27,7 @@ export const Fields = ({ Bind, fields, layout, contentModel, gridClassName }: Fi
<React.Fragment key={rowIndex}>
{row.map(fieldId => (
<Cell span={Math.floor(12 / row.length)} key={fieldId}>
<RenderFieldElement
<FieldElement
field={getFieldById(fields, fieldId) as CmsModelField}
Bind={Bind}
contentModel={contentModel}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from "react";
import get from "lodash/get";
import { CmsModelField } from "@webiny/app-headless-cms-common/types";
import { useModelField } from "~/admin/components/ModelFieldProvider";
import { useForm, FormAPI } from "@webiny/form";

declare global {
// eslint-disable-next-line
namespace JSX {
interface IntrinsicElements {
"hcms-parent-field-provider": React.HTMLProps<HTMLDivElement>;
}
}
}

interface ParentField {
value: any;
setValue: (fieldId: string, cb: (prevValue: any) => any) => void;
field: CmsModelField;
getParentField(level: number): ParentField | undefined;
path: string;
}

const ParentField = createContext<ParentField | undefined>(undefined);

export function useParentField(level = 0): ParentField | undefined {
const parent = useContext(ParentField);

if (!parent) {
return undefined;
}

return level === 0 ? parent : parent.getParentField(level);
}

interface ParentFieldProviderProps {
value: any;
path: string;
children: React.ReactNode;
}

export const ParentFieldProvider = ({ path, value, children }: ParentFieldProviderProps) => {
const parent = useContext(ParentField);
const form = useForm();
const formRef = useRef<FormAPI>();

let field: CmsModelField | undefined;
try {
const fieldContext = useModelField();
field = fieldContext.field;
} catch {
field = undefined;
}

const getParentField = (level = 0) => {
return parent ? (level === 0 ? parent : parent.getParentField(level - 1)) : undefined;
};

useEffect(() => {
formRef.current = form;
}, [form.data]);

const setValue = useCallback<ParentField["setValue"]>((fieldId, cb) => {
const fieldPath = `${path}.${fieldId}`;
if (!path || !formRef.current) {
return;
}

formRef.current.setValue(fieldPath, cb(get(formRef.current.data, fieldPath)));
}, []);

const context: ParentField | undefined = field
? {
value,
field,
getParentField,
path,
setValue
}
: undefined;

return (
<hcms-parent-field-provider data-path={path} data-field-type={field?.type}>
<ParentField.Provider value={context}>{children}</ParentField.Provider>
</hcms-parent-field-provider>
);
};

This file was deleted.

Loading

0 comments on commit 764c931

Please sign in to comment.