Merge pull request #16387 from strapi/chore/cm-field-sizes

Refactor content manager field sizes management
This commit is contained in:
Rémi de Juvigny 2023-04-18 10:49:54 +02:00 committed by GitHub
commit ecf439ab56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 375 additions and 241 deletions

View File

@ -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,
},
});

View File

@ -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';

View File

@ -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()
);

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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);
});
});

View File

@ -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;
})

View File

@ -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: [],

View File

@ -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;

View File

@ -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'),
};
};

View File

@ -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: [] };
}
};

View File

@ -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',

View File

@ -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();
});
});

View File

@ -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 = (
<GridItem col={6} key="size">
@ -152,7 +151,7 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => {
return (
<>
{metaFields}
{canResize && sizeField}
{isResizable && sizeField}
{hasTimePicker && timeStepField}
</>
);

View File

@ -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) => {

View File

@ -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;

View File

@ -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 (
<Router history={history}>
<QueryClientProvider client={client}>
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
<ThemeProvider theme={lightTheme}>
<DndProvider backend={HTML5Backend}>
<EditSettingsView
mainLayout={layout || mainLayout}
components={components}
isContentTypeView
slug="api::address.address"
/>
</DndProvider>
</ThemeProvider>
</IntlProvider>
</QueryClientProvider>
<Provider store={store}>
<QueryClientProvider client={client}>
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
<ThemeProvider theme={lightTheme}>
<DndProvider backend={HTML5Backend}>
<EditSettingsView
mainLayout={layout || mainLayout}
components={components}
isContentTypeView
slug="api::address.address"
/>
</DndProvider>
</ThemeProvider>
</IntlProvider>
</QueryClientProvider>
</Provider>
</Router>
);
};

View File

@ -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);
});
});

View File

@ -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 };

View File

@ -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 = [
{

View File

@ -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,

View File

@ -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),
},
};
},
};

View File

@ -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',

View File

@ -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');
});
});

View File

@ -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;
},
});

View File

@ -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,
};

View File

@ -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) {

View File

@ -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;