mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +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,
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { Schema } from '@strapi/types';
|
||||
import pipe from 'lodash/fp/pipe';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useDoc } from '../../hooks/useDocument';
|
||||
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 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';
|
||||
|
||||
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
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
@ -70,14 +157,16 @@ const FormPanel = ({ panel }: { panel: EditFieldLayout[][] }) => {
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
type UnknownField = EditFieldLayout & { shouldIgnoreRBAC: boolean };
|
||||
|
||||
const VersionContent = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { fieldSizes } = useTypedSelector((state) => state['content-manager'].app);
|
||||
const { version, layout } = useHistoryContext('VersionContent', (state) => ({
|
||||
version: state.selectedVersion,
|
||||
layout: state.layout,
|
||||
}));
|
||||
const version = useHistoryContext('VersionContent', (state) => state.selectedVersion);
|
||||
const layout = useHistoryContext('VersionContent', (state) => 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 removedAttributesAsFields = Object.entries(removedAttributes).map(
|
||||
([attributeName, attribute]) => {
|
||||
@ -95,34 +184,43 @@ const VersionContent = () => {
|
||||
return field;
|
||||
}
|
||||
);
|
||||
const unknownFieldsLayout = removedAttributesAsFields
|
||||
.reduce<Array<UnknownField[]>>((rows, field) => {
|
||||
if (field.type === 'dynamiczone') {
|
||||
// Dynamic zones take up all the columns in a row
|
||||
rows.push([field]);
|
||||
const unknownFieldsLayout = createLayoutFromFields(removedAttributesAsFields);
|
||||
|
||||
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]) {
|
||||
// Create a new row if there isn't one available
|
||||
rows.push([]);
|
||||
}
|
||||
const { components } = useDoc();
|
||||
|
||||
// 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;
|
||||
}, [])
|
||||
// Map the rows to panels
|
||||
.map((row) => [row]);
|
||||
return transform(version.schema, components)(version.data);
|
||||
}, [components, version.data, version.schema]);
|
||||
|
||||
return (
|
||||
<ContentLayout>
|
||||
<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">
|
||||
{layout.map((panel, index) => {
|
||||
{[...layout, ...remainingFieldsLayout].map((panel, index) => {
|
||||
return <FormPanel key={index} panel={panel} />;
|
||||
})}
|
||||
</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 { useDocumentRBAC } from '../../features/DocumentRBAC';
|
||||
import { useDoc } from '../../hooks/useDocument';
|
||||
import { useDocLayout } from '../../hooks/useDocumentLayout';
|
||||
import { useLazyComponents } from '../../hooks/useLazyComponents';
|
||||
import { useTypedSelector } from '../../modules/hooks';
|
||||
import { DocumentStatus } from '../../pages/EditView/components/DocumentStatus';
|
||||
import { BlocksInput } from '../../pages/EditView/components/FormInputs/BlocksInput/BlocksInput';
|
||||
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 { useHistoryContext } from '../pages/History';
|
||||
|
||||
import { getRemaingFieldsLayout } from './VersionContent';
|
||||
|
||||
import type { EditFieldLayout } from '../../hooks/useDocumentLayout';
|
||||
import type { RelationsFieldProps } from '../../pages/EditView/components/FormInputs/Relations';
|
||||
import type { RelationResult } from '../../services/relations';
|
||||
@ -233,10 +237,11 @@ const VersionInputRenderer = ({
|
||||
...props
|
||||
}: VersionInputRendererProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { version } = useHistoryContext('VersionContent', (state) => ({
|
||||
version: state.selectedVersion,
|
||||
}));
|
||||
const { id } = useDoc();
|
||||
const version = useHistoryContext('VersionContent', (state) => state.selectedVersion);
|
||||
const configuration = useHistoryContext('VersionContent', (state) => state.configuration);
|
||||
const fieldSizes = useTypedSelector((state) => state['content-manager'].app.fieldSizes);
|
||||
|
||||
const { id, components } = useDoc();
|
||||
const isFormDisabled = useForm('InputRenderer', (state) => state.disabled);
|
||||
|
||||
const isInDynamicZone = useDynamicZone('isInDynamicZone', (state) => state.isInDynamicZone);
|
||||
@ -261,6 +266,9 @@ const VersionInputRenderer = ({
|
||||
);
|
||||
|
||||
const hint = useFieldHint(providedHint, props.attribute);
|
||||
const {
|
||||
edit: { components: componentsLayout },
|
||||
} = useDocLayout();
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
@ -353,13 +361,24 @@ const VersionInputRenderer = ({
|
||||
case 'blocks':
|
||||
return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />;
|
||||
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 (
|
||||
<ComponentInput
|
||||
{...props}
|
||||
layout={[...layout, ...(remainingFieldsLayout || [])]}
|
||||
hint={hint}
|
||||
disabled={fieldIsDisabled}
|
||||
renderInput={(props) => <VersionInputRenderer {...props} shouldIgnoreRBAC={true} />}
|
||||
/>
|
||||
>
|
||||
{(inputProps) => <VersionInputRenderer {...inputProps} shouldIgnoreRBAC={true} />}
|
||||
</ComponentInput>
|
||||
);
|
||||
case 'dynamiczone':
|
||||
return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />;
|
||||
|
||||
@ -11,13 +11,17 @@ import { PERMISSIONS } from '../../constants/plugin';
|
||||
import { DocumentRBAC } from '../../features/DocumentRBAC';
|
||||
import { useDocument } from '../../hooks/useDocument';
|
||||
import { type EditLayout, useDocumentLayout } from '../../hooks/useDocumentLayout';
|
||||
import { useGetContentTypeConfigurationQuery } from '../../services/contentTypes';
|
||||
import { buildValidParams } from '../../utils/api';
|
||||
import { VersionContent } from '../components/VersionContent';
|
||||
import { VersionHeader } from '../components/VersionHeader';
|
||||
import { VersionsList } from '../components/VersionsList';
|
||||
import { useGetHistoryVersionsQuery } from '../services/historyVersion';
|
||||
|
||||
import type { ContentType } from '../../../../shared/contracts/content-types';
|
||||
import type {
|
||||
ContentType,
|
||||
FindContentTypeConfiguration,
|
||||
} from '../../../../shared/contracts/content-types';
|
||||
import type {
|
||||
HistoryVersionDataResponse,
|
||||
GetHistoryVersions,
|
||||
@ -32,6 +36,7 @@ interface HistoryContextValue {
|
||||
contentType: UID.ContentType;
|
||||
id?: string; // null for single types
|
||||
layout: EditLayout['layout'];
|
||||
configuration: FindContentTypeConfiguration.Response['data'];
|
||||
selectedVersion: HistoryVersionDataResponse;
|
||||
// Errors are handled outside of the provider, so we exclude errors from the response type
|
||||
versions: Extract<GetHistoryVersions.Response, { data: Array<HistoryVersionDataResponse> }>;
|
||||
@ -71,6 +76,8 @@ const HistoryPage = () => {
|
||||
settings: { displayName, mainField },
|
||||
},
|
||||
} = useDocumentLayout(slug!);
|
||||
const { data: configuration, isLoading: isLoadingConfiguration } =
|
||||
useGetContentTypeConfigurationQuery(slug!);
|
||||
|
||||
// Parse state from query params
|
||||
const [{ query }] = useQueryParams<{
|
||||
@ -114,7 +121,13 @@ const HistoryPage = () => {
|
||||
return <Navigate to="/content-manager" />;
|
||||
}
|
||||
|
||||
if (isLoadingDocument || isLoadingLayout || versionsResponse.isFetching || isStaleRequest) {
|
||||
if (
|
||||
isLoadingDocument ||
|
||||
isLoadingLayout ||
|
||||
versionsResponse.isFetching ||
|
||||
isStaleRequest ||
|
||||
isLoadingConfiguration
|
||||
) {
|
||||
return <Page.Loading />;
|
||||
}
|
||||
|
||||
@ -141,6 +154,7 @@ const HistoryPage = () => {
|
||||
!layout ||
|
||||
!schema ||
|
||||
!selectedVersion ||
|
||||
!configuration ||
|
||||
// This should not happen as it's covered by versionsResponse.isError, but we need it for TS
|
||||
versionsResponse.data.error
|
||||
) {
|
||||
@ -165,6 +179,7 @@ const HistoryPage = () => {
|
||||
id={documentId}
|
||||
schema={schema}
|
||||
layout={layout}
|
||||
configuration={configuration}
|
||||
selectedVersion={selectedVersion}
|
||||
versions={versionsResponse.data}
|
||||
page={page}
|
||||
|
||||
@ -224,7 +224,7 @@ type LayoutData = FindContentTypeConfiguration.Response['data'];
|
||||
/**
|
||||
* @internal
|
||||
* @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 = (
|
||||
data: LayoutData,
|
||||
|
||||
@ -10,7 +10,7 @@ import { EditFieldLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
import { transformDocument } from '../../../utils/data';
|
||||
import { createDefaultForm } from '../../../utils/forms';
|
||||
import { InputRendererProps } from '../../InputRenderer';
|
||||
import { type InputRendererProps } from '../../InputRenderer';
|
||||
|
||||
import { Initializer } from './Initializer';
|
||||
import { NonRepeatableComponent } from './NonRepeatable';
|
||||
@ -20,7 +20,12 @@ interface ComponentInputProps
|
||||
extends Omit<Extract<EditFieldLayout, { type: 'component' }>, 'size' | 'hint'>,
|
||||
Pick<InputProps, 'hint'> {
|
||||
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 = ({
|
||||
@ -90,15 +95,14 @@ const ComponentInput = ({
|
||||
<Initializer disabled={disabled} name={name} onClick={handleInitialisationClick} />
|
||||
)}
|
||||
{!attribute.repeatable && field.value ? (
|
||||
<NonRepeatableComponent
|
||||
attribute={attribute}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
<NonRepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props}>
|
||||
{props.children}
|
||||
</NonRepeatableComponent>
|
||||
) : null}
|
||||
{attribute.repeatable && (
|
||||
<RepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props} />
|
||||
<RepeatableComponent attribute={attribute} name={name} disabled={disabled} {...props}>
|
||||
{props.children}
|
||||
</RepeatableComponent>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@ -1,28 +1,20 @@
|
||||
import { useField } from '@strapi/admin/strapi-admin';
|
||||
import { Box, Flex, Grid, GridItem } from '@strapi/design-system';
|
||||
|
||||
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { InputRenderer } from '../../InputRenderer';
|
||||
import { ComponentProvider, useComponent } from '../ComponentContext';
|
||||
|
||||
import type { ComponentInputProps } from './Input';
|
||||
|
||||
interface NonRepeatableComponentProps extends Omit<ComponentInputProps, 'required' | 'label'> {}
|
||||
type NonRepeatableComponentProps = Omit<ComponentInputProps, 'required' | 'label'>;
|
||||
|
||||
const NonRepeatableComponent = ({
|
||||
attribute,
|
||||
name,
|
||||
renderInput = InputRenderer,
|
||||
children,
|
||||
layout,
|
||||
}: NonRepeatableComponentProps) => {
|
||||
const {
|
||||
edit: { components },
|
||||
} = useDocLayout();
|
||||
|
||||
const { value } = useField(name);
|
||||
const level = useComponent('NonRepeatableComponent', (state) => state.level);
|
||||
|
||||
const { layout } = components[attribute.component];
|
||||
|
||||
const isNested = level > 0;
|
||||
|
||||
return (
|
||||
@ -51,7 +43,7 @@ const NonRepeatableComponent = ({
|
||||
|
||||
return (
|
||||
<GridItem col={size} key={completeFieldName} s={12} xs={12}>
|
||||
{renderInput({ ...field, name: completeFieldName })}
|
||||
{children({ ...field, name: completeFieldName })}
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -9,12 +9,12 @@ import {
|
||||
Accordion,
|
||||
AccordionContent as DSAccordionContent,
|
||||
AccordionToggle,
|
||||
Grid,
|
||||
GridItem,
|
||||
IconButton,
|
||||
Typography,
|
||||
KeyboardNavigable,
|
||||
useComposedRefs,
|
||||
GridItem,
|
||||
Grid,
|
||||
} from '@strapi/design-system';
|
||||
import { Plus, Drag, Trash } from '@strapi/icons';
|
||||
import { getEmptyImage } from 'react-dnd-html5-backend';
|
||||
@ -24,13 +24,11 @@ import styled from 'styled-components';
|
||||
|
||||
import { ItemTypes } from '../../../../../constants/dragAndDrop';
|
||||
import { useDoc } from '../../../../../hooks/useDocument';
|
||||
import { useDocLayout } from '../../../../../hooks/useDocumentLayout';
|
||||
import { useDragAndDrop, type UseDragAndDropOptions } from '../../../../../hooks/useDragAndDrop';
|
||||
import { getIn } from '../../../../../utils/objects';
|
||||
import { getTranslation } from '../../../../../utils/translations';
|
||||
import { transformDocument } from '../../../utils/data';
|
||||
import { createDefaultForm } from '../../../utils/forms';
|
||||
import { InputRenderer } from '../../InputRenderer';
|
||||
import { ComponentProvider, useComponent } from '../ComponentContext';
|
||||
|
||||
import { Initializer } from './Initializer';
|
||||
@ -42,14 +40,15 @@ import type { Schema } from '@strapi/types';
|
||||
* RepeatableComponent
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
interface RepeatableComponentProps extends Omit<ComponentInputProps, 'label' | 'required'> {}
|
||||
type RepeatableComponentProps = Omit<ComponentInputProps, 'required' | 'label'>;
|
||||
|
||||
const RepeatableComponent = ({
|
||||
attribute,
|
||||
disabled,
|
||||
name,
|
||||
mainField,
|
||||
renderInput,
|
||||
children,
|
||||
layout,
|
||||
}: RepeatableComponentProps) => {
|
||||
const { toggleNotification } = useNotification();
|
||||
const { formatMessage } = useIntl();
|
||||
@ -211,6 +210,7 @@ const RepeatableComponent = ({
|
||||
<AccordionGroup error={error}>
|
||||
<AccordionContent aria-describedby={ariaDescriptionId}>
|
||||
{value.map(({ __temp_key__: key, id }, index) => {
|
||||
const nameWithIndex = `${name}.${index}`;
|
||||
return (
|
||||
<ComponentProvider
|
||||
key={key}
|
||||
@ -222,12 +222,11 @@ const RepeatableComponent = ({
|
||||
>
|
||||
<Component
|
||||
disabled={disabled}
|
||||
name={`${name}.${index}`}
|
||||
name={nameWithIndex}
|
||||
attribute={attribute}
|
||||
index={index}
|
||||
isOpen={collapseToOpen === key}
|
||||
mainField={mainField}
|
||||
renderInput={renderInput}
|
||||
onMoveItem={handleMoveComponentField}
|
||||
onClickToggle={handleToggle(key)}
|
||||
onDeleteComponent={() => {
|
||||
@ -238,7 +237,29 @@ const RepeatableComponent = ({
|
||||
onCancel={handleCancel}
|
||||
onDropItem={handleDropItem}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
@ -379,7 +400,7 @@ const ActionsFlex = styled(Flex)<{ expanded?: boolean }>`
|
||||
|
||||
interface ComponentProps
|
||||
extends Pick<UseDragAndDropOptions, 'onGrabItem' | 'onDropItem' | 'onCancel' | 'onMoveItem'>,
|
||||
Pick<RepeatableComponentProps, 'mainField' | 'renderInput'> {
|
||||
Pick<RepeatableComponentProps, 'mainField'> {
|
||||
attribute: Schema.Attribute.Component<`${string}.${string}`, boolean>;
|
||||
disabled?: boolean;
|
||||
index: number;
|
||||
@ -388,10 +409,10 @@ interface ComponentProps
|
||||
onClickToggle: () => void;
|
||||
onDeleteComponent?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
toggleCollapses: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Component = ({
|
||||
attribute,
|
||||
disabled,
|
||||
index,
|
||||
isOpen,
|
||||
@ -400,18 +421,13 @@ const Component = ({
|
||||
name: 'id',
|
||||
type: 'integer',
|
||||
},
|
||||
children,
|
||||
onClickToggle,
|
||||
onDeleteComponent,
|
||||
toggleCollapses,
|
||||
renderInput = InputRenderer,
|
||||
...dragProps
|
||||
}: ComponentProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
edit: { components },
|
||||
} = useDocLayout();
|
||||
|
||||
const { layout } = components[attribute.component];
|
||||
|
||||
const displayValue = useForm('RepeatableComponent', (state) => {
|
||||
return getIn(state.values, [...name.split('.'), mainField.name]);
|
||||
@ -500,27 +516,7 @@ const Component = ({
|
||||
padding={6}
|
||||
gap={6}
|
||||
>
|
||||
{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 = `${name}.${field.name}`;
|
||||
|
||||
return (
|
||||
<GridItem col={size} key={completeFieldName} s={12} xs={12}>
|
||||
{renderInput({ ...field, name: completeFieldName })}
|
||||
</GridItem>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
{children}
|
||||
</Flex>
|
||||
</DSAccordionContent>
|
||||
</Accordion>
|
||||
|
||||
@ -9,6 +9,7 @@ import { useIntl } from 'react-intl';
|
||||
|
||||
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
|
||||
import { useDoc } from '../../../hooks/useDocument';
|
||||
import { useDocLayout } from '../../../hooks/useDocumentLayout';
|
||||
import { useLazyComponents } from '../../../hooks/useLazyComponents';
|
||||
|
||||
import { BlocksInput } from './FormInputs/BlocksInput/BlocksInput';
|
||||
@ -58,6 +59,9 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
|
||||
);
|
||||
|
||||
const hint = useFieldHint(providedHint, props.attribute);
|
||||
const {
|
||||
edit: { components },
|
||||
} = useDocLayout();
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
@ -114,7 +118,16 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
|
||||
case 'blocks':
|
||||
return <BlocksInput {...props} hint={hint} type={props.type} disabled={fieldIsDisabled} />;
|
||||
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':
|
||||
return <DynamicZone {...props} hint={hint} disabled={fieldIsDisabled} />;
|
||||
case 'relation':
|
||||
|
||||
@ -10,6 +10,9 @@ import type { Schema, UID } from '@strapi/types';
|
||||
* 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>(
|
||||
attribute: TAttribute,
|
||||
value: Schema.Attribute.Value<TAttribute>
|
||||
@ -35,7 +38,7 @@ const BLOCK_LIST_ATTRIBUTE_KEYS = ['__component', '__temp_key__'];
|
||||
*/
|
||||
const traverseData =
|
||||
(predicate: Predicate, transform: Transform) =>
|
||||
(schema: Schema.Schema, components: ComponentsDictionary = {}) =>
|
||||
(schema: PartialSchema, components: ComponentsDictionary = {}) =>
|
||||
(data: AnyData = {}) => {
|
||||
const traverse = (datum: AnyData, attributes: Schema.Schema['attributes']) => {
|
||||
return Object.entries(datum).reduce<AnyData>((acc, [key, value]) => {
|
||||
@ -122,7 +125,7 @@ const prepareRelations = traverseData(
|
||||
/**
|
||||
* @internal
|
||||
* @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(
|
||||
(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),
|
||||
* 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 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.
|
||||
*/
|
||||
const transformDocument =
|
||||
(schema: Schema.Schema, components: ComponentsDictionary = {}) =>
|
||||
(schema: PartialSchema, components: ComponentsDictionary = {}) =>
|
||||
(document: AnyData) => {
|
||||
const transformations = pipe(
|
||||
removeFieldsThatDontExistOnSchema(schema),
|
||||
@ -207,4 +210,10 @@ const transformDocument =
|
||||
return transformations(document);
|
||||
};
|
||||
|
||||
export { removeProhibitedFields, prepareRelations, transformDocument };
|
||||
export {
|
||||
removeProhibitedFields,
|
||||
prepareRelations,
|
||||
prepareTempKeys,
|
||||
removeFieldsThatDontExistOnSchema,
|
||||
transformDocument,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user