Merge branch 'main' into enhancement/axios-refactoring

This commit is contained in:
Simone Taeggi 2022-11-29 14:23:08 +01:00
commit 70a08c6795
14 changed files with 296 additions and 76 deletions

View File

@ -21,7 +21,7 @@ permissions: {}
jobs: jobs:
deploy: deploy:
permissions: permissions:
contents: write # to push pages branch (peaceiris/actions-gh-pages) contents: write # to push pages branch (peaceiris/actions-gh-pages)
environment: environment:
name: github-pages name: github-pages

View File

@ -6,7 +6,7 @@ on:
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
publish: publish:

View File

@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
permissions: permissions:
contents: read # to fetch code (actions/checkout) contents: read # to fetch code (actions/checkout)
jobs: jobs:
lint: lint:

View File

@ -5,7 +5,7 @@ import get from 'lodash/get';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import take from 'lodash/take'; import take from 'lodash/take';
import isEqual from 'react-fast-compare'; import isEqual from 'react-fast-compare';
import { GenericInput, NotAllowedInput, useLibrary, useCustomFields } from '@strapi/helper-plugin'; import { GenericInput, NotAllowedInput, useLibrary } from '@strapi/helper-plugin';
import { useContentTypeLayout } from '../../hooks'; import { useContentTypeLayout } from '../../hooks';
import { getFieldName } from '../../utils'; import { getFieldName } from '../../utils';
import Wysiwyg from '../Wysiwyg'; import Wysiwyg from '../Wysiwyg';
@ -37,11 +37,11 @@ function Inputs({
queryInfos, queryInfos,
value, value,
size, size,
customFieldInputs,
}) { }) {
const { fields } = useLibrary(); const { fields } = useLibrary();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { contentType: currentContentTypeLayout } = useContentTypeLayout(); const { contentType: currentContentTypeLayout } = useContentTypeLayout();
const customFieldsRegistry = useCustomFields();
const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]); const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]);
const { type, customField: customFieldUid } = fieldSchema; const { type, customField: customFieldUid } = fieldSchema;
@ -194,19 +194,6 @@ function Inputs({
return minutes % metadatas.step === 0 ? metadatas.step : step; return minutes % metadatas.step === 0 ? metadatas.step : step;
}, [inputType, inputValue, metadatas.step, step]); }, [inputType, inputValue, metadatas.step, step]);
// Memoize the component to avoid remounting it and losing state
const CustomFieldInput = useMemo(() => {
if (customFieldUid) {
const customField = customFieldsRegistry.get(customFieldUid);
const CustomFieldInput = React.lazy(customField.components.Input);
return CustomFieldInput;
}
// Not a custom field, component won't be used
return null;
}, [customFieldUid, customFieldsRegistry]);
if (visible === false) { if (visible === false) {
return null; return null;
} }
@ -268,12 +255,9 @@ function Inputs({
media: fields.media, media: fields.media,
wysiwyg: Wysiwyg, wysiwyg: Wysiwyg,
...fields, ...fields,
...customFieldInputs,
}; };
if (customFieldUid) {
customInputs[customFieldUid] = CustomFieldInput;
}
return ( return (
<GenericInput <GenericInput
attribute={fieldSchema} attribute={fieldSchema}
@ -309,6 +293,7 @@ Inputs.defaultProps = {
size: undefined, size: undefined,
value: null, value: null,
queryInfos: {}, queryInfos: {},
customFieldInputs: {},
}; };
Inputs.propTypes = { Inputs.propTypes = {
@ -330,6 +315,7 @@ Inputs.propTypes = {
defaultParams: PropTypes.object, defaultParams: PropTypes.object,
endPoint: PropTypes.string, endPoint: PropTypes.string,
}), }),
customFieldInputs: PropTypes.object,
}; };
const Memoized = memo(Inputs, isEqual); const Memoized = memo(Inputs, isEqual);

View File

@ -0,0 +1,44 @@
import { useEffect, useState } from 'react';
import { useCustomFields } from '@strapi/helper-plugin';
/**
* @description
* A hook to lazy load custom field components
* @param {Array.<string>} componentUids - The uids to look up components
* @returns object
*/
const useLazyComponents = (componentUids) => {
const [lazyComponentStore, setLazyComponentStore] = useState({});
const [loading, setLoading] = useState(true);
const customFieldsRegistry = useCustomFields();
useEffect(() => {
const lazyLoadComponents = async (uids, components) => {
const modules = await Promise.all(components);
uids.forEach((uid, index) => {
if (!Object.keys(lazyComponentStore).includes(uid)) {
setLazyComponentStore({ ...lazyComponentStore, [uid]: modules[index].default });
}
});
};
if (componentUids.length) {
const componentPromises = componentUids.map((uid) => {
const customField = customFieldsRegistry.get(uid);
return customField.components.Input();
});
lazyLoadComponents(componentUids, componentPromises);
}
if (componentUids.length === Object.keys(lazyComponentStore).length) {
setLoading(false);
}
}, [componentUids, customFieldsRegistry, loading, lazyComponentStore]);
return { isLazyLoading: loading, lazyComponentStore };
};
export default useLazyComponents;

View File

@ -0,0 +1,50 @@
import { renderHook } from '@testing-library/react-hooks';
import useLazyComponents from '../index';
const mockCustomField = {
name: 'color',
pluginId: 'mycustomfields',
type: 'text',
icon: jest.fn(),
intlLabel: {
id: 'mycustomfields.color.label',
defaultMessage: 'Color',
},
intlDescription: {
id: 'mycustomfields.color.description',
defaultMessage: 'Select any color',
},
components: {
Input: jest.fn().mockResolvedValue({ default: jest.fn() }),
},
};
jest.mock('@strapi/helper-plugin', () => ({
useCustomFields: () => ({
get: jest.fn().mockReturnValue(mockCustomField),
}),
}));
describe('useLazyComponents', () => {
it('lazy loads the components', async () => {
const { result, waitForNextUpdate } = renderHook(() =>
useLazyComponents(['plugin::test.test'])
);
expect(result.current).toEqual({ isLazyLoading: true, lazyComponentStore: {} });
await waitForNextUpdate();
expect(JSON.stringify(result.current)).toEqual(
JSON.stringify({
isLazyLoading: false,
lazyComponentStore: { 'plugin::test.test': jest.fn() },
})
);
});
it('handles no components to load', async () => {
const { result } = renderHook(() => useLazyComponents([]));
expect(result.current).toEqual({ isLazyLoading: false, lazyComponentStore: {} });
});
});

View File

@ -0,0 +1,13 @@
import React from 'react';
import { AnErrorOccurred } from '@strapi/helper-plugin';
import { Box } from '@strapi/design-system/Box';
const ErrorFallback = () => {
return (
<Box padding={8}>
<AnErrorOccurred />
</Box>
);
};
export default ErrorFallback;

View File

@ -3,7 +3,7 @@ import { Switch, Route } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { get } from 'lodash'; import { get } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { ErrorFallback, LoadingIndicatorPage, CheckPagePermissions } from '@strapi/helper-plugin'; import { LoadingIndicatorPage, CheckPagePermissions } from '@strapi/helper-plugin';
import permissions from '../../../permissions'; import permissions from '../../../permissions';
import { ContentTypeLayoutContext } from '../../contexts'; import { ContentTypeLayoutContext } from '../../contexts';
import { useFetchContentTypeLayout } from '../../hooks'; import { useFetchContentTypeLayout } from '../../hooks';
@ -12,6 +12,7 @@ import EditViewLayoutManager from '../EditViewLayoutManager';
import EditSettingsView from '../EditSettingsView'; import EditSettingsView from '../EditSettingsView';
import ListViewLayout from '../ListViewLayoutManager'; import ListViewLayout from '../ListViewLayoutManager';
import ListSettingsView from '../ListSettingsView'; import ListSettingsView from '../ListSettingsView';
import ErrorFallback from './components/ErrorFallback';
const cmPermissions = permissions.contentManager; const cmPermissions = permissions.contentManager;

View File

@ -4,7 +4,7 @@ import { Grid, GridItem } from '@strapi/design-system/Grid';
import Inputs from '../../../components/Inputs'; import Inputs from '../../../components/Inputs';
import FieldComponent from '../../../components/FieldComponent'; import FieldComponent from '../../../components/FieldComponent';
const GridRow = ({ columns }) => { const GridRow = ({ columns, customFieldInputs }) => {
return ( return (
<Grid gap={4}> <Grid gap={4}>
{columns.map(({ fieldSchema, labelAction, metadatas, name, size, queryInfos }) => { {columns.map(({ fieldSchema, labelAction, metadatas, name, size, queryInfos }) => {
@ -41,6 +41,7 @@ const GridRow = ({ columns }) => {
labelAction={labelAction} labelAction={labelAction}
metadatas={metadatas} metadatas={metadatas}
queryInfos={queryInfos} queryInfos={queryInfos}
customFieldInputs={customFieldInputs}
/> />
</GridItem> </GridItem>
); );
@ -49,8 +50,13 @@ const GridRow = ({ columns }) => {
); );
}; };
GridRow.defaultProps = {
customFieldInputs: {},
};
GridRow.propTypes = { GridRow.propTypes = {
columns: PropTypes.array.isRequired, columns: PropTypes.array.isRequired,
customFieldInputs: PropTypes.object,
}; };
export default GridRow; export default GridRow;

View File

@ -1,11 +1,11 @@
import React, { Suspense, memo } from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
CheckPermissions, CheckPermissions,
LoadingIndicatorPage,
useTracking, useTracking,
LinkButton, LinkButton,
LoadingIndicatorPage,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { ContentLayout } from '@strapi/design-system/Layout'; import { ContentLayout } from '@strapi/design-system/Layout';
@ -23,13 +23,14 @@ import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrappe
import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider'; import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider';
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper'; import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
import { getTrad } from '../../utils'; import { getTrad } from '../../utils';
import useLazyComponents from '../../hooks/useLazyComponents';
import DraftAndPublishBadge from './DraftAndPublishBadge'; import DraftAndPublishBadge from './DraftAndPublishBadge';
import Informations from './Informations'; import Informations from './Informations';
import Header from './Header'; import Header from './Header';
import { getFieldsActionMatchingPermissions } from './utils'; import { getFieldsActionMatchingPermissions } from './utils';
import DeleteLink from './DeleteLink'; import DeleteLink from './DeleteLink';
import GridRow from './GridRow'; import GridRow from './GridRow';
import { selectCurrentLayout, selectAttributesLayout } from './selectors'; import { selectCurrentLayout, selectAttributesLayout, selectCustomFieldUids } from './selectors';
const cmPermissions = permissions.contentManager; const cmPermissions = permissions.contentManager;
const ctbPermissions = [{ action: 'plugin::content-type-builder.read', subject: null }]; const ctbPermissions = [{ action: 'plugin::content-type-builder.read', subject: null }];
@ -38,14 +39,18 @@ const ctbPermissions = [{ action: 'plugin::content-type-builder.read', subject:
const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, userPermissions }) => { const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, userPermissions }) => {
const { trackUsage } = useTracking(); const { trackUsage } = useTracking();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { createActionAllowedFields, readActionAllowedFields, updateActionAllowedFields } =
getFieldsActionMatchingPermissions(userPermissions, slug);
const { layout, formattedContentTypeLayout } = useSelector((state) => ({ const { layout, formattedContentTypeLayout, customFieldUids } = useSelector((state) => ({
layout: selectCurrentLayout(state), layout: selectCurrentLayout(state),
formattedContentTypeLayout: selectAttributesLayout(state), formattedContentTypeLayout: selectAttributesLayout(state),
customFieldUids: selectCustomFieldUids(state),
})); }));
const { isLazyLoading, lazyComponentStore } = useLazyComponents(customFieldUids);
const { createActionAllowedFields, readActionAllowedFields, updateActionAllowedFields } =
getFieldsActionMatchingPermissions(userPermissions, slug);
const configurationPermissions = isSingleType const configurationPermissions = isSingleType
? cmPermissions.singleTypesConfigurations ? cmPermissions.singleTypesConfigurations
: cmPermissions.collectionTypesConfigurations; : cmPermissions.collectionTypesConfigurations;
@ -64,6 +69,10 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
}); });
}; };
if (isLazyLoading) {
return <LoadingIndicatorPage />;
}
return ( return (
<DataManagementWrapper allLayoutData={layout} slug={slug} id={id} origin={origin}> <DataManagementWrapper allLayoutData={layout} slug={slug} id={id} origin={origin}>
{({ {({
@ -110,54 +119,56 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
<ContentLayout> <ContentLayout>
<Grid gap={4}> <Grid gap={4}>
<GridItem col={9} s={12}> <GridItem col={9} s={12}>
<Suspense fallback={<LoadingIndicatorPage />}> <Stack spacing={6}>
<Stack spacing={6}> {formattedContentTypeLayout.map((row, index) => {
{formattedContentTypeLayout.map((row, index) => { if (isDynamicZone(row)) {
if (isDynamicZone(row)) { const {
const { 0: {
0: { 0: { name, fieldSchema, metadatas, labelAction },
0: { name, fieldSchema, metadatas, labelAction }, },
}, } = row;
} = row;
return (
<Box key={index}>
<Grid gap={4}>
<GridItem col={12} s={12} xs={12}>
<DynamicZone
name={name}
fieldSchema={fieldSchema}
labelAction={labelAction}
metadatas={metadatas}
/>
</GridItem>
</Grid>
</Box>
);
}
return ( return (
<Box <Box key={index}>
key={index} <Grid gap={4}>
hasRadius <GridItem col={12} s={12} xs={12}>
background="neutral0" <DynamicZone
shadow="tableShadow" name={name}
paddingLeft={6} fieldSchema={fieldSchema}
paddingRight={6} labelAction={labelAction}
paddingTop={6} metadatas={metadatas}
paddingBottom={6} />
borderColor="neutral150" </GridItem>
> </Grid>
<Stack spacing={6}>
{row.map((grid, gridRowIndex) => (
<GridRow columns={grid} key={gridRowIndex} />
))}
</Stack>
</Box> </Box>
); );
})} }
</Stack>
</Suspense> return (
<Box
key={index}
hasRadius
background="neutral0"
shadow="tableShadow"
paddingLeft={6}
paddingRight={6}
paddingTop={6}
paddingBottom={6}
borderColor="neutral150"
>
<Stack spacing={6}>
{row.map((grid, gridRowIndex) => (
<GridRow
columns={grid}
customFieldInputs={lazyComponentStore}
key={gridRowIndex}
/>
))}
</Stack>
</Box>
);
})}
</Stack>
</GridItem> </GridItem>
<GridItem col={3} s={12}> <GridItem col={3} s={12}>
<Stack spacing={2}> <Stack spacing={2}>

View File

@ -1,5 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { createAttributesLayout } from './utils'; import { createAttributesLayout, getCustomFieldUidsFromLayout } from './utils';
const selectCurrentLayout = (state) => state['content-manager_editViewLayoutManager'].currentLayout; const selectCurrentLayout = (state) => state['content-manager_editViewLayoutManager'].currentLayout;
@ -7,4 +7,8 @@ const selectAttributesLayout = createSelector(selectCurrentLayout, (layout) =>
createAttributesLayout(layout?.contentType ?? {}) createAttributesLayout(layout?.contentType ?? {})
); );
export { selectCurrentLayout, selectAttributesLayout }; const selectCustomFieldUids = createSelector(selectCurrentLayout, (layout) =>
getCustomFieldUidsFromLayout(layout)
);
export { selectCurrentLayout, selectAttributesLayout, selectCustomFieldUids };

View File

@ -0,0 +1,18 @@
const getCustomFieldUidsFromLayout = (layout) => {
if (!layout) return [];
// Get all the fields on the content-type and its components
const allFields = [
...layout.contentType.layouts.edit,
...Object.values(layout.components).flatMap((component) => component.layouts.edit),
].flat();
// Filter that down to custom fields and map the uids
const customFieldUids = allFields
.filter((field) => field.fieldSchema.customField)
.map((customField) => customField.fieldSchema.customField);
// Make sure the list is unique
const uniqueCustomFieldUids = [...new Set(customFieldUids)];
return uniqueCustomFieldUids;
};
export default getCustomFieldUidsFromLayout;

View File

@ -1,3 +1,4 @@
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export { default as createAttributesLayout } from './createAttributesLayout'; export { default as createAttributesLayout } from './createAttributesLayout';
export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions'; export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions';
export { default as getCustomFieldUidsFromLayout } from './getCustomFieldUidsFromLayout';

View File

@ -0,0 +1,86 @@
import getCustomFieldUidsFromLayout from '../getCustomFieldUidsFromLayout';
describe('CONTENT MANAGER | CONTAINERS | EditView | utils | getCustomFieldUidsFromLayout', () => {
it('gets a unique list of custom field uids on the content-type layout', () => {
const mockLayoutData = {
contentType: {
layouts: {
edit: [
[
{
name: 'short_text',
size: 6,
fieldSchema: {
type: 'string',
},
},
],
[
{
name: 'dynamiczone',
size: 12,
fieldSchema: {
type: 'dynamiczone',
components: ['basic.simple'],
},
},
],
[
{
name: 'custom_field_2',
size: 6,
fieldSchema: {
type: 'string',
customField: 'plugin::color-picker.color',
},
},
],
[
{
name: 'custom_field_2',
size: 6,
fieldSchema: {
type: 'string',
customField: 'plugin::color-picker.color',
},
},
],
],
},
},
components: {
'basic.simple': {
uid: 'basic.simple',
layouts: {
edit: [
[
{
name: 'name',
size: 6,
fieldSchema: {
type: 'string',
required: true,
},
},
],
[
{
name: 'custom_field_3',
size: 6,
fieldSchema: {
type: 'string',
customField: 'plugin::test-plugin.test',
},
},
],
],
},
},
},
};
const expected = ['plugin::color-picker.color', 'plugin::test-plugin.test'];
expect(getCustomFieldUidsFromLayout(mockLayoutData)).toEqual(expected);
});
});