Skip to content

Commit

Permalink
feat: show hidden fields in history frontend (#20201)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
remidej committed Apr 30, 2024
1 parent 4a26739 commit 9d4475b
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 99 deletions.
Expand Up @@ -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
* -----------------------------------------------------------------------------------------------*/
Expand Down Expand Up @@ -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]) => {
Expand All @@ -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>
Expand Down Expand Up @@ -170,4 +268,4 @@ const VersionContent = () => {
);
};

export { VersionContent };
export { VersionContent, getRemaingFieldsLayout };
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -261,6 +266,9 @@ const VersionInputRenderer = ({
);

const hint = useFieldHint(providedHint, props.attribute);
const {
edit: { components: componentsLayout },
} = useDocLayout();

if (!visible) {
return null;
Expand Down Expand Up @@ -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} />;
Expand Down
19 changes: 17 additions & 2 deletions packages/core/content-manager/admin/src/history/pages/History.tsx
Expand Up @@ -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,
Expand All @@ -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> }>;
Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -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 />;
}

Expand All @@ -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
) {
Expand All @@ -165,6 +179,7 @@ const HistoryPage = () => {
id={documentId}
schema={schema}
layout={layout}
configuration={configuration}
selectedVersion={selectedVersion}
versions={versionsResponse.data}
page={page}
Expand Down
Expand Up @@ -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,
Expand Down
Expand Up @@ -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';
Expand All @@ -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 = ({
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 9d4475b

Please sign in to comment.