Merge pull request #10743 from strapi/migration/admin-setting-create-role

Migration/admin setting create role
This commit is contained in:
cyril lopez 2021-08-27 08:03:12 +02:00 committed by GitHub
commit 6b74e05a63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1949 additions and 1052 deletions

View File

@ -1,8 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Flex } from '@buffetjs/core'; import { Row } from '@strapi/parts';
const CollapseLabel = styled(Flex)` const CollapseLabel = styled(Row)`
padding-right: 10px; padding-right: ${({ theme }) => theme.spaces[2]};
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
${({ isCollapsable }) => isCollapsable && 'cursor: pointer;'} ${({ isCollapsable }) => isCollapsable && 'cursor: pointer;'}

View File

@ -1,28 +0,0 @@
/* 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

@ -1,32 +1,40 @@
import React from 'react'; import { Settings } from '@strapi/icons';
import styled from 'styled-components'; import { Button } from '@strapi/parts';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Flex, Text, Padded } from '@buffetjs/core'; import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Wrapper from './Wrapper'; const Wrapper = styled.div`
position: relative;
const ConditionsButton = ({ onClick, className, hasConditions, isRight }) => { ${({ hasConditions, disabled, theme }) =>
hasConditions &&
`
&:before {
content: '';
position: absolute;
top: -3px;
left: -10px;
width: 6px;
height: 6px;
border-radius: ${20 / 16}rem;;
background: ${disabled ? theme.colors.neutral100 : theme.colors.primary600};
}
`}
`;
const ConditionsButton = ({ onClick, className, hasConditions, variant }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<Wrapper <Wrapper hasConditions={hasConditions} className={className}>
isRight={isRight} <Button variant={variant} startIcon={<Settings />} onClick={onClick}>
hasConditions={hasConditions} {formatMessage({
className={className} id: 'app.components.LeftMenuLinkContainer.settings',
onClick={onClick} defaultMessage: 'Settings',
> })}
<Padded right size="smd"> </Button>
<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> </Wrapper>
); );
}; };
@ -34,13 +42,13 @@ const ConditionsButton = ({ onClick, className, hasConditions, isRight }) => {
ConditionsButton.defaultProps = { ConditionsButton.defaultProps = {
className: null, className: null,
hasConditions: false, hasConditions: false,
isRight: false, variant: 'secondary',
}; };
ConditionsButton.propTypes = { ConditionsButton.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
hasConditions: PropTypes.bool, hasConditions: PropTypes.bool,
isRight: PropTypes.bool, variant: PropTypes.string,
}; };
// This is a styled component advanced usage : // This is a styled component advanced usage :

View File

@ -1,12 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
display: flex;
height: 36px;
border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
margin-bottom: 18px;
background-color: ${({ theme, isGrey }) =>
isGrey ? theme.main.colors.content.background : theme.main.colors.white};
`;
export default Wrapper;

View File

@ -1,88 +1,81 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, Padded, Flex } from '@buffetjs/core'; import { Row, TableLabel } from '@strapi/parts';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import styled from 'styled-components';
import ConditionsSelect from '../ConditionsSelect'; // import ConditionsSelect from '../ConditionsSelect';
import Wrapper from './Wrapper'; import { rowHeight } from '../../Permissions/utils/constants';
const RowWrapper = styled(Row)`
height: ${rowHeight};
`;
const ActionRow = ({ const ActionRow = ({
arrayOfOptionsGroupedByCategory, // arrayOfOptionsGroupedByCategory,
isFormDisabled, // isFormDisabled,
isGrey, isGrey,
label, label,
name, // name,
onCategoryChange, // onCategoryChange,
onChange, // onChange,
value, // value,
}) => { }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<Wrapper isGrey={isGrey}> <RowWrapper as="li" background={isGrey ? 'neutral100' : 'neutral0'}>
<Padded style={{ width: 200 }} top left right bottom size="sm"> <Row paddingLeft={6} style={{ width: 180 }}>
<Flex> <TableLabel textColor="neutral600">
<Text {formatMessage({
lineHeight="19px" id: 'Settings.permissions.conditions.can',
color="grey" defaultMessage: 'Can',
fontSize="xs" })}
fontWeight="bold" &nbsp;
textTransform="uppercase" </TableLabel>
> <TableLabel
{formatMessage({ title={label}
id: 'Settings.permissions.conditions.can', textColor="primary600"
})} // ! REMOVE THIS WHEN DS IS UPDATED WITH ELLIPSIS PROP
&nbsp; style={{
</Text> overflow: 'hidden',
<Text whiteSpace: 'nowrap',
title={label} textOverflow: 'ellipsis',
lineHeight="19px" }}
fontWeight="bold" >
fontSize="xs" {formatMessage({
textTransform="uppercase" id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
color="mediumBlue" defaultMessage: label,
style={{ maxWidth: '60%' }} })}
ellipsis </TableLabel>
> <TableLabel textColor="neutral600">
{formatMessage({ &nbsp;
id: `Settings.roles.form.permissions.${label.toLowerCase()}`, {formatMessage({
defaultMessage: label, id: 'Settings.permissions.conditions.when',
})} defaultMessage: 'When',
</Text> })}
<Text </TableLabel>
lineHeight="19px" </Row>
color="grey" {/* <ConditionsSelect
fontSize="xs"
fontWeight="bold"
textTransform="uppercase"
>
&nbsp;
{formatMessage({
id: 'Settings.permissions.conditions.when',
})}
</Text>
</Flex>
</Padded>
<ConditionsSelect
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory} arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
name={name} name={name}
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
onCategoryChange={onCategoryChange} onCategoryChange={onCategoryChange}
onChange={onChange} onChange={onChange}
value={value} value={value}
/> /> */}
</Wrapper> </RowWrapper>
); );
}; };
ActionRow.propTypes = { ActionRow.propTypes = {
arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired, // arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired,
isFormDisabled: PropTypes.bool.isRequired, // isFormDisabled: PropTypes.bool.isRequired,
isGrey: PropTypes.bool.isRequired, isGrey: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired, // name: PropTypes.string.isRequired,
value: PropTypes.object.isRequired, // value: PropTypes.object.isRequired,
onCategoryChange: PropTypes.func.isRequired, // onCategoryChange: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, // onChange: PropTypes.func.isRequired,
}; };
export default ActionRow; export default ActionRow;

View File

@ -1,9 +0,0 @@
import styled from 'styled-components';
const Separator = styled.div`
padding-top: 1.4rem;
margin-bottom: 2.8rem;
border-bottom: 1px solid ${({ theme }) => theme.main.colors.brightGrey};
`;
export default Separator;

View File

@ -1,14 +1,24 @@
import React, { useMemo, useState } from 'react'; import {
Box,
Breadcrumbs,
Button,
Crumb,
H2,
ModalFooter,
ModalHeader,
ModalLayout,
Stack,
Text,
Divider,
} from '@strapi/parts';
import { cloneDeep, get, groupBy, set, upperFirst } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { cloneDeep, get, groupBy, set } from 'lodash'; import React, { useMemo, useState } from 'react';
import { Modal, ModalHeader, ModalFooter } from '@strapi/helper-plugin';
import { Button, Text, Padded } from '@buffetjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { usePermissionsDataManager } from '../../../hooks'; import { usePermissionsDataManager } from '../../../hooks';
import createDefaultConditionsForm from './utils/createDefaultConditionsForm';
import ActionRow from './ActionRow';
import Separator from './Separator';
import updateValues from '../Permissions/utils/updateValues'; import updateValues from '../Permissions/utils/updateValues';
import ActionRow from './ActionRow';
import createDefaultConditionsForm from './utils/createDefaultConditionsForm';
const ConditionsModal = ({ const ConditionsModal = ({
actions, actions,
@ -79,53 +89,83 @@ const ConditionsModal = ({
onToggle(); onToggle();
}; };
if (!isOpen) return null;
return ( return (
<Modal withoverflow="true" onClosed={onClosed} isOpen={isOpen} onToggle={onToggle}> <ModalLayout onClose={onClosed}>
<ModalHeader headerBreadcrumbs={headerBreadCrumbs} /> <ModalHeader>
<Padded top left right bottom size="md"> <Breadcrumbs label={headerBreadCrumbs.join(', ')}>
<Text fontSize="lg" fontWeight="bold"> {headerBreadCrumbs.map(label => (
{formatMessage({ <Crumb key={label}>
id: 'Settings.permissions.conditions.define-conditions', {upperFirst(
})} formatMessage({
</Text> id: label,
<Separator /> defaultMessage: label,
{actionsToDisplay.length === 0 && ( })
<Text fontSize="md" color="grey"> )}
{formatMessage({ id: 'Settings.permissions.conditions.no-actions' })} </Crumb>
</Text> ))}
)} </Breadcrumbs>
{actionsToDisplay.map(({ actionId, label, pathToConditionsObject }, index) => { </ModalHeader>
const name = pathToConditionsObject.join('..'); <Box padding={8}>
<Stack size={6}>
<H2>
{formatMessage({
id: 'Settings.permissions.conditions.define-conditions',
defaultMessage: 'Define conditions',
})}
</H2>
<Box>
<Divider />
</Box>
<Box>
{actionsToDisplay.length === 0 && (
<Text>
{formatMessage({
id: 'Settings.permissions.conditions.no-actions',
defaultMessage:
'You first need to select actions (create, read, update, ...) before defining conditions on them.',
})}
</Text>
)}
<ul>
{actionsToDisplay.map(({ actionId, label, pathToConditionsObject }, index) => {
const name = pathToConditionsObject.join('..');
return ( return (
<ActionRow <ActionRow
key={actionId} key={actionId}
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory} arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
label={label} label={label}
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
isGrey={index % 2 === 0} isGrey={index % 2 === 0}
name={name} name={name}
onCategoryChange={handleCategoryChange} onCategoryChange={handleCategoryChange}
onChange={handleChange} onChange={handleChange}
value={get(state, name, {})} value={get(state, name, {})}
/> />
); );
})} })}
</Padded> </ul>
<ModalFooter> </Box>
<section> </Stack>
<Button type="button" color="cancel" onClick={onToggle}> </Box>
{formatMessage({ id: 'app.components.Button.cancel' })} <ModalFooter
startActions={
<Button variant="tertiary" onClick={onToggle}>
{formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })}
</Button> </Button>
}
<Button type="button" color="success" onClick={handleSubmit}> endActions={
<Button onClick={handleSubmit}>
{formatMessage({ {formatMessage({
id: 'Settings.permissions.conditions.apply', id: 'Settings.permissions.conditions.apply',
defaultMessage: 'Apply',
})} })}
</Button> </Button>
</section> }
</ModalFooter> />
</Modal> </ModalLayout>
); );
}; };

View File

@ -1,38 +0,0 @@
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

@ -1,19 +1,97 @@
import React, { useMemo, useState } from 'react'; import { Down, Up } from '@strapi/icons';
import { Box, Checkbox, Row, Text } from '@strapi/parts';
import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/Collapse/utils/constants';
import { get, omit } from 'lodash'; import { get, omit } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Flex, Padded } from '@buffetjs/core'; import React, { useMemo, useState } from 'react';
import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/Collapse/utils/constants'; import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { usePermissionsDataManager } from '../../../../hooks'; import { usePermissionsDataManager } from '../../../../hooks';
import { getCheckboxState } from '../../utils';
import CheckboxWithCondition from '../../CheckboxWithCondition';
import Chevron from '../../Chevron';
import ConditionsButton from '../../ConditionsButton'; import ConditionsButton from '../../ConditionsButton';
import ConditionsModal from '../../ConditionsModal'; import ConditionsModal from '../../ConditionsModal';
import HiddenAction from '../../HiddenAction'; import HiddenAction from '../../HiddenAction';
import { cellWidth, rowHeight } from '../../Permissions/utils/constants';
import RowLabelWithCheckbox from '../../RowLabelWithCheckbox'; import RowLabelWithCheckbox from '../../RowLabelWithCheckbox';
import Wrapper from './Wrapper'; import { getCheckboxState } from '../../utils';
import generateCheckboxesActions from './utils/generateCheckboxesActions'; import generateCheckboxesActions from './utils/generateCheckboxesActions';
const activeRowStyle = (theme, isActive, isClicked) => `
${Wrapper} {
${isClicked ? `border: 1px solid ${theme.colors.primary600}; border-bottom: none;` : ''}
background-color: ${theme.colors.primary100};
color: ${theme.colors.primary600};
border-radius: ${isActive ? '2px 2px 0 0' : '2px'};
}
${Text} {
color: ${theme.colors.primary600};
font-weight: bold;
}
${Chevron} {
display: block;
}
${ConditionsButton} {
display: block;
}
`;
const Wrapper = styled.div`
flex: 1;
display: flex;
align-items: center;
height: ${rowHeight};
background-color: ${({ isGrey, theme }) =>
isGrey ? theme.colors.neutral100 : theme.colors.neutral0};
border: 1px solid transparent;
`;
const BoxWrapper = styled.div`
display: inline-flex;
min-width: 100%;
${ConditionsButton} {
display: none;
}
${({ isActive, theme }) => isActive && activeRowStyle(theme, isActive, true)}
&:hover {
${({ theme, isActive }) => activeRowStyle(theme, isActive)}
}
&:focus-within {
${({ theme, isActive }) => activeRowStyle(theme, isActive)}
}
`;
const Cell = styled(Row)`
width: ${cellWidth};
position: relative;
`;
const Chevron = styled(Box)`
display: none;
svg {
width: 11px;
}
* {
fill: ${({ theme }) => theme.colors.primary600};
}
`;
const TinyDot = styled(Box)`
position: absolute;
top: -6px;
left: 37px;
width: 6px;
height: 6px;
border-radius: 20px;
background: ${({ theme }) => theme.colors.primary600};
`;
const AbsoluteBox = styled(Box)`
position: absolute;
right: 9px;
transform: translateY(10px);
`;
const Collapse = ({ const Collapse = ({
availableActions, availableActions,
isActive, isActive,
@ -23,7 +101,8 @@ const Collapse = ({
onClickToggle, onClickToggle,
pathToData, pathToData,
}) => { }) => {
const [modalState, setModalState] = useState({ isOpen: false, isMounted: false }); const [isModalOpen, setModalOpen] = useState(false);
const { formatMessage } = useIntl();
const { const {
modifiedData, modifiedData,
onChangeParentCheckbox, onChangeParentCheckbox,
@ -31,11 +110,11 @@ const Collapse = ({
} = usePermissionsDataManager(); } = usePermissionsDataManager();
const handleToggleModalIsOpen = () => { const handleToggleModalIsOpen = () => {
setModalState(prevState => ({ isMounted: true, isOpen: !prevState.isOpen })); setModalOpen(s => !s);
}; };
const handleModalClose = () => { const handleModalClose = () => {
setModalState(prevState => ({ ...prevState, isMounted: false })); setModalOpen(false);
}; };
// This corresponds to the data related to the CT left checkbox // This corresponds to the data related to the CT left checkbox
@ -65,9 +144,8 @@ const Collapse = ({
); );
return ( return (
<Wrapper isActive={isActive} isGrey={isGrey}> <BoxWrapper isActive={isActive}>
<Flex style={{ flex: 1 }}> <Wrapper isGrey={isGrey}>
<Padded left size="sm" />
<RowLabelWithCheckbox <RowLabelWithCheckbox
isCollapsable isCollapsable
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
@ -77,11 +155,12 @@ const Collapse = ({
onClick={onClickToggle} onClick={onClickToggle}
someChecked={hasSomeActionsSelected} someChecked={hasSomeActionsSelected}
value={hasAllActionsSelected} value={hasAllActionsSelected}
isActive={isActive}
> >
<Chevron icon={isActive ? 'chevron-up' : 'chevron-down'} /> <Chevron paddingLeft={2}>{isActive ? <Up /> : <Down />}</Chevron>
</RowLabelWithCheckbox> </RowLabelWithCheckbox>
<Flex style={{ flex: 1 }}> <Row style={{ flex: 1 }}>
{checkboxesActions.map( {checkboxesActions.map(
({ ({
actionId, actionId,
@ -91,6 +170,7 @@ const Collapse = ({
isDisplayed, isDisplayed,
isParentCheckbox, isParentCheckbox,
checkboxName, checkboxName,
label: permissionLabel,
}) => { }) => {
if (!isDisplayed) { if (!isDisplayed) {
return <HiddenAction key={actionId} />; return <HiddenAction key={actionId} />;
@ -98,48 +178,72 @@ const Collapse = ({
if (isParentCheckbox) { if (isParentCheckbox) {
return ( return (
<CheckboxWithCondition <Cell key={actionId} justifyContent="center" alignItems="center">
key={actionId} {hasConditions && <TinyDot />}
disabled={isFormDisabled || IS_DISABLED} <Checkbox
hasConditions={hasConditions} disabled={isFormDisabled || IS_DISABLED}
name={checkboxName} name={checkboxName}
onChange={onChangeParentCheckbox} aria-label={formatMessage(
someChecked={hasSomeActionsSelected} {
value={hasAllActionsSelected} id: `Settings.permissions.select-by-permission`,
/> defaultMessage: 'Select {label} permission',
},
{ label: `${permissionLabel} ${label}` }
)}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeParentCheckbox({
target: {
name: checkboxName,
value,
},
})}
indeterminate={hasSomeActionsSelected}
value={hasAllActionsSelected}
/>
</Cell>
); );
} }
return ( return (
<CheckboxWithCondition <Cell key={actionId} justifyContent="center" alignItems="center">
key={actionId} {hasConditions && <TinyDot />}
disabled={isFormDisabled || IS_DISABLED} <Checkbox
hasConditions={hasConditions} disabled={isFormDisabled || IS_DISABLED}
name={checkboxName} indeterminate={hasConditions}
onChange={onChangeSimpleCheckbox} name={checkboxName}
value={hasAllActionsSelected} // Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
/> onValueChange={value =>
onChangeSimpleCheckbox({
target: {
name: checkboxName,
value,
},
})}
value={hasAllActionsSelected}
/>
</Cell>
); );
} }
)} )}
</Flex> </Row>
<ConditionsButton <Box style={{ width: 120 }} />
isRight
onClick={handleToggleModalIsOpen}
hasConditions={doesConditionButtonHasConditions}
/>
</Flex>
{modalState.isMounted && (
<ConditionsModal <ConditionsModal
headerBreadCrumbs={[label, 'app.components.LeftMenuLinkContainer.settings']} headerBreadCrumbs={[label, 'app.components.LeftMenuLinkContainer.settings']}
actions={checkboxesActions} actions={checkboxesActions}
isOpen={modalState.isOpen} isOpen={isModalOpen}
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
onClosed={handleModalClose} onClosed={handleModalClose}
onToggle={handleToggleModalIsOpen} onToggle={handleToggleModalIsOpen}
/> />
)} </Wrapper>
</Wrapper> <AbsoluteBox>
<ConditionsButton
onClick={handleToggleModalIsOpen}
hasConditions={doesConditionButtonHasConditions}
/>
</AbsoluteBox>
</BoxWrapper>
); );
}; };

View File

@ -1,28 +0,0 @@
/* 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

@ -1,19 +1,44 @@
import React, { memo, useState, useMemo, useCallback } from 'react'; import { Checkbox, Row } from '@strapi/parts';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { Padded, Flex } from '@buffetjs/core';
import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/CollapsePropertyMatrix/ActionRow/utils/constants'; import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/CollapsePropertyMatrix/ActionRow/utils/constants';
import { get } from 'lodash';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import React, { memo, useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { usePermissionsDataManager } from '../../../../../hooks'; import { usePermissionsDataManager } from '../../../../../hooks';
import { getCheckboxState } from '../../../utils';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import HiddenAction from '../../../HiddenAction'; import HiddenAction from '../../../HiddenAction';
import { cellWidth, rowHeight } from '../../../Permissions/utils/constants';
import RequiredSign from '../../../RequiredSign'; import RequiredSign from '../../../RequiredSign';
import RowLabelWithCheckbox from '../../../RowLabelWithCheckbox'; import RowLabelWithCheckbox from '../../../RowLabelWithCheckbox';
import { getCheckboxState } from '../../../utils';
import { activeStyle } from '../../utils';
import CarretIcon from '../CarretIcon';
import SubActionRow from '../SubActionRow'; import SubActionRow from '../SubActionRow';
import Wrapper from './Wrapper';
import getRowLabelCheckboxeState from './utils/getRowLabelCheckboxeState'; import getRowLabelCheckboxeState from './utils/getRowLabelCheckboxeState';
const Cell = styled(Row)`
width: ${cellWidth};
position: relative;
`;
const Wrapper = styled(Row)`
height: ${rowHeight};
flex: 1;
${({ isCollapsable, theme }) =>
isCollapsable &&
`
${CarretIcon} {
display: block;
color: ${theme.colors.neutral100};
}
&:hover {
${activeStyle(theme)}
}
`}
${({ isActive, theme }) => isActive && activeStyle(theme)};
`;
const ActionRow = ({ const ActionRow = ({
childrenForm, childrenForm,
label, label,
@ -23,7 +48,9 @@ const ActionRow = ({
pathToData, pathToData,
propertyActions, propertyActions,
propertyName, propertyName,
isOdd,
}) => { }) => {
const { formatMessage } = useIntl();
const [rowToOpen, setRowToOpen] = useState(null); const [rowToOpen, setRowToOpen] = useState(null);
const { const {
modifiedData, modifiedData,
@ -66,11 +93,14 @@ const ActionRow = ({
return ( return (
<> <>
<Wrapper alignItems="center" isCollapsable={isCollapsable} isActive={isActive}> <Wrapper
<Flex style={{ flex: 1 }}> alignItems="center"
<Padded left size="sm" /> isCollapsable={isCollapsable}
isActive={isActive}
background={isOdd ? 'neutral100' : 'neutral0'}
>
<Row>
<RowLabelWithCheckbox <RowLabelWithCheckbox
width="15rem"
onChange={handleChangeLeftRowCheckbox} onChange={handleChangeLeftRowCheckbox}
onClick={handleClick} onClick={handleClick}
isCollapsable={isCollapsable} isCollapsable={isCollapsable}
@ -78,11 +108,12 @@ const ActionRow = ({
label={label} label={label}
someChecked={hasSomeActionsSelected} someChecked={hasSomeActionsSelected}
value={hasAllActionsSelected} value={hasAllActionsSelected}
isActive={isActive}
> >
{required && <RequiredSign />} {required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} /> <CarretIcon $isActive={isActive} />
</RowLabelWithCheckbox> </RowLabelWithCheckbox>
<Flex style={{ flex: 1 }}> <Row>
{propertyActions.map(({ label, isActionRelatedToCurrentProperty, actionId }) => { {propertyActions.map(({ label, isActionRelatedToCurrentProperty, actionId }) => {
if (!isActionRelatedToCurrentProperty) { if (!isActionRelatedToCurrentProperty) {
return <HiddenAction key={label} />; return <HiddenAction key={label} />;
@ -100,13 +131,28 @@ const ActionRow = ({
const checkboxValue = get(modifiedData, checkboxName, false); const checkboxValue = get(modifiedData, checkboxName, false);
return ( return (
<CheckboxWithCondition <Cell key={actionId} justifyContent="center" alignItems="center">
key={actionId} <Checkbox
disabled={isFormDisabled || IS_DISABLED} disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')} name={checkboxName.join('..')}
onChange={onChangeSimpleCheckbox} aria-label={formatMessage(
value={checkboxValue} {
/> id: `Settings.permissions.select-by-permission`,
defaultMessage: 'Select {label} permission',
},
{ label: `${name} ${label}` }
)}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeSimpleCheckbox({
target: {
name: checkboxName.join('..'),
value,
},
})}
value={checkboxValue}
/>
</Cell>
); );
} }
@ -115,18 +161,33 @@ const ActionRow = ({
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(data); const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(data);
return ( return (
<CheckboxWithCondition <Cell key={label} justifyContent="center" alignItems="center">
key={label} <Checkbox
disabled={isFormDisabled || IS_DISABLED} disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')} name={checkboxName.join('..')}
onChange={onChangeParentCheckbox} // Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
value={hasAllActionsSelected} onValueChange={value =>
someChecked={hasSomeActionsSelected} onChangeParentCheckbox({
/> target: {
name: checkboxName.join('..'),
value,
},
})}
aria-label={formatMessage(
{
id: `Settings.permissions.select-by-permission`,
defaultMessage: 'Select {label} permission',
},
{ label: `${name} ${label}` }
)}
value={hasAllActionsSelected}
indeterminate={hasSomeActionsSelected}
/>
</Cell>
); );
})} })}
</Flex> </Row>
</Flex> </Row>
</Wrapper> </Wrapper>
{isActive && ( {isActive && (
<SubActionRow <SubActionRow
@ -157,6 +218,7 @@ ActionRow.propTypes = {
propertyActions: PropTypes.array.isRequired, propertyActions: PropTypes.array.isRequired,
propertyName: PropTypes.string.isRequired, propertyName: PropTypes.string.isRequired,
required: PropTypes.bool, required: PropTypes.bool,
isOdd: PropTypes.bool.isRequired,
}; };
export default memo(ActionRow); export default memo(ActionRow);

View File

@ -0,0 +1,14 @@
import { Carret } from '@strapi/icons';
import styled from 'styled-components';
const CarretIcon = styled(Carret)`
display: none;
width: ${10 / 16}rem;
transform: rotate(${({ $isActive }) => ($isActive ? '180' : '0')}deg);
margin-left: ${({ theme }) => theme.spaces[2]};
* {
fill: ${({ theme }) => theme.colors.primary600};
}
`;
export default CarretIcon;

View File

@ -1,20 +1,18 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { Flex, Text } from '@buffetjs/core'; import { Row, TableLabel } from '@strapi/parts';
import styled from 'styled-components'; import styled from 'styled-components';
// Those styles are very specific. import { cellWidth, firstRowWidth, rowHeight } from '../../../Permissions/utils/constants';
// so it is not a big problem to use custom paddings and widths.
const HeaderLabel = styled.div` const HeaderLabel = styled(Row)`
width: 12rem; width: ${cellWidth};
padding-top: 1rem; flex-shrink: 0;
padding-bottom: 1rem;
`; `;
const PropertyLabelWrapper = styled.div` const PropertyLabelWrapper = styled(Row)`
width: 18rem; width: ${firstRowWidth};
padding-top: 1rem; height: ${rowHeight};
padding-bottom: 1rem; flex-shrink: 0;
padding-left: 3.5rem;
`; `;
const Header = ({ headers, label }) => { const Header = ({ headers, label }) => {
@ -28,9 +26,9 @@ const Header = ({ headers, label }) => {
); );
return ( return (
<Flex> <Row>
<PropertyLabelWrapper> <PropertyLabelWrapper alignItems="center" paddingLeft={6}>
<Text fontWeight="bold">{translatedLabel}</Text> <TableLabel textColor="neutral500">{translatedLabel}</TableLabel>
</PropertyLabelWrapper> </PropertyLabelWrapper>
{headers.map(header => { {headers.map(header => {
if (!header.isActionRelatedToCurrentProperty) { if (!header.isActionRelatedToCurrentProperty) {
@ -38,17 +36,17 @@ const Header = ({ headers, label }) => {
} }
return ( return (
<HeaderLabel key={header.label}> <HeaderLabel justifyContent="center" key={header.label}>
<Text textTransform="capitalize" fontWeight="bold"> <TableLabel textColor="neutral500">
{formatMessage({ {formatMessage({
id: `Settings.roles.form.permissions.${header.label.toLowerCase()}`, id: `Settings.roles.form.permissions.${header.label.toLowerCase()}`,
defaultMessage: header.label, defaultMessage: header.label,
})} })}
</Text> </TableLabel>
</HeaderLabel> </HeaderLabel>
); );
})} })}
</Flex> </Row>
); );
}; };

View File

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

View File

@ -1,23 +1,70 @@
import React, { memo, useMemo, useState } from 'react'; import { Box, Checkbox, Row, Text } from '@strapi/parts';
import PropTypes from 'prop-types';
import { get } from 'lodash';
import { Flex, Text } from '@buffetjs/core';
import styled from 'styled-components';
import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/CollapsePropertyMatrix/SubActionRow/utils/constants'; import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/CollapsePropertyMatrix/SubActionRow/utils/constants';
import { get, upperFirst } from 'lodash';
import PropTypes from 'prop-types';
import React, { memo, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { usePermissionsDataManager } from '../../../../../hooks'; import { usePermissionsDataManager } from '../../../../../hooks';
import { getCheckboxState } from '../../../utils';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import CollapseLabel from '../../../CollapseLabel'; import CollapseLabel from '../../../CollapseLabel';
import Curve from '../../../Curve'; import Curve from '../../../Curve';
import HiddenAction from '../../../HiddenAction'; import HiddenAction from '../../../HiddenAction';
import { cellWidth, rowHeight } from '../../../Permissions/utils/constants';
import RequiredSign from '../../../RequiredSign'; import RequiredSign from '../../../RequiredSign';
import { RowStyle, RowWrapper } from './row'; import { getCheckboxState } from '../../../utils';
import { LeftBorderTimeline, TopTimeline } from './timeline'; import { activeStyle } from '../../utils';
import Wrapper from './Wrapper'; import CarretIcon from '../CarretIcon';
const SubLevelWrapper = styled.div` const Cell = styled(Row)`
padding-bottom: 8px; width: ${cellWidth};
position: relative;
`;
const RowWrapper = styled(Row)`
height: ${rowHeight};
`;
const Wrapper = styled(Box)`
padding-left: ${31 / 16}rem;
`;
const LeftBorderTimeline = styled(Box)`
border-left: ${({ isVisible, theme }) =>
isVisible ? `4px solid ${theme.colors.primary200}` : '4px solid transparent'};
`;
const RowStyle = styled(Row)`
padding-left: ${({ theme }) => theme.spaces[4]};
width: ${({ level }) => 145 - level * 36}px;
${({ isCollapsable, theme }) =>
isCollapsable &&
`
${CarretIcon} {
display: block;
color: ${theme.colors.neutral100};
}
&:hover {
${activeStyle(theme)}
}
`}
${({ isActive, theme }) => isActive && activeStyle(theme)};
`;
const TopTimeline = styled.div`
padding-top: 8px;
margin-top: 8px;
width: 4px;
background-color: ${({ theme }) => theme.colors.primary200};
border-top-left-radius: 2px;
border-top-right-radius: 2px;
`;
// ! REMOVE THIS WHEN DS IS UPDATED WITH ELLIPSIS PROP
const StyledText = styled(Text)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`; `;
const SubActionRow = ({ const SubActionRow = ({
@ -29,6 +76,7 @@ const SubActionRow = ({
parentName, parentName,
propertyName, propertyName,
}) => { }) => {
const { formatMessage } = useIntl();
const { const {
modifiedData, modifiedData,
onChangeParentCheckbox, onChangeParentCheckbox,
@ -60,91 +108,117 @@ const SubActionRow = ({
{childrenForm.map(({ label, value, required, children: subChildrenForm }, index) => { {childrenForm.map(({ label, value, required, children: subChildrenForm }, index) => {
const isVisible = index + 1 < childrenForm.length; const isVisible = index + 1 < childrenForm.length;
const isArrayType = Array.isArray(subChildrenForm); const isArrayType = Array.isArray(subChildrenForm);
const isSmall = isArrayType || index + 1 === childrenForm.length;
const isActive = rowToOpen === value; const isActive = rowToOpen === value;
return ( return (
<LeftBorderTimeline key={value} isVisible={isVisible}> <LeftBorderTimeline key={value} isVisible={isVisible}>
<RowWrapper isSmall={isSmall}> <RowWrapper>
<Curve fill="#a5d5ff" /> <Curve color="primary200" />
<Flex style={{ flex: 1 }}> <Row style={{ flex: 1 }}>
<RowStyle level={recursiveLevel} isActive={isActive} isCollapsable={isArrayType}> <RowStyle level={recursiveLevel} isActive={isActive} isCollapsable={isArrayType}>
<CollapseLabel <CollapseLabel
alignItems="center" alignItems="center"
isCollapsable={isArrayType} isCollapsable={isArrayType}
onClick={() => { {...(isArrayType && {
if (isArrayType) { onClick: () => handleClickToggleSubLevel(value),
handleClickToggleSubLevel(value); 'aria-expanded': isActive,
} onKeyDown: ({ key }) =>
}} (key === 'Enter' || key === ' ') && handleClickToggleSubLevel(value),
tabIndex: 0,
role: 'button',
})}
title={label} title={label}
> >
<Text <StyledText>{upperFirst(label)}</StyledText>
color={isActive ? 'mediumBlue' : 'grey'}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
{required && <RequiredSign />} {required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} /> <CarretIcon $isActive={isActive} />
</CollapseLabel> </CollapseLabel>
</RowStyle> </RowStyle>
<Flex style={{ flex: 1 }}> <Row style={{ flex: 1 }}>
{propertyActions.map(({ actionId, label, isActionRelatedToCurrentProperty }) => { {propertyActions.map(
if (!isActionRelatedToCurrentProperty) { ({ actionId, label: propertyLabel, isActionRelatedToCurrentProperty }) => {
return <HiddenAction key={actionId} />; if (!isActionRelatedToCurrentProperty) {
} return <HiddenAction key={actionId} />;
/* }
* Usually we use a 'dot' in order to know the key path of an object for which we want to change the value. /*
* Since an action and a subject are both separated by '.' or '::' we chose to use the '..' separators * Usually we use a 'dot' in order to know the key path of an object for which we want to change the value.
*/ * Since an action and a subject are both separated by '.' or '::' we chose to use the '..' separators
const checkboxName = [ */
...pathToDataFromActionRow.split('..'), const checkboxName = [
actionId, ...pathToDataFromActionRow.split('..'),
'properties', actionId,
propertyName, 'properties',
...parentName.split('..'), propertyName,
value, ...parentName.split('..'),
]; value,
];
const checkboxValue = get(modifiedData, checkboxName, false); const checkboxValue = get(modifiedData, checkboxName, false);
if (!subChildrenForm) {
return (
<Cell key={propertyLabel} justifyContent="center" alignItems="center">
<Checkbox
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
aria-label={formatMessage(
{
id: `Settings.permissions.select-by-permission`,
defaultMessage: 'Select {label} permission',
},
{ label: `${parentName} ${label} ${propertyLabel}` }
)}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeSimpleCheckbox({
target: {
name: checkboxName.join('..'),
value,
},
})}
value={checkboxValue}
/>
</Cell>
);
}
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(
checkboxValue
);
if (!subChildrenForm) {
return ( return (
<CheckboxWithCondition <Cell key={propertyLabel} justifyContent="center" alignItems="center">
key={label} <Checkbox
disabled={isFormDisabled || IS_DISABLED} key={propertyLabel}
name={checkboxName.join('..')} disabled={isFormDisabled || IS_DISABLED}
onChange={onChangeSimpleCheckbox} name={checkboxName.join('..')}
value={checkboxValue} aria-label={formatMessage(
/> {
id: `Settings.permissions.select-by-permission`,
defaultMessage: 'Select {label} permission',
},
{ label: `${parentName} ${label} ${propertyLabel}` }
)}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeParentCheckbox({
target: {
name: checkboxName.join('..'),
value,
},
})}
value={hasAllActionsSelected}
indeterminate={hasSomeActionsSelected}
/>
</Cell>
); );
} }
)}
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState( </Row>
checkboxValue </Row>
);
return (
<CheckboxWithCondition
key={label}
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
onChange={onChangeParentCheckbox}
value={hasAllActionsSelected}
someChecked={hasSomeActionsSelected}
/>
);
})}
</Flex>
</Flex>
</RowWrapper> </RowWrapper>
{displayedRecursiveChildren && isActive && ( {displayedRecursiveChildren && isActive && (
<SubLevelWrapper> <Box paddingBottom={2}>
<SubActionRow <SubActionRow
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
parentName={`${parentName}..${value}`} parentName={`${parentName}..${value}`}
@ -154,7 +228,7 @@ const SubActionRow = ({
recursiveLevel={recursiveLevel + 1} recursiveLevel={recursiveLevel + 1}
childrenForm={displayedRecursiveChildren.children} childrenForm={displayedRecursiveChildren.children}
/> />
</SubLevelWrapper> </Box>
)} )}
</LeftBorderTimeline> </LeftBorderTimeline>
); );

View File

@ -1,37 +0,0 @@
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

@ -1,15 +0,0 @@
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

@ -1,21 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
padding-top: 18px;
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

@ -1,10 +1,26 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core'; import styled from 'styled-components';
import { Box } from '@strapi/parts';
import generateHeadersFromActions from './utils/generateHeadersFromActions'; import generateHeadersFromActions from './utils/generateHeadersFromActions';
import Header from './Header'; import Header from './Header';
import ActionRow from './ActionRow'; import ActionRow from './ActionRow';
import Wrapper from './Wrapper';
const Wrapper = styled.div`
border: 1px solid ${({ theme }) => theme.colors.primary600};
border-top: none;
border-bottom: ${({ isLast, theme }) => {
if (isLast) {
return `1px solid ${theme.colors.primary600}`;
}
return `none`;
}};
`;
Wrapper.defaultProps = {
isLast: true,
};
const CollapsePropertyMatrix = ({ const CollapsePropertyMatrix = ({
availableActions, availableActions,
@ -23,8 +39,8 @@ const CollapsePropertyMatrix = ({
return ( return (
<Wrapper isLast={isLast}> <Wrapper isLast={isLast}>
<Header label={label} headers={propertyActions} /> <Header label={label} headers={propertyActions} />
<Padded left size="md"> <Box>
{childrenForm.map(({ children: childrenForm, label, value, required }) => ( {childrenForm.map(({ children: childrenForm, label, value, required }, i) => (
<ActionRow <ActionRow
childrenForm={childrenForm} childrenForm={childrenForm}
key={value} key={value}
@ -35,9 +51,10 @@ const CollapsePropertyMatrix = ({
propertyActions={propertyActions} propertyActions={propertyActions}
pathToData={pathToData} pathToData={pathToData}
propertyName={propertyName} propertyName={propertyName}
isOdd={i % 2 === 0}
/> />
))} ))}
</Padded> </Box>
</Wrapper> </Wrapper>
); );
}; };

View File

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

View File

@ -1,9 +1,8 @@
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useCallback, useMemo } from 'react';
import Collapse from './Collapse'; import Collapse from './Collapse';
import CollapsePropertyMatrix from './CollapsePropertyMatrix'; import CollapsePropertyMatrix from './CollapsePropertyMatrix';
import { getAvailableActions } from './utils'; import { getAvailableActions } from './utils';
import Wrapper from './Wrapper';
const ContentTypeCollapse = ({ const ContentTypeCollapse = ({
allActions, allActions,
@ -24,10 +23,8 @@ const ContentTypeCollapse = ({
return getAvailableActions(allActions, contentTypeName); return getAvailableActions(allActions, contentTypeName);
}, [allActions, contentTypeName]); }, [allActions, contentTypeName]);
const isOdd = useMemo(() => index % 2 !== 0, [index]);
return ( return (
<Wrapper withMargin={isOdd}> <>
<Collapse <Collapse
availableActions={availableActions} availableActions={availableActions}
isActive={isActive} isActive={isActive}
@ -38,22 +35,21 @@ const ContentTypeCollapse = ({
pathToData={pathToData} pathToData={pathToData}
/> />
{isActive && {isActive &&
properties.map(({ label, value, children: childrenForm }, i) => { properties.map(({ label: propertyLabel, value, children: childrenForm }, i) => {
return ( return (
<CollapsePropertyMatrix <CollapsePropertyMatrix
availableActions={availableActions} availableActions={availableActions}
childrenForm={childrenForm} childrenForm={childrenForm}
isFormDisabled={isFormDisabled} isFormDisabled={isFormDisabled}
label={label} label={propertyLabel}
pathToData={pathToData} pathToData={pathToData}
propertyName={value} propertyName={value}
key={value} key={value}
isLast={i === properties.length - 1} isLast={i === properties.length - 1}
isOdd={isOdd}
/> />
); );
})} })}
</Wrapper> </>
); );
}; };

View File

@ -1,14 +1,14 @@
import { Text } from '@buffetjs/core'; import { Text } from '@strapi/parts';
import Chevron from '../../Chevron'; import CarretIcon from '../CollapsePropertyMatrix/CarretIcon';
const activeStyle = theme => ` const activeStyle = theme => `
color: ${theme.main.colors.mediumBlue};
${Text} { ${Text} {
color: ${theme.main.colors.mediumBlue}; color: ${theme.colors.primary600};
font-weight: bold;
} }
${Chevron} { ${CarretIcon} {
display: block; display: block;
color: ${theme.main.colors.mediumBlue}; color: ${theme.colors.primary600};
} }
`; `;

View File

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

View File

@ -1,23 +1,25 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core'; import { Box } from '@strapi/parts';
import styled from 'styled-components';
import ContentTypeCollapses from '../ContentTypeCollapses'; import ContentTypeCollapses from '../ContentTypeCollapses';
import GlobalActions from '../GlobalActions'; import GlobalActions from '../GlobalActions';
import Wrapper from './Wrapper';
const StyledBox = styled(Box)`
overflow-x: auto;
`;
const ContentTypes = ({ isFormDisabled, kind, layout: { actions, subjects } }) => { const ContentTypes = ({ isFormDisabled, kind, layout: { actions, subjects } }) => {
return ( return (
<Wrapper> <StyledBox background="neutral0">
<Padded left right bottom size="md"> <GlobalActions actions={actions} kind={kind} isFormDisabled={isFormDisabled} />
<GlobalActions actions={actions} kind={kind} isFormDisabled={isFormDisabled} /> <ContentTypeCollapses
<ContentTypeCollapses actions={actions}
actions={actions} isFormDisabled={isFormDisabled}
isFormDisabled={isFormDisabled} pathToData={kind}
pathToData={kind} subjects={subjects}
subjects={subjects} />
/> </StyledBox>
</Padded>
</Wrapper>
); );
}; };

View File

@ -1,32 +1,52 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Box } from '@strapi/parts';
const StyledBox = styled(Box)`
transform: translate(-4px, -12px);
&:before {
content: '';
width: ${4 / 16}rem;
height: ${12 / 16}rem;
background: ${({ theme }) => theme.colors.primary200};
display: block;
}
`;
const Svg = styled.svg`
position: relative;
flex-shrink: 0;
transform: translate(-0.5px, -1px);
* {
fill: ${({ theme, color }) => theme.colors[color]};
}
`;
const Curve = props => ( const Curve = props => (
<svg <StyledBox>
style={{ <Svg
height: '14px', width="20"
transform: 'translate(-3.2px, -1px)', height="23"
position: 'relative', viewBox="0 0 20 23"
}} fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 21.08 21" {...props}
{...props} >
>
<g>
<path <path
d="M2.58 2.5q-1.2 16 16 16" fillRule="evenodd"
fill="none" clipRule="evenodd"
stroke={props.fill} d="M7.02477 14.7513C8.65865 17.0594 11.6046 18.6059 17.5596 18.8856C18.6836 18.9384 19.5976 19.8435 19.5976 20.9688V20.9688C19.5976 22.0941 18.6841 23.0125 17.5599 22.9643C10.9409 22.6805 6.454 20.9387 3.75496 17.1258C0.937988 13.1464 0.486328 7.39309 0.486328 0.593262H4.50974C4.50974 7.54693 5.06394 11.9813 7.02477 14.7513Z"
strokeLinecap="round" fill="#D9D8FF"
strokeLinejoin="round"
strokeWidth="5"
/> />
</g> </Svg>
</svg> </StyledBox>
); );
Curve.defaultProps = { Curve.defaultProps = {
fill: '#f3f4f4', fill: 'primary200',
}; };
Curve.propTypes = { Curve.propTypes = {
fill: PropTypes.string, fill: PropTypes.string,

View File

@ -1,18 +0,0 @@
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

@ -1,13 +1,20 @@
import React, { memo, useMemo } from 'react'; import { Checkbox, Stack, TableLabel, Box } from '@strapi/parts';
import IS_DISABLED from 'ee_else_ce/components/Roles/GlobalActions/utils/constants';
import { get } from 'lodash'; import { get } from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Flex } from '@buffetjs/core'; import React, { memo, useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import IS_DISABLED from 'ee_else_ce/components/Roles/GlobalActions/utils/constants'; import styled from 'styled-components';
import { usePermissionsDataManager } from '../../../hooks'; import { usePermissionsDataManager } from '../../../hooks';
import CheckboxWithCondition from '../CheckboxWithCondition'; import { cellWidth, firstRowWidth } from '../Permissions/utils/constants';
import { findDisplayedActions, getCheckboxesState } from './utils'; import { findDisplayedActions, getCheckboxesState } from './utils';
import Wrapper from './Wrapper';
const CenteredStack = styled(Stack)`
align-items: center;
justify-content: center;
width: ${cellWidth};
flex-shrink: 0;
`;
const GlobalActions = ({ actions, isFormDisabled, kind }) => { const GlobalActions = ({ actions, isFormDisabled, kind }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@ -22,28 +29,43 @@ const GlobalActions = ({ actions, isFormDisabled, kind }) => {
}, [modifiedData, displayedActions, kind]); }, [modifiedData, displayedActions, kind]);
return ( return (
<Wrapper disabled={isFormDisabled}> <Box paddingBottom={4} paddingTop={6} style={{ paddingLeft: firstRowWidth }}>
<Flex> <Stack horizontal>
{displayedActions.map(({ label, actionId }) => { {displayedActions.map(({ label, actionId }) => {
return ( return (
<CheckboxWithCondition <CenteredStack key={actionId} size={3}>
key={actionId} <TableLabel textColor="neutral500">
disabled={isFormDisabled || IS_DISABLED} {formatMessage({
message={formatMessage({ id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
id: `Settings.roles.form.permissions.${label.toLowerCase()}`, defaultMessage: label,
defaultMessage: label, })}
})} </TableLabel>
onChange={({ target: { value } }) => { <Checkbox
onChangeCollectionTypeGlobalActionCheckbox(kind, actionId, value); disabled={isFormDisabled || IS_DISABLED}
}} onValueChange={value => {
name={actionId} onChangeCollectionTypeGlobalActionCheckbox(kind, actionId, value);
value={get(checkboxesState, [actionId, 'hasAllActionsSelected'], false)} }}
someChecked={get(checkboxesState, [actionId, 'hasSomeActionsSelected'], false)} name={actionId}
/> aria-label={formatMessage(
{
id: `Settings.permissions.select-all-by-permission`,
defaultMessage: 'Select all {label} permissions',
},
{
label: formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
}),
}
)}
value={get(checkboxesState, [actionId, 'hasAllActionsSelected'], false)}
indeterminate={get(checkboxesState, [actionId, 'hasSomeActionsSelected'], false)}
/>
</CenteredStack>
); );
})} })}
</Flex> </Stack>
</Wrapper> </Box>
); );
}; };

View File

@ -1,9 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { cellWidth } from '../Permissions/utils/constants';
const HiddenAction = styled.div` const HiddenAction = styled.div`
min-width: 10rem; width: ${cellWidth};
max-width: 12rem;
flex: 1;
`; `;
export default HiddenAction; export default HiddenAction;

View File

@ -1,20 +1,22 @@
import React, { forwardRef, memo, useCallback, useImperativeHandle, useReducer } from 'react';
import PropTypes from 'prop-types';
import { difference } from '@strapi/helper-plugin'; import { difference } from '@strapi/helper-plugin';
import { Tab, TabGroup, TabPanel, TabPanels, Tabs } from '@strapi/parts';
import { has, isEmpty } from 'lodash'; import { has, isEmpty } from 'lodash';
import Tabs from '../Tabs'; import PropTypes from 'prop-types';
import PermissionsDataManagerProvider from '../PermissionsDataManagerProvider'; import React, { forwardRef, memo, useCallback, useImperativeHandle, useReducer } from 'react';
import { useIntl } from 'react-intl';
import ContentTypes from '../ContentTypes'; import ContentTypes from '../ContentTypes';
import PermissionsDataManagerProvider from '../PermissionsDataManagerProvider';
import PluginsAndSettings from '../PluginsAndSettings'; import PluginsAndSettings from '../PluginsAndSettings';
import TAB_LABELS from './utils/tabLabels';
import formatPermissionsToAPI from './utils/formatPermissionsToAPI';
import init from './init'; import init from './init';
import reducer, { initialState } from './reducer'; import reducer, { initialState } from './reducer';
import formatPermissionsToAPI from './utils/formatPermissionsToAPI';
import TAB_LABELS from './utils/tabLabels';
const Permissions = forwardRef(({ layout, isFormDisabled, permissions }, ref) => { const Permissions = forwardRef(({ layout, isFormDisabled, permissions }, ref) => {
const [{ initialData, layouts, modifiedData }, dispatch] = useReducer(reducer, initialState, () => const [{ initialData, layouts, modifiedData }, dispatch] = useReducer(reducer, initialState, () =>
init(layout, permissions) init(layout, permissions)
); );
const { formatMessage } = useIntl();
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {
return { return {
@ -106,28 +108,45 @@ const Permissions = forwardRef(({ layout, isFormDisabled, permissions }, ref) =>
onChangeCollectionTypeGlobalActionCheckbox: handleChangeCollectionTypeGlobalActionCheckbox, onChangeCollectionTypeGlobalActionCheckbox: handleChangeCollectionTypeGlobalActionCheckbox,
}} }}
> >
<Tabs tabsLabel={TAB_LABELS}> <TabGroup id="tabs">
<ContentTypes <Tabs>
layout={layouts.collectionTypes} {TAB_LABELS.map(tabLabel => (
kind="collectionTypes" <Tab key={tabLabel.id}>
isFormDisabled={isFormDisabled} {formatMessage({ id: tabLabel.labelId, defaultMessage: tabLabel.defaultMessage })}
/> </Tab>
<ContentTypes ))}
layout={layouts.singleTypes} </Tabs>
kind="singleTypes" <TabPanels style={{ position: 'relative' }}>
isFormDisabled={isFormDisabled} <TabPanel>
/> <ContentTypes
<PluginsAndSettings layout={layouts.collectionTypes}
layout={layouts.plugins} kind="collectionTypes"
kind="plugins" isFormDisabled={isFormDisabled}
isFormDisabled={isFormDisabled} />
/> </TabPanel>
<PluginsAndSettings <TabPanel>
layout={layouts.settings} <ContentTypes
kind="settings" layout={layouts.singleTypes}
isFormDisabled={isFormDisabled} kind="singleTypes"
/> isFormDisabled={isFormDisabled}
</Tabs> />
</TabPanel>
<TabPanel>
<PluginsAndSettings
layout={layouts.plugins}
kind="plugins"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
<TabPanel>
<PluginsAndSettings
layout={layouts.settings}
kind="settings"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</PermissionsDataManagerProvider> </PermissionsDataManagerProvider>
); );
}); });

View File

@ -0,0 +1,3 @@
export const cellWidth = `${120 / 16}rem`;
export const firstRowWidth = `${200 / 16}rem`;
export const rowHeight = `${53 / 16}rem`;

View File

@ -1,10 +0,0 @@
import styled from 'styled-components';
const ListWrapper = styled.div`
background-color: ${({ theme }) => theme.main.colors.white};
border-bottom-left-radius: ${({ theme }) => theme.main.sizes.borderRadius};
border-bottom-right-radius: ${({ theme }) => theme.main.sizes.borderRadius};
padding: 1.8rem 0;
`;
export default ListWrapper;

View File

@ -1,28 +0,0 @@
import styled from 'styled-components';
import { Text } from '@buffetjs/core';
const activeStyle = theme => `
background-color: ${theme.main.colors.lightestBlue};
border: 1px solid ${theme.main.colors.darkBlue};
${Text} {
color: ${theme.main.colors.mediumBlue};
}
svg {
color: ${theme.main.colors.mediumBlue};
}
`;
const RowStyle = styled.div`
height: 5.4rem;
padding: 1rem 3rem;
border: 1px solid transparent;
background-color: ${({ theme, isWhite }) => theme.main.colors[isWhite ? 'white' : 'lightGrey']};
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.main.colors.lightestBlue};
${({ theme }) => activeStyle(theme)};
}
${({ isActive, theme }) => isActive && activeStyle(theme)};
`;
export default RowStyle;

View File

@ -1,11 +1,9 @@
import React, { useMemo } from 'react'; import { Accordion, AccordionContent, AccordionToggle, Box } from '@strapi/parts';
import { Flex, Text } from '@buffetjs/core'; import upperFirst from 'lodash/upperFirst';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { PermissionsWrapper, RowContainer } from '@strapi/helper-plugin';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import SubCategory from '../SubCategory'; import SubCategory from '../SubCategory';
import RowStyle from './Wrapper';
const PermissionRow = ({ const PermissionRow = ({
childrenForm, childrenForm,
@ -30,26 +28,18 @@ const PermissionRow = ({
}, [name]); }, [name]);
return ( return (
<RowContainer isWhite={isWhite}> <Accordion expanded={isOpen} toggle={handleClick} id="acc-1">
<RowStyle isWhite={isWhite} isActive={isOpen} onClick={handleClick}> <AccordionToggle
<Flex alignItems="center" justifyContent="space-between"> title={upperFirst(categoryName)}
<div> description={`${formatMessage(
<Text color="grey" fontWeight="bold" fontSize="xs" textTransform="uppercase"> { id: 'Settings.permissions.category' },
{categoryName} { category: categoryName }
</Text> )} ${kind === 'plugins' ? 'plugin' : kind}`}
<Text lineHeight="22px" color="grey"> variant={isWhite ? 'primary' : 'secondary'}
{formatMessage({ id: 'Settings.permissions.category' }, { category: categoryName })} />
&nbsp;{kind === 'plugins' ? 'plugin' : kind}
</Text>
</div>
<div>
<FontAwesomeIcon style={{ width: '11px' }} color="#9EA7B8" icon="chevron-down" />
</div>
</Flex>
</RowStyle>
{isOpen && ( <AccordionContent>
<PermissionsWrapper isWhite={isWhite}> <Box padding={6}>
{childrenForm.map(({ actions, subCategoryName, subCategoryId }) => ( {childrenForm.map(({ actions, subCategoryName, subCategoryId }) => (
<SubCategory <SubCategory
key={subCategoryName} key={subCategoryName}
@ -60,9 +50,9 @@ const PermissionRow = ({
pathToData={[...pathToData, subCategoryId]} pathToData={[...pathToData, subCategoryId]}
/> />
))} ))}
</PermissionsWrapper> </Box>
)} </AccordionContent>
</RowContainer> </Accordion>
); );
}; };

View File

@ -1,22 +0,0 @@
/* eslint-disable indent */
import styled from 'styled-components';
const CheckboxWrapper = styled.div`
min-width: 33%;
padding: 0.9rem;
height: 3.6rem;
position: relative;
${({ hasConditions, disabled, theme }) =>
hasConditions &&
`
&:before {
content: '•';
position: absolute;
top: 2px;
left: 0px;
color: ${disabled ? theme.main.colors.grey : theme.main.colors.mediumBlue};
}
`}
`;
export default CheckboxWrapper;

View File

@ -1,13 +0,0 @@
/* eslint-disable indent */
import styled from 'styled-components';
const ConditionsButtonWrapper = styled.div`
padding: 0.9rem;
${({ hasConditions }) =>
hasConditions &&
`
padding-left: 22px;
`}
`;
export default ConditionsButtonWrapper;

View File

@ -1,26 +0,0 @@
/* eslint-disable indent */
import styled from 'styled-components';
const Wrapper = styled.div`
padding-bottom: 2.6rem;
input[type='checkbox'] {
&:after {
color: ${({ theme }) => theme.main.colors.mediumBlue};
}
}
${({ disabled, theme }) =>
disabled &&
`
label {
cursor: default !important;
color: ${theme.main.colors.grey};
}
input[type='checkbox'] {
&:after {
color: ${theme.main.colors.grey};
}
}
`}
`;
export default Wrapper;

View File

@ -1,29 +1,43 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Flex, Padded, Text, Checkbox } from '@buffetjs/core'; import { Row, Box, TableLabel, Checkbox, Grid, GridItem } from '@strapi/parts';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { BaselineAlignment } from '@strapi/helper-plugin';
import { get } from 'lodash'; import { get } from 'lodash';
import IS_DISABLED from 'ee_else_ce/components/Roles/PluginsAndSettings/SubCategory/utils/constants'; import IS_DISABLED from 'ee_else_ce/components/Roles/PluginsAndSettings/SubCategory/utils/constants';
import { usePermissionsDataManager } from '../../../../hooks'; import { usePermissionsDataManager } from '../../../../hooks';
import { getCheckboxState, removeConditionKeyFromData } from '../../utils'; import { getCheckboxState, removeConditionKeyFromData } from '../../utils';
import ConditionsButton from '../../ConditionsButton'; import ConditionsButton from '../../ConditionsButton';
import ConditionsModal from '../../ConditionsModal'; import ConditionsModal from '../../ConditionsModal';
import CheckboxWrapper from './CheckboxWrapper';
import ConditionsButtonWrapper from './ConditionsButtonWrapper';
import Wrapper from './Wrapper';
import { formatActions, getConditionsButtonState } from './utils'; import { formatActions, getConditionsButtonState } from './utils';
const Border = styled.div` const Border = styled.div`
flex: 1; flex: 1;
align-self: center; align-self: center;
border-top: 1px solid #f6f6f6; border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
padding: 0px 10px; `;
const CheckboxWrapper = styled.div`
position: relative;
${({ hasConditions, disabled, theme }) =>
hasConditions &&
`
&:before {
content: '';
position: absolute;
top: ${-4 / 16}rem;
left: ${-8 / 16}rem;
width: ${6 / 16}rem;
height: ${6 / 16}rem;
border-radius: ${20 / 16}rem;
background: ${disabled ? theme.colors.neutral100 : theme.colors.primary600};
}
`}
`; `;
const SubCategory = ({ categoryName, isFormDisabled, subCategoryName, actions, pathToData }) => { const SubCategory = ({ categoryName, isFormDisabled, subCategoryName, actions, pathToData }) => {
const [modalState, setModalState] = useState({ isOpen: false, isMounted: false }); const [isModalOpen, setModalOpen] = useState(false);
const { const {
modifiedData, modifiedData,
onChangeParentCheckbox, onChangeParentCheckbox,
@ -43,84 +57,87 @@ const SubCategory = ({ categoryName, isFormDisabled, subCategoryName, actions, p
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(dataWithoutCondition); const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(dataWithoutCondition);
const handleToggleModalIsOpen = () => { const handleToggleModalIsOpen = () => {
setModalState(prevState => ({ isMounted: true, isOpen: !prevState.isOpen })); setModalOpen(s => !s);
}; };
const handleModalClose = () => { const handleModalClose = () => {
setModalState(prevState => ({ ...prevState, isMounted: false })); setModalOpen(false);
}; };
// We need to format the actions so it matches the shape of the ConditionsModal actions props // We need to format the actions so it matches the shape of the ConditionsModal actions props
const formattedActions = formatActions(actions, modifiedData, pathToData); const formattedActions = formatActions(actions, modifiedData, pathToData);
const doesButtonHasCondition = getConditionsButtonState(get(modifiedData, [...pathToData], {})); const doesButtonHasCondition = getConditionsButtonState(get(modifiedData, [...pathToData], {}));
return ( return (
<> <>
<Wrapper> <Box>
<Flex justifyContent="space-between" alignItems="center"> <Row justifyContent="space-between" alignItems="center">
<Padded right size="sm"> <Box paddingRight={4}>
<Text <TableLabel textColor="neutral600">{subCategoryName}</TableLabel>
lineHeight="18px" </Box>
color="#919bae"
fontWeight="bold"
fontSize="xs"
textTransform="uppercase"
>
{subCategoryName}
</Text>
</Padded>
<Border /> <Border />
<Padded left size="sm"> <Box paddingLeft={4}>
<BaselineAlignment top size="1px" />
<Checkbox <Checkbox
name={pathToData.join('..')} name={pathToData.join('..')}
message={formatMessage({ id: 'app.utils.select-all' })}
disabled={isFormDisabled || IS_DISABLED} disabled={isFormDisabled || IS_DISABLED}
onChange={onChangeParentCheckbox} // Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
someChecked={hasSomeActionsSelected} onValueChange={value =>
onChangeParentCheckbox({
target: {
name: pathToData.join('..'),
value,
},
})}
indeterminate={hasSomeActionsSelected}
value={hasAllActionsSelected} value={hasAllActionsSelected}
/> >
</Padded> {formatMessage({ id: 'app.utils.select-all', defaultMessage: 'Select all' })}
</Flex> </Checkbox>
<BaselineAlignment top size="1px" /> </Box>
<Padded top size="xs"> </Row>
<Flex flexWrap="wrap"> <Row paddingTop={6} paddingBottom={6}>
<Grid gap={2} style={{ flex: 1 }}>
{formattedActions.map(({ checkboxName, value, action, displayName, hasConditions }) => { {formattedActions.map(({ checkboxName, value, action, displayName, hasConditions }) => {
return ( return (
<CheckboxWrapper <GridItem col={3} key={action}>
disabled={isFormDisabled || IS_DISABLED} <CheckboxWrapper
hasConditions={hasConditions}
key={action}
>
<Checkbox
name={checkboxName}
disabled={isFormDisabled || IS_DISABLED} disabled={isFormDisabled || IS_DISABLED}
message={displayName} hasConditions={hasConditions}
onChange={onChangeSimpleCheckbox} >
value={value} <Checkbox
/> name={checkboxName}
</CheckboxWrapper> disabled={isFormDisabled || IS_DISABLED}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeSimpleCheckbox({
target: {
name: checkboxName,
value,
},
})}
value={value}
>
{displayName}
</Checkbox>
</CheckboxWrapper>
</GridItem>
); );
})} })}
</Flex> </Grid>
<ConditionsButtonWrapper disabled={isFormDisabled} hasConditions={doesButtonHasCondition}> <ConditionsButton
<ConditionsButton hasConditions={doesButtonHasCondition}
hasConditions={doesButtonHasCondition} onClick={handleToggleModalIsOpen}
onClick={handleToggleModalIsOpen} variant="tertiary"
/> />
</ConditionsButtonWrapper> </Row>
</Padded> </Box>
</Wrapper> <ConditionsModal
{modalState.isMounted && ( headerBreadCrumbs={[categoryName, subCategoryName]}
<ConditionsModal actions={formattedActions}
headerBreadCrumbs={[categoryName, subCategoryName]} isOpen={isModalOpen}
actions={formattedActions} isFormDisabled={isFormDisabled}
isOpen={modalState.isOpen} onClosed={handleModalClose}
isFormDisabled={isFormDisabled} onToggle={handleToggleModalIsOpen}
onClosed={handleModalClose} />
onToggle={handleToggleModalIsOpen}
/>
)}
</> </>
); );
}; };

View File

@ -1,7 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core'; import { Box } from '@strapi/parts';
import ListWrapper from './ListWrapper';
import PermissionRow from './Row'; import PermissionRow from './Row';
const PluginsAndSettingsPermissions = ({ isFormDisabled, kind, layout }) => { const PluginsAndSettingsPermissions = ({ isFormDisabled, kind, layout }) => {
@ -12,25 +11,23 @@ const PluginsAndSettingsPermissions = ({ isFormDisabled, kind, layout }) => {
}; };
return ( return (
<ListWrapper> <Box padding={6} background="neutral0">
<Padded left right size="md"> {layout.map(({ category, categoryId, childrenForm }, index) => {
{layout.map(({ category, categoryId, childrenForm }, index) => { return (
return ( <PermissionRow
<PermissionRow key={category}
key={category} childrenForm={childrenForm}
childrenForm={childrenForm} kind={kind}
kind={kind} isFormDisabled={isFormDisabled}
isFormDisabled={isFormDisabled} isOpen={openedCategory === category}
isOpen={openedCategory === category} isWhite={index % 2 === 1}
isWhite={index % 2 === 1} name={category}
name={category} onOpenCategory={handleOpenCategory}
onOpenCategory={handleOpenCategory} pathToData={[kind, categoryId]}
pathToData={[kind, categoryId]} />
/> );
); })}
})} </Box>
</Padded>
</ListWrapper>
); );
}; };

View File

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

View File

@ -1,29 +0,0 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
const Wrapper = styled.div`
display: flex;
align-items: center;
width: ${({ width }) => width};
${({ disabled, theme }) =>
disabled &&
`
input[type='checkbox'] {
cursor: not-allowed;
&:after {
color: ${theme.main.colors.grey};
}
}
`}
`;
Wrapper.defaultProps = {
width: '18rem',
};
Wrapper.propTypes = {
width: PropTypes.string,
};
export default Wrapper;

View File

@ -1,50 +1,72 @@
import React, { memo } from 'react'; import { Row, Checkbox, Text } from '@strapi/parts';
import upperFirst from 'lodash/upperFirst';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Checkbox, Text } from '@buffetjs/core'; import React, { memo } from 'react';
import styled from 'styled-components';
import { useIntl } from 'react-intl';
import CollapseLabel from '../CollapseLabel'; import CollapseLabel from '../CollapseLabel';
import Wrapper from './Wrapper'; import { firstRowWidth } from '../Permissions/utils/constants';
// ! REMOVE THIS WHEN DS IS UPDATED WITH ELLIPSIS PROP
const StyledText = styled(Text)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const RowLabelWithCheckbox = ({ const RowLabelWithCheckbox = ({
children, children,
isCollapsable, isCollapsable,
isActive,
isFormDisabled, isFormDisabled,
label, label,
onChange, onChange,
onClick, onClick,
checkboxName, checkboxName,
someChecked, someChecked,
textColor,
value, value,
width,
}) => { }) => {
const { formatMessage } = useIntl();
return ( return (
<Wrapper width={width} disabled={isFormDisabled}> <Row alignItems="center" paddingLeft={6} style={{ width: firstRowWidth, flexShrink: 0 }}>
<Checkbox <Checkbox
name={checkboxName} name={checkboxName}
aria-label={formatMessage(
{
id: `Settings.permissions.select-all-by-permission`,
defaultMessage: 'Select all {label} permissions',
},
{ label }
)}
disabled={isFormDisabled} disabled={isFormDisabled}
onChange={onChange} // Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
someChecked={someChecked} onValueChange={value =>
onChange({
target: {
name: checkboxName,
value,
},
})}
indeterminate={someChecked}
value={value} value={value}
/> />
<CollapseLabel <CollapseLabel
title={label} title={label}
alignItems="center" alignItems="center"
isCollapsable={isCollapsable} isCollapsable={isCollapsable}
onClick={onClick} {...(isCollapsable && {
onClick,
'aria-expanded': isActive,
onKeyDown: ({ key }) => (key === 'Enter' || key === ' ') && onClick(),
tabIndex: 0,
role: 'button',
})}
> >
<Text <StyledText>{upperFirst(label)}</StyledText>
color={textColor}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
{children} {children}
</CollapseLabel> </CollapseLabel>
</Wrapper> </Row>
); );
}; };
@ -55,8 +77,6 @@ RowLabelWithCheckbox.defaultProps = {
value: false, value: false,
someChecked: false, someChecked: false,
isCollapsable: false, isCollapsable: false,
textColor: 'grey',
width: '18rem',
}; };
RowLabelWithCheckbox.propTypes = { RowLabelWithCheckbox.propTypes = {
@ -68,9 +88,8 @@ RowLabelWithCheckbox.propTypes = {
onChange: PropTypes.func, onChange: PropTypes.func,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
someChecked: PropTypes.bool, someChecked: PropTypes.bool,
textColor: PropTypes.string,
value: PropTypes.bool, value: PropTypes.bool,
width: PropTypes.string, isActive: PropTypes.bool.isRequired,
}; };
export default memo(RowLabelWithCheckbox); export default memo(RowLabelWithCheckbox);

View File

@ -5,7 +5,10 @@ import PageTitle from '../PageTitle';
const SettingsPageTitle = ({ name }) => { const SettingsPageTitle = ({ name }) => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const text = formatMessage({ id: 'Settings.PageTitle' }, { name }); const text = formatMessage(
{ id: 'Settings.PageTitle', defaultMessage: 'Settings - {name}' },
{ name }
);
return <PageTitle title={text} />; return <PageTitle title={text} />;
}; };

View File

@ -132,6 +132,7 @@ const Login = ({ onSubmit, schema, children }) => {
handleChange({ target: { value: checked, name: 'rememberMe' } }); handleChange({ target: { value: checked, name: 'rememberMe' } });
}} }}
value={values.rememberMe} value={values.rememberMe}
aria-label="rememberMe"
name="rememberMe" name="rememberMe"
> >
{formatMessage({ {formatMessage({

View File

@ -674,6 +674,7 @@ describe('ADMIN | PAGES | AUTH | BaseLogin', () => {
class="c22 c23" class="c22 c23"
> >
<input <input
aria-label="rememberMe"
class="c24" class="c24"
id="checkbox-3" id="checkbox-3"
name="rememberMe" name="rememberMe"

View File

@ -280,6 +280,7 @@ const Register = ({ fieldsToDisable, noSignin, onSubmit, schema }) => {
}} }}
value={values.news} value={values.news}
name="news" name="news"
aria-label="news"
> >
{formatMessage( {formatMessage(
{ {

View File

@ -906,6 +906,7 @@ describe('ADMIN | PAGES | AUTH | Register', () => {
class="c4 c33" class="c4 c33"
> >
<input <input
aria-label="news"
class="c34" class="c34"
id="checkbox-7" id="checkbox-7"
name="news" name="news"

View File

@ -63,7 +63,10 @@ function SettingsPage() {
return <Redirect to="/settings/application-infos" />; return <Redirect to="/settings/application-infos" />;
} }
const settingTitle = formatMessage({ id: 'app.components.LeftMenuLinkContainer.settings' }); const settingTitle = formatMessage({
id: 'app.components.LeftMenuLinkContainer.settings',
defaultMessage: 'Settings',
});
return ( return (
<SettingsSearchHeaderProvider value={{ toggleHeaderSearch }}> <SettingsSearchHeaderProvider value={{ toggleHeaderSearch }}>

View File

@ -118,6 +118,8 @@
"Settings.permissions.users.listview.header.description.plural": "{number} users found", "Settings.permissions.users.listview.header.description.plural": "{number} users found",
"Settings.permissions.users.listview.header.description.singular": "{number} user found", "Settings.permissions.users.listview.header.description.singular": "{number} user found",
"Settings.permissions.users.listview.header.title": "Users", "Settings.permissions.users.listview.header.title": "Users",
"Settings.permissions.select-all-by-permission": "Select all {label} permissions",
"Settings.permissions.select-by-permission": "Select {label} permission",
"Settings.profile.form.section.experience.interfaceLanguage": "Interface language", "Settings.profile.form.section.experience.interfaceLanguage": "Interface language",
"Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.", "Settings.profile.form.section.experience.interfaceLanguage.hint": "This will only display your own interface in the chosen language.",
"Settings.profile.form.section.experience.title": "Experience", "Settings.profile.form.section.experience.title": "Experience",
@ -127,7 +129,7 @@
"Settings.roles.create.title": "Create a role", "Settings.roles.create.title": "Create a role",
"Settings.roles.created": "Role created", "Settings.roles.created": "Role created",
"Settings.roles.edit.title": "Edit a role", "Settings.roles.edit.title": "Edit a role",
"Settings.roles.form.button.users-with-role": "Users with this role", "Settings.roles.form.button.users-with-role": "{number, plural, =0 {# users} one {# user} other {# users}} with this role",
"Settings.roles.form.created": "Created", "Settings.roles.form.created": "Created",
"Settings.roles.form.description": "Name and description of the role", "Settings.roles.form.description": "Name and description of the role",
"Settings.roles.form.input.description": "Description", "Settings.roles.form.input.description": "Description",

View File

@ -1,36 +1,54 @@
import React, { useState, useRef } from 'react';
import { Header } from '@buffetjs/custom';
import { Padded } from '@buffetjs/core';
import moment from 'moment';
import { Formik } from 'formik';
import { get, isEmpty } from 'lodash';
import { useIntl } from 'react-intl';
import { HeaderLayout, Button } from '@strapi/parts';
import { AddIcon, EditIcon } from '@strapi/icons';
import { import {
BaselineAlignment,
CheckPagePermissions, CheckPagePermissions,
Form,
LoadingIndicatorPage,
request, request,
useTracking,
useNotification, useNotification,
useOverlayBlocker, useOverlayBlocker,
useTracking,
} from '@strapi/helper-plugin'; } from '@strapi/helper-plugin';
import {
Box,
Button,
ContentLayout,
Grid,
GridItem,
HeaderLayout,
Main,
Row,
Stack,
Text,
Textarea,
TextInput,
} from '@strapi/parts';
import { Formik } from 'formik';
import { get, isEmpty } from 'lodash';
import moment from 'moment';
import React, { useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { useHistory, useRouteMatch } from 'react-router-dom'; import { useHistory, useRouteMatch } from 'react-router-dom';
import adminPermissions from '../../../../../admin/src/permissions'; import styled from 'styled-components';
import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/src/hooks';
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
import FormCard from '../../../../../admin/src/components/FormBloc';
import { ButtonWithNumber } from '../../../../../admin/src/components/Roles';
import SizedInput from '../../../../../admin/src/components/SizedInput';
import Permissions from '../../../../../admin/src/components/Roles/Permissions'; import Permissions from '../../../../../admin/src/components/Roles/Permissions';
import PageTitle from '../../../../../admin/src/components/SettingsPageTitle';
import { useFetchPermissionsLayout, useFetchRole } from '../../../../../admin/src/hooks';
import adminPermissions from '../../../../../admin/src/permissions';
import schema from './utils/schema'; import schema from './utils/schema';
const UsersRoleNumber = styled.div`
border: 1px solid ${({ theme }) => theme.colors.primary200};
background: ${({ theme }) => theme.colors.primary100};
padding: ${({ theme }) => `${theme.spaces[2]} ${theme.spaces[4]}`};
color: ${({ theme }) => theme.colors.primary600};
border-radius: ${({ theme }) => theme.borderRadius};
font-size: ${12 / 16}rem;
font-width: bold;
`;
const CreatePage = () => { const CreatePage = () => {
const toggleNotification = useNotification(); const toggleNotification = useNotification();
const { lockApp, unlockApp } = useOverlayBlocker(); const { lockApp, unlockApp } = useOverlayBlocker();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [isSubmiting, setIsSubmiting] = useState(false); const [isSubmitting, setIsSubmiting] = useState(false);
const { replace } = useHistory(); const { replace } = useHistory();
const permissionsRef = useRef(); const permissionsRef = useRef();
const { trackUsage } = useTracking(); const { trackUsage } = useTracking();
@ -39,31 +57,6 @@ const CreatePage = () => {
const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout(); const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout();
const { permissions: rolePermissions, isLoading: isRoleLoading } = useFetchRole(id); const { permissions: rolePermissions, isLoading: isRoleLoading } = useFetchRole(id);
const headerActions = (handleSubmit, handleReset) => [
{
label: formatMessage({
id: 'app.components.Button.reset',
defaultMessage: 'Reset',
}),
onClick: () => {
handleReset();
permissionsRef.current.resetForm();
},
color: 'cancel',
type: 'button',
},
{
label: formatMessage({
id: 'app.components.Button.save',
defaultMessage: 'Save',
}),
onClick: handleSubmit,
color: 'success',
type: 'submit',
isLoading: isSubmiting,
},
];
const handleCreateRoleSubmit = data => { const handleCreateRoleSubmit = data => {
lockApp(); lockApp();
setIsSubmiting(true); setIsSubmiting(true);
@ -119,21 +112,12 @@ const CreatePage = () => {
}); });
}; };
const actions = [
<ButtonWithNumber number={0} onClick={() => console.log('Open user modal')} key="user-button">
{formatMessage({
id: 'Settings.roles.form.button.users-with-role',
defaultMessage: 'Users with this role',
})}
</ButtonWithNumber>,
];
const defaultDescription = `${formatMessage({ const defaultDescription = `${formatMessage({
id: 'Settings.roles.form.created', id: 'Settings.roles.form.created',
})} ${moment().format('LL')}`; })} ${moment().format('LL')}`;
return ( return (
<> <Main labelledBy="title">
<PageTitle name="Roles" /> <PageTitle name="Roles" />
<Formik <Formik
initialValues={{ name: '', description: defaultDescription }} initialValues={{ name: '', description: defaultDescription }}
@ -141,84 +125,125 @@ const CreatePage = () => {
validationSchema={schema} validationSchema={schema}
validateOnChange={false} validateOnChange={false}
> >
{({ handleSubmit, values, errors, handleReset, handleChange, handleBlur }) => ( {({ handleSubmit, values, errors, handleReset, handleChange }) => (
<form onSubmit={handleSubmit}> <Form noValidate>
<> <>
<HeaderLayout <HeaderLayout
primaryAction={<Button startIcon={<AddIcon />}>Add an entry</Button>} id="title"
secondaryAction={ primaryAction={
<Button variant="tertiary" startIcon={<EditIcon />}> <Stack horizontal size={2}>
Edit <Button
</Button> variant="secondary"
onClick={() => {
handleReset();
permissionsRef.current.resetForm();
}}
>
{formatMessage({
id: 'app.components.Button.reset',
defaultMessage: 'Reset',
})}
</Button>
<Button onClick={handleSubmit} loading={isSubmitting}>
{formatMessage({
id: 'app.components.Button.save',
defaultMessage: 'Save',
})}
</Button>
</Stack>
} }
title="Other CT" title={formatMessage({
subtitle="36 entries found" id: 'Settings.roles.create.title',
as="h1" defaultMessage: 'Create a role',
/> })}
<Header subtitle={formatMessage({
title={{
label: formatMessage({
id: 'Settings.roles.create.title',
defaultMessage: 'Create a role',
}),
}}
content={formatMessage({
id: 'Settings.roles.create.description', id: 'Settings.roles.create.description',
defaultMessage: 'Define the rights given to the role', defaultMessage: 'Define the rights given to the role',
})} })}
actions={headerActions(handleSubmit, handleReset)} as="h1"
isLoading={isLayoutLoading}
/> />
<BaselineAlignment top size="3px" /> <ContentLayout>
<FormCard <Stack size={6}>
actions={actions} <Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
title={formatMessage({ <Stack size={4}>
id: 'Settings.roles.form.title', <Row justifyContent="space-between">
defaultMessage: 'Details', <Box>
})} <Box>
subtitle={formatMessage({ <Text highlighted>
id: 'Settings.roles.form.description', {formatMessage({
defaultMessage: 'Name and description of the role', id: 'Settings.roles.form.title',
})} defaultMessage: 'Details',
> })}
<SizedInput </Text>
label="Settings.roles.form.input.name" </Box>
defaultMessage="Name" <Box>
name="name" <Text textColor="neutral600" small>
type="text" {formatMessage({
error={errors.name ? { id: errors.name } : null} id: 'Settings.roles.form.description',
onBlur={handleBlur} defaultMessage: 'Name and description of the role',
value={values.name} })}
onChange={handleChange} </Text>
/> </Box>
</Box>
<SizedInput <UsersRoleNumber>
label="Settings.roles.form.input.description" {formatMessage(
defaultMessage="Description" {
name="description" id: 'Settings.roles.form.button.users-with-role',
type="textarea" defaultMessage:
onBlur={handleBlur} '{number, plural, =0 {# users} one {# user} other {# users}} with this role',
value={values.description} },
onChange={handleChange} { number: 0 }
// Override the default height of the textarea )}
style={{ height: 115 }} </UsersRoleNumber>
/> </Row>
</FormCard> <Grid gap={4}>
{!isLayoutLoading && !isRoleLoading && ( <GridItem col={6}>
<Padded top bottom size="md"> <TextInput
<Permissions name="name"
isFormDisabled={false} error={errors.name && formatMessage({ id: errors.name })}
ref={permissionsRef} label={formatMessage({
permissions={rolePermissions} id: 'Settings.roles.form.input.name',
layout={permissionsLayout} defaultMessage: 'Name',
/> })}
</Padded> onChange={handleChange}
)} value={values.name}
/>
</GridItem>
<GridItem col={6}>
<Textarea
label={formatMessage({
id: 'Settings.roles.form.input.description',
defaultMessage: 'Description',
})}
name="description"
error={errors.description && formatMessage({ id: errors.description })}
onChange={handleChange}
>
{values.description}
</Textarea>
</GridItem>
</Grid>
</Stack>
</Box>
{!isLayoutLoading && !isRoleLoading ? (
<Box shadow="filterShadow" hasRadius>
<Permissions
isFormDisabled={false}
ref={permissionsRef}
permissions={rolePermissions}
layout={permissionsLayout}
/>
</Box>
) : (
<LoadingIndicatorPage />
)}
</Stack>
</ContentLayout>
</> </>
</form> </Form>
)} )}
</Formik> </Formik>
</> </Main>
); );
}; };

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
/**
*
* Tests for CreatePage
*
*/
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { Router, Switch, Route } from 'react-router';
import { createMemoryHistory } from 'history';
import Theme from '../../../../../../admin/src/components/Theme';
import { CreatePage } from '../index';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
useNotification: jest.fn(() => jest.fn()),
useOverlayBlocker: jest.fn(() => ({ lockApp: jest.fn(), unlockApp: jest.fn() })),
useTracking: jest.fn(() => ({ trackUsage: jest.fn() })),
}));
const makeApp = history => (
<IntlProvider messages={{ en: {} }} textComponent="span" locale="en">
<Theme>
<Router history={history}>
<Switch>
<Route path="/settings/roles/duplicate/:id">
<CreatePage />
</Route>
<Route path="/settings/roles/new">
<CreatePage />
</Route>
</Switch>
</Router>
</Theme>
</IntlProvider>
);
describe('<CreatePage />', () => {
it('renders and matches the snapshot', () => {
const history = createMemoryHistory();
const App = makeApp(history);
const { container } = render(App);
history.push('/settings/roles/new');
expect(container).toMatchSnapshot();
});
});

View File

@ -3,6 +3,7 @@ import { translatedErrors } from '@strapi/helper-plugin';
const schema = yup.object().shape({ const schema = yup.object().shape({
name: yup.string().required(translatedErrors.required), name: yup.string().required(translatedErrors.required),
description: yup.string().required(translatedErrors.required),
}); });
export default schema; export default schema;