diff --git a/packages/core/admin/admin/src/content-manager/pages/App/actions.js b/packages/core/admin/admin/src/content-manager/pages/App/actions.js index 9125a7caae..eb85daab62 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/actions.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/actions.js @@ -1,12 +1,24 @@ -import { GET_DATA, RESET_PROPS, SET_CONTENT_TYPE_LINKS } from './constants'; +import { GET_INIT_DATA, RESET_INIT_DATA, SET_INIT_DATA } from './constants'; -export const getData = () => ({ - type: GET_DATA, +export const getInitData = () => ({ + type: GET_INIT_DATA, }); -export const resetProps = () => ({ type: RESET_PROPS }); +export const resetInitData = () => ({ type: RESET_INIT_DATA }); -export const setContentTypeLinks = (authorizedCtLinks, authorizedStLinks, models, components) => ({ - type: SET_CONTENT_TYPE_LINKS, - data: { authorizedCtLinks, authorizedStLinks, components, contentTypeSchemas: models }, +export const setInitData = ({ + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + contentTypeSchemas, + components, + fieldSizes, +}) => ({ + type: SET_INIT_DATA, + data: { + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + components, + contentTypeSchemas, + fieldSizes, + }, }); diff --git a/packages/core/admin/admin/src/content-manager/pages/App/constants.js b/packages/core/admin/admin/src/content-manager/pages/App/constants.js index 35ead6c083..5d71dae2c8 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/constants.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/constants.js @@ -1,3 +1,3 @@ -export const GET_DATA = 'ContentManager/App/GET_DATA'; -export const RESET_PROPS = 'ContentManager/App/RESET_PROPS'; -export const SET_CONTENT_TYPE_LINKS = 'ContentManager/App/SET_CONTENT_TYPE_LINKS'; +export const GET_INIT_DATA = 'ContentManager/App/GET_INIT_DATA'; +export const RESET_INIT_DATA = 'ContentManager/App/RESET_INIT_DATA'; +export const SET_INIT_DATA = 'ContentManager/App/SET_INIT_DATA'; diff --git a/packages/core/admin/admin/src/content-manager/pages/App/index.js b/packages/core/admin/admin/src/content-manager/pages/App/index.js index 8dae70fecc..4f8a64d5a6 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/index.js @@ -20,13 +20,14 @@ import NoContentType from '../NoContentType'; import NoPermissions from '../NoPermissions'; import SingleTypeRecursivePath from '../SingleTypeRecursivePath'; import LeftMenu from './LeftMenu'; -import useModels from './useModels'; +import useContentManagerInitData from './useContentManagerInitData'; const cmPermissions = permissions.contentManager; const App = () => { const contentTypeMatch = useRouteMatch(`/content-manager/:kind/:uid`); - const { status, collectionTypeLinks, singleTypeLinks, models, refetchData } = useModels(); + const { status, collectionTypeLinks, singleTypeLinks, models, refetchData } = + useContentManagerInitData(); const authorisedModels = sortBy([...collectionTypeLinks, ...singleTypeLinks], (model) => model.title.toLowerCase() ); diff --git a/packages/core/admin/admin/src/content-manager/pages/App/reducer.js b/packages/core/admin/admin/src/content-manager/pages/App/reducer.js index c43268d117..e3dbd20f2f 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/reducer.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/reducer.js @@ -4,7 +4,7 @@ */ /* eslint-disable consistent-return */ import produce from 'immer'; -import { GET_DATA, RESET_PROPS, SET_CONTENT_TYPE_LINKS } from './constants'; +import { GET_INIT_DATA, RESET_INIT_DATA, SET_INIT_DATA } from './constants'; const initialState = { components: [], @@ -20,22 +20,23 @@ const initialState = { const mainReducer = (state = initialState, action) => produce(state, (draftState) => { switch (action.type) { - case GET_DATA: { + case GET_INIT_DATA: { draftState.status = 'loading'; break; } - case RESET_PROPS: { + case RESET_INIT_DATA: { return initialState; } - case SET_CONTENT_TYPE_LINKS: { - draftState.collectionTypeLinks = action.data.authorizedCtLinks.filter( + case SET_INIT_DATA: { + draftState.collectionTypeLinks = action.data.authorizedCollectionTypeLinks.filter( ({ isDisplayed }) => isDisplayed ); - draftState.singleTypeLinks = action.data.authorizedStLinks.filter( + draftState.singleTypeLinks = action.data.authorizedSingleTypeLinks.filter( ({ isDisplayed }) => isDisplayed ); draftState.components = action.data.components; draftState.models = action.data.contentTypeSchemas; + draftState.fieldSizes = action.data.fieldSizes; draftState.status = 'resolved'; break; } diff --git a/packages/core/admin/admin/src/content-manager/pages/App/selectors.js b/packages/core/admin/admin/src/content-manager/pages/App/selectors.js index e5b433387e..8096c8e87a 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/selectors.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/selectors.js @@ -23,10 +23,13 @@ const makeSelectModelAndComponentSchemas = () => schemas: [...components, ...models], })); +const selectFieldSizes = createSelector(selectAppDomain(), (state) => state.fieldSizes); + export default makeSelectApp; export { makeSelectModelAndComponentSchemas, makeSelectModelLinks, makeSelectModels, + selectFieldSizes, selectAppDomain, }; diff --git a/packages/core/admin/admin/src/content-manager/pages/App/tests/actions.test.js b/packages/core/admin/admin/src/content-manager/pages/App/tests/actions.test.js index d2920167c5..fff0e17381 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/tests/actions.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/tests/actions.test.js @@ -1,27 +1,32 @@ -import { setContentTypeLinks } from '../actions'; +import { setInitData } from '../actions'; describe('Content Manager | App | actions', () => { - it('should format the setContentTypeLinks action', () => { - const authorizedCtLinks = [{ title: 'addresses', uid: 'address' }]; - const authorizedStLinks = [{ title: 'Home page', uid: 'homepage' }]; - const models = [ + it('should format the setInitData action', () => { + const authorizedCollectionTypeLinks = [{ title: 'addresses', uid: 'address' }]; + const authorizedSingleTypeLinks = [{ title: 'Home page', uid: 'homepage' }]; + const contentTypeSchemas = [ { kind: 'singleType', uid: 'homepage' }, { kind: 'collectionType', uid: 'address' }, ]; const components = []; const expected = { - type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS', + type: 'ContentManager/App/SET_INIT_DATA', data: { - authorizedCtLinks, - authorizedStLinks, - contentTypeSchemas: models, + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + contentTypeSchemas, components, }, }; - expect(setContentTypeLinks(authorizedCtLinks, authorizedStLinks, models, components)).toEqual( - expected - ); + expect( + setInitData({ + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + contentTypeSchemas, + components, + }) + ).toEqual(expected); }); }); diff --git a/packages/core/admin/admin/src/content-manager/pages/App/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/App/tests/index.test.js index a780f69d59..4be62d601d 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/tests/index.test.js @@ -13,9 +13,9 @@ import Theme from '../../../../components/Theme'; import ThemeToggleProvider from '../../../../components/ThemeToggleProvider'; import { App as ContentManagerApp } from '..'; import cmReducers from '../../../../reducers'; -import useModels from '../useModels'; +import useContentManagerInitData from '../useContentManagerInitData'; -jest.mock('../useModels', () => +jest.mock('../useContentManagerInitData', () => jest.fn(() => { return {}; }) @@ -88,7 +88,7 @@ describe('Content manager | App | main', () => { components: [], status: 'resolved', }; - useModels.mockImplementation(() => contentManagerState); + useContentManagerInitData.mockImplementation(() => contentManagerState); const rootReducer = combineReducers(cmReducers); const store = createStore(rootReducer, { 'content-manager_app': contentManagerState }); const history = createMemoryHistory(); @@ -815,7 +815,7 @@ describe('Content manager | App | main', () => { components: [], status: 'resolved', }; - useModels.mockImplementation(() => contentManagerState); + useContentManagerInitData.mockImplementation(() => contentManagerState); const rootReducer = combineReducers(cmReducers); const store = createStore(rootReducer, { 'content-manager_app': contentManagerState }); const history = createMemoryHistory(); @@ -857,8 +857,8 @@ describe('Content manager | App | main', () => { components: [], status: 'resolved', }; - useModels.mockImplementation(() => contentManagerState); - jest.mock('../useModels', () => + useContentManagerInitData.mockImplementation(() => contentManagerState); + jest.mock('../useContentManagerInitData', () => jest.fn(() => { return contentManagerState; }) @@ -902,8 +902,8 @@ describe('Content manager | App | main', () => { components: [], status: 'resolved', }; - useModels.mockImplementation(() => contentManagerState); - jest.mock('../useModels', () => + useContentManagerInitData.mockImplementation(() => contentManagerState); + jest.mock('../useContentManagerInitData', () => jest.fn(() => { return contentManagerState; }) diff --git a/packages/core/admin/admin/src/content-manager/pages/App/tests/reducer.test.js b/packages/core/admin/admin/src/content-manager/pages/App/tests/reducer.test.js index 7817b09107..de21071bff 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/tests/reducer.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/tests/reducer.test.js @@ -1,5 +1,5 @@ import produce from 'immer'; -import { getData, setContentTypeLinks, resetProps } from '../actions'; +import { getInitData, setInitData, resetInitData } from '../actions'; import mainReducer from '../reducer'; describe('Content Manager | App | reducer', () => { @@ -12,6 +12,7 @@ describe('Content Manager | App | reducer', () => { models: [], collectionTypeLinks: [], singleTypeLinks: [], + fieldSizes: {}, }; }); @@ -19,18 +20,18 @@ describe('Content Manager | App | reducer', () => { expect(mainReducer(state, {})).toEqual(state); }); - it('should handle the getData action correctly', () => { + it('should handle the getInitData action correctly', () => { state.status = 'resolved'; const expected = produce(state, (draft) => { draft.status = 'loading'; }); - expect(mainReducer(state, getData())).toEqual(expected); + expect(mainReducer(state, getInitData())).toEqual(expected); }); - it('should handle the getData action correctly', () => { - const collectionTypeLinks = [ + it('should handle the setInitData action correctly', () => { + const authorizedCollectionTypeLinks = [ { name: 'authorizedCt', isDisplayed: true, @@ -40,7 +41,7 @@ describe('Content Manager | App | reducer', () => { isDisplayed: false, }, ]; - const singleTypeLinks = [ + const authorizedSingleTypeLinks = [ { name: 'authorizedSt', isDisplayed: false, @@ -71,15 +72,21 @@ describe('Content Manager | App | reducer', () => { expect( mainReducer( state, - setContentTypeLinks(collectionTypeLinks, singleTypeLinks, ['test'], ['test']) + setInitData({ + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + contentTypeSchemas: ['test'], + components: ['test'], + fieldSizes: {}, + }) ) ).toEqual(expected); }); - it('should handle the resetProps action correctly', () => { + it('should handle the resetInitData action correctly', () => { state = 'test'; - expect(mainReducer(state, resetProps())).toEqual({ + expect(mainReducer(state, resetInitData())).toEqual({ components: [], models: [], collectionTypeLinks: [], diff --git a/packages/core/admin/admin/src/content-manager/pages/App/useModels.js b/packages/core/admin/admin/src/content-manager/pages/App/useContentManagerInitData.js similarity index 63% rename from packages/core/admin/admin/src/content-manager/pages/App/useModels.js rename to packages/core/admin/admin/src/content-manager/pages/App/useContentManagerInitData.js index 973467c6dc..133bb5d749 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/useModels.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/useContentManagerInitData.js @@ -11,11 +11,11 @@ import axios from 'axios'; import { useIntl } from 'react-intl'; import { MUTATE_COLLECTION_TYPES_LINKS, MUTATE_SINGLE_TYPES_LINKS } from '../../../exposedHooks'; import { getRequestUrl, getTrad } from '../../utils'; -import { getData, resetProps, setContentTypeLinks } from './actions'; +import { getInitData, resetInitData, setInitData } from './actions'; import { selectAppDomain } from './selectors'; import getContentTypeLinks from './utils/getContentTypeLinks'; -const useModels = () => { +const useContentManagerInitData = () => { const dispatch = useDispatch(); const toggleNotification = useNotification(); const state = useSelector(selectAppDomain()); @@ -29,22 +29,14 @@ const useModels = () => { const { get } = useFetchClient(); const fetchData = async () => { - dispatch(getData()); + dispatch(getInitData()); try { - const [ - { - data: { data: components }, + const { + data: { + data: { components, contentTypes: models, fieldSizes }, }, - { - data: { data: models }, - }, - ] = await Promise.all( - ['components', 'content-types'].map((endPoint) => - get(getRequestUrl(endPoint), { cancelToken: source.token }) - ) - ); - + } = await get(getRequestUrl('init'), { cancelToken: source.token }); notifyStatus( formatMessage({ id: getTrad('App.schemas.data-loaded'), @@ -52,22 +44,31 @@ const useModels = () => { }) ); - const { authorizedCtLinks, authorizedStLinks } = await getContentTypeLinks( + const unmutatedContentTypeLinks = await getContentTypeLinks({ models, - allPermissions, - toggleNotification + userPermissions: allPermissions, + toggleNotification, + }); + + const { ctLinks: authorizedCollectionTypeLinks } = runHookWaterfall( + MUTATE_COLLECTION_TYPES_LINKS, + { + ctLinks: unmutatedContentTypeLinks.authorizedCollectionTypeLinks, + models, + } ); - - const { ctLinks } = runHookWaterfall(MUTATE_COLLECTION_TYPES_LINKS, { - ctLinks: authorizedCtLinks, - models, - }); - const { stLinks } = runHookWaterfall(MUTATE_SINGLE_TYPES_LINKS, { - stLinks: authorizedStLinks, + const { stLinks: authorizedSingleTypeLinks } = runHookWaterfall(MUTATE_SINGLE_TYPES_LINKS, { + stLinks: unmutatedContentTypeLinks.authorizedSingleTypeLinks, models, }); - const actionToDispatch = setContentTypeLinks(ctLinks, stLinks, models, components); + const actionToDispatch = setInitData({ + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + contentTypeSchemas: models, + components, + fieldSizes, + }); dispatch(actionToDispatch); } catch (err) { @@ -88,7 +89,7 @@ const useModels = () => { return () => { source.cancel('Operation canceled by the user.'); - dispatch(resetProps()); + dispatch(resetInitData()); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [dispatch, toggleNotification]); @@ -96,4 +97,4 @@ const useModels = () => { return { ...state, refetchData: fetchDataRef.current }; }; -export default useModels; +export default useContentManagerInitData; diff --git a/packages/core/admin/admin/src/content-manager/pages/App/utils/generateModelsLinks.js b/packages/core/admin/admin/src/content-manager/pages/App/utils/generateModelsLinks.js index db121ef756..7af9f4be85 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/utils/generateModelsLinks.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/utils/generateModelsLinks.js @@ -52,12 +52,12 @@ const generateModelsLinks = (models, modelsConfigurations) => { const [collectionTypes, singleTypes] = sortBy(groupedModels, 'name'); return { - collectionTypesSectionLinks: generateLinks( + collectionTypeSectionLinks: generateLinks( collectionTypes?.links || [], 'collectionTypes', modelsConfigurations ), - singleTypesSectionLinks: generateLinks(singleTypes?.links ?? [], 'singleTypes'), + singleTypeSectionLinks: generateLinks(singleTypes?.links ?? [], 'singleTypes'), }; }; diff --git a/packages/core/admin/admin/src/content-manager/pages/App/utils/getContentTypeLinks.js b/packages/core/admin/admin/src/content-manager/pages/App/utils/getContentTypeLinks.js index 759913657f..44a5084740 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/utils/getContentTypeLinks.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/utils/getContentTypeLinks.js @@ -3,36 +3,38 @@ import generateModelsLinks from './generateModelsLinks'; import checkPermissions from './checkPermissions'; import { getRequestUrl } from '../../../utils'; -const getContentTypeLinks = async (models, userPermissions, toggleNotification) => { +const getContentTypeLinks = async ({ models, userPermissions, toggleNotification }) => { const { get } = getFetchClient(); try { const { data: { data: contentTypeConfigurations }, } = await get(getRequestUrl('content-types-settings')); - const { collectionTypesSectionLinks, singleTypesSectionLinks } = generateModelsLinks( + const { collectionTypeSectionLinks, singleTypeSectionLinks } = generateModelsLinks( models, contentTypeConfigurations ); - // Content Types verifications - const ctLinksPermissionsPromises = checkPermissions( - userPermissions, - collectionTypesSectionLinks + // Collection Types verifications + const collectionTypeLinksPermissions = await Promise.all( + checkPermissions(userPermissions, collectionTypeSectionLinks) ); - const ctLinksPermissions = await Promise.all(ctLinksPermissionsPromises); - const authorizedCtLinks = collectionTypesSectionLinks.filter( - (_, index) => ctLinksPermissions[index] + const authorizedCollectionTypeLinks = collectionTypeSectionLinks.filter( + (_, index) => collectionTypeLinksPermissions[index] ); // Single Types verifications - const stLinksPermissionsPromises = checkPermissions(userPermissions, singleTypesSectionLinks); - const stLinksPermissions = await Promise.all(stLinksPermissionsPromises); - const authorizedStLinks = singleTypesSectionLinks.filter( - (_, index) => stLinksPermissions[index] + const singleTypeLinksPermissions = await Promise.all( + checkPermissions(userPermissions, singleTypeSectionLinks) + ); + const authorizedSingleTypeLinks = singleTypeSectionLinks.filter( + (_, index) => singleTypeLinksPermissions[index] ); - return { authorizedCtLinks, authorizedStLinks }; + return { + authorizedCollectionTypeLinks, + authorizedSingleTypeLinks, + }; } catch (err) { console.error(err); @@ -41,7 +43,7 @@ const getContentTypeLinks = async (models, userPermissions, toggleNotification) message: { id: 'notification.error' }, }); - return { authorizedCtLinks: [], authorizedStLinks: [], contentTypes: [] }; + return { authorizedCollectionTypeLinks: [], authorizedSingleTypeLinks: [] }; } }; diff --git a/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/generateModelsLinks.test.js b/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/generateModelsLinks.test.js index a160f7551c..fb67531fd9 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/generateModelsLinks.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/generateModelsLinks.test.js @@ -119,7 +119,7 @@ describe('ADMIN | LeftMenu | utils', () => { ]; const expected = { - collectionTypesSectionLinks: [ + collectionTypeSectionLinks: [ { isDisplayed: true, search: null, @@ -140,7 +140,7 @@ describe('ADMIN | LeftMenu | utils', () => { ], }, ], - singleTypesSectionLinks: [ + singleTypeSectionLinks: [ { isDisplayed: true, kind: 'singleType', diff --git a/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.js b/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.test.js similarity index 63% rename from packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.js rename to packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.test.js index 37dae5842d..71a65d5f02 100644 --- a/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.js +++ b/packages/core/admin/admin/src/content-manager/pages/App/utils/tests/getContentTypeLinks.test.js @@ -1,4 +1,4 @@ -import { request, hasPermissions } from '@strapi/helper-plugin'; +import { getFetchClient, hasPermissions } from '@strapi/helper-plugin'; import getContentTypeLinks from '../getContentTypeLinks'; // FIXME @@ -44,24 +44,24 @@ describe('checkPermissions', () => { }, ]; - const data = [ + const contentTypes = [ { - uid: 'api::address.address', - isDisplayed: true, apiID: 'address', - kind: 'collectionType', info: { - label: 'address', + displayName: 'Address', }, + isDisplayed: true, + kind: 'collectionType', + uid: 'api::address.address', }, { - uid: 'api::article.article', - isDisplayed: true, apiID: 'article', - kind: 'collectionType', info: { - label: 'article', + displayName: 'Article', }, + isDisplayed: true, + kind: 'collectionType', + uid: 'api::article.article', pluginOptions: { i18n: { localized: true, @@ -70,32 +70,36 @@ describe('checkPermissions', () => { }, ]; - request.mockImplementation((url) => { - if (url === '/content-manager/content-types') { - return Promise.resolve({ data }); - } - - return Promise.resolve({ - data: [ - { - uid: 'api::address.address', - settings: { - pageSize: 10, - defaultSortBy: 'name', - defaultSortOrder: 'ASC', + getFetchClient.mockImplementation(() => ({ + get(url) { + if (url === '/content-manager/content-types-settings') { + return Promise.resolve({ + data: { + data: [ + { + uid: 'api::address.address', + settings: { + pageSize: 10, + defaultSortBy: 'name', + defaultSortOrder: 'ASC', + }, + }, + ], }, - }, - ], - }); - }); + }); + } + + // To please the linter + return Promise.resolve(null); + }, + })); const expected = { - authorizedCtLinks: [ + authorizedCollectionTypeLinks: [ { - destination: '/content-manager/collectionType/api::address.address', - icon: 'circle', isDisplayed: true, - label: 'address', + kind: 'collectionType', + name: 'api::address.address', permissions: [ { action: 'plugin::content-manager.explorer.create', @@ -107,13 +111,14 @@ describe('checkPermissions', () => { }, ], search: 'page=1&pageSize=10&sort=name:ASC', + title: 'Address', + to: '/content-manager/collectionType/api::address.address', + uid: 'api::address.address', }, { - destination: '/content-manager/collectionType/api::article.article', - icon: 'circle', isDisplayed: true, - label: 'article', - search: null, + kind: 'collectionType', + name: 'api::article.article', permissions: [ { action: 'plugin::content-manager.explorer.create', @@ -124,12 +129,15 @@ describe('checkPermissions', () => { subject: 'api::article.article', }, ], + search: null, + title: 'Article', + to: '/content-manager/collectionType/api::article.article', + uid: 'api::article.article', }, ], - authorizedStLinks: [], - contentTypes: data, + authorizedSingleTypeLinks: [], }; - const actual = await getContentTypeLinks(userPermissions); + const actual = await getContentTypeLinks({ userPermissions, models: contentTypes }); expect(actual).toEqual(expected); }); @@ -139,11 +147,13 @@ describe('checkPermissions', () => { const toggleNotification = jest.fn(); const userPermissions = []; - request.mockImplementation(() => { - throw new Error('Something went wrong'); - }); + getFetchClient.mockImplementation(() => ({ + get() { + throw new Error('Something went wrong'); + }, + })); - await getContentTypeLinks(userPermissions, toggleNotification); + await getContentTypeLinks({ userPermissions, toggleNotification }); expect(toggleNotification).toBeCalled(); }); }); diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js index 6394a77bcf..3b532666bb 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js @@ -6,7 +6,7 @@ import { useSelector, shallowEqual } from 'react-redux'; import { useIntl } from 'react-intl'; import { useLayoutDnd } from '../../../hooks'; import { createPossibleMainFieldsForModelsAndComponents, getInputProps } from '../utils'; -import { makeSelectModelAndComponentSchemas } from '../../App/selectors'; +import { makeSelectModelAndComponentSchemas, selectFieldSizes } from '../../App/selectors'; import getTrad from '../../../utils/getTrad'; import GenericInput from './GenericInput'; @@ -17,8 +17,6 @@ const FIELD_SIZES = [ [12, '100%'], ]; -const NON_RESIZABLE_FIELD_TYPES = ['dynamiczone', 'component', 'json', 'richtext']; - const TIME_FIELD_OPTIONS = [1, 5, 10, 15, 30, 60]; const TIME_FIELD_TYPES = ['datetime', 'time']; @@ -28,6 +26,7 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => { const { modifiedData, selectedField, attributes, fieldForm } = useLayoutDnd(); const schemasSelector = useMemo(makeSelectModelAndComponentSchemas, []); const { schemas } = useSelector((state) => schemasSelector(state), shallowEqual); + const fieldSizes = useSelector(selectFieldSizes); const formToDisplay = useMemo(() => { if (!selectedField) { @@ -103,7 +102,7 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => { ); }); - const canResize = !NON_RESIZABLE_FIELD_TYPES.includes(attributes[selectedField].type); + const { isResizable } = fieldSizes[attributes[selectedField].type]; const sizeField = ( @@ -152,7 +151,7 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => { return ( <> {metaFields} - {canResize && sizeField} + {isResizable && sizeField} {hasTimePicker && timeStepField} ); diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js index e4bc62ac59..77c3af9a47 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js @@ -26,6 +26,7 @@ import { Divider, } from '@strapi/design-system'; import { ArrowLeft, Check } from '@strapi/icons'; +import { useSelector } from 'react-redux'; import { getTrad } from '../../utils'; import reducer, { initialState } from './reducer'; import init from './init'; @@ -34,6 +35,7 @@ import ModalForm from './components/FormModal'; import LayoutDndProvider from '../../components/LayoutDndProvider'; import { unformatLayout } from './utils/layout'; import putCMSettingsEV from './utils/api'; +import { selectFieldSizes } from '../App/selectors'; const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, updateLayout }) => { const [reducerState, dispatch] = useReducer(reducer, initialState, () => @@ -49,6 +51,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd const { formatMessage } = useIntl(); const modelName = get(mainLayout, ['info', 'displayName'], ''); const attributes = get(modifiedData, ['attributes'], {}); + const fieldSizes = useSelector(selectFieldSizes); const entryTitleOptions = Object.keys(attributes).filter((attr) => { const type = get(attributes, [attr, 'type'], ''); @@ -318,6 +321,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd dispatch({ type: 'ON_ADD_FIELD', name: field, + fieldSizes, }); }} onRemoveField={(rowId, index) => { diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js index 57190bc166..6b79791202 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js @@ -2,9 +2,10 @@ import produce from 'immer'; import set from 'lodash/set'; import get from 'lodash/get'; import cloneDeep from 'lodash/cloneDeep'; - import { arrayMoveItem } from '../../utils'; -import { formatLayout, getDefaultInputSize, getFieldSize, setFieldSize } from './utils/layout'; +import { formatLayout, getFieldSize, setFieldSize } from './utils/layout'; + +const DEFAULT_FIELD_SIZE = 6; const initialState = { fieldForm: {}, @@ -29,9 +30,8 @@ const reducer = (state = initialState, action) => } case 'ON_ADD_FIELD': { const newState = cloneDeep(state); - const size = getDefaultInputSize( - get(newState, ['modifiedData', 'attributes', action.name, 'type'], '') - ); + const type = get(newState, ['modifiedData', 'attributes', action.name, 'type'], ''); + const size = action.fieldSizes[type]?.default ?? DEFAULT_FIELD_SIZE; const listSize = get(newState, layoutPathEdit, []).length; const actualRowContentPath = [...layoutPathEdit, listSize - 1, 'rowContent']; const rowContentToSet = get(newState, actualRowContentPath, []); @@ -149,8 +149,7 @@ const reducer = (state = initialState, action) => draftState.metaToEdit = action.name; draftState.metaForm = { metadata: get(state, ['modifiedData', 'metadatas', action.name, 'edit'], {}), - size: - getFieldSize(action.name, state.modifiedData?.layouts?.edit) ?? getDefaultInputSize(), + size: getFieldSize(action.name, state.modifiedData?.layouts?.edit) ?? DEFAULT_FIELD_SIZE, }; break; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/index.test.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/index.test.js index d8d4337ebc..66c85a1021 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/index.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/index.test.js @@ -4,10 +4,13 @@ import { Router } from 'react-router-dom'; import { createMemoryHistory } from 'history'; import { IntlProvider } from 'react-intl'; import { QueryClient, QueryClientProvider } from 'react-query'; +import { combineReducers, createStore } from 'redux'; +import { Provider } from 'react-redux'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { ThemeProvider, lightTheme } from '@strapi/design-system'; import EditSettingsView from '../index'; +import cmReducers from '../../../../reducers'; jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), @@ -57,22 +60,31 @@ const makeApp = (history, layout) => { compo1: { uid: 'compo1' }, }; + const rootReducer = combineReducers(cmReducers); + const store = createStore(rootReducer, { + 'content-manager_app': { + fieldSizes: {}, + }, + }); + return ( - - - - - - - - - + + + + + + + + + + + ); }; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/reducer.test.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/reducer.test.js index 23553533a0..439e430511 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/reducer.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/tests/reducer.test.js @@ -1,5 +1,11 @@ import reducer from '../reducer'; +const fieldSizes = { + richtext: { default: 12, isResizable: false }, + string: { default: 6, isResizable: true }, + boolean: { default: 4, isResizable: true }, +}; + describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => { let state; @@ -82,7 +88,11 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => { }, }, }; - const action = { type: 'ON_ADD_FIELD', name: 'description' }; + const action = { + type: 'ON_ADD_FIELD', + name: 'description', + fieldSizes, + }; expect(reducer(state, action)).toEqual(expected); }); @@ -121,7 +131,7 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => { }, }, }; - const action = { type: 'ON_ADD_FIELD', name: 'title' }; + const action = { type: 'ON_ADD_FIELD', name: 'title', fieldSizes }; expect(reducer(state, action)).toEqual(expected); }); @@ -163,7 +173,7 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => { }, }, }; - const action = { type: 'ON_ADD_FIELD', name: 'isActive' }; + const action = { type: 'ON_ADD_FIELD', name: 'isActive', fieldSizes }; expect(reducer(state, action)).toEqual(expected); }); @@ -214,7 +224,7 @@ describe('CONTENT MANAGER | CONTAINERS | EditSettingsView | reducer', () => { }, }, }; - const action = { type: 'ON_ADD_FIELD', name: 'title' }; + const action = { type: 'ON_ADD_FIELD', name: 'title', fieldSizes }; expect(reducer(state, action)).toEqual(expected); }); }); diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/layout.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/layout.js index 2b655b6476..8db770aa50 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/layout.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/layout.js @@ -1,4 +1,3 @@ -/* eslint-disable indent */ const getRowSize = (arr) => arr.reduce((sum, value) => sum + value.size, 0); const createLayout = (arr) => { @@ -75,26 +74,6 @@ const unformatLayout = (arr) => { }, []); }; -const getDefaultInputSize = (type) => { - switch (type) { - case 'boolean': - case 'date': - case 'integer': - case 'float': - case 'biginteger': - case 'decimal': - case 'time': - return 4; - case 'json': - case 'component': - case 'richtext': - case 'dynamiczone': - return 12; - default: - return 6; - } -}; - const getFieldSize = (name, layouts = []) => { return layouts.reduce((acc, { rowContent }) => { const size = rowContent.find((row) => row.name === name)?.size ?? null; @@ -124,12 +103,4 @@ const setFieldSize = (name, size, layouts = []) => { }); }; -export { - createLayout, - formatLayout, - getDefaultInputSize, - getFieldSize, - setFieldSize, - getRowSize, - unformatLayout, -}; +export { createLayout, formatLayout, getFieldSize, setFieldSize, getRowSize, unformatLayout }; diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/tests/layout.test.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/tests/layout.test.js index 2558d5ff3e..2ba8730b57 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/tests/layout.test.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/utils/tests/layout.test.js @@ -1,7 +1,6 @@ import { createLayout, formatLayout, - getDefaultInputSize, getFieldSize, setFieldSize, getRowSize, @@ -118,24 +117,6 @@ describe('Content Manager | containers | EditSettingsView | utils | layout', () }); }); - describe('getDefaultInputSize', () => { - it('Should return 6 if the type is unknown, undefined or text', () => { - expect(getDefaultInputSize(undefined)).toBe(6); - expect(getDefaultInputSize('unkown')).toBe(6); - expect(getDefaultInputSize('text')).toBe(6); - }); - - it('Should return 12 if the type is either json, component or richtext', () => { - expect(getDefaultInputSize('json')).toBe(12); - expect(getDefaultInputSize('richtext')).toBe(12); - expect(getDefaultInputSize('component')).toBe(12); - }); - - it('Should return 4 if the type is boolean', () => { - expect(getDefaultInputSize('boolean')).toBe(4); - }); - }); - describe('getFieldSize', () => { const fixture = [ { diff --git a/packages/core/content-manager/server/controllers/index.js b/packages/core/content-manager/server/controllers/index.js index c5cf9b56c8..f6e1275aea 100644 --- a/packages/core/content-manager/server/controllers/index.js +++ b/packages/core/content-manager/server/controllers/index.js @@ -3,6 +3,7 @@ const collectionTypes = require('./collection-types'); const components = require('./components'); const contentTypes = require('./content-types'); +const init = require('./init'); const relations = require('./relations'); const singleTypes = require('./single-types'); const uid = require('./uid'); @@ -11,6 +12,7 @@ module.exports = { 'collection-types': collectionTypes, components, 'content-types': contentTypes, + init, relations, 'single-types': singleTypes, uid, diff --git a/packages/core/content-manager/server/controllers/init.js b/packages/core/content-manager/server/controllers/init.js new file mode 100644 index 0000000000..82f76492a8 --- /dev/null +++ b/packages/core/content-manager/server/controllers/init.js @@ -0,0 +1,20 @@ +'use strict'; + +const { getService } = require('../utils'); + +module.exports = { + getInitData(ctx) { + const { toDto } = getService('data-mapper'); + const { findAllComponents } = getService('components'); + const { getAllFieldSizes } = getService('field-sizes'); + const { findAllContentTypes } = getService('content-types'); + + ctx.body = { + data: { + fieldSizes: getAllFieldSizes(), + components: findAllComponents().map(toDto), + contentTypes: findAllContentTypes().map(toDto), + }, + }; + }, +}; diff --git a/packages/core/content-manager/server/routes/admin.js b/packages/core/content-manager/server/routes/admin.js index 7bc0e8cdcc..c33cee7c6e 100644 --- a/packages/core/content-manager/server/routes/admin.js +++ b/packages/core/content-manager/server/routes/admin.js @@ -5,6 +5,14 @@ const { routing } = require('../middlewares'); module.exports = { type: 'admin', routes: [ + { + method: 'GET', + path: '/init', + handler: 'init.getInitData', + config: { + policies: [], + }, + }, { method: 'GET', path: '/content-types', diff --git a/packages/core/content-manager/server/services/__tests__/field-sizes.test.js b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js new file mode 100644 index 0000000000..43c79cc101 --- /dev/null +++ b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js @@ -0,0 +1,33 @@ +'use strict'; + +const fieldSizesService = require('../field-sizes'); + +describe('field sizes service', () => { + it('should return the correct field sizes', () => { + const { getAllFieldSizes } = fieldSizesService(); + const fieldSizes = getAllFieldSizes(); + Object.values(fieldSizes).forEach((fieldSize) => { + expect(typeof fieldSize.isResizable).toBe('boolean'); + expect([4, 6, 8, 12]).toContain(fieldSize.default); + }); + }); + + it('should return the correct field size for a given type', () => { + const { getFieldSize } = fieldSizesService(); + const fieldSize = getFieldSize('string'); + expect(fieldSize.isResizable).toBe(true); + expect(fieldSize.default).toBe(6); + }); + + it('should throw an error if the type is not found', () => { + const { getFieldSize } = fieldSizesService(); + expect(() => getFieldSize('not-found')).toThrowError( + 'Could not find field size for type not-found' + ); + }); + + it('should throw an error if the type is not provided', () => { + const { getFieldSize } = fieldSizesService(); + expect(() => getFieldSize()).toThrowError('The type is required'); + }); +}); diff --git a/packages/core/content-manager/server/services/field-sizes.js b/packages/core/content-manager/server/services/field-sizes.js new file mode 100644 index 0000000000..bdbeffe83f --- /dev/null +++ b/packages/core/content-manager/server/services/field-sizes.js @@ -0,0 +1,63 @@ +'use strict'; + +const needsFullSize = { + default: 12, + isResizable: false, +}; + +const smallSize = { + default: 4, + isResizable: true, +}; + +const defaultSize = { + default: 6, + isResizable: true, +}; + +const fieldSizes = { + // Full row and not resizable + dynamiczone: needsFullSize, + component: needsFullSize, + json: needsFullSize, + richtext: needsFullSize, + // Small and resizable + checkbox: smallSize, + boolean: smallSize, + date: smallSize, + time: smallSize, + biginteger: smallSize, + decimal: smallSize, + float: smallSize, + integer: smallSize, + number: smallSize, + // Medium and resizable + datetime: defaultSize, + email: defaultSize, + enumeration: defaultSize, + media: defaultSize, + password: defaultSize, + relation: defaultSize, + string: defaultSize, + text: defaultSize, + timestamp: defaultSize, + uid: defaultSize, +}; + +module.exports = () => ({ + getAllFieldSizes() { + return fieldSizes; + }, + getFieldSize(type) { + if (!type) { + throw new Error('The type is required'); + } + + const fieldSize = fieldSizes[type]; + if (!fieldSize) { + throw new Error(`Could not find field size for type ${type}`); + } + + return fieldSize; + }, +}); diff --git a/packages/core/content-manager/server/services/index.js b/packages/core/content-manager/server/services/index.js index 745c54ba25..7a416e428b 100644 --- a/packages/core/content-manager/server/services/index.js +++ b/packages/core/content-manager/server/services/index.js @@ -3,19 +3,21 @@ const components = require('./components'); const contentTypes = require('./content-types'); const dataMapper = require('./data-mapper'); +const entityManager = require('./entity-manager'); +const fieldSizes = require('./field-sizes'); const metrics = require('./metrics'); const permissionChecker = require('./permission-checker'); const permission = require('./permission'); const uid = require('./uid'); -const entityManager = require('./entity-manager'); module.exports = { components, 'content-types': contentTypes, 'data-mapper': dataMapper, + 'entity-manager': entityManager, + 'field-sizes': fieldSizes, metrics, 'permission-checker': permissionChecker, permission, uid, - 'entity-manager': entityManager, }; diff --git a/packages/core/content-manager/server/services/utils/configuration/layouts.js b/packages/core/content-manager/server/services/utils/configuration/layouts.js index ba6f0a07f2..18dd5f5c6e 100644 --- a/packages/core/content-manager/server/services/utils/configuration/layouts.js +++ b/packages/core/content-manager/server/services/utils/configuration/layouts.js @@ -1,42 +1,28 @@ 'use strict'; const _ = require('lodash'); +const { getService } = require('../../../utils'); const { isListable, hasEditableAttribute, hasRelationAttribute } = require('./attributes'); const DEFAULT_LIST_LENGTH = 4; const MAX_ROW_SIZE = 12; -const FIELD_TYPES_FULL_SIZE = ['dynamiczone', 'component', 'json', 'richtext']; -const FIELD_TYPES_SMALL = [ - 'checkbox', - 'boolean', - 'date', - 'time', - 'biginteger', - 'decimal', - 'float', - 'integer', - 'number', -]; const isAllowedFieldSize = (type, size) => { - if (FIELD_TYPES_FULL_SIZE.includes(type)) { - return size === MAX_ROW_SIZE; + const { getFieldSize } = getService('field-sizes'); + const fieldSize = getFieldSize(type); + + // Check if field was locked to another size + if (!fieldSize.isResizable && size !== fieldSize.default) { + return false; } - // validate, whether the field has 4, 6, 8 or 12 columns? + // Otherwise allow unless it's bigger than a row return size <= MAX_ROW_SIZE; }; const getDefaultFieldSize = (type) => { - if (FIELD_TYPES_FULL_SIZE.includes(type)) { - return MAX_ROW_SIZE; - } - - if (FIELD_TYPES_SMALL.includes(type)) { - return MAX_ROW_SIZE / 3; - } - - return MAX_ROW_SIZE / 2; + const { getFieldSize } = getService('field-sizes'); + return getFieldSize(type).default; }; async function createDefaultLayouts(schema) { diff --git a/packages/core/content-manager/server/utils/index.d.ts b/packages/core/content-manager/server/utils/index.d.ts index 1eed522b60..2ad4310ce1 100644 --- a/packages/core/content-manager/server/utils/index.d.ts +++ b/packages/core/content-manager/server/utils/index.d.ts @@ -3,6 +3,7 @@ import * as configuration from '../services/configuration'; import * as contentTypes from '../services/content-types'; import * as dataMapper from '../services/data-mapper'; import * as entityManager from '../services/entity-manager'; +import * as fieldSizes from '../services/field-sizes'; import * as metris from '../services/metris'; import * as permissionChecker from '../services/permission-checker'; import * as permission from '../services/permission'; @@ -15,6 +16,7 @@ type S = { ['permission-checker']: typeof permissionChecker; components: typeof components; configuration: typeof configuration; + ['field-sizes']: typeof fieldSizes; metris: typeof metris; permission: typeof permission; uid: typeof uid;