mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 15:13:21 +00:00
Add permissions on the configure button in the ctm
Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
parent
3c8c15efca
commit
67c8cbd1d7
@ -341,6 +341,21 @@ const data = {
|
||||
subject: 'application::homepage.homepage',
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.single-types.configure-view',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.collection-types.configure-view',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugins::content-manager.components.configure-layout',
|
||||
subject: null,
|
||||
conditions: [],
|
||||
},
|
||||
|
||||
// Content type builder
|
||||
{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import useUser from '../../hooks/useUser';
|
||||
@ -8,22 +8,32 @@ import LoadingIndicatorPage from '../LoadingIndicatorPage';
|
||||
const WithPagePermissions = ({ permissions, children }) => {
|
||||
const userPermissions = useUser();
|
||||
const [state, setState] = useState({ isLoading: true, canAccess: false });
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermission = async () => {
|
||||
try {
|
||||
const canAccess = await hasPermissions(userPermissions, permissions);
|
||||
|
||||
setState({ isLoading: false, canAccess });
|
||||
if (isMounted.current) {
|
||||
setState({ isLoading: false, canAccess });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
strapi.notification.error('notification.error');
|
||||
if (isMounted.current) {
|
||||
console.error(err);
|
||||
|
||||
setState({ isLoading: false });
|
||||
strapi.notification.error('notification.error');
|
||||
|
||||
setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkPermission();
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import useUser from '../../hooks/useUser';
|
||||
@ -10,23 +10,32 @@ import hasPermissions from '../../utils/hasPermissions';
|
||||
const WithPermissions = ({ permissions, children }) => {
|
||||
const userPermissions = useUser();
|
||||
const [state, setState] = useState({ isLoading: true, canAccess: false });
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermission = async () => {
|
||||
try {
|
||||
const canAccess = await hasPermissions(userPermissions, permissions);
|
||||
|
||||
setState({ isLoading: false, canAccess });
|
||||
if (isMounted.current) {
|
||||
setState({ isLoading: false, canAccess });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
strapi.notification.error('notification.error');
|
||||
if (isMounted.current) {
|
||||
console.error(err);
|
||||
strapi.notification.error('notification.error');
|
||||
|
||||
setState({ isLoading: false });
|
||||
setState({ isLoading: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkPermission();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (state.isLoading) {
|
||||
|
||||
@ -4,7 +4,9 @@ import { isEmpty } from 'lodash';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Grab, GrabLarge, Pencil } from '@buffetjs/icons';
|
||||
import { WithPermissions } from 'strapi-helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import useLayoutDnd from '../../hooks/useLayoutDnd';
|
||||
import GrabWrapper from './GrabWrapper';
|
||||
import Link from './Link';
|
||||
@ -122,9 +124,7 @@ const DraggedField = forwardRef(
|
||||
onMouseLeave={() => setIsOverRemove(false)}
|
||||
>
|
||||
{isOverRemove && !isSelected && <Close />}
|
||||
{((showEditBlockOverState && !isOverRemove) || isSelected) && (
|
||||
<Pencil />
|
||||
)}
|
||||
{((showEditBlockOverState && !isOverRemove) || isSelected) && <Pencil />}
|
||||
{!showEditBlockOverState && !isOverRemove && !isSelected && (
|
||||
<Close width="10px" height="10px" />
|
||||
)}
|
||||
@ -132,24 +132,24 @@ const DraggedField = forwardRef(
|
||||
</SubWrapper>
|
||||
)}
|
||||
{type === 'component' && (
|
||||
<FormattedMessage
|
||||
id={`${pluginId}.components.FieldItem.linkToComponentLayout`}
|
||||
>
|
||||
{msg => (
|
||||
<Link
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
<WithPermissions permissions={pluginPermissions.componentsConfigurations}>
|
||||
<FormattedMessage id={`${pluginId}.components.FieldItem.linkToComponentLayout`}>
|
||||
{msg => (
|
||||
<Link
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
goTo(
|
||||
`/plugins/${pluginId}/ctm-configurations/edit-settings/components/${componentUid}/`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon="cog" />
|
||||
{msg}
|
||||
</Link>
|
||||
)}
|
||||
</FormattedMessage>
|
||||
goTo(
|
||||
`/plugins/${pluginId}/ctm-configurations/edit-settings/components/${componentUid}/`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon="cog" />
|
||||
{msg}
|
||||
</Link>
|
||||
)}
|
||||
</FormattedMessage>
|
||||
</WithPermissions>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@ -1,22 +1,39 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { hasPermissions, useUser } from 'strapi-helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import DynamicComponentCard from '../DynamicComponentCard';
|
||||
import Tooltip from './Tooltip';
|
||||
|
||||
const DynamicComponent = ({
|
||||
componentUid,
|
||||
friendlyName,
|
||||
icon,
|
||||
setIsOverDynamicZone,
|
||||
}) => {
|
||||
const [state, setState] = useState(false);
|
||||
const DynamicComponent = ({ componentUid, friendlyName, icon, setIsOverDynamicZone }) => {
|
||||
const [isOver, setIsOver] = useState(false);
|
||||
const [{ isLoading, canAccess }, setState] = useState({ isLoading: true, canAccess: false });
|
||||
const { push } = useHistory();
|
||||
const userPermissions = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermission = async () => {
|
||||
try {
|
||||
const canAccess = await hasPermissions(
|
||||
userPermissions,
|
||||
pluginPermissions.componentsConfigurations
|
||||
);
|
||||
|
||||
setState({ isLoading: false, canAccess });
|
||||
} catch (err) {
|
||||
setState({ isLoading: false });
|
||||
}
|
||||
};
|
||||
|
||||
checkPermission();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleMouseEvent = () => {
|
||||
setIsOverDynamicZone(v => !v);
|
||||
setState(v => !v);
|
||||
setIsOver(v => !v);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -24,16 +41,16 @@ const DynamicComponent = ({
|
||||
componentUid={componentUid}
|
||||
friendlyName={friendlyName}
|
||||
icon={icon}
|
||||
isOver={state}
|
||||
isOver={isOver}
|
||||
onClick={() => {
|
||||
push(
|
||||
`/plugins/${pluginId}/ctm-configurations/edit-settings/components/${componentUid}/`
|
||||
);
|
||||
if (!isLoading && canAccess) {
|
||||
push(`/plugins/${pluginId}/ctm-configurations/edit-settings/components/${componentUid}/`);
|
||||
}
|
||||
}}
|
||||
onMouseEvent={handleMouseEvent}
|
||||
tradId="components.DraggableAttr.edit"
|
||||
>
|
||||
<Tooltip isOver={state}>{componentUid}</Tooltip>
|
||||
<Tooltip isOver={isOver}>{componentUid}</Tooltip>
|
||||
</DynamicComponentCard>
|
||||
);
|
||||
};
|
||||
|
||||
@ -38,15 +38,7 @@ const DraggedFieldWithPreview = forwardRef(
|
||||
const isFullSize = size === 12;
|
||||
const display = isFullSize && dragStart ? 'none' : '';
|
||||
const width = isFullSize && dragStart ? 0 : '100%';
|
||||
const higherFields = [
|
||||
'json',
|
||||
'text',
|
||||
'file',
|
||||
'media',
|
||||
'component',
|
||||
'richtext',
|
||||
'dynamiczone',
|
||||
];
|
||||
const higherFields = ['json', 'text', 'file', 'media', 'component', 'richtext', 'dynamiczone'];
|
||||
const withLongerHeight = higherFields.includes(type) && !dragStart;
|
||||
const getCompoInfos = uid =>
|
||||
get(componentLayouts, [uid, 'schema', 'info'], { name: '', icon: '' });
|
||||
@ -69,14 +61,8 @@ const DraggedFieldWithPreview = forwardRef(
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Wrapper
|
||||
ref={refs.dropRef}
|
||||
withLongerHeight={withLongerHeight}
|
||||
style={style}
|
||||
>
|
||||
{dragStart && isFullSize && (
|
||||
<PreviewCarret style={{ marginRight: '-10px' }} />
|
||||
)}
|
||||
<Wrapper ref={refs.dropRef} withLongerHeight={withLongerHeight} style={style}>
|
||||
{dragStart && isFullSize && <PreviewCarret style={{ marginRight: '-10px' }} />}
|
||||
<>
|
||||
{showLeftCarret && <Carret />}
|
||||
|
||||
@ -98,8 +84,7 @@ const DraggedFieldWithPreview = forwardRef(
|
||||
>
|
||||
{type === 'component' &&
|
||||
componentLayout.map((row, i) => {
|
||||
const marginBottom =
|
||||
i === componentLayout.length - 1 ? '29px' : '';
|
||||
const marginBottom = i === componentLayout.length - 1 ? '29px' : '';
|
||||
const marginTop = i === 0 ? '5px' : '';
|
||||
|
||||
return (
|
||||
@ -135,9 +120,7 @@ const DraggedFieldWithPreview = forwardRef(
|
||||
label={label}
|
||||
name={field.name}
|
||||
isSub
|
||||
withLongerHeight={higherFields.includes(
|
||||
fieldType
|
||||
)}
|
||||
withLongerHeight={higherFields.includes(fieldType)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { Switch, Route, useRouteMatch, useParams } from 'react-router-dom';
|
||||
import { LoadingIndicatorPage } from 'strapi-helper-plugin';
|
||||
import { LoadingIndicatorPage, WithPagePermissions } from 'strapi-helper-plugin';
|
||||
import pluginPermissions from '../../permissions';
|
||||
|
||||
const EditView = lazy(() => import('../EditView'));
|
||||
const EditSettingsView = lazy(() => import('../EditSettingsView'));
|
||||
@ -14,8 +15,15 @@ const CollectionTypeRecursivePath = props => {
|
||||
const renderRoute = (routeProps, Component) => {
|
||||
return <Component {...props} {...routeProps} slug={slug} />;
|
||||
};
|
||||
const renderPermissionsRoute = (routeProps, Component) => {
|
||||
return (
|
||||
<WithPagePermissions permissions={pluginPermissions.collectionTypesConfigurations}>
|
||||
<Component {...props} {...routeProps} slug={slug} />
|
||||
</WithPagePermissions>
|
||||
);
|
||||
};
|
||||
|
||||
const routes = [
|
||||
const settingsRoutes = [
|
||||
{
|
||||
path: 'ctm-configurations/list-settings',
|
||||
comp: ListSettingsView,
|
||||
@ -24,6 +32,15 @@ const CollectionTypeRecursivePath = props => {
|
||||
path: 'ctm-configurations/edit-settings/:type',
|
||||
comp: EditSettingsView,
|
||||
},
|
||||
].map(({ path, comp }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={`${url}/${path}`}
|
||||
render={props => renderPermissionsRoute(props, comp)}
|
||||
/>
|
||||
));
|
||||
|
||||
const routes = [
|
||||
{ path: ':id', comp: EditView },
|
||||
{ path: '', comp: ListView },
|
||||
].map(({ path, comp }) => (
|
||||
@ -32,7 +49,10 @@ const CollectionTypeRecursivePath = props => {
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Switch>{routes}</Switch>
|
||||
<Switch>
|
||||
{settingsRoutes}
|
||||
{routes}
|
||||
</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,8 +2,9 @@ import React, { memo, useCallback, useMemo, useEffect, useReducer, useRef } from
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
|
||||
import { BackHeader, LiLink } from 'strapi-helper-plugin';
|
||||
import { BackHeader, LiLink, WithPermissions } from 'strapi-helper-plugin';
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import Container from '../../components/Container';
|
||||
import DynamicZone from '../../components/DynamicZone';
|
||||
import FormWrapper from '../../components/FormWrapper';
|
||||
@ -231,19 +232,27 @@ const EditView = ({ components, currentEnvironment, deleteLayout, layouts, plugi
|
||||
)}
|
||||
<LinkWrapper>
|
||||
<ul>
|
||||
<LiLink
|
||||
message={{
|
||||
id: 'app.links.configure-view',
|
||||
}}
|
||||
icon="layout"
|
||||
key={`${pluginId}.link`}
|
||||
url={`${
|
||||
isSingleType ? `${pathname}/` : ''
|
||||
}ctm-configurations/edit-settings/content-types`}
|
||||
onClick={() => {
|
||||
// emitEvent('willEditContentTypeLayoutFromEditView');
|
||||
}}
|
||||
/>
|
||||
<WithPermissions
|
||||
permissions={
|
||||
isSingleType
|
||||
? pluginPermissions.singleTypesConfigurations
|
||||
: pluginPermissions.collectionTypesConfigurations
|
||||
}
|
||||
>
|
||||
<LiLink
|
||||
message={{
|
||||
id: 'app.links.configure-view',
|
||||
}}
|
||||
icon="layout"
|
||||
key={`${pluginId}.link`}
|
||||
url={`${
|
||||
isSingleType ? `${pathname}/` : ''
|
||||
}ctm-configurations/edit-settings/content-types`}
|
||||
onClick={() => {
|
||||
// emitEvent('willEditContentTypeLayoutFromEditView');
|
||||
}}
|
||||
/>
|
||||
</WithPermissions>
|
||||
{getInjectedComponents(
|
||||
'editView',
|
||||
'right.links',
|
||||
|
||||
@ -13,9 +13,11 @@ import {
|
||||
getQueryParameters,
|
||||
useGlobalContext,
|
||||
request,
|
||||
WithPermissions,
|
||||
} from 'strapi-helper-plugin';
|
||||
|
||||
import pluginId from '../../pluginId';
|
||||
import pluginPermissions from '../../permissions';
|
||||
import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown';
|
||||
import Container from '../../components/Container';
|
||||
import CustomTable from '../../components/CustomTable';
|
||||
@ -385,16 +387,18 @@ function ListView({
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-2">
|
||||
<DisplayedFieldsDropdown
|
||||
isOpen={isLabelPickerOpen}
|
||||
items={getAllLabels()}
|
||||
onChange={handleChangeListLabels}
|
||||
onClickReset={() => {
|
||||
resetListLabels(slug);
|
||||
}}
|
||||
slug={slug}
|
||||
toggle={toggleLabelPickerState}
|
||||
/>
|
||||
<WithPermissions permissions={pluginPermissions.collectionTypesConfigurations}>
|
||||
<DisplayedFieldsDropdown
|
||||
isOpen={isLabelPickerOpen}
|
||||
items={getAllLabels()}
|
||||
onChange={handleChangeListLabels}
|
||||
onClickReset={() => {
|
||||
resetListLabels(slug);
|
||||
}}
|
||||
slug={slug}
|
||||
toggle={toggleLabelPickerState}
|
||||
/>
|
||||
</WithPermissions>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row" style={{ paddingTop: '12px' }}>
|
||||
|
||||
@ -3,10 +3,16 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { Switch, Route, useRouteMatch } from 'react-router-dom';
|
||||
import { LoadingIndicatorPage, useGlobalContext, request } from 'strapi-helper-plugin';
|
||||
import {
|
||||
LoadingIndicatorPage,
|
||||
useGlobalContext,
|
||||
request,
|
||||
WithPagePermissions,
|
||||
} 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 DragLayer from '../../components/DragLayer';
|
||||
import getRequestUrl from '../../utils/getRequestUrl';
|
||||
import createPossibleMainFieldsForModelsAndComponents from './utils/createPossibleMainFieldsForModelsAndComponents';
|
||||
@ -110,10 +116,6 @@ function Main({
|
||||
/>
|
||||
);
|
||||
const routes = [
|
||||
{
|
||||
path: 'ctm-configurations/edit-settings/:type/:componentSlug',
|
||||
comp: EditSettingsView,
|
||||
},
|
||||
{ path: 'singleType/:slug', comp: SingleTypeRecursivePath },
|
||||
{ path: 'collectionType/:slug', comp: CollectionTypeRecursivePath },
|
||||
].map(({ path, comp }) => (
|
||||
@ -128,7 +130,30 @@ function Main({
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<DragLayer />
|
||||
<Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Switch>{routes}</Switch>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`/plugins/${pluginId}/ctm-configurations/edit-settings/:type/:componentSlug`}
|
||||
render={routeProps => (
|
||||
<WithPagePermissions permissions={pluginPermissions.componentsConfigurations}>
|
||||
<EditSettingsView
|
||||
currentEnvironment={currentEnvironment}
|
||||
deleteLayout={deleteLayout}
|
||||
deleteLayouts={deleteLayouts}
|
||||
emitEvent={emitEvent}
|
||||
components={components}
|
||||
componentsAndModelsMainPossibleMainFields={
|
||||
componentsAndModelsMainPossibleMainFields
|
||||
}
|
||||
layouts={layouts}
|
||||
models={models}
|
||||
plugins={plugins}
|
||||
{...routeProps}
|
||||
/>
|
||||
</WithPagePermissions>
|
||||
)}
|
||||
/>
|
||||
{routes}
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</DndProvider>
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { Suspense, lazy } from 'react';
|
||||
import { Switch, Route, useRouteMatch, useParams } from 'react-router-dom';
|
||||
import { LoadingIndicatorPage } from 'strapi-helper-plugin';
|
||||
import { LoadingIndicatorPage, WithPagePermissions } from 'strapi-helper-plugin';
|
||||
import pluginPermissions from '../../permissions';
|
||||
|
||||
const EditView = lazy(() => import('../EditView'));
|
||||
const EditSettingsView = lazy(() => import('../EditSettingsView'));
|
||||
@ -9,23 +10,22 @@ 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>
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${url}/ctm-configurations/edit-settings/:type`}
|
||||
render={routeProps => (
|
||||
<WithPagePermissions permissions={pluginPermissions.singleTypesConfigurations}>
|
||||
<EditSettingsView {...props} {...routeProps} slug={slug} />
|
||||
</WithPagePermissions>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={`${url}`}
|
||||
render={routeProps => <EditView {...props} {...routeProps} slug={slug} />}
|
||||
/>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
const pluginPermissions = {
|
||||
// This permission regards the main component (App) and is used to tell
|
||||
// If the plugin link should be displayed in the menu
|
||||
// And also if the plugin is accessible. This use case is found when a user types the url of the
|
||||
// plugin directly in the browser
|
||||
main: [],
|
||||
collectionTypesConfigurations: [
|
||||
{
|
||||
action: 'plugins::content-manager.collection-types.configure-view',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
componentsConfigurations: [
|
||||
{
|
||||
action: 'plugins::content-manager.components.configure-layout',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
singleTypesConfigurations: [
|
||||
{
|
||||
action: 'plugins::content-manager.single-types.configure-view',
|
||||
subject: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default pluginPermissions;
|
||||
Loading…
x
Reference in New Issue
Block a user