mirror of
https://github.com/strapi/strapi.git
synced 2025-08-15 12:18:38 +00:00
Add single type navigation
Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
parent
79870fbd15
commit
f5c6ef0a44
@ -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 (
|
||||
|
@ -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({
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
CustomTable.defaultProps = {
|
||||
data: [],
|
||||
|
@ -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;
|
@ -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 });
|
||||
|
@ -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 (
|
||||
<EditViewProvider
|
||||
@ -152,7 +153,7 @@ const EditView = ({
|
||||
redirectToPreviousPage={redirectToPreviousPage}
|
||||
slug={slug}
|
||||
>
|
||||
<BackHeader onClick={() => redirectToPreviousPage()} />
|
||||
<BackHeader onClick={redirectToPreviousPage} />
|
||||
<Container className="container-fluid">
|
||||
<Header />
|
||||
<div className="row" style={{ paddingTop: 3 }}>
|
||||
@ -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');
|
||||
}}
|
||||
|
@ -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 ? (
|
||||
|
@ -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;
|
||||
|
@ -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';
|
@ -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);
|
||||
|
@ -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 }) => (
|
||||
<Route
|
||||
key={path}
|
||||
|
@ -0,0 +1,37 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { Switch, Route, useRouteMatch, useParams } from 'react-router-dom';
|
||||
import { LoadingIndicatorPage } from 'strapi-helper-plugin';
|
||||
|
||||
const EditView = lazy(() => import('../EditView'));
|
||||
const EditSettingsView = lazy(() => import('../EditSettingsView'));
|
||||
|
||||
const SingleTypeRecursivePath = props => {
|
||||
const { url } = useRouteMatch();
|
||||
const { slug } = useParams();
|
||||
|
||||
const renderRoute = (routeProps, Component) => {
|
||||
return <Component {...props} {...routeProps} slug={slug} />;
|
||||
};
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: 'ctm-configurations/edit-settings/:type',
|
||||
comp: EditSettingsView,
|
||||
},
|
||||
{ path: '', comp: EditView },
|
||||
].map(({ path, comp }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={`${url}/${path}`}
|
||||
render={props => renderRoute(props, comp)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Switch>{routes}</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleTypeRecursivePath;
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"api.id": "API ID",
|
||||
"models": "Collection Types",
|
||||
"models.numbered": "Collection Types ({number})",
|
||||
"groups": "Groups",
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user