mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
Merge branch 'main' into enhancement/axios-refactoring
This commit is contained in:
commit
70a08c6795
2
.github/workflows/contributor-doc.yml
vendored
2
.github/workflows/contributor-doc.yml
vendored
@ -21,7 +21,7 @@ permissions: {}
|
||||
jobs:
|
||||
deploy:
|
||||
permissions:
|
||||
contents: write # to push pages branch (peaceiris/actions-gh-pages)
|
||||
contents: write # to push pages branch (peaceiris/actions-gh-pages)
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
|
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@ -6,7 +6,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -14,7 +14,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -5,7 +5,7 @@ import get from 'lodash/get';
|
||||
import omit from 'lodash/omit';
|
||||
import take from 'lodash/take';
|
||||
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 { getFieldName } from '../../utils';
|
||||
import Wysiwyg from '../Wysiwyg';
|
||||
@ -37,11 +37,11 @@ function Inputs({
|
||||
queryInfos,
|
||||
value,
|
||||
size,
|
||||
customFieldInputs,
|
||||
}) {
|
||||
const { fields } = useLibrary();
|
||||
const { formatMessage } = useIntl();
|
||||
const { contentType: currentContentTypeLayout } = useContentTypeLayout();
|
||||
const customFieldsRegistry = useCustomFields();
|
||||
|
||||
const disabled = useMemo(() => !get(metadatas, 'editable', true), [metadatas]);
|
||||
const { type, customField: customFieldUid } = fieldSchema;
|
||||
@ -194,19 +194,6 @@ function Inputs({
|
||||
return minutes % metadatas.step === 0 ? 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) {
|
||||
return null;
|
||||
}
|
||||
@ -268,12 +255,9 @@ function Inputs({
|
||||
media: fields.media,
|
||||
wysiwyg: Wysiwyg,
|
||||
...fields,
|
||||
...customFieldInputs,
|
||||
};
|
||||
|
||||
if (customFieldUid) {
|
||||
customInputs[customFieldUid] = CustomFieldInput;
|
||||
}
|
||||
|
||||
return (
|
||||
<GenericInput
|
||||
attribute={fieldSchema}
|
||||
@ -309,6 +293,7 @@ Inputs.defaultProps = {
|
||||
size: undefined,
|
||||
value: null,
|
||||
queryInfos: {},
|
||||
customFieldInputs: {},
|
||||
};
|
||||
|
||||
Inputs.propTypes = {
|
||||
@ -330,6 +315,7 @@ Inputs.propTypes = {
|
||||
defaultParams: PropTypes.object,
|
||||
endPoint: PropTypes.string,
|
||||
}),
|
||||
customFieldInputs: PropTypes.object,
|
||||
};
|
||||
|
||||
const Memoized = memo(Inputs, isEqual);
|
||||
|
@ -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;
|
@ -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: {} });
|
||||
});
|
||||
});
|
@ -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;
|
@ -3,7 +3,7 @@ import { Switch, Route } from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { get } from 'lodash';
|
||||
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 { ContentTypeLayoutContext } from '../../contexts';
|
||||
import { useFetchContentTypeLayout } from '../../hooks';
|
||||
@ -12,6 +12,7 @@ import EditViewLayoutManager from '../EditViewLayoutManager';
|
||||
import EditSettingsView from '../EditSettingsView';
|
||||
import ListViewLayout from '../ListViewLayoutManager';
|
||||
import ListSettingsView from '../ListSettingsView';
|
||||
import ErrorFallback from './components/ErrorFallback';
|
||||
|
||||
const cmPermissions = permissions.contentManager;
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { Grid, GridItem } from '@strapi/design-system/Grid';
|
||||
import Inputs from '../../../components/Inputs';
|
||||
import FieldComponent from '../../../components/FieldComponent';
|
||||
|
||||
const GridRow = ({ columns }) => {
|
||||
const GridRow = ({ columns, customFieldInputs }) => {
|
||||
return (
|
||||
<Grid gap={4}>
|
||||
{columns.map(({ fieldSchema, labelAction, metadatas, name, size, queryInfos }) => {
|
||||
@ -41,6 +41,7 @@ const GridRow = ({ columns }) => {
|
||||
labelAction={labelAction}
|
||||
metadatas={metadatas}
|
||||
queryInfos={queryInfos}
|
||||
customFieldInputs={customFieldInputs}
|
||||
/>
|
||||
</GridItem>
|
||||
);
|
||||
@ -49,8 +50,13 @@ const GridRow = ({ columns }) => {
|
||||
);
|
||||
};
|
||||
|
||||
GridRow.defaultProps = {
|
||||
customFieldInputs: {},
|
||||
};
|
||||
|
||||
GridRow.propTypes = {
|
||||
columns: PropTypes.array.isRequired,
|
||||
customFieldInputs: PropTypes.object,
|
||||
};
|
||||
|
||||
export default GridRow;
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { Suspense, memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
CheckPermissions,
|
||||
LoadingIndicatorPage,
|
||||
useTracking,
|
||||
LinkButton,
|
||||
LoadingIndicatorPage,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { ContentLayout } from '@strapi/design-system/Layout';
|
||||
@ -23,13 +23,14 @@ import CollectionTypeFormWrapper from '../../components/CollectionTypeFormWrappe
|
||||
import EditViewDataManagerProvider from '../../components/EditViewDataManagerProvider';
|
||||
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
|
||||
import { getTrad } from '../../utils';
|
||||
import useLazyComponents from '../../hooks/useLazyComponents';
|
||||
import DraftAndPublishBadge from './DraftAndPublishBadge';
|
||||
import Informations from './Informations';
|
||||
import Header from './Header';
|
||||
import { getFieldsActionMatchingPermissions } from './utils';
|
||||
import DeleteLink from './DeleteLink';
|
||||
import GridRow from './GridRow';
|
||||
import { selectCurrentLayout, selectAttributesLayout } from './selectors';
|
||||
import { selectCurrentLayout, selectAttributesLayout, selectCustomFieldUids } from './selectors';
|
||||
|
||||
const cmPermissions = permissions.contentManager;
|
||||
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 { trackUsage } = useTracking();
|
||||
const { formatMessage } = useIntl();
|
||||
const { createActionAllowedFields, readActionAllowedFields, updateActionAllowedFields } =
|
||||
getFieldsActionMatchingPermissions(userPermissions, slug);
|
||||
|
||||
const { layout, formattedContentTypeLayout } = useSelector((state) => ({
|
||||
const { layout, formattedContentTypeLayout, customFieldUids } = useSelector((state) => ({
|
||||
layout: selectCurrentLayout(state),
|
||||
formattedContentTypeLayout: selectAttributesLayout(state),
|
||||
customFieldUids: selectCustomFieldUids(state),
|
||||
}));
|
||||
|
||||
const { isLazyLoading, lazyComponentStore } = useLazyComponents(customFieldUids);
|
||||
|
||||
const { createActionAllowedFields, readActionAllowedFields, updateActionAllowedFields } =
|
||||
getFieldsActionMatchingPermissions(userPermissions, slug);
|
||||
|
||||
const configurationPermissions = isSingleType
|
||||
? cmPermissions.singleTypesConfigurations
|
||||
: cmPermissions.collectionTypesConfigurations;
|
||||
@ -64,6 +69,10 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
|
||||
});
|
||||
};
|
||||
|
||||
if (isLazyLoading) {
|
||||
return <LoadingIndicatorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DataManagementWrapper allLayoutData={layout} slug={slug} id={id} origin={origin}>
|
||||
{({
|
||||
@ -110,54 +119,56 @@ const EditView = ({ allowedActions, isSingleType, goBack, slug, id, origin, user
|
||||
<ContentLayout>
|
||||
<Grid gap={4}>
|
||||
<GridItem col={9} s={12}>
|
||||
<Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Stack spacing={6}>
|
||||
{formattedContentTypeLayout.map((row, index) => {
|
||||
if (isDynamicZone(row)) {
|
||||
const {
|
||||
0: {
|
||||
0: { name, fieldSchema, metadatas, labelAction },
|
||||
},
|
||||
} = 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>
|
||||
);
|
||||
}
|
||||
<Stack spacing={6}>
|
||||
{formattedContentTypeLayout.map((row, index) => {
|
||||
if (isDynamicZone(row)) {
|
||||
const {
|
||||
0: {
|
||||
0: { name, fieldSchema, metadatas, labelAction },
|
||||
},
|
||||
} = row;
|
||||
|
||||
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} key={gridRowIndex} />
|
||||
))}
|
||||
</Stack>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</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 col={3} s={12}>
|
||||
<Stack spacing={2}>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { createAttributesLayout } from './utils';
|
||||
import { createAttributesLayout, getCustomFieldUidsFromLayout } from './utils';
|
||||
|
||||
const selectCurrentLayout = (state) => state['content-manager_editViewLayoutManager'].currentLayout;
|
||||
|
||||
@ -7,4 +7,8 @@ const selectAttributesLayout = createSelector(selectCurrentLayout, (layout) =>
|
||||
createAttributesLayout(layout?.contentType ?? {})
|
||||
);
|
||||
|
||||
export { selectCurrentLayout, selectAttributesLayout };
|
||||
const selectCustomFieldUids = createSelector(selectCurrentLayout, (layout) =>
|
||||
getCustomFieldUidsFromLayout(layout)
|
||||
);
|
||||
|
||||
export { selectCurrentLayout, selectAttributesLayout, selectCustomFieldUids };
|
||||
|
@ -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;
|
@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { default as createAttributesLayout } from './createAttributesLayout';
|
||||
export { default as getFieldsActionMatchingPermissions } from './getFieldsActionMatchingPermissions';
|
||||
export { default as getCustomFieldUidsFromLayout } from './getCustomFieldUidsFromLayout';
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user