Move CM menu

Signed-off-by: HichamELBSI <elabbassih@gmail.com>
This commit is contained in:
HichamELBSI 2021-06-14 23:30:10 +02:00
parent 923cb57fbe
commit a68384567f
37 changed files with 372 additions and 302 deletions

View File

@ -6,7 +6,6 @@ const { combineReducers, createStore } = require('redux');
const reducers = {
language: jest.fn(() => ({ locale: 'en' })),
menu: jest.fn(() => ({
collectionTypesSectionLinks: [],
generalSectionLinks: [
{
icon: 'list',
@ -43,7 +42,6 @@ const reducers = {
notificationsCount: 0,
},
],
singleTypesSectionLinks: [],
pluginsSectionLinks: [],
isLoading: true,
})),

View File

@ -1,14 +1,4 @@
import {
TOGGLE_IS_LOADING,
SET_CT_OR_ST_LINKS,
SET_SECTION_LINKS,
UNSET_IS_LOADING,
} from './constants';
export const setCtOrStLinks = (authorizedCtLinks, authorizedStLinks, contentTypeSchemas) => ({
type: SET_CT_OR_ST_LINKS,
data: { authorizedCtLinks, authorizedStLinks, contentTypeSchemas },
});
import { TOGGLE_IS_LOADING, SET_SECTION_LINKS, UNSET_IS_LOADING } from './constants';
export const setSectionLinks = (authorizedGeneralLinks, authorizedPluginLinks) => ({
type: SET_SECTION_LINKS,

View File

@ -1,4 +1,3 @@
export const SET_CT_OR_ST_LINKS = 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS';
export const SET_SECTION_LINKS = 'StrapiAdmin/LeftMenu/SET_SECTION_LINKS';
export const TOGGLE_IS_LOADING = 'StrapiAdmin/LeftMenu/TOGGLE_IS_LOADING';
export const UNSET_IS_LOADING = 'StrapiAdmin/LeftMenu/UNSET_IS_LOADING';

View File

@ -11,22 +11,11 @@ const LeftMenu = ({ setUpdateMenu }) => {
const { shouldUpdateStrapi } = useAppInfos();
const { plugins } = useStrapiApp();
const {
state: {
isLoading,
collectionTypesSectionLinks,
singleTypesSectionLinks,
generalSectionLinks,
pluginsSectionLinks,
},
state: { isLoading, generalSectionLinks, pluginsSectionLinks },
toggleLoading,
generateMenu,
} = useMenuSections(plugins, shouldUpdateStrapi);
const filteredCollectionTypeLinks = collectionTypesSectionLinks.filter(
({ isDisplayed }) => isDisplayed
);
const filteredSingleTypeLinks = singleTypesSectionLinks.filter(({ isDisplayed }) => isDisplayed);
// This effect is really temporary until we create the menu api
// We need this because we need to regenerate the links when the settings are being changed
// in the content manager configurations list
@ -43,25 +32,6 @@ const LeftMenu = ({ setUpdateMenu }) => {
<Loader show={isLoading} />
<Header />
<LinksContainer>
{filteredCollectionTypeLinks.length > 0 && (
<LinksSection
section="collectionType"
name="collectionType"
links={filteredCollectionTypeLinks}
location={location}
searchable
/>
)}
{filteredSingleTypeLinks.length > 0 && (
<LinksSection
section="singleType"
name="singleType"
links={filteredSingleTypeLinks}
location={location}
searchable
/>
)}
{pluginsSectionLinks.length > 0 && (
<LinksSection
section="plugins"

View File

@ -1,15 +1,9 @@
/* eslint-disable consistent-return */
import produce from 'immer';
import adminPermissions from '../../permissions';
import {
SET_CT_OR_ST_LINKS,
SET_SECTION_LINKS,
TOGGLE_IS_LOADING,
UNSET_IS_LOADING,
} from './constants';
import { SET_SECTION_LINKS, TOGGLE_IS_LOADING, UNSET_IS_LOADING } from './constants';
const initialState = {
collectionTypesSectionLinks: [],
generalSectionLinks: [
{
icon: 'list',
@ -38,7 +32,6 @@ const initialState = {
notificationsCount: 0,
},
],
singleTypesSectionLinks: [],
pluginsSectionLinks: [],
isLoading: true,
};
@ -46,13 +39,6 @@ const initialState = {
const reducer = (state = initialState, action) =>
produce(state, draftState => {
switch (action.type) {
case SET_CT_OR_ST_LINKS: {
const { authorizedCtLinks, authorizedStLinks } = action.data;
draftState.collectionTypesSectionLinks = authorizedCtLinks;
draftState.singleTypesSectionLinks = authorizedStLinks;
break;
}
case SET_SECTION_LINKS: {
const { authorizedGeneralLinks, authorizedPluginLinks } = action.data;
draftState.generalSectionLinks = authorizedGeneralLinks;

View File

@ -1,5 +1,5 @@
import reducer, { initialState } from '../reducer';
import { SET_CT_OR_ST_LINKS, SET_SECTION_LINKS, TOGGLE_IS_LOADING } from '../constants';
import { SET_SECTION_LINKS, TOGGLE_IS_LOADING } from '../constants';
describe('ADMIN | LeftMenu | reducer', () => {
describe('DEFAULT_ACTION', () => {
@ -51,27 +51,4 @@ describe('ADMIN | LeftMenu | reducer', () => {
expect(actual).toEqual(expected);
});
});
describe('SET_CT_OR_ST_LINKS', () => {
it('sets the generalSectionLinks and the pluginsSectionLinks with the action', () => {
const state = { ...initialState };
const action = {
type: SET_CT_OR_ST_LINKS,
data: {
authorizedCtLinks: ['authorizd', 'ct-links'],
authorizedStLinks: ['authorizd', 'st-links'],
},
};
const expected = {
...initialState,
collectionTypesSectionLinks: ['authorizd', 'ct-links'],
singleTypesSectionLinks: ['authorizd', 'st-links'],
};
const actual = reducer(state, action);
expect(actual).toEqual(expected);
});
});
});

View File

@ -1,15 +1,13 @@
import { useEffect, useRef } from 'react';
import { useNotification, useRBACProvider } from '@strapi/helper-plugin';
import { useRBACProvider } from '@strapi/helper-plugin';
import { useSelector, useDispatch } from 'react-redux';
import getCtOrStLinks from './utils/getCtOrStLinks';
import getPluginSectionLinks from './utils/getPluginSectionLinks';
import getGeneralLinks from './utils/getGeneralLinks';
import { setCtOrStLinks, setSectionLinks, toggleIsLoading, unsetIsLoading } from './actions';
import { setSectionLinks, toggleIsLoading, unsetIsLoading } from './actions';
import toPluginLinks from './utils/toPluginLinks';
import selectMenuLinks from './selectors';
const useMenuSections = (plugins, shouldUpdateStrapi) => {
const toggleNotification = useNotification();
const state = useSelector(selectMenuLinks);
const dispatch = useDispatch();
const { allPermissions } = useRBACProvider();
@ -26,10 +24,6 @@ const useMenuSections = (plugins, shouldUpdateStrapi) => {
const resolvePermissions = async (permissions = allPermissions) => {
const pluginsSectionLinks = toPluginLinks(pluginsRef.current);
const { authorizedCtLinks, authorizedStLinks, contentTypes } = await getCtOrStLinks(
permissions,
toggleNotification
);
const authorizedPluginSectionLinks = await getPluginSectionLinks(
permissions,
@ -42,7 +36,6 @@ const useMenuSections = (plugins, shouldUpdateStrapi) => {
shouldUpdateStrapiRef.current
);
dispatch(setCtOrStLinks(authorizedCtLinks, authorizedStLinks, contentTypes));
dispatch(setSectionLinks(authorizedGeneralSectionLinks, authorizedPluginSectionLinks));
dispatch(unsetIsLoading());
};

View File

@ -1,2 +0,0 @@
// eslint-disable-next-line import/prefer-default-export
export { default as generateModelsLinks } from './generateModelsLinks';

View File

@ -1,7 +1,9 @@
const selectMenuLinks = state => {
const menuState = state.menu;
import pluginId from '../../pluginId';
return menuState.collectionTypesSectionLinks;
const selectMenuLinks = state => {
const cmState = state[`${pluginId}_app`];
return cmState.collectionTypeLinks;
};
export default selectMenuLinks;

View File

@ -23,23 +23,23 @@ const mergeParams = (initialParams, params) => {
}, {});
};
const getDeleteRedirectionLink = (links, slug, rawQuery) => {
const matchingLink = links.find(({ destination }) => destination.includes(slug));
const getRedirectionLink = (links, slug, rawQuery) => {
const matchingLink = links.find(({ to }) => to.includes(slug));
if (!matchingLink) {
return '/';
}
const { destination, search } = matchingLink;
const { to, search } = matchingLink;
const searchQueryParams = parse(search);
const currentQueryParams = parse(rawQuery.substring(1));
const mergedParams = mergeParams(searchQueryParams, currentQueryParams);
const link = `${destination}?${stringify(mergedParams, { encode: false })}`;
const link = `${to}?${stringify(mergedParams, { encode: false })}`;
return link;
};
export default getDeleteRedirectionLink;
export default getRedirectionLink;
export { mergeParams };

View File

@ -5,11 +5,11 @@ describe('CONTENT MANAGER | Containers | CollectionTypeFormWrapper | utils ', ()
it('should return an when no links is matching the slug', () => {
const links = [
{
destination: '/cm/foo',
to: '/cm/foo',
search: 'page=1&pageSize=10',
},
{
destination: '/cm/bar',
to: '/cm/bar',
search: 'page=1&pageSize=10',
},
];
@ -21,11 +21,11 @@ describe('CONTENT MANAGER | Containers | CollectionTypeFormWrapper | utils ', ()
it('should not mutate the link when the rawQuery is empty', () => {
const links = [
{
destination: '/cm/foo',
to: '/cm/foo',
search: 'page=1&pageSize=10',
},
{
destination: '/cm/bar',
to: '/cm/bar',
search: 'page=1&pageSize=10',
},
];
@ -38,11 +38,11 @@ describe('CONTENT MANAGER | Containers | CollectionTypeFormWrapper | utils ', ()
it('should merge the current search with the link original one', () => {
const links = [
{
destination: '/cm/foo',
to: '/cm/foo',
search: 'page=1&pageSize=10&plugins[i18n][locale]=en',
},
{
destination: '/cm/bar',
to: '/cm/bar',
search: 'page=1&pageSize=10&plugins[i18n][locale]=en',
},
];

View File

@ -10,6 +10,7 @@ import pluginId from './pluginId';
import pluginLogo from './assets/images/logo.svg';
import reducers from './reducers';
import trads from './translations';
import pluginPermissions from './permissions';
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const icon = pluginPkg.strapi.icon;
@ -32,6 +33,20 @@ export default {
name,
pluginLogo,
trads,
menu: {
pluginsSectionLinks: [
{
destination: `/plugins/${pluginId}`,
icon,
label: {
id: `${pluginId}.plugin.name`,
defaultMessage: 'Content manager',
},
name,
permissions: pluginPermissions.main,
},
],
},
});
},
boot() {},

View File

@ -0,0 +1,58 @@
/**
*
* LeftMenu
*
*/
import React, { useMemo } from 'react';
import { LeftMenuList, colors, sizes } from '@strapi/helper-plugin';
import styled from 'styled-components';
import { useSelector, shallowEqual } from 'react-redux';
import getTrad from '../../../utils/getTrad';
import { makeSelectModelLinks } from '../selectors';
const Wrapper = styled.div`
width: 100%;
min-height: calc(100vh - ${sizes.header.height});
background-color: ${colors.leftMenu.mediumGrey};
padding-top: 3.1rem;
padding-left: 2rem;
padding-right: 2rem;
`;
const LeftMenu = () => {
const modelLinksSelector = useMemo(makeSelectModelLinks, []);
const { collectionTypeLinks, singleTypeLinks } = useSelector(
state => modelLinksSelector(state),
shallowEqual
);
const data = [
{
name: 'models',
title: {
id: getTrad('components.LeftMenu.collection-types.'),
},
searchable: true,
links: collectionTypeLinks,
},
{
name: 'singleTypes',
title: {
id: getTrad('components.LeftMenu.single-types.'),
},
searchable: true,
links: singleTypeLinks,
},
];
return (
<Wrapper className="col-md-3">
{data.map(list => {
return <LeftMenuList {...list} key={list.name} />;
})}
</Wrapper>
);
};
export default LeftMenu;

View File

@ -1,13 +1,12 @@
import { GET_DATA, GET_DATA_SUCCEEDED, RESET_PROPS } from './constants';
import { GET_DATA, RESET_PROPS, SET_CONTENT_TYPE_LINKS } from './constants';
export const getData = () => ({
type: GET_DATA,
});
export const getDataSucceeded = (models, components) => ({
type: GET_DATA_SUCCEEDED,
components,
models,
});
export const resetProps = () => ({ type: RESET_PROPS });
export const setContentTypeLinks = (authorizedCtLinks, authorizedStLinks, models, components) => ({
type: SET_CONTENT_TYPE_LINKS,
data: { authorizedCtLinks, authorizedStLinks, components, contentTypeSchemas: models },
});

View File

@ -1,3 +1,3 @@
export const GET_DATA = 'ContentManager/App/GET_DATA';
export const GET_DATA_SUCCEEDED = 'ContentManager/App/GET_DATA_SUCCEEDED';
export const RESET_PROPS = 'ContentManager/App/RESET_PROPS';
export const SET_CONTENT_TYPE_LINKS = 'ContentManager/App/SET_CONTENT_TYPE_LINKS';

View File

@ -1,103 +1,59 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { Switch, Route } from 'react-router-dom';
import {
CheckPagePermissions,
LoadingIndicatorPage,
NotFound,
request,
useNotification,
} from '@strapi/helper-plugin';
import React from 'react';
import { Switch, Route, useRouteMatch, Redirect } from 'react-router-dom';
import { CheckPagePermissions, LoadingIndicatorPage, NotFound } from '@strapi/helper-plugin';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import pluginId from '../../pluginId';
import pluginPermissions from '../../permissions';
import { getRequestUrl } from '../../utils';
import DragLayer from '../../components/DragLayer';
import CollectionTypeRecursivePath from '../CollectionTypeRecursivePath';
import ComponentSettingsView from '../ComponentSetttingsView';
import SingleTypeRecursivePath from '../SingleTypeRecursivePath';
import { getData, getDataSucceeded, resetProps } from './actions';
import makeSelectApp from './selectors';
import LeftMenu from './LeftMenu';
import useModels from './useModels';
function Main({ getData, getDataSucceeded, isLoading, resetProps }) {
const toggleNotification = useNotification();
const App = () => {
const contentTypeMatch = useRouteMatch(`/plugins/${pluginId}/:kind/:uid`);
const { status, collectionTypeLinks, singleTypeLinks } = useModels();
const models = [...collectionTypeLinks, ...singleTypeLinks];
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
const fetchData = async signal => {
getData();
try {
const [{ data: components }, { data: models }] = await Promise.all(
['components', 'content-types'].map(endPoint =>
request(getRequestUrl(endPoint), { method: 'GET', signal })
)
);
getDataSucceeded(models, components);
} catch (err) {
console.error(err);
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
}
};
fetchData(signal);
return () => {
abortController.abort();
resetProps();
};
}, [getData, getDataSucceeded, resetProps, toggleNotification]);
if (isLoading) {
if (status === 'loading') {
return <LoadingIndicatorPage />;
}
if (!contentTypeMatch && models.length > 0) {
return <Redirect to={`${models[0].to}${models[0].search ? `?${models[0].search}` : ''}`} />;
}
return (
<DndProvider backend={HTML5Backend}>
<DragLayer />
<Switch>
<Route path={`/plugins/${pluginId}/components/:uid/configurations/edit`}>
<CheckPagePermissions permissions={pluginPermissions.componentsConfigurations}>
<ComponentSettingsView />
</CheckPagePermissions>
</Route>
<Route
path={`/plugins/${pluginId}/collectionType/:slug`}
component={CollectionTypeRecursivePath}
/>
<Route path={`/plugins/${pluginId}/singleType/:slug`} component={SingleTypeRecursivePath} />
<Route path="" component={NotFound} />
</Switch>
<div className="container-fluid">
<div className="row">
<LeftMenu />
<div className="col-md-9 content" style={{ padding: '0 30px' }}>
<Switch>
<Route path={`/plugins/${pluginId}/components/:uid/configurations/edit`}>
<CheckPagePermissions permissions={pluginPermissions.componentsConfigurations}>
<ComponentSettingsView />
</CheckPagePermissions>
</Route>
<Route
path={`/plugins/${pluginId}/collectionType/:slug`}
component={CollectionTypeRecursivePath}
/>
<Route
path={`/plugins/${pluginId}/singleType/:slug`}
component={SingleTypeRecursivePath}
/>
<Route path="" component={NotFound} />
</Switch>
</div>
</div>
</div>
</DndProvider>
);
}
Main.propTypes = {
getData: PropTypes.func.isRequired,
getDataSucceeded: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
resetProps: PropTypes.func.isRequired,
};
const mapStateToProps = makeSelectApp();
export function mapDispatchToProps(dispatch) {
return bindActionCreators(
{
getData,
getDataSucceeded,
resetProps,
},
dispatch
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);
export default compose(withConnect)(Main);
export default App;

View File

@ -4,30 +4,34 @@
*/
/* eslint-disable consistent-return */
import produce from 'immer';
import { GET_DATA, GET_DATA_SUCCEEDED, RESET_PROPS } from './constants';
import { GET_DATA, RESET_PROPS, SET_CONTENT_TYPE_LINKS } from './constants';
const initialState = {
components: [],
isLoading: true,
status: 'loading',
models: [],
collectionTypeLinks: [],
singleTypeLinks: [],
};
const mainReducer = (state = initialState, action) =>
produce(state, draftState => {
switch (action.type) {
case GET_DATA: {
draftState.isLoading = true;
break;
}
case GET_DATA_SUCCEEDED: {
draftState.isLoading = false;
draftState.components = action.components;
draftState.models = action.models;
draftState.status = 'loading';
break;
}
case RESET_PROPS: {
return initialState;
}
case SET_CONTENT_TYPE_LINKS: {
draftState.collectionTypeLinks = action.data.authorizedCtLinks;
draftState.singleTypeLinks = action.data.authorizedStLinks;
draftState.components = action.data.components;
draftState.models = action.data.contentTypeSchemas;
draftState.status = 'resolved';
break;
}
default:
return draftState;
}

View File

@ -11,10 +11,23 @@ const makeSelectApp = () =>
return substate;
});
const makeSelectModels = () => createSelector(selectAppDomain(), state => state.models);
const makeSelectModelLinks = () =>
createSelector(selectAppDomain(), state => ({
collectionTypeLinks: state.collectionTypeLinks,
singleTypeLinks: state.singleTypeLinks,
}));
const makeSelectModelAndComponentSchemas = () =>
createSelector(selectAppDomain(), ({ components, models }) => ({
schemas: [...components, ...models],
}));
export default makeSelectApp;
export { makeSelectModelAndComponentSchemas, selectAppDomain };
export {
makeSelectModelAndComponentSchemas,
makeSelectModelLinks,
makeSelectModels,
selectAppDomain,
};

View File

@ -1,5 +1,5 @@
import produce from 'immer';
import { getData, getDataSucceeded, resetProps } from '../actions';
import { getData, setContentTypeLinks, resetProps } from '../actions';
import mainReducer from '../reducer';
describe('Content Manager | App | reducer', () => {
@ -8,8 +8,10 @@ describe('Content Manager | App | reducer', () => {
beforeEach(() => {
state = {
components: [],
isLoading: true,
status: 'loading',
models: [],
collectionTypeLinks: [],
singleTypeLinks: [],
};
});
@ -18,10 +20,10 @@ describe('Content Manager | App | reducer', () => {
});
it('should handle the getData action correctly', () => {
state.isLoading = false;
state.status = 'resolved';
const expected = produce(state, draft => {
draft.isLoading = true;
draft.status = 'loading';
});
expect(mainReducer(state, getData())).toEqual(expected);
@ -29,12 +31,19 @@ describe('Content Manager | App | reducer', () => {
it('should handle the getData action correctly', () => {
const expected = produce(state, draft => {
draft.isLoading = false;
draft.status = 'resolved';
draft.components = ['test'];
draft.models = ['test'];
draft.collectionTypeLinks = ['authorizedCt'];
draft.singleTypeLinks = ['authorizedSt'];
});
expect(mainReducer(state, getDataSucceeded(['test'], ['test']))).toEqual(expected);
expect(
mainReducer(
state,
setContentTypeLinks(['authorizedCt'], ['authorizedSt'], ['test'], ['test'])
)
).toEqual(expected);
});
it('should handle the resetProps action correctly', () => {
@ -43,7 +52,9 @@ describe('Content Manager | App | reducer', () => {
expect(mainReducer(state, resetProps())).toEqual({
components: [],
models: [],
isLoading: true,
collectionTypeLinks: [],
singleTypeLinks: [],
status: 'loading',
});
});
});

View File

@ -0,0 +1,63 @@
import { request, useNotification, useRBACProvider } from '@strapi/helper-plugin';
import { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getData, resetProps, setContentTypeLinks } from './actions';
import { getRequestUrl } from '../../utils';
import { selectAppDomain } from './selectors';
import getContentTypeLinks from './utils/getContentTypeLinks';
const useModels = () => {
const dispatch = useDispatch();
const toggleNotification = useNotification();
const state = useSelector(selectAppDomain());
const fetchDataRef = useRef();
const { allPermissions } = useRBACProvider();
const fetchData = async signal => {
dispatch(getData());
try {
const [{ data: components }, { data: models }] = await Promise.all(
['components', 'content-types'].map(endPoint =>
request(getRequestUrl(endPoint), { method: 'GET', signal })
)
);
const { authorizedCtLinks, authorizedStLinks } = await getContentTypeLinks(
models,
allPermissions,
toggleNotification
);
const actionToDispatch = setContentTypeLinks(
authorizedCtLinks,
authorizedStLinks,
models,
components
);
dispatch(actionToDispatch);
} catch (err) {
console.error(err);
toggleNotification({ type: 'warning', message: { id: 'notification.error' } });
}
};
fetchDataRef.current = fetchData;
useEffect(() => {
const abortController = new AbortController();
const { signal } = abortController;
fetchDataRef.current(signal);
return () => {
abortController.abort();
dispatch(resetProps());
};
}, [dispatch, toggleNotification]);
return state;
};
export default useModels;

View File

@ -0,0 +1,13 @@
import { hasPermissions } from '@strapi/helper-plugin';
/**
* This function resolves an array of Promises<boolean>
* It puts at a specific index the status of a specific permission.
* While this might look weird, we then iterate on this array
* and check the different CT/ST/general/plugin sections
* and make an index based comparisons
*/
const checkPermissions = (userPermissions, permissionsToCheck) =>
permissionsToCheck.map(({ permissions }) => hasPermissions(userPermissions, permissions));
export default checkPermissions;

View File

@ -30,12 +30,15 @@ const generateLinks = (links, type, configurations = []) => {
}
return {
icon: 'circle',
destination: `/plugins/content-manager/${link.kind}/${link.uid}`,
isDisplayed: true,
label: link.info.label,
permissions,
search,
kind: link.kind,
title: link.info.label,
to: `/plugins/content-manager/${link.kind}/${link.uid}`,
uid: link.uid,
// Used for the list item key in the helper plugin
name: link.uid,
isDisplayed: link.isDisplayed,
};
});
};

View File

@ -2,17 +2,14 @@ import { request } from '@strapi/helper-plugin';
import generateModelsLinks from './generateModelsLinks';
import checkPermissions from './checkPermissions';
const getCtOrStLinks = async (userPermissions, toggleNotification) => {
const requestURL = '/content-manager/content-types';
const getContentTypeLinks = async (models, userPermissions, toggleNotification) => {
try {
const {
data: contentTypeConfigurations,
} = await request('/content-manager/content-types-settings', { method: 'GET' });
const { data } = await request(requestURL, { method: 'GET' });
const { collectionTypesSectionLinks, singleTypesSectionLinks } = generateModelsLinks(
data,
models,
contentTypeConfigurations
);
@ -33,7 +30,7 @@ const getCtOrStLinks = async (userPermissions, toggleNotification) => {
(_, index) => stLinksPermissions[index]
);
return { authorizedCtLinks, authorizedStLinks, contentTypes: data };
return { authorizedCtLinks, authorizedStLinks };
} catch (err) {
console.error(err);
@ -46,4 +43,4 @@ const getCtOrStLinks = async (userPermissions, toggleNotification) => {
}
};
export default getCtOrStLinks;
export default getContentTypeLinks;

View File

@ -0,0 +1,17 @@
import checkPermissions from '../checkPermissions';
jest.mock('@strapi/helper-plugin', () => ({
hasPermissions: () => Promise.resolve(true),
}));
describe('checkPermissions', () => {
it('creates an array of boolean corresponding to the permission state', async () => {
const userPermissions = {};
const permissions = [{ permissions: {} }, { permissions: {} }];
const expected = [true, true];
const actual = await Promise.all(checkPermissions(userPermissions, permissions));
expect(actual).toEqual(expected);
});
});

View File

@ -20,7 +20,6 @@ describe('ADMIN | LeftMenu | utils', () => {
const data = [
{
isDisplayed: true,
kind: 'collectionType',
uid: 'application::address.address',
info: {
@ -47,10 +46,8 @@ describe('ADMIN | LeftMenu | utils', () => {
const expected = [
{
icon: 'circle',
destination: '/plugins/content-manager/collectionType/application::address.address',
to: '/plugins/content-manager/collectionType/application::address.address',
isDisplayed: true,
label: 'Addresses',
search: `page=1&pageSize=2&_sort=name:ASC`,
permissions: [
{
@ -62,13 +59,19 @@ describe('ADMIN | LeftMenu | utils', () => {
subject: 'application::address.address',
},
],
kind: 'collectionType',
title: 'Addresses',
uid: 'application::address.address',
name: 'application::address.address',
},
{
icon: 'circle',
destination: '/plugins/content-manager/singleType/application::test1.test1',
to: '/plugins/content-manager/singleType/application::test1.test1',
isDisplayed: true,
label: 'Test 1',
search: null,
kind: 'singleType',
title: 'Test 1',
uid: 'application::test1.test1',
name: 'application::test1.test1',
permissions: [
{
action: 'plugins::content-manager.explorer.create',
@ -118,11 +121,13 @@ describe('ADMIN | LeftMenu | utils', () => {
const expected = {
collectionTypesSectionLinks: [
{
icon: 'circle',
destination: '/plugins/content-manager/collectionType/application::address.address',
isDisplayed: true,
label: 'Addresses',
search: null,
kind: 'collectionType',
title: 'Addresses',
to: '/plugins/content-manager/collectionType/application::address.address',
uid: 'application::address.address',
name: 'application::address.address',
permissions: [
{
action: 'plugins::content-manager.explorer.create',
@ -137,11 +142,13 @@ describe('ADMIN | LeftMenu | utils', () => {
],
singleTypesSectionLinks: [
{
icon: 'circle',
destination: '/plugins/content-manager/singleType/application::test1.test1',
isDisplayed: true,
label: 'Test 1',
kind: 'singleType',
search: null,
title: 'Test 1',
to: '/plugins/content-manager/singleType/application::test1.test1',
uid: 'application::test1.test1',
name: 'application::test1.test1',
permissions: [
{
action: 'plugins::content-manager.explorer.read',

View File

@ -1,5 +1,5 @@
import { request, hasPermissions } from '@strapi/helper-plugin';
import getCtOrStLinks from '../getCtOrStLinks';
import getContentTypeLinks from '../getContentTypeLinks';
jest.mock('@strapi/helper-plugin');
@ -128,7 +128,7 @@ describe('checkPermissions', () => {
authorizedStLinks: [],
contentTypes: data,
};
const actual = await getCtOrStLinks(userPermissions);
const actual = await getContentTypeLinks(userPermissions);
expect(actual).toEqual(expected);
});
@ -142,7 +142,7 @@ describe('checkPermissions', () => {
throw new Error('Something went wrong');
});
await getCtOrStLinks(userPermissions, toggleNotification);
await getContentTypeLinks(userPermissions, toggleNotification);
expect(toggleNotification).toBeCalled();
});
});

View File

@ -1,6 +1,11 @@
import styled from 'styled-components';
import { BackHeader as BaseBackHeader } from '@strapi/helper-plugin';
import { Flex, Text } from '@buffetjs/core';
const BackHeader = styled(BaseBackHeader)`
left: 24rem;
`;
const SubWrapper = styled.div`
background: #ffffff;
border-radius: 2px;
@ -71,4 +76,4 @@ const StatusWrapper = styled.div`
`}
`;
export { LinkWrapper, MainWrapper, SubWrapper, DeleteButton, StatusWrapper };
export { LinkWrapper, MainWrapper, SubWrapper, DeleteButton, StatusWrapper, BackHeader };

View File

@ -2,7 +2,6 @@ import React, { memo, useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import {
BackHeader,
BaselineAlignment,
InjectionZone,
LiLink,
@ -23,7 +22,7 @@ import EditViewDataManagerProvider from '../../components/EditViewDataManagerPro
import SingleTypeFormWrapper from '../../components/SingleTypeFormWrapper';
import Header from './Header';
import { createAttributesLayout, getFieldsActionMatchingPermissions } from './utils';
import { LinkWrapper, SubWrapper } from './components';
import { LinkWrapper, SubWrapper, BackHeader } from './components';
import DeleteLink from './DeleteLink';
import InformationCard from './InformationCard';
import { getTrad } from '../../utils';

View File

@ -18,6 +18,10 @@
"components.FiltersPickWrapper.PluginHeader.description": "Set the conditions to apply to filter the entries",
"components.FiltersPickWrapper.PluginHeader.title.filter": "Filters",
"components.FiltersPickWrapper.hide": "Hide",
"components.LeftMenu.collection-types.plural": "Collection Types",
"components.LeftMenu.collection-types.singular": "Collection Type",
"components.LeftMenu.single-types.plural": "Single Types",
"components.LeftMenu.single-types.singular": "Single Type",
"components.LimitSelect.itemsPerPage": "Items per page",
"components.NotAllowedInput.text": "No permissions to see this field",
"components.Search.placeholder": "Search for an entry...",

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
position: fixed;
top: 0;
left: 25rem;
display: flex;
align-items: center;
overflow: hidden;

View File

@ -4,9 +4,9 @@ import { NavLink } from 'react-router-dom';
import Icon from './Icon';
function LeftMenuLink({ children, to, CustomComponent }) {
function LeftMenuLink({ children, to, search, CustomComponent }) {
return (
<NavLink to={to}>
<NavLink to={`${to}${search ? `?${search}` : ''}`}>
<Icon />
{CustomComponent ? <CustomComponent /> : <p>{children}</p>}
</NavLink>
@ -16,11 +16,13 @@ function LeftMenuLink({ children, to, CustomComponent }) {
LeftMenuLink.defaultProps = {
children: null,
CustomComponent: null,
search: null,
};
LeftMenuLink.propTypes = {
children: PropTypes.node,
to: PropTypes.string.isRequired,
search: PropTypes.string,
};
export default memo(LeftMenuLink);

View File

@ -1,7 +1,7 @@
import addLocaleToLinksSearch from './utils/addLocaleToLinksSearch';
const addLocaleToCollectionTypesMiddleware = () => ({ getState }) => next => action => {
if (action.type !== 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS') {
if (action.type !== 'ContentManager/App/SET_CONTENT_TYPE_LINKS') {
return next(action);
}

View File

@ -1,7 +1,7 @@
import addLocaleToLinksSearch from './utils/addLocaleToLinksSearch';
const addLocaleToSingleTypesMiddleware = () => ({ getState }) => next => action => {
if (action.type !== 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS') {
if (action.type !== 'ContentManager/App/SET_CONTENT_TYPE_LINKS') {
return next(action);
}

View File

@ -33,7 +33,7 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
expect(next).toBeCalledWith(action);
});
it('should forward the action when the type is not StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS', () => {
it('should forward the action when the type is not ContentManager/App/SET_CONTENT_TYPE_LINKS', () => {
const action = { test: true, type: 'TEST' };
const next = jest.fn();
@ -46,7 +46,7 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
it('should forward when the authorizedStLinks array is empty', () => {
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [],
},
@ -62,9 +62,9 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
it('should not add the search key to a single type link when i18n is not enabled on the single type', () => {
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [{ destination: 'cm/collectionType/test' }],
authorizedCtLinks: [{ to: 'cm/collectionType/test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: false } } }],
},
};
@ -84,9 +84,9 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
] = [{ properties: { locales: ['en'] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [{ destination: 'cm/collectionType/test', search: null }],
authorizedCtLinks: [{ to: 'cm/collectionType/test', search: null }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -97,11 +97,9 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [
{ destination: 'cm/collectionType/test', search: 'plugins[i18n][locale]=en' },
],
authorizedCtLinks: [{ to: 'cm/collectionType/test', search: 'plugins[i18n][locale]=en' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -116,11 +114,9 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
] = [{ properties: { locales: [] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [
{ destination: 'cm/collectionType/test', search: 'page=1&pageSize=10' },
],
authorizedCtLinks: [{ to: 'cm/collectionType/test', search: 'page=1&pageSize=10' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -131,11 +127,11 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [
{
destination: 'cm/collectionType/test',
to: 'cm/collectionType/test',
isDisplayed: false,
search: 'page=1&pageSize=10',
},
@ -154,11 +150,9 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
] = [{ properties: { locales: ['en'] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [
{ destination: 'cm/collectionType/test', search: 'plugins[plugin][test]=test' },
],
authorizedCtLinks: [{ to: 'cm/collectionType/test', search: 'plugins[plugin][test]=test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -169,11 +163,11 @@ describe('i18n | middlewares | addLocaleToCollectionTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedCtLinks: [
{
destination: 'cm/collectionType/test',
to: 'cm/collectionType/test',
search: 'plugins[plugin][test]=test&plugins[i18n][locale]=en',
},
],

View File

@ -33,7 +33,7 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
expect(next).toBeCalledWith(action);
});
it('should forward the action when the type is not StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS', () => {
it('should forward the action when the type is not ContentManager/App/SET_CONTENT_TYPE_LINKS', () => {
const action = { test: true, type: 'TEST' };
const next = jest.fn();
@ -46,7 +46,7 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
it('should forward when the authorizedStLinks array is empty', () => {
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [],
},
@ -62,9 +62,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
it('should not add the search key to a single type link when i18n is not enabled on the single type', () => {
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [{ destination: 'cm/singleType/test' }],
authorizedStLinks: [{ to: 'cm/singleType/test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: false } } }],
},
};
@ -84,9 +84,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
] = [{ properties: { locales: ['en'] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [{ destination: 'cm/singleType/test' }],
authorizedStLinks: [{ to: 'cm/singleType/test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -97,11 +97,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [
{ destination: 'cm/singleType/test', search: 'plugins[i18n][locale]=en' },
],
authorizedStLinks: [{ to: 'cm/singleType/test', search: 'plugins[i18n][locale]=en' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -116,9 +114,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
] = [{ properties: { locales: [] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [{ destination: 'cm/singleType/test' }],
authorizedStLinks: [{ to: 'cm/singleType/test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -129,9 +127,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [{ destination: 'cm/singleType/test', isDisplayed: false }],
authorizedStLinks: [{ to: 'cm/singleType/test', isDisplayed: false }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -146,11 +144,9 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
] = [{ properties: { locales: ['en'] } }];
const action = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [
{ destination: 'cm/singleType/test', search: 'plugins[plugin][test]=test' },
],
authorizedStLinks: [{ to: 'cm/singleType/test', search: 'plugins[plugin][test]=test' }],
contentTypeSchemas: [{ uid: 'test', pluginOptions: { i18n: { localized: true } } }],
},
};
@ -161,11 +157,11 @@ describe('i18n | middlewares | addLocaleToSingleTypesMiddleware', () => {
middleware(next)(action);
const expected = {
type: 'StrapiAdmin/LeftMenu/SET_CT_OR_ST_LINKS',
type: 'ContentManager/App/SET_CONTENT_TYPE_LINKS',
data: {
authorizedStLinks: [
{
destination: 'cm/singleType/test',
to: 'cm/singleType/test',
search: 'plugins[plugin][test]=test&plugins[i18n][locale]=en',
},
],

View File

@ -4,7 +4,7 @@ import getDefaultLocale from '../../utils/getDefaultLocale';
const addLocaleToLinksSearch = (links, kind, contentTypeSchemas, locales, permissions) => {
return links.map(link => {
const contentTypeUID = link.destination.split(`/${kind}/`)[1];
const contentTypeUID = link.to.split(`/${kind}/`)[1];
const contentTypeSchema = contentTypeSchemas.find(({ uid }) => uid === contentTypeUID);

View File

@ -6,7 +6,7 @@ describe('i18n | middlewares | utils | addLocaleToLinksSearch', () => {
});
it('should not modify the links when i18n is not enabled on a content type', () => {
const links = [{ uid: 'test', destination: 'cm/collectionType/test' }];
const links = [{ uid: 'test', to: 'cm/collectionType/test' }];
const schemas = [{ uid: 'test', pluginOptions: { i18n: { localized: false } } }];
expect(addLocaleToLinksSearch(links, 'collectionType', schemas)).toEqual(links);
@ -14,8 +14,8 @@ describe('i18n | middlewares | utils | addLocaleToLinksSearch', () => {
it('should set the isDisplayed key to false when the user does not have the permission to read or create a locale on a collection type', () => {
const links = [
{ uid: 'foo', destination: 'cm/collectionType/foo', isDisplayed: true },
{ uid: 'bar', destination: 'cm/collectionType/bar', isDisplayed: true },
{ uid: 'foo', to: 'cm/collectionType/foo', isDisplayed: true },
{ uid: 'bar', to: 'cm/collectionType/bar', isDisplayed: true },
];
const schemas = [
{ uid: 'foo', pluginOptions: { i18n: { localized: true } } },
@ -58,8 +58,8 @@ describe('i18n | middlewares | utils | addLocaleToLinksSearch', () => {
},
};
const expected = [
{ uid: 'foo', destination: 'cm/collectionType/foo', isDisplayed: false },
{ uid: 'bar', destination: 'cm/collectionType/bar', isDisplayed: false },
{ uid: 'foo', to: 'cm/collectionType/foo', isDisplayed: false },
{ uid: 'bar', to: 'cm/collectionType/bar', isDisplayed: false },
];
const locales = [{ code: 'en', isDefault: true }, { code: 'fr' }];
@ -70,8 +70,8 @@ describe('i18n | middlewares | utils | addLocaleToLinksSearch', () => {
it('should add the locale to a link search', () => {
const links = [
{ uid: 'foo', destination: 'cm/collectionType/foo', isDisplayed: true, search: 'page=1' },
{ uid: 'bar', destination: 'cm/collectionType/bar', isDisplayed: true },
{ uid: 'foo', to: 'cm/collectionType/foo', isDisplayed: true, search: 'page=1' },
{ uid: 'bar', to: 'cm/collectionType/bar', isDisplayed: true },
];
const schemas = [
{ uid: 'foo', pluginOptions: { i18n: { localized: true } } },
@ -117,13 +117,13 @@ describe('i18n | middlewares | utils | addLocaleToLinksSearch', () => {
const expected = [
{
uid: 'foo',
destination: 'cm/collectionType/foo',
to: 'cm/collectionType/foo',
isDisplayed: true,
search: 'page=1&plugins[i18n][locale]=fr',
},
{
uid: 'bar',
destination: 'cm/collectionType/bar',
to: 'cm/collectionType/bar',
isDisplayed: true,
search: 'plugins[i18n][locale]=en',
},