Add single type navigation

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2020-02-17 10:01:56 +01:00
parent 79870fbd15
commit f5c6ef0a44
14 changed files with 171 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
{
"api.id": "API ID",
"models": "Collection Types",
"models.numbered": "Collection Types ({number})",
"groups": "Groups",

View File

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

View File

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