Merge pull request #9368 from strapi/i18n/rbac-refactoring

[I18n] RBAC CT & ST refactoring
This commit is contained in:
cyril lopez 2021-02-15 14:01:32 +01:00 committed by GitHub
commit 43b44f3b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1782 additions and 87 deletions

View File

@ -10,6 +10,7 @@ import Tab from './Tab';
const Tabs = ({ children, isLoading, tabsLabel }) => {
const { formatMessage } = useIntl();
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const selectedChild = React.Children.toArray(children)[selectedTabIndex];
const handleSelectedTab = index => {
if (index !== selectedTabIndex) {
@ -38,7 +39,7 @@ const Tabs = ({ children, isLoading, tabsLabel }) => {
</Tab>
))}
</Flex>
{children[selectedTabIndex]}
{selectedChild}
</>
)}
</TabsWrapper>

View File

@ -23,15 +23,15 @@ window.strapi = Object.assign(window.strapi || {}, {
});
module.exports = {
'strapi-plugin-documentation': require('../../../strapi-plugin-documentation/admin/src').default,
'strapi-plugin-users-permissions': require('../../../strapi-plugin-users-permissions/admin/src')
.default,
'strapi-plugin-content-manager': require('../../../strapi-plugin-content-manager/admin/src')
.default,
'strapi-plugin-content-type-builder': require('../../../strapi-plugin-content-type-builder/admin/src')
.default,
'strapi-plugin-email': require('../../../strapi-plugin-email/admin/src').default,
'strapi-plugin-upload': require('../../../strapi-plugin-upload/admin/src').default,
'strapi-plugin-graphql': require('../../../strapi-plugin-graphql/admin/src').default,
'strapi-plugin-i18n': require('../../../strapi-plugin-i18n/admin/src').default,
// 'strapi-plugin-documentation': require('../../../strapi-plugin-documentation/admin/src').default,
// 'strapi-plugin-users-permissions': require('../../../strapi-plugin-users-permissions/admin/src')
// .default,
// 'strapi-plugin-content-manager': require('../../../strapi-plugin-content-manager/admin/src')
// .default,
// 'strapi-plugin-content-type-builder': require('../../../strapi-plugin-content-type-builder/admin/src')
// .default,
// 'strapi-plugin-email': require('../../../strapi-plugin-email/admin/src').default,
// 'strapi-plugin-upload': require('../../../strapi-plugin-upload/admin/src').default,
// 'strapi-plugin-graphql': require('../../../strapi-plugin-graphql/admin/src').default,
// 'strapi-plugin-i18n': require('../../../strapi-plugin-i18n/admin/src').default,
};

View File

@ -112,6 +112,7 @@
"Settings.roles.form.description": "Name and description of the role",
"Settings.roles.form.input.description": "Description",
"Settings.roles.form.input.name": "Name",
"Settings.roles.form.permission.property-label": "{label} permissions",
"Settings.roles.form.permissions.attributesPermissions": "Fields permissions",
"Settings.roles.form.permissions.create": "Create",
"Settings.roles.form.permissions.delete": "Delete",

View File

@ -1,83 +1,83 @@
import ar from './ar.json';
import cs from './cs.json';
import de from './de.json';
import dk from './dk.json';
// import ar from './ar.json';
// import cs from './cs.json';
// import de from './de.json';
// import dk from './dk.json';
import en from './en.json';
import es from './es.json';
// import es from './es.json';
import fr from './fr.json';
import he from './he.json';
import id from './id.json';
import it from './it.json';
import ja from './ja.json';
import ko from './ko.json';
import ms from './ms.json';
import nl from './nl.json';
import pl from './pl.json';
import ptBR from './pt-BR.json';
import pt from './pt.json';
import ru from './ru.json';
import th from './th.json';
import tr from './tr.json';
import vi from './vi.json';
import zhHans from './zh-Hans.json';
import zh from './zh.json';
import sk from './sk.json';
import uk from './uk.json';
// import he from './he.json';
// import id from './id.json';
// import it from './it.json';
// import ja from './ja.json';
// import ko from './ko.json';
// import ms from './ms.json';
// import nl from './nl.json';
// import pl from './pl.json';
// import ptBR from './pt-BR.json';
// import pt from './pt.json';
// import ru from './ru.json';
// import th from './th.json';
// import tr from './tr.json';
// import vi from './vi.json';
// import zhHans from './zh-Hans.json';
// import zh from './zh.json';
// import sk from './sk.json';
// import uk from './uk.json';
const trads = {
ar,
cs,
de,
dk,
// ar,
// cs,
// de,
// dk,
en,
es,
// es,
fr,
he,
id,
it,
ja,
ko,
ms,
nl,
pl,
'pt-BR': ptBR,
pt,
ru,
sk,
th,
tr,
uk,
vi,
'zh-Hans': zhHans,
zh,
// he,
// id,
// it,
// ja,
// ko,
// ms,
// nl,
// pl,
// 'pt-BR': ptBR,
// pt,
// ru,
// sk,
// th,
// tr,
// uk,
// vi,
// 'zh-Hans': zhHans,
// zh,
};
export const languageNativeNames = {
ar: 'العربية',
cs: 'Čeština',
de: 'Deutsch',
dk: 'Dansk',
// ar: 'العربية',
// cs: 'Čeština',
// de: 'Deutsch',
// dk: 'Dansk',
en: 'English',
es: 'Español',
// es: 'Español',
fr: 'Français',
he: 'עברית',
id: 'Indonesian',
it: 'Italiano',
ja: '日本語',
ko: '한국어',
ms: 'Melayu',
nl: 'Nederlands',
pl: 'Polski',
'pt-BR': 'Português (Brasil)',
pt: 'Português (Portugal)',
ru: 'Русский',
sk: 'Slovenčina',
th: 'ไทย',
tr: 'Türkçe',
uk: 'Українська',
vi: 'Tiếng Việt',
'zh-Hans': '中文 (简体)',
zh: '中文 (繁體)',
// he: 'עברית',
// id: 'Indonesian',
// it: 'Italiano',
// ja: '日本語',
// ko: '한국어',
// ms: 'Melayu',
// nl: 'Nederlands',
// pl: 'Polski',
// 'pt-BR': 'Português (Brasil)',
// pt: 'Português (Portugal)',
// ru: 'Русский',
// sk: 'Slovenčina',
// th: 'ไทย',
// tr: 'Türkçe',
// uk: 'Українська',
// vi: 'Tiếng Việt',
// 'zh-Hans': '中文 (简体)',
// zh: '中文 (繁體)',
};
export default trads;

View File

@ -0,0 +1,41 @@
/* eslint-disable indent */
import styled from 'styled-components';
import { Checkbox } from '@buffetjs/core';
const CheckboxWithCondition = styled(Checkbox)`
min-width: 10rem;
max-width: 12rem;
flex: 1;
position: relative;
input[type='checkbox'] {
z-index: 10;
&:after {
color: ${({ theme }) => theme.main.colors.mediumBlue};
}
}
${({ hasConditions, disabled, theme }) =>
hasConditions &&
`
&:before {
content: '•';
font-size: 11px;
position: absolute;
top: -6px;
left: -7px;
color: ${disabled ? theme.main.colors.grey : theme.main.colors.mediumBlue};
}
`}
${({ disabled, theme }) =>
disabled &&
`
input[type='checkbox'] {
cursor: not-allowed;
&:after {
color: ${theme.main.colors.grey};
}
}
`}
`;
export default CheckboxWithCondition;

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const Chevron = styled(FontAwesomeIcon)`
width: 18px !important;
display: none;
padding-left: 1rem;
font-size: 2.1rem;
`;
export default Chevron;

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
import { Flex } from '@buffetjs/core';
const CollapseLabel = styled(Flex)`
padding-right: 10px;
overflow: hidden;
flex: 1;
${({ isCollapsable }) => isCollapsable && 'cursor: pointer;'}
`;
export default CollapseLabel;

View File

@ -0,0 +1,28 @@
/* eslint-disable indent */
import styled from 'styled-components';
const Wrapper = styled.div`
position: relative;
cursor: pointer;
color: ${({ theme }) => theme.main.colors.mediumBlue};
${({ isRight }) =>
isRight &&
`
position: absolute;
right: 5rem;
`}
${({ hasConditions, disabled, theme }) =>
hasConditions &&
`
&:before {
content: '•';
position: absolute;
top: -4px;
left: -15px;
font-size: 18px;
color: ${disabled ? theme.main.colors.grey : theme.main.colors.mediumBlue};
}
`}
`;
export default Wrapper;

View File

@ -0,0 +1,49 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Flex, Text, Padded } from '@buffetjs/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Wrapper from './Wrapper';
const ConditionsButton = ({ onClick, className, hasConditions, isRight }) => {
const { formatMessage } = useIntl();
return (
<Wrapper
isRight={isRight}
hasConditions={hasConditions}
className={className}
onClick={onClick}
>
<Padded right size="smd">
<Flex alignItems="center">
<Text color="mediumBlue">
{formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' })}
</Text>
<Padded style={{ height: '18px', lineHeight: 'normal' }} left size="xs">
<FontAwesomeIcon style={{ fontSize: '11px' }} icon="cog" />
</Padded>
</Flex>
</Padded>
</Wrapper>
);
};
ConditionsButton.defaultProps = {
className: null,
hasConditions: false,
isRight: false,
};
ConditionsButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
hasConditions: PropTypes.bool,
isRight: PropTypes.bool,
};
// This is a styled component advanced usage :
// Used to make a ref to a non styled component.
// https://styled-components.com/docs/advanced#caveat
export default styled(ConditionsButton)``;

View File

@ -0,0 +1,38 @@
import styled from 'styled-components';
import { Text } from '@buffetjs/core';
import ConditionsButton from '../../ConditionsButton';
import Chevron from '../../Chevron';
const activeRowStyle = (theme, isActive) => `
border: 1px solid ${theme.main.colors.darkBlue};
background-color: ${theme.main.colors.lightBlue};
color: ${theme.main.colors.mediumBlue};
border-radius: ${isActive ? '2px 2px 0 0' : '2px'};
${Text} {
color: ${theme.main.colors.mediumBlue};
}
${Chevron} {
display: block;
}
${ConditionsButton} {
display: block;
}
`;
const StyledRow = styled.div`
display: flex;
align-items: center;
height: 36px;
background-color: ${({ isGrey, theme }) =>
isGrey ? theme.main.colors.content.background : theme.main.colors.white};
border: 1px solid transparent;
${ConditionsButton} {
display: none;
}
${({ isActive, theme }) => isActive && activeRowStyle(theme, isActive)}
&:hover {
${({ theme, isActive }) => activeRowStyle(theme, isActive)}
}
`;
export default StyledRow;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Flex, Padded } from '@buffetjs/core';
import CheckboxWithCondition from '../../CheckboxWithCondition';
import Chevron from '../../Chevron';
import ConditionsButton from '../../ConditionsButton';
import HiddenAction from '../../HiddenAction';
import Wrapper from './Wrapper';
import RowLabel from '../../RowLabel';
const Collapse = ({ availableActions, isActive, isGrey, name, onClickToggle }) => {
return (
<Wrapper isActive={isActive} isGrey={isGrey}>
<Flex style={{ flex: 1 }}>
<Padded left size="sm" />
<RowLabel label={name} onClick={onClickToggle} isCollapsable>
<Chevron icon={isActive ? 'chevron-up' : 'chevron-down'} />
</RowLabel>
<Flex style={{ flex: 1 }}>
{availableActions.map(action => {
if (!action.isDisplayed) {
return <HiddenAction key={action.actionId} />;
}
return <CheckboxWithCondition key={action.actionId} name={action.actionId} />;
})}
</Flex>
<ConditionsButton isRight onClick={() => console.log('todo')} />
</Flex>
</Wrapper>
);
};
Collapse.propTypes = {
availableActions: PropTypes.array.isRequired,
isActive: PropTypes.bool.isRequired,
isGrey: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
onClickToggle: PropTypes.func.isRequired,
};
export default Collapse;

View File

@ -0,0 +1,28 @@
/* eslint-disable indent */
import styled from 'styled-components';
import { Flex } from '@buffetjs/core';
import { activeStyle } from '../../utils';
import Chevron from '../../../Chevron';
const RowWrapper = styled(Flex)`
height: 36px;
padding: 1rem 0;
flex: 1;
${Chevron} {
width: 13px;
}
${({ isCollapsable, theme }) =>
isCollapsable &&
`
${Chevron} {
display: block;
color: ${theme.main.colors.grey};
}
&:hover {
${activeStyle(theme)}
}
`}
${({ isActive, theme }) => isActive && activeStyle(theme)};
`;
export default RowWrapper;

View File

@ -0,0 +1,91 @@
import React, { memo, useState, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import { Padded, Flex } from '@buffetjs/core';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import HiddenAction from '../../../HiddenAction';
import RequiredSign from '../../../RequiredSign';
import RowLabel from '../../../RowLabel';
import SubActionRow from '../SubActionRow';
import Wrapper from './Wrapper';
const ActionRow = ({ childrenForm, label, name, required, propertyActions }) => {
const [rowToOpen, setRowToOpen] = useState(null);
const isActive = rowToOpen === name;
const recursiveValues = useMemo(() => {
if (!Array.isArray(childrenForm)) {
return [];
}
return childrenForm;
}, [childrenForm]);
const isCollapsable = recursiveValues.length > 0;
const handleClick = useCallback(() => {
if (isCollapsable) {
setRowToOpen(prev => {
if (prev === name) {
return null;
}
return name;
});
}
}, [isCollapsable, name]);
return (
<>
<Wrapper alignItems="center" isCollapsable={isCollapsable}>
<Flex style={{ flex: 1 }}>
<Padded left size="sm" />
<RowLabel
width="15rem"
onClick={handleClick}
isCollapsable={isCollapsable}
label={label}
// TODO
textColor="grey"
>
{required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} />
</RowLabel>
<Flex style={{ flex: 1 }}>
{propertyActions.map(action => {
if (!action.isActionRelatedToCurrentProperty) {
return <HiddenAction key={action.label} />;
}
return <CheckboxWithCondition key={action.label} name="todo" />;
})}
</Flex>
</Flex>
</Wrapper>
{isActive && (
<SubActionRow
// label={label}
// name={name}
propertyActions={propertyActions}
childrenForm={recursiveValues}
/>
)}
</>
);
};
ActionRow.defaultProps = {
childrenForm: [],
required: false,
};
ActionRow.propTypes = {
childrenForm: PropTypes.array,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
propertyActions: PropTypes.array.isRequired,
required: PropTypes.bool,
};
export default memo(ActionRow);

View File

@ -0,0 +1,62 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Flex, Text } from '@buffetjs/core';
import styled from 'styled-components';
// Those styles are very specific.
// so it is not a big problem to use custom paddings and widths.
const HeaderLabel = styled.div`
width: 12rem;
padding-top: 1rem;
padding-bottom: 1rem;
`;
const PropertyLabelWrapper = styled.div`
width: 18rem;
padding-top: 1rem;
padding-bottom: 1rem;
padding-left: 3.5rem;
`;
const Header = ({ headers, label }) => {
const { formatMessage } = useIntl();
const translatedLabel = formatMessage(
{
id: 'Settings.roles.form.permission.property-label',
defaultMessage: '{label} permissions',
},
{ label }
);
return (
<Flex>
<PropertyLabelWrapper>
<Text fontWeight="bold">{translatedLabel}</Text>
</PropertyLabelWrapper>
{headers.map(header => {
if (!header.isActionRelatedToCurrentProperty) {
return <HeaderLabel key={header.label} />;
}
return (
<HeaderLabel key={header.label}>
<Text textTransform="capitalize" fontWeight="bold">
{header.label}
</Text>
</HeaderLabel>
);
})}
</Flex>
);
};
Header.propTypes = {
headers: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
isActionRelatedToCurrentProperty: PropTypes.bool.isRequired,
})
).isRequired,
label: PropTypes.string.isRequired,
};
export default Header;

View File

@ -0,0 +1,8 @@
/* eslint-disable indent */
import styled from 'styled-components';
const Wrapper = styled.div`
padding-left: 15px;
`;
export default Wrapper;

View File

@ -0,0 +1,117 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { Flex, Text } from '@buffetjs/core';
import styled from 'styled-components';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import CollapseLabel from '../../../CollapseLabel';
import Curve from '../../../Curve';
import HiddenAction from '../../../HiddenAction';
import RequiredSign from '../../../RequiredSign';
import { RowStyle, RowWrapper } from './row';
import { LeftBorderTimeline, TopTimeline } from './timeline';
import Wrapper from './Wrapper';
const SubLevelWrapper = styled.div`
padding-bottom: 8px;
`;
const SubActionRow = ({ childrenForm, recursiveLevel, propertyActions }) => {
const [rowToOpen, setRowToOpen] = useState(null);
const handleClickToggleSubLevel = useCallback(name => {
setRowToOpen(prev => {
if (prev === name) {
return null;
}
return name;
});
}, []);
const displayedRecursiveValue = useMemo(() => {
if (!rowToOpen) {
return null;
}
return childrenForm.find(({ value }) => value === rowToOpen);
}, [rowToOpen, childrenForm]);
return (
<Wrapper>
<TopTimeline />
{childrenForm.map(({ label, value, required, children: subChildrenForm }, index) => {
const isVisible = index + 1 < childrenForm.length;
const isArrayType = Array.isArray(subChildrenForm);
const isSmall = isArrayType || index + 1 === childrenForm.length;
const isActive = rowToOpen === value;
console.log({ label, isSmall });
return (
<LeftBorderTimeline key={value} isVisible={isVisible}>
<RowWrapper isSmall={isSmall}>
<Curve fill="#a5d5ff" />
<Flex style={{ flex: 1 }}>
<RowStyle level={recursiveLevel} isActive={isActive} isCollapsable={isArrayType}>
<CollapseLabel
alignItems="center"
isCollapsable={isArrayType}
onClick={() => {
if (isArrayType) {
handleClickToggleSubLevel(value);
}
}}
title={label}
>
<Text
color={isActive ? 'mediumBlue' : 'grey'}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
{required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} />
</CollapseLabel>
</RowStyle>
<Flex style={{ flex: 1 }}>
{propertyActions.map(action => {
if (!action.isActionRelatedToCurrentProperty) {
return <HiddenAction key={action.label} />;
}
return <CheckboxWithCondition key={action.label} name="todo" />;
})}
</Flex>
</Flex>
</RowWrapper>
{displayedRecursiveValue && isActive && (
<SubLevelWrapper>
<SubActionRow
// name={displayedRecursiveValue.key}
propertyActions={propertyActions}
recursiveLevel={recursiveLevel + 1}
childrenForm={displayedRecursiveValue.children}
/>
</SubLevelWrapper>
)}
</LeftBorderTimeline>
);
})}
</Wrapper>
);
};
SubActionRow.defaultProps = {
recursiveLevel: 0,
};
SubActionRow.propTypes = {
childrenForm: PropTypes.array.isRequired,
propertyActions: PropTypes.array.isRequired,
recursiveLevel: PropTypes.number,
};
export default memo(SubActionRow);

View File

@ -0,0 +1,37 @@
import styled from 'styled-components';
import { Flex } from '@buffetjs/core';
import PropTypes from 'prop-types';
import Chevron from '../../../Chevron';
import { activeStyle } from '../../utils';
const RowStyle = styled.div`
padding-left: ${({ theme }) => theme.main.sizes.paddings.xs};
width: ${({ level }) => 128 - level * 18}px;
${Chevron} {
width: 13px;
}
${({ isCollapsable, theme }) =>
isCollapsable &&
`
${Chevron} {
display: block;
color: ${theme.main.colors.grey};
}
&:hover {
${activeStyle(theme)}
}
`}
${({ isActive, theme }) => isActive && activeStyle(theme)}}
`;
RowStyle.propTypes = {
isActive: PropTypes.bool.isRequired,
isCollapsable: PropTypes.bool.isRequired,
level: PropTypes.number.isRequired,
};
const RowWrapper = styled(Flex)`
height: ${({ isSmall }) => (isSmall ? '28px' : '36px')};
`;
export { RowStyle, RowWrapper };

View File

@ -0,0 +1,15 @@
import styled from 'styled-components';
const LeftBorderTimeline = styled.div`
border-left: ${({ isVisible }) => (isVisible ? '3px solid #a5d5ff' : '3px solid transparent')};
`;
const TopTimeline = styled.div`
padding-top: 8px;
width: 3px;
background-color: #a5d5ff;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
`;
export { LeftBorderTimeline, TopTimeline };

View File

@ -0,0 +1,21 @@
import styled from 'styled-components';
const Wrapper = styled.div`
padding-top: ${({ withPadding }) => (withPadding ? '9px' : '0px')};
border: 1px solid ${({ theme }) => theme.main.colors.darkBlue};
border-top: none;
border-bottom: ${({ isLast, theme }) => {
if (isLast) {
return `1px solid ${theme.main.colors.darkBlue}`;
}
return `none`;
}};
border-radius: 0px 0px 2px 2px;
`;
Wrapper.defaultProps = {
isLast: true,
};
export default Wrapper;

View File

@ -0,0 +1,50 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core';
import generateHeadersFromActions from './utils/generateHeadersFromActions';
import Header from './Header';
import ActionRow from './ActionRow';
import Wrapper from './Wrapper';
const CollapsePropertyMatrix = ({
availableActions,
childrenForm,
isLast,
isOdd,
label,
propertyName,
}) => {
const propertyActions = useMemo(
() => generateHeadersFromActions(availableActions, propertyName),
[availableActions, propertyName]
);
return (
<Wrapper withPadding={isOdd} isLast={isLast}>
<Header label={label} headers={propertyActions} />
<Padded left size="md">
{childrenForm.map(({ children: childrenForm, label, value, required }) => (
<ActionRow
childrenForm={childrenForm}
key={value}
label={label}
name={value}
required={required}
propertyActions={propertyActions}
/>
))}
</Padded>
</Wrapper>
);
};
CollapsePropertyMatrix.propTypes = {
childrenForm: PropTypes.array.isRequired,
availableActions: PropTypes.array.isRequired,
isOdd: PropTypes.bool.isRequired,
isLast: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
propertyName: PropTypes.string.isRequired,
};
export default CollapsePropertyMatrix;

View File

@ -0,0 +1,12 @@
const generateHeadersFromActions = (actions, propertyName) => {
return actions.map(action => {
const isActionRelatedToCurrentProperty =
Array.isArray(action.applyToProperties) &&
action.applyToProperties.indexOf(propertyName) !== -1 &&
action.isDisplayed;
return { label: action.label, isActionRelatedToCurrentProperty };
});
};
export default generateHeadersFromActions;

View File

@ -0,0 +1,52 @@
import generateHeadersFromActions from '../generateHeadersFromActions';
describe('ADMIN | COMPONENTS | Permissions | CollapsePropertyMatrix | utils', () => {
it('should set isActionRelatedToCurrentProperty key to false if the applyToProperties is not an array', () => {
const data = [{ label: 'test' }, { label: 'test1' }];
const expected = [
{ label: 'test', isActionRelatedToCurrentProperty: false },
{ label: 'test1', isActionRelatedToCurrentProperty: false },
];
expect(generateHeadersFromActions(data, 'test')).toEqual(expected);
});
it('should set isActionRelatedToCurrentProperty key to false if the propertyName is not in the applyToProperties array', () => {
const data = [
{ label: 'test', applyToProperties: ['foo'] },
{ label: 'test1', applyToProperties: ['foo'] },
];
const expected = [
{ label: 'test', isActionRelatedToCurrentProperty: false },
{ label: 'test1', isActionRelatedToCurrentProperty: false },
];
expect(generateHeadersFromActions(data, 'test')).toEqual(expected);
});
it('should set isActionRelatedToCurrentProperty key to false if the isDisplayed key is falsy', () => {
const data = [
{ label: 'test', isDisplayed: false },
{ label: 'test1', isDisplayed: false },
];
const expected = [
{ label: 'test', isActionRelatedToCurrentProperty: false },
{ label: 'test1', isActionRelatedToCurrentProperty: false },
];
expect(generateHeadersFromActions(data, 'test')).toEqual(expected);
});
it('should set the isActionRelatedToCurrentProperty key to true if isDisplayed is truthy and the propertyName is in the applyToProperties array', () => {
const data = [
{ label: 'test', isDisplayed: true, applyToProperties: ['test'] },
{ label: 'test1', isDisplayed: false, applyToProperties: ['test'] },
];
const expected = [
{ label: 'test', isActionRelatedToCurrentProperty: true },
{ label: 'test1', isActionRelatedToCurrentProperty: false },
];
expect(generateHeadersFromActions(data, 'test')).toEqual(expected);
});
});

View File

@ -0,0 +1,12 @@
/* eslint-disable indent */
import styled from 'styled-components';
const RowWrapper = styled.div`
${({ withMargin }) =>
withMargin &&
`
margin: 9px 0;
`}
`;
export default RowWrapper;

View File

@ -0,0 +1,62 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import Collapse from './Collapse';
import CollapsePropertyMatrix from './CollapsePropertyMatrix';
import { getAvailableActions } from './utils';
import Wrapper from './Wrapper';
const ContentTypeCollapse = ({
allActions,
contentTypeName,
index,
isActive,
onClickToggleCollapse,
properties,
}) => {
const handleClickToggleCollapse = useCallback(() => {
onClickToggleCollapse(contentTypeName);
}, [contentTypeName, onClickToggleCollapse]);
const availableActions = useMemo(() => {
return getAvailableActions(allActions, contentTypeName);
}, [allActions, contentTypeName]);
const isOdd = useMemo(() => index % 2 !== 0, [index]);
return (
<Wrapper withMargin={isOdd}>
<Collapse
availableActions={availableActions}
isActive={isActive}
isGrey={index % 2 === 0}
name={contentTypeName}
onClickToggle={handleClickToggleCollapse}
/>
{isActive &&
properties.map(({ label, value, children: childrenForm }, i) => {
return (
<CollapsePropertyMatrix
availableActions={availableActions}
childrenForm={childrenForm}
label={label}
propertyName={value}
key={value}
isLast={i === properties.length - 1}
isOdd={isOdd}
/>
);
})}
</Wrapper>
);
};
ContentTypeCollapse.propTypes = {
allActions: PropTypes.array.isRequired,
contentTypeName: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
isActive: PropTypes.bool.isRequired,
onClickToggleCollapse: PropTypes.func.isRequired,
properties: PropTypes.array.isRequired,
};
export default ContentTypeCollapse;

View File

@ -0,0 +1,16 @@
// import styled from 'styled-components';
import { Text } from '@buffetjs/core';
import Chevron from '../../Chevron';
const activeStyle = theme => `
color: ${theme.main.colors.mediumBlue};
${Text} {
color: ${theme.main.colors.mediumBlue};
}
${Chevron} {
display: block;
color: ${theme.main.colors.mediumBlue};
}
`;
export default activeStyle;

View File

@ -0,0 +1,10 @@
const getAvailableActions = (actions, targetSubject) => {
return actions.map(action => {
const isDisplayed =
Array.isArray(action.subjects) && action.subjects.indexOf(targetSubject) !== -1;
return { ...action, isDisplayed };
});
};
export default getAvailableActions;

View File

@ -0,0 +1,2 @@
export { default as activeStyle } from './activeStyle';
export { default as getAvailableActions } from './getAvailableActions';

View File

@ -0,0 +1,110 @@
import getAvailableActions from '../getAvailableActions';
describe('ADMIN | COMPONENTS | Permissions | ContentTypeCollapse | utils | getAvailableActions', () => {
it('should return an empty array', () => {
expect(getAvailableActions([])).toHaveLength(0);
});
it('should set the isDisplayed key to false to all actions if the subject value does not match the target', () => {
const actions = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
},
];
const expected = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
isDisplayed: false,
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
isDisplayed: false,
},
];
expect(getAvailableActions(actions, 'test')).toEqual(expected);
});
it('should set the isDisplayed key to false for the actions which the type of the subjects value is not an array', () => {
const actions = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: 'test',
applyToProperties: ['fields', 'locales'],
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
applyToProperties: ['fields', 'locales'],
},
];
const expected = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: 'test',
applyToProperties: ['fields', 'locales'],
isDisplayed: false,
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
applyToProperties: ['fields', 'locales'],
isDisplayed: false,
},
];
expect(getAvailableActions(actions, 'test')).toEqual(expected);
});
it('should set the isDisplayed key to false for the action which include the targetSubject in the subjects value array', () => {
const actions = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
subjects: [],
applyToProperties: ['fields', 'locales'],
},
];
const expected = [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
isDisplayed: true,
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
subjects: [],
applyToProperties: ['fields', 'locales'],
isDisplayed: false,
},
];
expect(getAvailableActions(actions, 'address')).toEqual(expected);
});
});

View File

@ -0,0 +1,42 @@
import React, { memo, useCallback, useState } from 'react';
import PropTypes from 'prop-types';
import ContentTypeCollapse from '../ContentTypeCollapse';
const ContentTypeCollapses = ({ actions, subjects }) => {
const [collapseToOpen, setCollapseToOpen] = useState(null);
const handleClickToggleCollapse = useCallback(
collapseName => {
const nextCollapseToOpen = collapseToOpen === collapseName ? null : collapseName;
setCollapseToOpen(nextCollapseToOpen);
},
[collapseToOpen]
);
return Object.keys(subjects).map((subject, index) => {
return (
<ContentTypeCollapse
allActions={actions}
key={subject}
contentTypeName={subject}
isActive={collapseToOpen === subject}
index={index}
onClickToggleCollapse={handleClickToggleCollapse}
properties={subjects[subject].properties}
/>
);
});
};
ContentTypeCollapses.defaultProps = {
actions: [],
subjects: {},
};
ContentTypeCollapses.propTypes = {
actions: PropTypes.array.isRequired,
subjects: PropTypes.object,
};
export default memo(ContentTypeCollapses);

View File

@ -0,0 +1,12 @@
import styled from 'styled-components';
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.main.colors.white};
overflow: auto;
::-webkit-scrollbar {
height: 10px;
}
`;
export default Wrapper;

View File

@ -0,0 +1,26 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core';
import ContentTypeCollapses from '../ContentTypeCollapses';
import GlobalActions from '../GlobalActions';
import Wrapper from './Wrapper';
const ContentTypes = ({ layout: { actions, subjects } }) => {
return (
<Wrapper>
<Padded left right bottom size="md">
<GlobalActions actions={actions} />
<ContentTypeCollapses subjects={subjects} actions={actions} />
</Padded>
</Wrapper>
);
};
ContentTypes.propTypes = {
layout: PropTypes.shape({
actions: PropTypes.array,
subjects: PropTypes.object,
}).isRequired,
};
export default memo(ContentTypes);

View File

@ -0,0 +1,35 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
const Curve = props => (
<svg
style={{
height: '14px',
transform: 'translate(-3.2px, -1px)',
position: 'relative',
}}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 21.08 21"
{...props}
>
<g>
<path
d="M2.58 2.5q-1.2 16 16 16"
fill="none"
stroke={props.fill}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="5"
/>
</g>
</svg>
);
Curve.defaultProps = {
fill: '#f3f4f4',
};
Curve.propTypes = {
fill: PropTypes.string,
};
export default memo(Curve);

View File

@ -0,0 +1,18 @@
import styled from 'styled-components';
const Wrapper = styled.div`
padding-left: 165px;
padding-bottom: 25px;
padding-top: 26px;
${({ disabled, theme }) =>
`
input[type='checkbox'] {
&:after {
color: ${!disabled ? theme.main.colors.mediumBlue : theme.main.colors.grey};
}
}
cursor: initial;
`}
`;
export default Wrapper;

View File

@ -0,0 +1,52 @@
import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';
import { Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import CheckboxWithCondition from '../CheckboxWithCondition';
import Wrapper from './Wrapper';
const GlobalActions = ({ actions }) => {
const { formatMessage } = useIntl();
const displayedActions = useMemo(() => {
return actions.filter(({ subjects }) => {
return subjects.length;
});
}, [actions]);
return (
<Wrapper>
<Flex>
{displayedActions.map(({ label, actionId }) => {
return (
<CheckboxWithCondition
key={actionId}
message={formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
})}
name={actionId}
value={false}
/>
);
})}
</Flex>
</Wrapper>
);
};
GlobalActions.defaultProps = {
actions: [],
};
GlobalActions.propTypes = {
actions: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
actionId: PropTypes.string.isRequired,
subjects: PropTypes.array.isRequired,
})
),
};
export default memo(GlobalActions);

View File

@ -0,0 +1,4 @@
const findDisplayedActions = actions =>
actions.filter(({ subjects }) => subjects && subjects.length);
export default findDisplayedActions;

View File

@ -0,0 +1,20 @@
import findDisplayedActions from '../findDisplayedActions';
describe('ADMIN | COMPONENTS | Permissions | GlobalActions | utils', () => {
describe('findDisplayedActions', () => {
it('should return an empty array', () => {
expect(findDisplayedActions([])).toHaveLength(0);
});
it('should filter the actions that have no subjects', () => {
const actions = [
{ label: 'Create' },
{ label: 'Read', subject: [] },
{ label: 'Update', subjects: ['test'] },
];
expect(findDisplayedActions(actions)).toHaveLength(1);
expect(findDisplayedActions(actions)[0]).toEqual({ label: 'Update', subjects: ['test'] });
});
});
});

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
const HiddenAction = styled.div`
min-width: 10rem;
max-width: 12rem;
flex: 1;
`;
export default HiddenAction;

View File

@ -0,0 +1,38 @@
import React, { forwardRef, memo, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { Tabs } from '../../../../../../admin/src/components/Roles';
import { roleTabsLabel as TAB_LABELS } from '../../../../../../admin/src/utils';
import ContentTypes from '../ContentTypes';
import layout from '../temp/fakeData';
const Permissions = forwardRef(({ layout }, ref) => {
useImperativeHandle(ref, () => {
return {
getPermissions: () => {
console.log('todo');
},
resetForm: () => {
console.log('todo');
},
};
});
return (
<Tabs tabsLabel={TAB_LABELS}>
<ContentTypes layout={layout.sections.collectionTypes} />
<ContentTypes layout={layout.sections.singleTypes} />
<div>Plugins</div>
<div>Settings</div>
</Tabs>
);
});
Permissions.defaultProps = {
layout,
};
Permissions.propTypes = {
// Todo
layout: PropTypes.object,
};
export default memo(Permissions);

View File

@ -0,0 +1,11 @@
import React from 'react';
import styled from 'styled-components';
const Required = styled.span`
color: ${({ theme }) => theme.main.colors.red};
padding-left: 2px;
`;
const RequiredSign = () => <Required>*</Required>;
export default RequiredSign;

View File

@ -0,0 +1,18 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
const Wrapper = styled.div`
display: flex;
align-items: center;
width: ${({ width }) => width};
`;
Wrapper.defaultProps = {
width: '18rem',
};
Wrapper.propTypes = {
width: PropTypes.string,
};
export default Wrapper;

View File

@ -0,0 +1,49 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { Checkbox, Text } from '@buffetjs/core';
import CollapseLabel from '../CollapseLabel';
import Wrapper from './Wrapper';
const RowLabel = ({ isCollapsable, label, children, onClick, textColor, width }) => {
return (
<Wrapper width={width}>
<Checkbox name="todo" value={false} />
<CollapseLabel
title={label}
alignItems="center"
isCollapsable={isCollapsable}
onClick={onClick}
>
<Text
color={textColor}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
{children}
</CollapseLabel>
</Wrapper>
);
};
RowLabel.defaultProps = {
children: null,
isCollapsable: false,
textColor: 'grey',
width: '18rem',
};
RowLabel.propTypes = {
children: PropTypes.node,
isCollapsable: PropTypes.bool,
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
textColor: PropTypes.string,
width: PropTypes.string,
};
export default memo(RowLabel);

View File

@ -17,9 +17,10 @@ import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/sr
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
import ContainerFluid from '../../../../../admin/src/components/ContainerFluid';
import FormCard from '../../../../../admin/src/components/FormBloc';
import { ButtonWithNumber, Permissions } from '../../../../../admin/src/components/Roles';
import { ButtonWithNumber } from '../../../../../admin/src/components/Roles';
import SizedInput from '../../../../../admin/src/components/SizedInput';
import { formatPermissionsToApi } from '../../../../../admin/src/utils';
import Permissions from './Permissions';
import schema from './utils/schema';
@ -31,8 +32,15 @@ const CreatePage = () => {
const { emitEvent, settingsBaseURL } = useGlobalContext();
const params = useRouteMatch(`${settingsBaseURL}/roles/duplicate/:id`);
const id = get(params, 'params.id', null);
const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout();
const { role, permissions: rolePermissions, isLoading: isRoleLoading } = useFetchRole(id);
const {
isLoading: isLayoutLoading,
// data: permissionsLayout
} = useFetchPermissionsLayout();
const {
// role, permissions: rolePermissions,
isLoading: isRoleLoading,
} = useFetchRole(id);
// console.log({ role, rolePermissions });
const headerActions = (handleSubmit, handleReset) => [
{
@ -191,10 +199,10 @@ const CreatePage = () => {
{!isLayoutLoading && !isRoleLoading && (
<Padded top bottom size="md">
<Permissions
permissionsLayout={permissionsLayout}
// permissionsLayout={permissionsLayout}
ref={permissionsRef}
rolePermissions={rolePermissions}
role={role}
// rolePermissions={rolePermissions}
// role={role}
/>
</Padded>
)}

View File

@ -0,0 +1,425 @@
const data = {
conditions: [
{
id: 'admin::is-creator',
displayName: 'Is creator',
category: 'default',
},
{
id: 'admin::has-same-role-as-creator',
displayName: 'Has same role as creator',
category: 'default',
},
],
sections: {
plugins: [
{
displayName: 'Read',
action: 'plugins::content-type-builder.read',
subCategory: 'general',
plugin: 'plugin::content-type-builder',
},
{
displayName: 'Access the Documentation',
action: 'plugins::documentation.read',
subCategory: 'general',
plugin: 'plugin::documentation',
},
{
displayName: 'Update and delete',
action: 'plugins::documentation.settings.update',
subCategory: 'settings',
plugin: 'plugin::documentation',
},
{
displayName: 'Regenerate',
action: 'plugins::documentation.settings.regenerate',
subCategory: 'settings',
plugin: 'plugin::documentation',
},
{
displayName: 'Access the Media Library',
action: 'plugins::upload.read',
subCategory: 'general',
plugin: 'plugin::upload',
},
{
displayName: 'Create (upload)',
action: 'plugins::upload.assets.create',
subCategory: 'assets',
plugin: 'plugin::upload',
},
{
displayName: 'Update (crop, details, replace) + delete',
action: 'plugins::upload.assets.update',
subCategory: 'assets',
plugin: 'plugin::upload',
},
{
displayName: 'Download',
action: 'plugins::upload.assets.download',
subCategory: 'assets',
plugin: 'plugin::upload',
},
{
displayName: 'Copy link',
action: 'plugins::upload.assets.copy-link',
subCategory: 'assets',
plugin: 'plugin::upload',
},
{
displayName: 'Create',
action: 'plugins::users-permissions.roles.create',
subCategory: 'roles',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Read',
action: 'plugins::users-permissions.roles.read',
subCategory: 'roles',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Update',
action: 'plugins::users-permissions.roles.update',
subCategory: 'roles',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Delete',
action: 'plugins::users-permissions.roles.delete',
subCategory: 'roles',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Read',
action: 'plugins::users-permissions.providers.read',
subCategory: 'providers',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Edit',
action: 'plugins::users-permissions.providers.update',
subCategory: 'providers',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Read',
action: 'plugins::users-permissions.email-templates.read',
subCategory: 'emailTemplates',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Edit',
action: 'plugins::users-permissions.email-templates.update',
subCategory: 'emailTemplates',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Read',
action: 'plugins::users-permissions.advanced-settings.read',
subCategory: 'advancedSettings',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Edit',
action: 'plugins::users-permissions.advanced-settings.update',
subCategory: 'advancedSettings',
plugin: 'plugin::users-permissions',
},
{
displayName: 'Configure view',
action: 'plugins::content-manager.single-types.configure-view',
subCategory: 'single types',
plugin: 'plugin::content-manager',
},
{
displayName: 'Configure view',
action: 'plugins::content-manager.collection-types.configure-view',
subCategory: 'collection types',
plugin: 'plugin::content-manager',
},
{
displayName: 'Configure Layout',
action: 'plugins::content-manager.components.configure-layout',
subCategory: 'components',
plugin: 'plugin::content-manager',
},
],
settings: [
{
displayName: 'Create',
action: 'plugins::i18n.locale.create',
category: 'Internationalization',
subCategory: 'Locales',
},
{
displayName: 'Read',
action: 'plugins::i18n.locale.read',
category: 'Internationalization',
subCategory: 'Locales',
},
{
displayName: 'Update',
action: 'plugins::i18n.locale.update',
category: 'Internationalization',
subCategory: 'Locales',
},
{
displayName: 'Delete',
action: 'plugins::i18n.locale.delete',
category: 'Internationalization',
subCategory: 'Locales',
},
{
displayName: 'Access the Media Library settings page',
action: 'plugins::upload.settings.read',
category: 'media library',
subCategory: 'general',
},
{
displayName: 'Read',
action: 'admin::provider-login.read',
category: 'single sign on',
subCategory: 'options',
},
{
displayName: 'Update',
action: 'admin::provider-login.update',
category: 'single sign on',
subCategory: 'options',
},
{
displayName: 'Access the marketplace',
action: 'admin::marketplace.read',
category: 'plugins and marketplace',
subCategory: 'marketplace',
},
{
displayName: 'Install (only for dev env)',
action: 'admin::marketplace.plugins.install',
category: 'plugins and marketplace',
subCategory: 'plugins',
},
{
displayName: 'Uninstall (only for dev env)',
action: 'admin::marketplace.plugins.uninstall',
category: 'plugins and marketplace',
subCategory: 'plugins',
},
{
displayName: 'Create',
action: 'admin::webhooks.create',
category: 'webhooks',
subCategory: 'general',
},
{
displayName: 'Read',
action: 'admin::webhooks.read',
category: 'webhooks',
subCategory: 'general',
},
{
displayName: 'Update',
action: 'admin::webhooks.update',
category: 'webhooks',
subCategory: 'general',
},
{
displayName: 'Delete',
action: 'admin::webhooks.delete',
category: 'webhooks',
subCategory: 'general',
},
{
displayName: 'Create (invite)',
action: 'admin::users.create',
category: 'users and roles',
subCategory: 'users',
},
{
displayName: 'Read',
action: 'admin::users.read',
category: 'users and roles',
subCategory: 'users',
},
{
displayName: 'Update',
action: 'admin::users.update',
category: 'users and roles',
subCategory: 'users',
},
{
displayName: 'Delete',
action: 'admin::users.delete',
category: 'users and roles',
subCategory: 'users',
},
{
displayName: 'Create',
action: 'admin::roles.create',
category: 'users and roles',
subCategory: 'roles',
},
{
displayName: 'Read',
action: 'admin::roles.read',
category: 'users and roles',
subCategory: 'roles',
},
{
displayName: 'Update',
action: 'admin::roles.update',
category: 'users and roles',
subCategory: 'roles',
},
{
displayName: 'Delete',
action: 'admin::roles.delete',
category: 'users and roles',
subCategory: 'roles',
},
],
singleTypes: {}, // same format as under,
collectionTypes: {
subjects: {
address: {
properties: [
{
label: 'Fields',
value: 'fields',
children: [
{
label: 'POST',
value: 'postal_coder',
required: true,
},
{
label: 'categories',
value: 'categories',
},
{
label: 'cover',
value: 'cover',
},
{
label: 'images',
value: 'images',
},
{
label: 'city',
value: 'city',
},
],
},
],
},
restaurant: {
properties: [
{
label: 'Fields',
value: 'fields',
children: [
{
label: 'f1',
value: 'f1',
required: true,
},
{
label: 'f2',
value: 'f2',
},
{
// nested compo
label: 'services',
value: 'services',
children: [
{
label: 'name',
value: 'name',
},
{
label: 'media',
value: 'media',
required: true,
},
{
label: 'closing',
value: 'closing',
children: [
{
label: 'name',
value: 'name',
children: [{ label: 'test', value: 'test' }],
},
],
},
],
},
{
label: 'dz',
value: 'dz',
},
{
label: 'relation',
value: 'relation',
},
],
},
{
label: 'Locales',
value: 'locales',
children: [
{
label: 'French',
value: 'fr',
},
{
label: 'English',
required: true,
value: 'en',
},
],
},
],
},
// test: {
// properties: [],
// },
},
actions: [
{
label: 'Create',
actionId: 'content-manager.explorer.create',
subjects: ['restaurant', 'address'],
applyToProperties: ['fields', 'locales'],
},
{
label: 'Read',
actionId: 'content-manager.explorer.read',
subjects: ['restaurant', 'addresse'],
applyToProperties: ['fields', 'locales'],
},
{
label: 'Update',
actionId: 'content-manager.explorer.update',
subjects: ['addresse', 'restaurant'],
applyToProperties: ['fields'],
},
{
label: 'Delete',
actionId: 'content-manager.explorer.delete',
subjects: ['restaurant', 'address'],
},
{
label: 'Publish',
actionId: 'content-manager.explorer.publish',
subjects: ['restaurant'],
},
],
},
},
};
export default data;