Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show hidden fields in history frontend #20201

Merged
merged 17 commits into from Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,8 @@ interface ComponentInputProps
extends Omit<Extract<EditFieldLayout, { type: 'component' }>, 'size' | 'hint'>,
Pick<InputProps, 'hint'> {
labelAction?: React.ReactNode;
renderInput?: (props: InputRendererProps) => React.ReactNode;
children: (name: InputRendererProps) => React.ReactNode;
remidej marked this conversation as resolved.
Show resolved Hide resolved
layout: EditFieldLayout[][];
remidej marked this conversation as resolved.
Show resolved Hide resolved
}

const ComponentInput = ({
Expand Down Expand Up @@ -90,15 +91,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