feat: show hidden fields in history frontend (#20201)

* chore: add configuration to history context

* feat: show fields that aren't in the layout in history

* chore: add renderLayout prop to ComponentInput

* feat: render remaining fields in components

* fix: types

* chore: refactor to composition api

* chore: move renderInput to children

* fix: repeatable components index

* fix: repeatable components toggling together

* chore: move ComponentLayout

* fix: generate temp keys for history values

* chore: delete ComponentLayout

* fix: components with no hidden fields

* fix: add comments

* chore: add comment
This commit is contained in:
Rémi de Juvigny 2024-04-30 17:53:27 +02:00 committed by GitHub
parent 4a26739ee0
commit 9d4475b11a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 245 additions and 99 deletions

View File

@ -10,15 +10,102 @@ import {
GridItem, GridItem,
Typography, Typography,
} from '@strapi/design-system'; } from '@strapi/design-system';
import { Schema } from '@strapi/types';
import pipe from 'lodash/fp/pipe';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useDoc } from '../../hooks/useDocument';
import { useTypedSelector } from '../../modules/hooks'; import { useTypedSelector } from '../../modules/hooks';
import { useHistoryContext } from '../pages/History'; import {
prepareTempKeys,
removeFieldsThatDontExistOnSchema,
} from '../../pages/EditView/utils/data';
import { HistoryContextValue, useHistoryContext } from '../pages/History';
import { VersionInputRenderer } from './VersionInputRenderer'; import { VersionInputRenderer } from './VersionInputRenderer';
import type { Metadatas } from '../../../../shared/contracts/content-types';
import type { GetInitData } from '../../../../shared/contracts/init';
import type { ComponentsDictionary, Document } from '../../hooks/useDocument';
import type { EditFieldLayout } from '../../hooks/useDocumentLayout'; import type { EditFieldLayout } from '../../hooks/useDocumentLayout';
const createLayoutFromFields = <T extends EditFieldLayout | UnknownField>(fields: T[]) => {
return (
fields
.reduce<Array<T[]>>((rows, field) => {
if (field.type === 'dynamiczone') {
// Dynamic zones take up all the columns in a row
rows.push([field]);
return rows;
}
if (!rows[rows.length - 1]) {
// Create a new row if there isn't one available
rows.push([]);
}
// Push fields to the current row, they wrap and handle their own column size
rows[rows.length - 1].push(field);
return rows;
}, [])
// Map the rows to panels
.map((row) => [row])
);
};
/* -------------------------------------------------------------------------------------------------
* getRemainingFieldsLayout
* -----------------------------------------------------------------------------------------------*/
interface GetRemainingFieldsLayoutOptions
extends Pick<HistoryContextValue, 'layout'>,
Pick<GetInitData.Response['data'], 'fieldSizes'> {
schemaAttributes: HistoryContextValue['schema']['attributes'];
metadatas: Metadatas;
}
/**
* Build a layout for the fields that are were deleted from the edit view layout
* via the configure the view page. This layout will be merged with the main one.
* Those fields would be restored if the user restores the history version, which is why it's
* important to show them, even if they're not in the normal layout.
*/
function getRemaingFieldsLayout({
layout,
metadatas,
schemaAttributes,
fieldSizes,
}: GetRemainingFieldsLayoutOptions) {
const fieldsInLayout = layout.flatMap((panel) =>
panel.flatMap((row) => row.flatMap((field) => field.name))
);
const remainingFields = Object.entries(metadatas).reduce<EditFieldLayout[]>(
(currentRemainingFields, [name, field]) => {
// Make sure we do not fields that are not visible, e.g. "id"
if (!fieldsInLayout.includes(name) && field.edit.visible === true) {
const attribute = schemaAttributes[name];
// @ts-expect-error not sure why attribute causes type error
currentRemainingFields.push({
attribute,
type: attribute.type,
visible: true,
disabled: true,
label: field.edit.label || name,
name: name,
size: fieldSizes[attribute.type].default ?? 12,
});
}
return currentRemainingFields;
},
[]
);
return createLayoutFromFields(remainingFields);
}
/* ------------------------------------------------------------------------------------------------- /* -------------------------------------------------------------------------------------------------
* FormPanel * FormPanel
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
@ -70,14 +157,16 @@ const FormPanel = ({ panel }: { panel: EditFieldLayout[][] }) => {
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
type UnknownField = EditFieldLayout & { shouldIgnoreRBAC: boolean }; type UnknownField = EditFieldLayout & { shouldIgnoreRBAC: boolean };
const VersionContent = () => { const VersionContent = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { fieldSizes } = useTypedSelector((state) => state['content-manager'].app); const { fieldSizes } = useTypedSelector((state) => state['content-manager'].app);
const { version, layout } = useHistoryContext('VersionContent', (state) => ({ const version = useHistoryContext('VersionContent', (state) => state.selectedVersion);
version: state.selectedVersion, const layout = useHistoryContext('VersionContent', (state) => state.layout);
layout: state.layout, const configuration = useHistoryContext('VersionContent', (state) => state.configuration);
})); const schema = useHistoryContext('VersionContent', (state) => state.schema);
// Build a layout for the unknown fields section
const removedAttributes = version.meta.unknownAttributes.removed; const removedAttributes = version.meta.unknownAttributes.removed;
const removedAttributesAsFields = Object.entries(removedAttributes).map( const removedAttributesAsFields = Object.entries(removedAttributes).map(
([attributeName, attribute]) => { ([attributeName, attribute]) => {
@ -95,34 +184,43 @@ const VersionContent = () => {
return field; return field;
} }
); );
const unknownFieldsLayout = removedAttributesAsFields const unknownFieldsLayout = createLayoutFromFields(removedAttributesAsFields);
.reduce<Array<UnknownField[]>>((rows, field) => {
if (field.type === 'dynamiczone') {
// Dynamic zones take up all the columns in a row
rows.push([field]);
return rows; // Build a layout for the fields that are were deleted from the layout
} const remainingFieldsLayout = getRemaingFieldsLayout({
metadatas: configuration.contentType.metadatas,
layout,
schemaAttributes: schema.attributes,
fieldSizes,
});
if (!rows[rows.length - 1]) { const { components } = useDoc();
// Create a new row if there isn't one available
rows.push([]);
}
// Push fields to the current row, they wrap and handle their own column size /**
rows[rows.length - 1].push(field); * Transform the data before passing it to the form so that each field
* has a uniquely generated key
*/
const transformedData = React.useMemo(() => {
const transform =
(schemaAttributes: Schema.Attributes, components: ComponentsDictionary = {}) =>
(document: Omit<Document, 'id'>) => {
const schema = { attributes: schemaAttributes };
const transformations = pipe(
removeFieldsThatDontExistOnSchema(schema),
prepareTempKeys(schema, components)
);
return transformations(document);
};
return rows; return transform(version.schema, components)(version.data);
}, []) }, [components, version.data, version.schema]);
// Map the rows to panels
.map((row) => [row]);
return ( return (
<ContentLayout> <ContentLayout>
<Box paddingBottom={8}> <Box paddingBottom={8}>
<Form disabled={true} method="PUT" initialValues={version.data}> <Form disabled={true} method="PUT" initialValues={transformedData}>
<Flex direction="column" alignItems="stretch" gap={6} position="relative"> <Flex direction="column" alignItems="stretch" gap={6} position="relative">
{layout.map((panel, index) => { {[...layout, ...remainingFieldsLayout].map((panel, index) => {
return <FormPanel key={index} panel={panel} />; return <FormPanel key={index} panel={panel} />;
})} })}
</Flex> </Flex>
@ -170,4 +268,4 @@ const VersionContent = () => {
); );
}; };
export { VersionContent }; export { VersionContent, getRemaingFieldsLayout };

View File

@ -15,7 +15,9 @@ import styled from 'styled-components';
import { COLLECTION_TYPES } from '../../constants/collections'; import { COLLECTION_TYPES } from '../../constants/collections';
import { useDocumentRBAC } from '../../features/DocumentRBAC'; import { useDocumentRBAC } from '../../features/DocumentRBAC';
import { useDoc } from '../../hooks/useDocument'; import { useDoc } from '../../hooks/useDocument';
import { useDocLayout } from '../../hooks/useDocumentLayout';
import { useLazyComponents } from '../../hooks/useLazyComponents'; import { useLazyComponents } from '../../hooks/useLazyComponents';
import { useTypedSelector } from '../../modules/hooks';
import { DocumentStatus } from '../../pages/EditView/components/DocumentStatus'; import { DocumentStatus } from '../../pages/EditView/components/DocumentStatus';
import { BlocksInput } from '../../pages/EditView/components/FormInputs/BlocksInput/BlocksInput'; import { BlocksInput } from '../../pages/EditView/components/FormInputs/BlocksInput/BlocksInput';
import { ComponentInput } from '../../pages/EditView/components/FormInputs/Component/Input'; import { ComponentInput } from '../../pages/EditView/components/FormInputs/Component/Input';
@ -30,6 +32,8 @@ import { useFieldHint } from '../../pages/EditView/components/InputRenderer';
import { getRelationLabel } from '../../utils/relations'; import { getRelationLabel } from '../../utils/relations';
import { useHistoryContext } from '../pages/History'; import { useHistoryContext } from '../pages/History';
import { getRemaingFieldsLayout } from './VersionContent';
import type { EditFieldLayout } from '../../hooks/useDocumentLayout'; import type { EditFieldLayout } from '../../hooks/useDocumentLayout';
import type { RelationsFieldProps } from '../../pages/EditView/components/FormInputs/Relations'; import type { RelationsFieldProps } from '../../pages/EditView/components/FormInputs/Relations';
import type { RelationResult } from '../../services/relations'; import type { RelationResult } from '../../services/relations';
@ -233,10 +237,11 @@ const VersionInputRenderer = ({
...props ...props
}: VersionInputRendererProps) => { }: VersionInputRendererProps) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { version } = useHistoryContext('VersionContent', (state) => ({ const version = useHistoryContext('VersionContent', (state) => state.selectedVersion);
version: state.selectedVersion, const configuration = useHistoryContext('VersionContent', (state) => state.configuration);
})); const fieldSizes = useTypedSelector((state) => state['content-manager'].app.fieldSizes);
const { id } = useDoc();
const { id, components } = useDoc();
const isFormDisabled = useForm('InputRenderer', (state) => state.disabled); const isFormDisabled = useForm('InputRenderer', (state) => state.disabled);
const isInDynamicZone = useDynamicZone('isInDynamicZone', (state) => state.isInDynamicZone); const isInDynamicZone = useDynamicZone('isInDynamicZone', (state) => state.isInDynamicZone);
@ -261,6 +266,9 @@ const VersionInputRenderer = ({
); );
const hint = useFieldHint(providedHint, props.attribute); const hint = useFieldHint(providedHint, props.attribute);
const {
edit: { components: componentsLayout },
} = useDocLayout();
if (!visible) { if (!visible) {
return null; return null;
@ -353,13 +361,24 @@ const VersionInputRenderer = ({
case 'blocks': case 'blocks':
return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />; return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />;
case 'component': case 'component':
const { layout } = componentsLayout[props.attribute.component];
// Components can only have one panel, so only save the first layout item
const [remainingFieldsLayout] = getRemaingFieldsLayout({
layout: [layout],
metadatas: configuration.components[props.attribute.component].metadatas,
fieldSizes,
schemaAttributes: components[props.attribute.component].attributes,
});
return ( return (
<ComponentInput <ComponentInput
{...props} {...props}
layout={[...layout, ...(remainingFieldsLayout || [])]}
hint={hint} hint={hint}
disabled={fieldIsDisabled} disabled={fieldIsDisabled}
renderInput={(props) => <VersionInputRenderer {...props} shouldIgnoreRBAC={true} />} >
/> {(inputProps) => <VersionInputRenderer {...inputProps} shouldIgnoreRBAC={true} />}
</ComponentInput>
); );
case 'dynamiczone': case 'dynamiczone':
return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />; return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />;

View File

@ -11,13 +11,17 @@ import { PERMISSIONS } from '../../constants/plugin';
import { DocumentRBAC } from '../../features/DocumentRBAC'; import { DocumentRBAC } from '../../features/DocumentRBAC';
import { useDocument } from '../../hooks/useDocument'; import { useDocument } from '../../hooks/useDocument';
import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout'; import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
import { useGetContentTypeConfigurationQuery } from '../../services/contentTypes';
import { buildValidParams } from '../../utils/api'; import { buildValidParams } from '../../utils/api';
import { VersionContent } from '../components/VersionContent'; import { VersionContent } from '../components/VersionContent';
import { VersionHeader } from '../components/VersionHeader'; import { VersionHeader } from '../components/VersionHeader';
import { VersionsList } from '../components/VersionsList'; import { VersionsList } from '../components/VersionsList';
import { useGetHistoryVersionsQuery } from '../services/historyVersion'; import { useGetHistoryVersionsQuery } from '../services/historyVersion';
import type { ContentType } from '../../../../shared/contracts/content-types'; import type {
ContentType,
FindContentTypeConfiguration,
} from '../../../../shared/contracts/content-types';
import type { import type {
HistoryVersionDataResponse, HistoryVersionDataResponse,
GetHistoryVersions, GetHistoryVersions,
@ -32,6 +36,7 @@ interface HistoryContextValue {
contentType: UID.ContentType; contentType: UID.ContentType;
id?: string; // null for single types id?: string; // null for single types
layout: EditLayout['layout']; layout: EditLayout['layout'];
configuration: FindContentTypeConfiguration.Response['data'];
selectedVersion: HistoryVersionDataResponse; selectedVersion: HistoryVersionDataResponse;
// Errors are handled outside of the provider, so we exclude errors from the response type // Errors are handled outside of the provider, so we exclude errors from the response type
versions: Extract<GetHistoryVersions.Response, { data: Array<HistoryVersionDataResponse> }>; versions: Extract<GetHistoryVersions.Response, { data: Array<HistoryVersionDataResponse> }>;
@ -71,6 +76,8 @@ const HistoryPage = () => {
settings: { displayName, mainField }, settings: { displayName, mainField },
}, },
} = useDocumentLayout(slug!); } = useDocumentLayout(slug!);
const { data: configuration, isLoading: isLoadingConfiguration } =
useGetContentTypeConfigurationQuery(slug!);
// Parse state from query params // Parse state from query params
const [{ query }] = useQueryParams<{ const [{ query }] = useQueryParams<{
@ -114,7 +121,13 @@ const HistoryPage = () => {
return <Navigate to="/content-manager" />; return <Navigate to="/content-manager" />;
} }
if (isLoadingDocument || isLoadingLayout || versionsResponse.isFetching || isStaleRequest) { if (
isLoadingDocument ||
isLoadingLayout ||
versionsResponse.isFetching ||
isStaleRequest ||
isLoadingConfiguration
) {
return <Page.Loading />; return <Page.Loading />;
} }
@ -141,6 +154,7 @@ const HistoryPage = () => {
!layout || !layout ||
!schema || !schema ||
!selectedVersion || !selectedVersion ||
!configuration ||
// This should not happen as it's covered by versionsResponse.isError, but we need it for TS // This should not happen as it's covered by versionsResponse.isError, but we need it for TS
versionsResponse.data.error versionsResponse.data.error
) { ) {
@ -165,6 +179,7 @@ const HistoryPage = () => {
id={documentId} id={documentId}
schema={schema} schema={schema}
layout={layout} layout={layout}
configuration={configuration}
selectedVersion={selectedVersion} selectedVersion={selectedVersion}
versions={versionsResponse.data} versions={versionsResponse.data}
page={page} page={page}

View File

@ -224,7 +224,7 @@ type LayoutData = FindContentTypeConfiguration.Response['data'];
/** /**
* @internal * @internal
* @description takes the configuration data, the schema & the components used in the schema and formats the edit view * @description takes the configuration data, the schema & the components used in the schema and formats the edit view
* vesions of the schema & components. This is then used to redner the edit view of the content-type. * versions of the schema & components. This is then used to render the edit view of the content-type.
*/ */
const formatEditLayout = ( const formatEditLayout = (
data: LayoutData, data: LayoutData,

View File

@ -10,7 +10,7 @@ import { EditFieldLayout } from '../../../../../hooks/useDocumentLayout';
import { getTranslation } from '../../../../../utils/translations'; import { getTranslation } from '../../../../../utils/translations';
import { transformDocument } from '../../../utils/data'; import { transformDocument } from '../../../utils/data';
import { createDefaultForm } from '../../../utils/forms'; import { createDefaultForm } from '../../../utils/forms';
import { InputRendererProps } from '../../InputRenderer'; import { type InputRendererProps } from '../../InputRenderer';
import { Initializer } from './Initializer'; import { Initializer } from './Initializer';
import { NonRepeatableComponent } from './NonRepeatable'; import { NonRepeatableComponent } from './NonRepeatable';
@ -20,7 +20,12 @@ interface ComponentInputProps
extends Omit<Extract<EditFieldLayout, { type: 'component' }>, 'size' | 'hint'>, extends Omit<Extract<EditFieldLayout, { type: 'component' }>, 'size' | 'hint'>,
Pick<InputProps, 'hint'> { Pick<InputProps, 'hint'> {
labelAction?: React.ReactNode; labelAction?: React.ReactNode;
renderInput?: (props: InputRendererProps) => React.ReactNode; children: (props: InputRendererProps) => React.ReactNode;
/**
* We need layout to come from the props, and not via a hook, because Content History needs
* a way to modify the normal component layout to add hidden fields.
*/
layout: EditFieldLayout[][];
} }
const ComponentInput = ({ const ComponentInput = ({
@ -90,15 +95,14 @@ const ComponentInput = ({
<Initializer disabled={disabled} name={name} onClick={handleInitialisationClick} /> <Initializer disabled={disabled} name={name} onClick={handleInitialisationClick} />
)} )}
{!attribute.repeatable && field.value ? ( {!attribute.repeatable && field.value ? (
<NonRepeatableComponent <NonRepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props}>
attribute={attribute} {props.children}
name={name} </NonRepeatableComponent>
disabled={disabled}
{...props}
/>
) : null} ) : null}
{attribute.repeatable && ( {attribute.repeatable && (
<RepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props} /> <RepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props}>
{props.children}
</RepeatableComponent>
)} )}
</Flex> </Flex>
</Box> </Box>

View File

@ -1,28 +1,20 @@
import { useField } from '@strapi/admin/strapi-admin'; import { useField } from '@strapi/admin/strapi-admin';
import { Box, Flex, Grid, GridItem } from '@strapi/design-system'; import { Box, Flex, Grid, GridItem } from '@strapi/design-system';
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
import { InputRenderer } from '../../InputRenderer';
import { ComponentProvider, useComponent } from '../ComponentContext'; import { ComponentProvider, useComponent } from '../ComponentContext';
import type { ComponentInputProps } from './Input'; import type { ComponentInputProps } from './Input';
interface NonRepeatableComponentProps extends Omit<ComponentInputProps, 'required' | 'label'> {} type NonRepeatableComponentProps = Omit<ComponentInputProps, 'required' | 'label'>;
const NonRepeatableComponent = ({ const NonRepeatableComponent = ({
attribute, attribute,
name, name,
renderInput = InputRenderer, children,
layout,
}: NonRepeatableComponentProps) => { }: NonRepeatableComponentProps) => {
const {
edit: { components },
} = useDocLayout();
const { value } = useField(name); const { value } = useField(name);
const level = useComponent('NonRepeatableComponent', (state) => state.level); const level = useComponent('NonRepeatableComponent', (state) => state.level);
const { layout } = components[attribute.component];
const isNested = level > 0; const isNested = level > 0;
return ( return (
@ -51,7 +43,7 @@ const NonRepeatableComponent = ({
return ( return (
<GridItem col={size} key={completeFieldName} s={12} xs={12}> <GridItem col={size} key={completeFieldName} s={12} xs={12}>
{renderInput({ ...field, name: completeFieldName })} {children({ ...field, name: completeFieldName })}
</GridItem> </GridItem>
); );
})} })}

View File

@ -9,12 +9,12 @@ import {
Accordion, Accordion,
AccordionContent as DSAccordionContent, AccordionContent as DSAccordionContent,
AccordionToggle, AccordionToggle,
Grid,
GridItem,
IconButton, IconButton,
Typography, Typography,
KeyboardNavigable, KeyboardNavigable,
useComposedRefs, useComposedRefs,
GridItem,
Grid,
} from '@strapi/design-system'; } from '@strapi/design-system';
import { Plus, Drag, Trash } from '@strapi/icons'; import { Plus, Drag, Trash } from '@strapi/icons';
import { getEmptyImage } from 'react-dnd-html5-backend'; import { getEmptyImage } from 'react-dnd-html5-backend';
@ -24,13 +24,11 @@ import styled from 'styled-components';
import { ItemTypes } from '../../../../../constants/dragAndDrop'; import { ItemTypes } from '../../../../../constants/dragAndDrop';
import { useDoc } from '../../../../../hooks/useDocument'; import { useDoc } from '../../../../../hooks/useDocument';
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
import { useDragAndDrop, type UseDragAndDropOptions } from '../../../../../hooks/useDragAndDrop'; import { useDragAndDrop, type UseDragAndDropOptions } from '../../../../../hooks/useDragAndDrop';
import { getIn } from '../../../../../utils/objects'; import { getIn } from '../../../../../utils/objects';
import { getTranslation } from '../../../../../utils/translations'; import { getTranslation } from '../../../../../utils/translations';
import { transformDocument } from '../../../utils/data'; import { transformDocument } from '../../../utils/data';
import { createDefaultForm } from '../../../utils/forms'; import { createDefaultForm } from '../../../utils/forms';
import { InputRenderer } from '../../InputRenderer';
import { ComponentProvider, useComponent } from '../ComponentContext'; import { ComponentProvider, useComponent } from '../ComponentContext';
import { Initializer } from './Initializer'; import { Initializer } from './Initializer';
@ -42,14 +40,15 @@ import type { Schema } from '@strapi/types';
* RepeatableComponent * RepeatableComponent
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
interface RepeatableComponentProps extends Omit<ComponentInputProps, 'label' | 'required'> {} type RepeatableComponentProps = Omit<ComponentInputProps, 'required' | 'label'>;
const RepeatableComponent = ({ const RepeatableComponent = ({
attribute, attribute,
disabled, disabled,
name, name,
mainField, mainField,
renderInput, children,
layout,
}: RepeatableComponentProps) => { }: RepeatableComponentProps) => {
const { toggleNotification } = useNotification(); const { toggleNotification } = useNotification();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -211,6 +210,7 @@ const RepeatableComponent = ({
<AccordionGroup error={error}> <AccordionGroup error={error}>
<AccordionContent aria-describedby={ariaDescriptionId}> <AccordionContent aria-describedby={ariaDescriptionId}>
{value.map(({ __temp_key__: key, id }, index) => { {value.map(({ __temp_key__: key, id }, index) => {
const nameWithIndex = `${name}.${index}`;
return ( return (
<ComponentProvider <ComponentProvider
key={key} key={key}
@ -222,12 +222,11 @@ const RepeatableComponent = ({
> >
<Component <Component
disabled={disabled} disabled={disabled}
name={`${name}.${index}`} name={nameWithIndex}
attribute={attribute} attribute={attribute}
index={index} index={index}
isOpen={collapseToOpen === key} isOpen={collapseToOpen === key}
mainField={mainField} mainField={mainField}
renderInput={renderInput}
onMoveItem={handleMoveComponentField} onMoveItem={handleMoveComponentField}
onClickToggle={handleToggle(key)} onClickToggle={handleToggle(key)}
onDeleteComponent={() => { onDeleteComponent={() => {
@ -238,7 +237,29 @@ const RepeatableComponent = ({
onCancel={handleCancel} onCancel={handleCancel}
onDropItem={handleDropItem} onDropItem={handleDropItem}
onGrabItem={handleGrabItem} onGrabItem={handleGrabItem}
/> >
{layout.map((row, index) => {
return (
<Grid gap={4} key={index}>
{row.map(({ size, ...field }) => {
/**
* Layouts are built from schemas so they don't understand the complete
* schema tree, for components we append the parent name to the field name
* because this is the structure for the data & permissions also understand
* the nesting involved.
*/
const completeFieldName = `${nameWithIndex}.${field.name}`;
return (
<GridItem col={size} key={completeFieldName} s={12} xs={12}>
{children({ ...field, name: completeFieldName })}
</GridItem>
);
})}
</Grid>
);
})}
</Component>
</ComponentProvider> </ComponentProvider>
); );
})} })}
@ -379,7 +400,7 @@ const ActionsFlex = styled(Flex)<{ expanded?: boolean }>`
interface ComponentProps interface ComponentProps
extends Pick<UseDragAndDropOptions, 'onGrabItem' | 'onDropItem' | 'onCancel' | 'onMoveItem'>, extends Pick<UseDragAndDropOptions, 'onGrabItem' | 'onDropItem' | 'onCancel' | 'onMoveItem'>,
Pick<RepeatableComponentProps, 'mainField' | 'renderInput'> { Pick<RepeatableComponentProps, 'mainField'> {
attribute: Schema.Attribute.Component<`${string}.${string}`, boolean>; attribute: Schema.Attribute.Component<`${string}.${string}`, boolean>;
disabled?: boolean; disabled?: boolean;
index: number; index: number;
@ -388,10 +409,10 @@ interface ComponentProps
onClickToggle: () => void; onClickToggle: () => void;
onDeleteComponent?: React.MouseEventHandler<HTMLButtonElement>; onDeleteComponent?: React.MouseEventHandler<HTMLButtonElement>;
toggleCollapses: () => void; toggleCollapses: () => void;
children: React.ReactNode;
} }
const Component = ({ const Component = ({
attribute,
disabled, disabled,
index, index,
isOpen, isOpen,
@ -400,18 +421,13 @@ const Component = ({
name: 'id', name: 'id',
type: 'integer', type: 'integer',
}, },
children,
onClickToggle, onClickToggle,
onDeleteComponent, onDeleteComponent,
toggleCollapses, toggleCollapses,
renderInput = InputRenderer,
...dragProps ...dragProps
}: ComponentProps) => { }: ComponentProps) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const {
edit: { components },
} = useDocLayout();
const { layout } = components[attribute.component];
const displayValue = useForm('RepeatableComponent', (state) => { const displayValue = useForm('RepeatableComponent', (state) => {
return getIn(state.values, [...name.split('.'), mainField.name]); return getIn(state.values, [...name.split('.'), mainField.name]);
@ -500,27 +516,7 @@ const Component = ({
padding={6} padding={6}
gap={6} gap={6}
> >
{layout.map((row, index) => { {children}
return (
<Grid gap={4} key={index}>
{row.map(({ size, ...field }) => {
/**
* Layouts are built from schemas so they don't understand the complete
* schema tree, for components we append the parent name to the field name
* because this is the structure for the data & permissions also understand
* the nesting involved.
*/
const completeFieldName = `${name}.${field.name}`;
return (
<GridItem col={size} key={completeFieldName} s={12} xs={12}>
{renderInput({ ...field, name: completeFieldName })}
</GridItem>
);
})}
</Grid>
);
})}
</Flex> </Flex>
</DSAccordionContent> </DSAccordionContent>
</Accordion> </Accordion>

View File

@ -9,6 +9,7 @@ import { useIntl } from 'react-intl';
import { useDocumentRBAC } from '../../../features/DocumentRBAC'; import { useDocumentRBAC } from '../../../features/DocumentRBAC';
import { useDoc } from '../../../hooks/useDocument'; import { useDoc } from '../../../hooks/useDocument';
import { useDocLayout } from '../../../hooks/useDocumentLayout';
import { useLazyComponents } from '../../../hooks/useLazyComponents'; import { useLazyComponents } from '../../../hooks/useLazyComponents';
import { BlocksInput } from './FormInputs/BlocksInput/BlocksInput'; import { BlocksInput } from './FormInputs/BlocksInput/BlocksInput';
@ -58,6 +59,9 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
); );
const hint = useFieldHint(providedHint, props.attribute); const hint = useFieldHint(providedHint, props.attribute);
const {
edit: { components },
} = useDocLayout();
if (!visible) { if (!visible) {
return null; return null;
@ -114,7 +118,16 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
case 'blocks': case 'blocks':
return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />; return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />;
case 'component': case 'component':
return <ComponentInput {...props} hint={hint} disabled={fieldIsDisabled} />; return (
<ComponentInput
{...props}
hint={hint}
layout={components[props.attribute.component].layout}
disabled={fieldIsDisabled}
>
{(inputProps) => <InputRenderer {...inputProps} />}
</ComponentInput>
);
case 'dynamiczone': case 'dynamiczone':
return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />; return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />;
case 'relation': case 'relation':

View File

@ -10,6 +10,9 @@ import type { Schema, UID } from '@strapi/types';
* traverseData * traverseData
* -----------------------------------------------------------------------------------------------*/ * -----------------------------------------------------------------------------------------------*/
// Make only attributes required since it's the only one Content History has
type PartialSchema = Partial<Schema.Schema> & Pick<Schema.Schema, 'attributes'>;
type Predicate = <TAttribute extends Schema.Attribute.AnyAttribute>( type Predicate = <TAttribute extends Schema.Attribute.AnyAttribute>(
attribute: TAttribute, attribute: TAttribute,
value: Schema.Attribute.Value<TAttribute> value: Schema.Attribute.Value<TAttribute>
@ -35,7 +38,7 @@ const BLOCK_LIST_ATTRIBUTE_KEYS = ['__component', '__temp_key__'];
*/ */
const traverseData = const traverseData =
(predicate: Predicate, transform: Transform) => (predicate: Predicate, transform: Transform) =>
(schema: Schema.Schema, components: ComponentsDictionary = {}) => (schema: PartialSchema, components: ComponentsDictionary = {}) =>
(data: AnyData = {}) => { (data: AnyData = {}) => {
const traverse = (datum: AnyData, attributes: Schema.Schema['attributes']) => { const traverse = (datum: AnyData, attributes: Schema.Schema['attributes']) => {
return Object.entries(datum).reduce<AnyData>((acc, [key, value]) => { return Object.entries(datum).reduce<AnyData>((acc, [key, value]) => {
@ -122,7 +125,7 @@ const prepareRelations = traverseData(
/** /**
* @internal * @internal
* @description Adds a `__temp_key__` to each component and dynamiczone item. This gives us * @description Adds a `__temp_key__` to each component and dynamiczone item. This gives us
* a stable identifier regardless of it's ids etc. that we can then use for drag and drop. * a stable identifier regardless of its ids etc. that we can then use for drag and drop.
*/ */
const prepareTempKeys = traverseData( const prepareTempKeys = traverseData(
(attribute) => (attribute) =>
@ -150,7 +153,7 @@ const prepareTempKeys = traverseData(
* @description Fields that don't exist in the schema like createdAt etc. are only on the first level (not nested), * @description Fields that don't exist in the schema like createdAt etc. are only on the first level (not nested),
* as such we don't need to traverse the components to remove them. * as such we don't need to traverse the components to remove them.
*/ */
const removeFieldsThatDontExistOnSchema = (schema: Schema.Schema) => (data: AnyData) => { const removeFieldsThatDontExistOnSchema = (schema: PartialSchema) => (data: AnyData) => {
const schemaKeys = Object.keys(schema.attributes); const schemaKeys = Object.keys(schema.attributes);
const dataKeys = Object.keys(data); const dataKeys = Object.keys(data);
@ -194,7 +197,7 @@ const removeNullValues = (data: AnyData) => {
* form to ensure the data is correctly prepared from their default state e.g. relations are set to an empty array. * form to ensure the data is correctly prepared from their default state e.g. relations are set to an empty array.
*/ */
const transformDocument = const transformDocument =
(schema: Schema.Schema, components: ComponentsDictionary = {}) => (schema: PartialSchema, components: ComponentsDictionary = {}) =>
(document: AnyData) => { (document: AnyData) => {
const transformations = pipe( const transformations = pipe(
removeFieldsThatDontExistOnSchema(schema), removeFieldsThatDontExistOnSchema(schema),
@ -207,4 +210,10 @@ const transformDocument =
return transformations(document); return transformations(document);
}; };
export { removeProhibitedFields, prepareRelations, transformDocument }; export {
removeProhibitedFields,
prepareRelations,
prepareTempKeys,
removeFieldsThatDontExistOnSchema,
transformDocument,
};