diff --git a/packages/strapi-admin/admin/src/components/LeftMenuLinkSection/index.js b/packages/strapi-admin/admin/src/components/LeftMenuLinkSection/index.js index 39c41483e0..2bf8656e99 100644 --- a/packages/strapi-admin/admin/src/components/LeftMenuLinkSection/index.js +++ b/packages/strapi-admin/admin/src/components/LeftMenuLinkSection/index.js @@ -26,9 +26,15 @@ const LeftMenuLinksSection = ({ ); const getLinkDestination = link => { - return ['plugins', 'general'].includes(section) - ? link.destination - : `/plugins/${link.plugin}/${link.destination || link.uid}`; + if (['plugins', 'general'].includes(section)) { + return link.destination; + } + if (link.schema && link.schema.kind) { + return `/plugins/${link.plugin}/${link.schema.kind}/${link.destination || + link.uid}`; + } + + return `/plugins/${link.plugin}/${link.destination || link.uid}`; }; return ( diff --git a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js index 8fbf7f54ce..4973f632a4 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/CustomTable/index.js @@ -3,14 +3,13 @@ import PropTypes from 'prop-types'; import { withRouter } from 'react-router'; import { FormattedMessage } from 'react-intl'; import { upperFirst } from 'lodash'; -import pluginId from '../../pluginId'; import useListView from '../../hooks/useListView'; import TableHeader from './TableHeader'; import { Table, TableEmpty, TableRow } from './styledComponents'; import ActionCollapse from './ActionCollapse'; import Row from './Row'; -function CustomTable({ +const CustomTable = ({ data, headers, history: { @@ -18,13 +17,12 @@ function CustomTable({ push, }, isBulkable, -}) { +}) => { const { emitEvent, entriesToDelete, label, searchParams: { filters, _q }, - slug, } = useListView(); const redirectUrl = `redirectUrl=${pathname}${search}`; @@ -33,7 +31,7 @@ function CustomTable({ const handleGoTo = id => { emitEvent('willEditEntryFromList'); push({ - pathname: `/plugins/${pluginId}/${slug}/${id}`, + pathname: `${pathname}/${id}`, search: redirectUrl, }); }; @@ -88,7 +86,7 @@ function CustomTable({ ); -} +}; CustomTable.defaultProps = { data: [], diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/RecursivePath/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/CollectionTypeRecursivePath/index.js similarity index 92% rename from packages/strapi-plugin-content-manager/admin/src/containers/RecursivePath/index.js rename to packages/strapi-plugin-content-manager/admin/src/containers/CollectionTypeRecursivePath/index.js index deea85b569..8f5f84fa7d 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/RecursivePath/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/CollectionTypeRecursivePath/index.js @@ -7,7 +7,7 @@ const EditSettingsView = lazy(() => import('../EditSettingsView')); const ListView = lazy(() => import('../ListView')); const ListSettingsView = lazy(() => import('../ListSettingsView')); -const RecursivePath = props => { +const CollectionTypeRecursivePath = props => { const { url } = useRouteMatch(); const { slug } = useParams(); @@ -41,4 +41,4 @@ const RecursivePath = props => { ); }; -export default RecursivePath; +export default CollectionTypeRecursivePath; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js index 99ea1892c6..ffdf0009df 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/Header.js @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useLocation } from 'react-router-dom'; import { Header as PluginHeader } from '@buffetjs/custom'; import { @@ -20,6 +20,7 @@ const Header = () => { const { formatMessage, emitEvent } = useGlobalContext(); const { id } = useParams(); + const { pathname } = useLocation(); const { deleteSuccess, initialData, @@ -28,23 +29,28 @@ const Header = () => { resetData, setIsSubmitting, slug, + clearData, } = useDataManager(); + const isSingleType = pathname.split('/')[3] === 'singleType'; const currentContentTypeMainField = get( layout, ['settings', 'mainField'], 'id' ); + const currentContentTypeName = get(layout, ['schema', 'info', 'name']); + const apiId = layout.uid.split('.')[1]; const isCreatingEntry = id === 'create'; /* eslint-disable indent */ - const headerTitle = isCreatingEntry + const entryHeaderTitle = isCreatingEntry ? formatMessage({ id: `${pluginId}.containers.Edit.pluginHeader.title.new`, }) : templateObject({ mainField: currentContentTypeMainField }, initialData) .mainField; /* eslint-enable indent */ + const headerTitle = isSingleType ? currentContentTypeName : entryHeaderTitle; const getHeaderActions = () => { const headerActions = [ @@ -101,6 +107,9 @@ const Header = () => { title: { label: headerTitle && headerTitle.toString(), }, + content: isSingleType + ? `${formatMessage({ id: `${pluginId}.api.id` })} : ${apiId}` + : '', actions: getHeaderActions(), }; @@ -114,17 +123,21 @@ const Header = () => { const handleConfirmDelete = async () => { toggleWarningDelete(); setIsSubmitting(); - try { emitEvent('willDeleteEntry'); - await request(getRequestUrl(`${slug}/${id}`), { + await request(getRequestUrl(`${slug}/${initialData.id}`), { method: 'DELETE', }); strapi.notification.success(`${pluginId}.success.record.delete`); deleteSuccess(); emitEvent('didDeleteEntry'); - redirectToPreviousPage(); + + if (!isSingleType) { + redirectToPreviousPage(); + } else { + clearData(); + } } catch (err) { setIsSubmitting(false); emitEvent('didNotDeleteEntry', { error: err }); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/index.js index f43e27e95b..24b48e35ba 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/index.js @@ -39,8 +39,8 @@ const EditView = ({ formatLayoutRef.current = createAttributesLayout; // Retrieve push to programmatically navigate between views const { push } = useHistory(); - // Retrieve the search - const { search } = useLocation(); + // Retrieve the search and the location + const { search, pathname } = useLocation(); // eslint-disable-next-line react-hooks/exhaustive-deps const [reducerState, dispatch] = useReducer(reducer, initialState, () => init(initialState) @@ -129,6 +129,7 @@ const EditView = ({ .filter((_, index) => index !== 0) .join(''); const redirectToPreviousPage = () => push(redirectURL); + const isSingleType = pathname.includes('singleType'); return ( - redirectToPreviousPage()} /> +
@@ -276,7 +277,9 @@ const EditView = ({ }} icon="layout" key={`${pluginId}.link`} - url="ctm-configurations/edit-settings/content-types" + url={`${ + isSingleType ? `${pathname}/` : '' + }ctm-configurations/edit-settings/content-types`} onClick={() => { // emitEvent('willEditContentTypeLayoutFromEditView'); }} diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js index 6c9961498d..ce7d5a38c8 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/index.js @@ -1,21 +1,23 @@ -import React, { useEffect, useReducer } from 'react'; -import { Prompt, useParams } from 'react-router-dom'; -import PropTypes from 'prop-types'; import { cloneDeep, get, isEmpty, isEqual, set } from 'lodash'; +import PropTypes from 'prop-types'; +import React, { useEffect, useReducer, useState } from 'react'; +import { Prompt, useParams, useLocation } from 'react-router-dom'; import { - request, LoadingIndicatorPage, + request, useGlobalContext, } from 'strapi-helper-plugin'; -import pluginId from '../../pluginId'; import EditViewDataManagerContext from '../../contexts/EditViewDataManager'; -import createYupSchema from './utils/schema'; -import createDefaultForm from './utils/createDefaultForm'; -import getFilesToUpload from './utils/getFilesToUpload'; -import cleanData from './utils/cleanData'; -import getYupInnerErrors from './utils/getYupInnerErrors'; +import pluginId from '../../pluginId'; import init from './init'; import reducer, { initialState } from './reducer'; +import { + createYupSchema, + getYupInnerErrors, + getFilesToUpload, + createDefaultForm, + cleanData, +} from './utils'; const getRequestUrl = path => `/${pluginId}/explorer/${path}`; @@ -26,6 +28,7 @@ const EditViewDataManagerProvider = ({ slug, }) => { const { id } = useParams(); + const { pathname } = useLocation(); // Retrieve the search const [reducerState, dispatch] = useReducer(reducer, initialState, init); const { @@ -37,13 +40,12 @@ const EditViewDataManagerProvider = ({ shouldShowLoadingState, shouldCheckErrors, } = reducerState.toJS(); - + const [isCreatingEntry, setIsCreatingEntry] = useState(id === 'create'); const currentContentTypeLayout = get(allLayoutData, ['contentType'], {}); const abortController = new AbortController(); const { signal } = abortController; - const isCreatingEntry = id === 'create'; - const { emitEvent, formatMessage } = useGlobalContext(); + const isSingleType = pathname.split('/')[3] === 'singleType'; useEffect(() => { if (!isLoading) { @@ -55,7 +57,7 @@ const EditViewDataManagerProvider = ({ useEffect(() => { const fetchData = async () => { try { - const data = await request(getRequestUrl(`${slug}/${id}`), { + const data = await request(getRequestUrl(`${slug}/${id || ''}`), { method: 'GET', signal, }); @@ -65,9 +67,12 @@ const EditViewDataManagerProvider = ({ data, }); } catch (err) { - if (err.code !== 20) { + if (id && err.code !== 20) { strapi.notification.error(`${pluginId}.error.record.fetch`); } + if (!id && err.response.status === 404) { + setIsCreatingEntry(true); + } } }; @@ -108,7 +113,7 @@ const EditViewDataManagerProvider = ({ abortController.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id, slug]); + }, [id, slug, isCreatingEntry]); const addComponentToDynamicZone = ( keys, @@ -246,7 +251,15 @@ const EditViewDataManagerProvider = ({ // Change the request helper default headers so we can pass a FormData const headers = {}; const method = isCreatingEntry ? 'POST' : 'PUT'; - const endPoint = isCreatingEntry ? slug : `${slug}/${id}`; + let endPoint; + + if (isCreatingEntry) { + endPoint = slug; + } else if (modifiedData) { + endPoint = `${slug}/${modifiedData.id}`; + } else { + endPoint = `${slug}/${id}`; + } emitEvent(isCreatingEntry ? 'willCreateEntry' : 'willEditEntry'); @@ -267,7 +280,13 @@ const EditViewDataManagerProvider = ({ dispatch({ type: 'SUBMIT_SUCCESS', }); - redirectToPreviousPage(); + strapi.notification.success(`${pluginId}.success.record.save`); + + if (isSingleType) { + setIsCreatingEntry(false); + } else { + redirectToPreviousPage(); + } } catch (err) { console.error({ err }); const error = get( @@ -375,6 +394,31 @@ const EditViewDataManagerProvider = ({ dispatch({ type: 'IS_SUBMITTING', value }); }; + const deleteSuccess = () => { + dispatch({ + type: 'DELETE_SUCCEEDED', + }); + }; + + const resetData = () => { + dispatch({ + type: 'RESET_DATA', + }); + }; + + const clearData = () => { + dispatch({ + type: 'SET_DEFAULT_MODIFIED_DATA_STRUCTURE', + contentTypeDataStructure: {}, + }); + }; + + const triggerFormValidation = () => { + dispatch({ + type: 'TRIGGER_FORM_VALIDATION', + }); + }; + const showLoader = !isCreatingEntry && isLoading; return ( @@ -386,11 +430,8 @@ const EditViewDataManagerProvider = ({ addRepeatableComponentToField, allLayoutData, checkFormErrors, - deleteSuccess: () => { - dispatch({ - type: 'DELETE_SUCCEEDED', - }); - }, + clearData, + deleteSuccess, formErrors, initialData, layout: currentContentTypeLayout, @@ -405,19 +446,11 @@ const EditViewDataManagerProvider = ({ removeComponentFromDynamicZone, removeComponentFromField, removeRepeatableField, - resetData: () => { - dispatch({ - type: 'RESET_DATA', - }); - }, + resetData, setIsSubmitting, shouldShowLoadingState, slug, - triggerFormValidation: () => { - dispatch({ - type: 'TRIGGER_FORM_VALIDATION', - }); - }, + triggerFormValidation, }} > {showLoader ? ( diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js index b343e9b76d..837be83065 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/reducer.js @@ -232,7 +232,9 @@ const reducer = (state, action) => { .update('shouldShowLoadingState', () => false); case 'SUBMIT_SUCCESS': case 'DELETE_SUCCEEDED': - return state.update('initialData', () => state.get('modifiedData')); + return state + .update('isLoading', () => false) + .update('initialData', () => state.get('modifiedData')); case 'TRIGGER_FORM_VALIDATION': return state.update('shouldCheckErrors', v => { const hasErrors = state.get('formErrors').keySeq().size > 0; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/utils/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/utils/index.js new file mode 100644 index 0000000000..ca42663d3c --- /dev/null +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditViewDataManagerProvider/utils/index.js @@ -0,0 +1,5 @@ +export { default as cleanData } from './cleanData'; +export { default as createDefaultForm } from './createDefaultForm'; +export { default as getFilesToUpload } from './getFilesToUpload'; +export { default as getYupInnerErrors } from './getYupInnerErrors'; +export { default as createYupSchema } from './schema'; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Initializer/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/Initializer/index.js index 251e20a149..4f597068dd 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Initializer/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Initializer/index.js @@ -23,12 +23,18 @@ const Initializer = ({ updatePlugin }) => { try { const { data } = await request(requestURL, { method: 'GET' }); + // Two thinks to know here: + // First, we group content types by schema.kind to get an object with two separated content types (singleTypes, collectionTypes) + // Then, we sort by name to keep collection types at the first position everytime. + // As all content types are sort by name, if a single type name start with abc, the single types section will be at the first position. + // However, we want to keep collection types at the first position in the admin menu ref.current( pluginId, 'leftMenuSections', chain(data) .groupBy('schema.kind') .map((value, key) => ({ name: key, links: value })) + .sortBy('name') .value() ); ref.current(pluginId, 'isReady', true); diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/Main/index.js b/packages/strapi-plugin-content-manager/admin/src/containers/Main/index.js index ce859f9e2e..4811249c61 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/Main/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/Main/index.js @@ -25,7 +25,12 @@ import reducer from './reducer'; import makeSelectMain from './selectors'; const EditSettingsView = lazy(() => import('../EditSettingsView')); -const RecursivePath = lazy(() => import('../RecursivePath')); +const CollectionTypeRecursivePath = lazy(() => + import('../CollectionTypeRecursivePath') +); +const SingleTypeRecursivePath = lazy(() => + import('../SingleTypeRecursivePath') +); function Main({ deleteLayout, @@ -45,7 +50,7 @@ function Main({ strapi.useInjectReducer({ key: 'main', reducer, pluginId }); const { emitEvent } = useGlobalContext(); - const slug = pathname.split('/')[3]; + const slug = pathname.split('/')[4]; const getDataRef = useRef(); const getLayoutRef = useRef(); const resetPropsRef = useRef(); @@ -123,7 +128,8 @@ function Main({ path: 'ctm-configurations/edit-settings/:type/:componentSlug', comp: EditSettingsView, }, - { path: ':slug', comp: RecursivePath }, + { path: 'singleType/:slug', comp: SingleTypeRecursivePath }, + { path: 'collectionType/:slug', comp: CollectionTypeRecursivePath }, ].map(({ path, comp }) => ( import('../EditView')); +const EditSettingsView = lazy(() => import('../EditSettingsView')); + +const SingleTypeRecursivePath = props => { + const { url } = useRouteMatch(); + const { slug } = useParams(); + + const renderRoute = (routeProps, Component) => { + return ; + }; + + const routes = [ + { + path: 'ctm-configurations/edit-settings/:type', + comp: EditSettingsView, + }, + { path: '', comp: EditView }, + ].map(({ path, comp }) => ( + renderRoute(props, comp)} + /> + )); + + return ( + }> + {routes} + + ); +}; + +export default SingleTypeRecursivePath; diff --git a/packages/strapi-plugin-content-manager/admin/src/translations/en.json b/packages/strapi-plugin-content-manager/admin/src/translations/en.json index c04f3811bb..59c7e78886 100644 --- a/packages/strapi-plugin-content-manager/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-manager/admin/src/translations/en.json @@ -1,4 +1,5 @@ { + "api.id": "API ID", "models": "Collection Types", "models.numbered": "Collection Types ({number})", "groups": "Groups", diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/DataManagerProvider/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/DataManagerProvider/index.js index c52dd569ee..19fbee0932 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/DataManagerProvider/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/DataManagerProvider/index.js @@ -482,6 +482,7 @@ const DataManagerProvider = ({ allIcons, children }) => { chain(data) .groupBy('schema.kind') .map((value, key) => ({ name: key, links: value })) + .sortBy('name') .value() ); } catch (err) { diff --git a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js index d75b3409d3..bf8d864d47 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/containers/FormModal/index.js @@ -88,7 +88,7 @@ const FormModal = () => { const dynamicZoneTarget = query.get('dynamicZoneTarget'); const forTarget = query.get('forTarget'); const modalType = query.get('modalType'); - const contentTypeKind = query.get('kind'); + const kind = query.get('kind'); const targetUid = query.get('targetUid'); const settingType = query.get('settingType'); const headerId = query.get('headerId'); @@ -127,7 +127,7 @@ const FormModal = () => { actionType, attributeName, attributeType, - contentTypeKind, + kind, dynamicZoneTarget, forTarget, modalType, @@ -626,7 +626,7 @@ const FormModal = () => { // Create the content type schema if (isCreating) { createSchema( - { ...modifiedData, kind: state.contentTypeKind }, + { ...modifiedData, kind: state.kind }, state.modalType, uid );