mirror of
https://github.com/strapi/strapi.git
synced 2025-11-18 02:58:05 +00:00
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:
parent
4a26739ee0
commit
9d4475b11a
@ -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 };
|
||||||
|
|||||||
@ -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} />;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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':
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user