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 { Flex } from '@buffetjs/core';
import { Row } from '@strapi/parts';
const CollapseLabel = styled(Flex)`
padding-right: 10px;
const CollapseLabel = styled(Row)`
padding-right: ${({ theme }) => theme.spaces[2]};
overflow: hidden;
flex: 1;
${({ 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 styled from 'styled-components';
import { Settings } from '@strapi/icons';
import { Button } from '@strapi/parts';
import PropTypes from 'prop-types';
import React from 'react';
import { useIntl } from 'react-intl';
import { Flex, Text, Padded } from '@buffetjs/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import styled from 'styled-components';
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();
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 hasConditions={hasConditions} className={className}>
<Button variant={variant} startIcon={<Settings />} onClick={onClick}>
{formatMessage({
id: 'app.components.LeftMenuLinkContainer.settings',
defaultMessage: 'Settings',
})}
</Button>
</Wrapper>
);
};
@ -34,13 +42,13 @@ const ConditionsButton = ({ onClick, className, hasConditions, isRight }) => {
ConditionsButton.defaultProps = {
className: null,
hasConditions: false,
isRight: false,
variant: 'secondary',
};
ConditionsButton.propTypes = {
onClick: PropTypes.func.isRequired,
className: PropTypes.string,
hasConditions: PropTypes.bool,
isRight: PropTypes.bool,
variant: PropTypes.string,
};
// 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 PropTypes from 'prop-types';
import { Text, Padded, Flex } from '@buffetjs/core';
import { Row, TableLabel } from '@strapi/parts';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import ConditionsSelect from '../ConditionsSelect';
import Wrapper from './Wrapper';
// import ConditionsSelect from '../ConditionsSelect';
import { rowHeight } from '../../Permissions/utils/constants';
const RowWrapper = styled(Row)`
height: ${rowHeight};
`;
const ActionRow = ({
arrayOfOptionsGroupedByCategory,
isFormDisabled,
// arrayOfOptionsGroupedByCategory,
// isFormDisabled,
isGrey,
label,
name,
onCategoryChange,
onChange,
value,
// name,
// onCategoryChange,
// onChange,
// value,
}) => {
const { formatMessage } = useIntl();
return (
<Wrapper isGrey={isGrey}>
<Padded style={{ width: 200 }} top left right bottom size="sm">
<Flex>
<Text
lineHeight="19px"
color="grey"
fontSize="xs"
fontWeight="bold"
textTransform="uppercase"
>
{formatMessage({
id: 'Settings.permissions.conditions.can',
})}
&nbsp;
</Text>
<Text
title={label}
lineHeight="19px"
fontWeight="bold"
fontSize="xs"
textTransform="uppercase"
color="mediumBlue"
style={{ maxWidth: '60%' }}
ellipsis
>
{formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
})}
</Text>
<Text
lineHeight="19px"
color="grey"
fontSize="xs"
fontWeight="bold"
textTransform="uppercase"
>
&nbsp;
{formatMessage({
id: 'Settings.permissions.conditions.when',
})}
</Text>
</Flex>
</Padded>
<ConditionsSelect
<RowWrapper as="li" background={isGrey ? 'neutral100' : 'neutral0'}>
<Row paddingLeft={6} style={{ width: 180 }}>
<TableLabel textColor="neutral600">
{formatMessage({
id: 'Settings.permissions.conditions.can',
defaultMessage: 'Can',
})}
&nbsp;
</TableLabel>
<TableLabel
title={label}
textColor="primary600"
// ! REMOVE THIS WHEN DS IS UPDATED WITH ELLIPSIS PROP
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
})}
</TableLabel>
<TableLabel textColor="neutral600">
&nbsp;
{formatMessage({
id: 'Settings.permissions.conditions.when',
defaultMessage: 'When',
})}
</TableLabel>
</Row>
{/* <ConditionsSelect
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
name={name}
isFormDisabled={isFormDisabled}
onCategoryChange={onCategoryChange}
onChange={onChange}
value={value}
/>
</Wrapper>
/> */}
</RowWrapper>
);
};
ActionRow.propTypes = {
arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired,
isFormDisabled: PropTypes.bool.isRequired,
// arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired,
// isFormDisabled: PropTypes.bool.isRequired,
isGrey: PropTypes.bool.isRequired,
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.object.isRequired,
onCategoryChange: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
// name: PropTypes.string.isRequired,
// value: PropTypes.object.isRequired,
// onCategoryChange: PropTypes.func.isRequired,
// onChange: PropTypes.func.isRequired,
};
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 { cloneDeep, get, groupBy, set } from 'lodash';
import { Modal, ModalHeader, ModalFooter } from '@strapi/helper-plugin';
import { Button, Text, Padded } from '@buffetjs/core';
import React, { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import { usePermissionsDataManager } from '../../../hooks';
import createDefaultConditionsForm from './utils/createDefaultConditionsForm';
import ActionRow from './ActionRow';
import Separator from './Separator';
import updateValues from '../Permissions/utils/updateValues';
import ActionRow from './ActionRow';
import createDefaultConditionsForm from './utils/createDefaultConditionsForm';
const ConditionsModal = ({
actions,
@ -79,53 +89,83 @@ const ConditionsModal = ({
onToggle();
};
if (!isOpen) return null;
return (
<Modal withoverflow="true" onClosed={onClosed} isOpen={isOpen} onToggle={onToggle}>
<ModalHeader headerBreadcrumbs={headerBreadCrumbs} />
<Padded top left right bottom size="md">
<Text fontSize="lg" fontWeight="bold">
{formatMessage({
id: 'Settings.permissions.conditions.define-conditions',
})}
</Text>
<Separator />
{actionsToDisplay.length === 0 && (
<Text fontSize="md" color="grey">
{formatMessage({ id: 'Settings.permissions.conditions.no-actions' })}
</Text>
)}
{actionsToDisplay.map(({ actionId, label, pathToConditionsObject }, index) => {
const name = pathToConditionsObject.join('..');
<ModalLayout onClose={onClosed}>
<ModalHeader>
<Breadcrumbs label={headerBreadCrumbs.join(', ')}>
{headerBreadCrumbs.map(label => (
<Crumb key={label}>
{upperFirst(
formatMessage({
id: label,
defaultMessage: label,
})
)}
</Crumb>
))}
</Breadcrumbs>
</ModalHeader>
<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 (
<ActionRow
key={actionId}
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
label={label}
isFormDisabled={isFormDisabled}
isGrey={index % 2 === 0}
name={name}
onCategoryChange={handleCategoryChange}
onChange={handleChange}
value={get(state, name, {})}
/>
);
})}
</Padded>
<ModalFooter>
<section>
<Button type="button" color="cancel" onClick={onToggle}>
{formatMessage({ id: 'app.components.Button.cancel' })}
return (
<ActionRow
key={actionId}
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
label={label}
isFormDisabled={isFormDisabled}
isGrey={index % 2 === 0}
name={name}
onCategoryChange={handleCategoryChange}
onChange={handleChange}
value={get(state, name, {})}
/>
);
})}
</ul>
</Box>
</Stack>
</Box>
<ModalFooter
startActions={
<Button variant="tertiary" onClick={onToggle}>
{formatMessage({ id: 'app.components.Button.cancel', defaultMessage: 'Cancel' })}
</Button>
<Button type="button" color="success" onClick={handleSubmit}>
}
endActions={
<Button onClick={handleSubmit}>
{formatMessage({
id: 'Settings.permissions.conditions.apply',
defaultMessage: 'Apply',
})}
</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 PropTypes from 'prop-types';
import { Flex, Padded } from '@buffetjs/core';
import IS_DISABLED from 'ee_else_ce/components/Roles/ContentTypeCollapse/Collapse/utils/constants';
import React, { useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import styled from 'styled-components';
import { usePermissionsDataManager } from '../../../../hooks';
import { getCheckboxState } from '../../utils';
import CheckboxWithCondition from '../../CheckboxWithCondition';
import Chevron from '../../Chevron';
import ConditionsButton from '../../ConditionsButton';
import ConditionsModal from '../../ConditionsModal';
import HiddenAction from '../../HiddenAction';
import { cellWidth, rowHeight } from '../../Permissions/utils/constants';
import RowLabelWithCheckbox from '../../RowLabelWithCheckbox';
import Wrapper from './Wrapper';
import { getCheckboxState } from '../../utils';
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 = ({
availableActions,
isActive,
@ -23,7 +101,8 @@ const Collapse = ({
onClickToggle,
pathToData,
}) => {
const [modalState, setModalState] = useState({ isOpen: false, isMounted: false });
const [isModalOpen, setModalOpen] = useState(false);
const { formatMessage } = useIntl();
const {
modifiedData,
onChangeParentCheckbox,
@ -31,11 +110,11 @@ const Collapse = ({
} = usePermissionsDataManager();
const handleToggleModalIsOpen = () => {
setModalState(prevState => ({ isMounted: true, isOpen: !prevState.isOpen }));
setModalOpen(s => !s);
};
const handleModalClose = () => {
setModalState(prevState => ({ ...prevState, isMounted: false }));
setModalOpen(false);
};
// This corresponds to the data related to the CT left checkbox
@ -65,9 +144,8 @@ const Collapse = ({
);
return (
<Wrapper isActive={isActive} isGrey={isGrey}>
<Flex style={{ flex: 1 }}>
<Padded left size="sm" />
<BoxWrapper isActive={isActive}>
<Wrapper isGrey={isGrey}>
<RowLabelWithCheckbox
isCollapsable
isFormDisabled={isFormDisabled}
@ -77,11 +155,12 @@ const Collapse = ({
onClick={onClickToggle}
someChecked={hasSomeActionsSelected}
value={hasAllActionsSelected}
isActive={isActive}
>
<Chevron icon={isActive ? 'chevron-up' : 'chevron-down'} />
<Chevron paddingLeft={2}>{isActive ? <Up /> : <Down />}</Chevron>
</RowLabelWithCheckbox>
<Flex style={{ flex: 1 }}>
<Row style={{ flex: 1 }}>
{checkboxesActions.map(
({
actionId,
@ -91,6 +170,7 @@ const Collapse = ({
isDisplayed,
isParentCheckbox,
checkboxName,
label: permissionLabel,
}) => {
if (!isDisplayed) {
return <HiddenAction key={actionId} />;
@ -98,48 +178,72 @@ const Collapse = ({
if (isParentCheckbox) {
return (
<CheckboxWithCondition
key={actionId}
disabled={isFormDisabled || IS_DISABLED}
hasConditions={hasConditions}
name={checkboxName}
onChange={onChangeParentCheckbox}
someChecked={hasSomeActionsSelected}
value={hasAllActionsSelected}
/>
<Cell key={actionId} justifyContent="center" alignItems="center">
{hasConditions && <TinyDot />}
<Checkbox
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName}
aria-label={formatMessage(
{
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 (
<CheckboxWithCondition
key={actionId}
disabled={isFormDisabled || IS_DISABLED}
hasConditions={hasConditions}
name={checkboxName}
onChange={onChangeSimpleCheckbox}
value={hasAllActionsSelected}
/>
<Cell key={actionId} justifyContent="center" alignItems="center">
{hasConditions && <TinyDot />}
<Checkbox
disabled={isFormDisabled || IS_DISABLED}
indeterminate={hasConditions}
name={checkboxName}
// 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>
<ConditionsButton
isRight
onClick={handleToggleModalIsOpen}
hasConditions={doesConditionButtonHasConditions}
/>
</Flex>
{modalState.isMounted && (
</Row>
<Box style={{ width: 120 }} />
<ConditionsModal
headerBreadCrumbs={[label, 'app.components.LeftMenuLinkContainer.settings']}
actions={checkboxesActions}
isOpen={modalState.isOpen}
isOpen={isModalOpen}
isFormDisabled={isFormDisabled}
onClosed={handleModalClose}
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 PropTypes from 'prop-types';
import { get } from 'lodash';
import { Padded, Flex } from '@buffetjs/core';
import { Checkbox, Row } from '@strapi/parts';
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 { getCheckboxState } from '../../../utils';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import HiddenAction from '../../../HiddenAction';
import { cellWidth, rowHeight } from '../../../Permissions/utils/constants';
import RequiredSign from '../../../RequiredSign';
import RowLabelWithCheckbox from '../../../RowLabelWithCheckbox';
import { getCheckboxState } from '../../../utils';
import { activeStyle } from '../../utils';
import CarretIcon from '../CarretIcon';
import SubActionRow from '../SubActionRow';
import Wrapper from './Wrapper';
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 = ({
childrenForm,
label,
@ -23,7 +48,9 @@ const ActionRow = ({
pathToData,
propertyActions,
propertyName,
isOdd,
}) => {
const { formatMessage } = useIntl();
const [rowToOpen, setRowToOpen] = useState(null);
const {
modifiedData,
@ -66,11 +93,14 @@ const ActionRow = ({
return (
<>
<Wrapper alignItems="center" isCollapsable={isCollapsable} isActive={isActive}>
<Flex style={{ flex: 1 }}>
<Padded left size="sm" />
<Wrapper
alignItems="center"
isCollapsable={isCollapsable}
isActive={isActive}
background={isOdd ? 'neutral100' : 'neutral0'}
>
<Row>
<RowLabelWithCheckbox
width="15rem"
onChange={handleChangeLeftRowCheckbox}
onClick={handleClick}
isCollapsable={isCollapsable}
@ -78,11 +108,12 @@ const ActionRow = ({
label={label}
someChecked={hasSomeActionsSelected}
value={hasAllActionsSelected}
isActive={isActive}
>
{required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} />
<CarretIcon $isActive={isActive} />
</RowLabelWithCheckbox>
<Flex style={{ flex: 1 }}>
<Row>
{propertyActions.map(({ label, isActionRelatedToCurrentProperty, actionId }) => {
if (!isActionRelatedToCurrentProperty) {
return <HiddenAction key={label} />;
@ -100,13 +131,28 @@ const ActionRow = ({
const checkboxValue = get(modifiedData, checkboxName, false);
return (
<CheckboxWithCondition
key={actionId}
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
onChange={onChangeSimpleCheckbox}
value={checkboxValue}
/>
<Cell key={actionId} 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: `${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);
return (
<CheckboxWithCondition
key={label}
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
onChange={onChangeParentCheckbox}
value={hasAllActionsSelected}
someChecked={hasSomeActionsSelected}
/>
<Cell key={label} justifyContent="center" alignItems="center">
<Checkbox
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
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>
</Flex>
</Row>
</Row>
</Wrapper>
{isActive && (
<SubActionRow
@ -157,6 +218,7 @@ ActionRow.propTypes = {
propertyActions: PropTypes.array.isRequired,
propertyName: PropTypes.string.isRequired,
required: PropTypes.bool,
isOdd: PropTypes.bool.isRequired,
};
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 PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Flex, Text } from '@buffetjs/core';
import { Row, TableLabel } from '@strapi/parts';
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;
import { cellWidth, firstRowWidth, rowHeight } from '../../../Permissions/utils/constants';
const HeaderLabel = styled(Row)`
width: ${cellWidth};
flex-shrink: 0;
`;
const PropertyLabelWrapper = styled.div`
width: 18rem;
padding-top: 1rem;
padding-bottom: 1rem;
padding-left: 3.5rem;
const PropertyLabelWrapper = styled(Row)`
width: ${firstRowWidth};
height: ${rowHeight};
flex-shrink: 0;
`;
const Header = ({ headers, label }) => {
@ -28,9 +26,9 @@ const Header = ({ headers, label }) => {
);
return (
<Flex>
<PropertyLabelWrapper>
<Text fontWeight="bold">{translatedLabel}</Text>
<Row>
<PropertyLabelWrapper alignItems="center" paddingLeft={6}>
<TableLabel textColor="neutral500">{translatedLabel}</TableLabel>
</PropertyLabelWrapper>
{headers.map(header => {
if (!header.isActionRelatedToCurrentProperty) {
@ -38,17 +36,17 @@ const Header = ({ headers, label }) => {
}
return (
<HeaderLabel key={header.label}>
<Text textTransform="capitalize" fontWeight="bold">
<HeaderLabel justifyContent="center" key={header.label}>
<TableLabel textColor="neutral500">
{formatMessage({
id: `Settings.roles.form.permissions.${header.label.toLowerCase()}`,
defaultMessage: header.label,
})}
</Text>
</TableLabel>
</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 PropTypes from 'prop-types';
import { get } from 'lodash';
import { Flex, Text } from '@buffetjs/core';
import styled from 'styled-components';
import { Box, Checkbox, Row, Text } from '@strapi/parts';
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 { getCheckboxState } from '../../../utils';
import CheckboxWithCondition from '../../../CheckboxWithCondition';
import Chevron from '../../../Chevron';
import CollapseLabel from '../../../CollapseLabel';
import Curve from '../../../Curve';
import HiddenAction from '../../../HiddenAction';
import { cellWidth, rowHeight } from '../../../Permissions/utils/constants';
import RequiredSign from '../../../RequiredSign';
import { RowStyle, RowWrapper } from './row';
import { LeftBorderTimeline, TopTimeline } from './timeline';
import Wrapper from './Wrapper';
import { getCheckboxState } from '../../../utils';
import { activeStyle } from '../../utils';
import CarretIcon from '../CarretIcon';
const SubLevelWrapper = styled.div`
padding-bottom: 8px;
const Cell = styled(Row)`
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 = ({
@ -29,6 +76,7 @@ const SubActionRow = ({
parentName,
propertyName,
}) => {
const { formatMessage } = useIntl();
const {
modifiedData,
onChangeParentCheckbox,
@ -60,91 +108,117 @@ const SubActionRow = ({
{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;
return (
<LeftBorderTimeline key={value} isVisible={isVisible}>
<RowWrapper isSmall={isSmall}>
<Curve fill="#a5d5ff" />
<Flex style={{ flex: 1 }}>
<RowWrapper>
<Curve color="primary200" />
<Row style={{ flex: 1 }}>
<RowStyle level={recursiveLevel} isActive={isActive} isCollapsable={isArrayType}>
<CollapseLabel
alignItems="center"
isCollapsable={isArrayType}
onClick={() => {
if (isArrayType) {
handleClickToggleSubLevel(value);
}
}}
{...(isArrayType && {
onClick: () => handleClickToggleSubLevel(value),
'aria-expanded': isActive,
onKeyDown: ({ key }) =>
(key === 'Enter' || key === ' ') && handleClickToggleSubLevel(value),
tabIndex: 0,
role: 'button',
})}
title={label}
>
<Text
color={isActive ? 'mediumBlue' : 'grey'}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
<StyledText>{upperFirst(label)}</StyledText>
{required && <RequiredSign />}
<Chevron icon={isActive ? 'caret-up' : 'caret-down'} />
<CarretIcon $isActive={isActive} />
</CollapseLabel>
</RowStyle>
<Flex style={{ flex: 1 }}>
{propertyActions.map(({ actionId, label, isActionRelatedToCurrentProperty }) => {
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
*/
const checkboxName = [
...pathToDataFromActionRow.split('..'),
actionId,
'properties',
propertyName,
...parentName.split('..'),
value,
];
<Row style={{ flex: 1 }}>
{propertyActions.map(
({ actionId, label: propertyLabel, isActionRelatedToCurrentProperty }) => {
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
*/
const checkboxName = [
...pathToDataFromActionRow.split('..'),
actionId,
'properties',
propertyName,
...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 (
<CheckboxWithCondition
key={label}
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
onChange={onChangeSimpleCheckbox}
value={checkboxValue}
/>
<Cell key={propertyLabel} justifyContent="center" alignItems="center">
<Checkbox
key={propertyLabel}
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 =>
onChangeParentCheckbox({
target: {
name: checkboxName.join('..'),
value,
},
})}
value={hasAllActionsSelected}
indeterminate={hasSomeActionsSelected}
/>
</Cell>
);
}
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(
checkboxValue
);
return (
<CheckboxWithCondition
key={label}
disabled={isFormDisabled || IS_DISABLED}
name={checkboxName.join('..')}
onChange={onChangeParentCheckbox}
value={hasAllActionsSelected}
someChecked={hasSomeActionsSelected}
/>
);
})}
</Flex>
</Flex>
)}
</Row>
</Row>
</RowWrapper>
{displayedRecursiveChildren && isActive && (
<SubLevelWrapper>
<Box paddingBottom={2}>
<SubActionRow
isFormDisabled={isFormDisabled}
parentName={`${parentName}..${value}`}
@ -154,7 +228,7 @@ const SubActionRow = ({
recursiveLevel={recursiveLevel + 1}
childrenForm={displayedRecursiveChildren.children}
/>
</SubLevelWrapper>
</Box>
)}
</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 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 Header from './Header';
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 = ({
availableActions,
@ -23,8 +39,8 @@ const CollapsePropertyMatrix = ({
return (
<Wrapper isLast={isLast}>
<Header label={label} headers={propertyActions} />
<Padded left size="md">
{childrenForm.map(({ children: childrenForm, label, value, required }) => (
<Box>
{childrenForm.map(({ children: childrenForm, label, value, required }, i) => (
<ActionRow
childrenForm={childrenForm}
key={value}
@ -35,9 +51,10 @@ const CollapsePropertyMatrix = ({
propertyActions={propertyActions}
pathToData={pathToData}
propertyName={propertyName}
isOdd={i % 2 === 0}
/>
))}
</Padded>
</Box>
</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 React, { useCallback, useMemo } from 'react';
import Collapse from './Collapse';
import CollapsePropertyMatrix from './CollapsePropertyMatrix';
import { getAvailableActions } from './utils';
import Wrapper from './Wrapper';
const ContentTypeCollapse = ({
allActions,
@ -24,10 +23,8 @@ const ContentTypeCollapse = ({
return getAvailableActions(allActions, contentTypeName);
}, [allActions, contentTypeName]);
const isOdd = useMemo(() => index % 2 !== 0, [index]);
return (
<Wrapper withMargin={isOdd}>
<>
<Collapse
availableActions={availableActions}
isActive={isActive}
@ -38,22 +35,21 @@ const ContentTypeCollapse = ({
pathToData={pathToData}
/>
{isActive &&
properties.map(({ label, value, children: childrenForm }, i) => {
properties.map(({ label: propertyLabel, value, children: childrenForm }, i) => {
return (
<CollapsePropertyMatrix
availableActions={availableActions}
childrenForm={childrenForm}
isFormDisabled={isFormDisabled}
label={label}
label={propertyLabel}
pathToData={pathToData}
propertyName={value}
key={value}
isLast={i === properties.length - 1}
isOdd={isOdd}
/>
);
})}
</Wrapper>
</>
);
};

View File

@ -1,14 +1,14 @@
import { Text } from '@buffetjs/core';
import Chevron from '../../Chevron';
import { Text } from '@strapi/parts';
import CarretIcon from '../CollapsePropertyMatrix/CarretIcon';
const activeStyle = theme => `
color: ${theme.main.colors.mediumBlue};
${Text} {
color: ${theme.main.colors.mediumBlue};
color: ${theme.colors.primary600};
font-weight: bold;
}
${Chevron} {
${CarretIcon} {
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 PropTypes from 'prop-types';
import { Padded } from '@buffetjs/core';
import { Box } from '@strapi/parts';
import styled from 'styled-components';
import ContentTypeCollapses from '../ContentTypeCollapses';
import GlobalActions from '../GlobalActions';
import Wrapper from './Wrapper';
const StyledBox = styled(Box)`
overflow-x: auto;
`;
const ContentTypes = ({ isFormDisabled, kind, layout: { actions, subjects } }) => {
return (
<Wrapper>
<Padded left right bottom size="md">
<GlobalActions actions={actions} kind={kind} isFormDisabled={isFormDisabled} />
<ContentTypeCollapses
actions={actions}
isFormDisabled={isFormDisabled}
pathToData={kind}
subjects={subjects}
/>
</Padded>
</Wrapper>
<StyledBox background="neutral0">
<GlobalActions actions={actions} kind={kind} isFormDisabled={isFormDisabled} />
<ContentTypeCollapses
actions={actions}
isFormDisabled={isFormDisabled}
pathToData={kind}
subjects={subjects}
/>
</StyledBox>
);
};

View File

@ -1,32 +1,52 @@
import React, { memo } from 'react';
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 => (
<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>
<StyledBox>
<Svg
width="20"
height="23"
viewBox="0 0 20 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2.58 2.5q-1.2 16 16 16"
fill="none"
stroke={props.fill}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="5"
fillRule="evenodd"
clipRule="evenodd"
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"
fill="#D9D8FF"
/>
</g>
</svg>
</Svg>
</StyledBox>
);
Curve.defaultProps = {
fill: '#f3f4f4',
fill: 'primary200',
};
Curve.propTypes = {
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 PropTypes from 'prop-types';
import { Flex } from '@buffetjs/core';
import React, { memo, useMemo } from 'react';
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 CheckboxWithCondition from '../CheckboxWithCondition';
import { cellWidth, firstRowWidth } from '../Permissions/utils/constants';
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 { formatMessage } = useIntl();
@ -22,28 +29,43 @@ const GlobalActions = ({ actions, isFormDisabled, kind }) => {
}, [modifiedData, displayedActions, kind]);
return (
<Wrapper disabled={isFormDisabled}>
<Flex>
<Box paddingBottom={4} paddingTop={6} style={{ paddingLeft: firstRowWidth }}>
<Stack horizontal>
{displayedActions.map(({ label, actionId }) => {
return (
<CheckboxWithCondition
key={actionId}
disabled={isFormDisabled || IS_DISABLED}
message={formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
})}
onChange={({ target: { value } }) => {
onChangeCollectionTypeGlobalActionCheckbox(kind, actionId, value);
}}
name={actionId}
value={get(checkboxesState, [actionId, 'hasAllActionsSelected'], false)}
someChecked={get(checkboxesState, [actionId, 'hasSomeActionsSelected'], false)}
/>
<CenteredStack key={actionId} size={3}>
<TableLabel textColor="neutral500">
{formatMessage({
id: `Settings.roles.form.permissions.${label.toLowerCase()}`,
defaultMessage: label,
})}
</TableLabel>
<Checkbox
disabled={isFormDisabled || IS_DISABLED}
onValueChange={value => {
onChangeCollectionTypeGlobalActionCheckbox(kind, actionId, value);
}}
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>
</Wrapper>
</Stack>
</Box>
);
};

View File

@ -1,9 +1,8 @@
import styled from 'styled-components';
import { cellWidth } from '../Permissions/utils/constants';
const HiddenAction = styled.div`
min-width: 10rem;
max-width: 12rem;
flex: 1;
width: ${cellWidth};
`;
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 { Tab, TabGroup, TabPanel, TabPanels, Tabs } from '@strapi/parts';
import { has, isEmpty } from 'lodash';
import Tabs from '../Tabs';
import PermissionsDataManagerProvider from '../PermissionsDataManagerProvider';
import PropTypes from 'prop-types';
import React, { forwardRef, memo, useCallback, useImperativeHandle, useReducer } from 'react';
import { useIntl } from 'react-intl';
import ContentTypes from '../ContentTypes';
import PermissionsDataManagerProvider from '../PermissionsDataManagerProvider';
import PluginsAndSettings from '../PluginsAndSettings';
import TAB_LABELS from './utils/tabLabels';
import formatPermissionsToAPI from './utils/formatPermissionsToAPI';
import init from './init';
import reducer, { initialState } from './reducer';
import formatPermissionsToAPI from './utils/formatPermissionsToAPI';
import TAB_LABELS from './utils/tabLabels';
const Permissions = forwardRef(({ layout, isFormDisabled, permissions }, ref) => {
const [{ initialData, layouts, modifiedData }, dispatch] = useReducer(reducer, initialState, () =>
init(layout, permissions)
);
const { formatMessage } = useIntl();
useImperativeHandle(ref, () => {
return {
@ -106,28 +108,45 @@ const Permissions = forwardRef(({ layout, isFormDisabled, permissions }, ref) =>
onChangeCollectionTypeGlobalActionCheckbox: handleChangeCollectionTypeGlobalActionCheckbox,
}}
>
<Tabs tabsLabel={TAB_LABELS}>
<ContentTypes
layout={layouts.collectionTypes}
kind="collectionTypes"
isFormDisabled={isFormDisabled}
/>
<ContentTypes
layout={layouts.singleTypes}
kind="singleTypes"
isFormDisabled={isFormDisabled}
/>
<PluginsAndSettings
layout={layouts.plugins}
kind="plugins"
isFormDisabled={isFormDisabled}
/>
<PluginsAndSettings
layout={layouts.settings}
kind="settings"
isFormDisabled={isFormDisabled}
/>
</Tabs>
<TabGroup id="tabs">
<Tabs>
{TAB_LABELS.map(tabLabel => (
<Tab key={tabLabel.id}>
{formatMessage({ id: tabLabel.labelId, defaultMessage: tabLabel.defaultMessage })}
</Tab>
))}
</Tabs>
<TabPanels style={{ position: 'relative' }}>
<TabPanel>
<ContentTypes
layout={layouts.collectionTypes}
kind="collectionTypes"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
<TabPanel>
<ContentTypes
layout={layouts.singleTypes}
kind="singleTypes"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
<TabPanel>
<PluginsAndSettings
layout={layouts.plugins}
kind="plugins"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
<TabPanel>
<PluginsAndSettings
layout={layouts.settings}
kind="settings"
isFormDisabled={isFormDisabled}
/>
</TabPanel>
</TabPanels>
</TabGroup>
</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 { Flex, Text } from '@buffetjs/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { PermissionsWrapper, RowContainer } from '@strapi/helper-plugin';
import { Accordion, AccordionContent, AccordionToggle, Box } from '@strapi/parts';
import upperFirst from 'lodash/upperFirst';
import PropTypes from 'prop-types';
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import SubCategory from '../SubCategory';
import RowStyle from './Wrapper';
const PermissionRow = ({
childrenForm,
@ -30,26 +28,18 @@ const PermissionRow = ({
}, [name]);
return (
<RowContainer isWhite={isWhite}>
<RowStyle isWhite={isWhite} isActive={isOpen} onClick={handleClick}>
<Flex alignItems="center" justifyContent="space-between">
<div>
<Text color="grey" fontWeight="bold" fontSize="xs" textTransform="uppercase">
{categoryName}
</Text>
<Text lineHeight="22px" color="grey">
{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>
<Accordion expanded={isOpen} toggle={handleClick} id="acc-1">
<AccordionToggle
title={upperFirst(categoryName)}
description={`${formatMessage(
{ id: 'Settings.permissions.category' },
{ category: categoryName }
)} ${kind === 'plugins' ? 'plugin' : kind}`}
variant={isWhite ? 'primary' : 'secondary'}
/>
{isOpen && (
<PermissionsWrapper isWhite={isWhite}>
<AccordionContent>
<Box padding={6}>
{childrenForm.map(({ actions, subCategoryName, subCategoryId }) => (
<SubCategory
key={subCategoryName}
@ -60,9 +50,9 @@ const PermissionRow = ({
pathToData={[...pathToData, subCategoryId]}
/>
))}
</PermissionsWrapper>
)}
</RowContainer>
</Box>
</AccordionContent>
</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 styled from 'styled-components';
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 { BaselineAlignment } from '@strapi/helper-plugin';
import { get } from 'lodash';
import IS_DISABLED from 'ee_else_ce/components/Roles/PluginsAndSettings/SubCategory/utils/constants';
import { usePermissionsDataManager } from '../../../../hooks';
import { getCheckboxState, removeConditionKeyFromData } from '../../utils';
import ConditionsButton from '../../ConditionsButton';
import ConditionsModal from '../../ConditionsModal';
import CheckboxWrapper from './CheckboxWrapper';
import ConditionsButtonWrapper from './ConditionsButtonWrapper';
import Wrapper from './Wrapper';
import { formatActions, getConditionsButtonState } from './utils';
const Border = styled.div`
flex: 1;
align-self: center;
border-top: 1px solid #f6f6f6;
padding: 0px 10px;
border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
`;
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 [modalState, setModalState] = useState({ isOpen: false, isMounted: false });
const [isModalOpen, setModalOpen] = useState(false);
const {
modifiedData,
onChangeParentCheckbox,
@ -43,84 +57,87 @@ const SubCategory = ({ categoryName, isFormDisabled, subCategoryName, actions, p
const { hasAllActionsSelected, hasSomeActionsSelected } = getCheckboxState(dataWithoutCondition);
const handleToggleModalIsOpen = () => {
setModalState(prevState => ({ isMounted: true, isOpen: !prevState.isOpen }));
setModalOpen(s => !s);
};
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
const formattedActions = formatActions(actions, modifiedData, pathToData);
const doesButtonHasCondition = getConditionsButtonState(get(modifiedData, [...pathToData], {}));
return (
<>
<Wrapper>
<Flex justifyContent="space-between" alignItems="center">
<Padded right size="sm">
<Text
lineHeight="18px"
color="#919bae"
fontWeight="bold"
fontSize="xs"
textTransform="uppercase"
>
{subCategoryName}
</Text>
</Padded>
<Box>
<Row justifyContent="space-between" alignItems="center">
<Box paddingRight={4}>
<TableLabel textColor="neutral600">{subCategoryName}</TableLabel>
</Box>
<Border />
<Padded left size="sm">
<BaselineAlignment top size="1px" />
<Box paddingLeft={4}>
<Checkbox
name={pathToData.join('..')}
message={formatMessage({ id: 'app.utils.select-all' })}
disabled={isFormDisabled || IS_DISABLED}
onChange={onChangeParentCheckbox}
someChecked={hasSomeActionsSelected}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChangeParentCheckbox({
target: {
name: pathToData.join('..'),
value,
},
})}
indeterminate={hasSomeActionsSelected}
value={hasAllActionsSelected}
/>
</Padded>
</Flex>
<BaselineAlignment top size="1px" />
<Padded top size="xs">
<Flex flexWrap="wrap">
>
{formatMessage({ id: 'app.utils.select-all', defaultMessage: 'Select all' })}
</Checkbox>
</Box>
</Row>
<Row paddingTop={6} paddingBottom={6}>
<Grid gap={2} style={{ flex: 1 }}>
{formattedActions.map(({ checkboxName, value, action, displayName, hasConditions }) => {
return (
<CheckboxWrapper
disabled={isFormDisabled || IS_DISABLED}
hasConditions={hasConditions}
key={action}
>
<Checkbox
name={checkboxName}
<GridItem col={3} key={action}>
<CheckboxWrapper
disabled={isFormDisabled || IS_DISABLED}
message={displayName}
onChange={onChangeSimpleCheckbox}
value={value}
/>
</CheckboxWrapper>
hasConditions={hasConditions}
>
<Checkbox
name={checkboxName}
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>
<ConditionsButtonWrapper disabled={isFormDisabled} hasConditions={doesButtonHasCondition}>
<ConditionsButton
hasConditions={doesButtonHasCondition}
onClick={handleToggleModalIsOpen}
/>
</ConditionsButtonWrapper>
</Padded>
</Wrapper>
{modalState.isMounted && (
<ConditionsModal
headerBreadCrumbs={[categoryName, subCategoryName]}
actions={formattedActions}
isOpen={modalState.isOpen}
isFormDisabled={isFormDisabled}
onClosed={handleModalClose}
onToggle={handleToggleModalIsOpen}
/>
)}
</Grid>
<ConditionsButton
hasConditions={doesButtonHasCondition}
onClick={handleToggleModalIsOpen}
variant="tertiary"
/>
</Row>
</Box>
<ConditionsModal
headerBreadCrumbs={[categoryName, subCategoryName]}
actions={formattedActions}
isOpen={isModalOpen}
isFormDisabled={isFormDisabled}
onClosed={handleModalClose}
onToggle={handleToggleModalIsOpen}
/>
</>
);
};

View File

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

View File

@ -2,8 +2,8 @@ import React from 'react';
import styled from 'styled-components';
const Required = styled.span`
color: ${({ theme }) => theme.main.colors.red};
padding-left: 2px;
color: ${({ theme }) => theme.colors.danger700};
padding-left: ${({ theme }) => theme.spaces[1]}px;
`;
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 { 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 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 = ({
children,
isCollapsable,
isActive,
isFormDisabled,
label,
onChange,
onClick,
checkboxName,
someChecked,
textColor,
value,
width,
}) => {
const { formatMessage } = useIntl();
return (
<Wrapper width={width} disabled={isFormDisabled}>
<Row alignItems="center" paddingLeft={6} style={{ width: firstRowWidth, flexShrink: 0 }}>
<Checkbox
name={checkboxName}
aria-label={formatMessage(
{
id: `Settings.permissions.select-all-by-permission`,
defaultMessage: 'Select all {label} permissions',
},
{ label }
)}
disabled={isFormDisabled}
onChange={onChange}
someChecked={someChecked}
// Keep same signature as packages/core/admin/admin/src/components/Roles/Permissions/index.js l.91
onValueChange={value =>
onChange({
target: {
name: checkboxName,
value,
},
})}
indeterminate={someChecked}
value={value}
/>
<CollapseLabel
title={label}
alignItems="center"
isCollapsable={isCollapsable}
onClick={onClick}
{...(isCollapsable && {
onClick,
'aria-expanded': isActive,
onKeyDown: ({ key }) => (key === 'Enter' || key === ' ') && onClick(),
tabIndex: 0,
role: 'button',
})}
>
<Text
color={textColor}
ellipsis
fontSize="xs"
fontWeight="bold"
lineHeight="20px"
textTransform="uppercase"
>
{label}
</Text>
<StyledText>{upperFirst(label)}</StyledText>
{children}
</CollapseLabel>
</Wrapper>
</Row>
);
};
@ -55,8 +77,6 @@ RowLabelWithCheckbox.defaultProps = {
value: false,
someChecked: false,
isCollapsable: false,
textColor: 'grey',
width: '18rem',
};
RowLabelWithCheckbox.propTypes = {
@ -68,9 +88,8 @@ RowLabelWithCheckbox.propTypes = {
onChange: PropTypes.func,
onClick: PropTypes.func.isRequired,
someChecked: PropTypes.bool,
textColor: PropTypes.string,
value: PropTypes.bool,
width: PropTypes.string,
isActive: PropTypes.bool.isRequired,
};
export default memo(RowLabelWithCheckbox);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,10 @@ function SettingsPage() {
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 (
<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.singular": "{number} user found",
"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.hint": "This will only display your own interface in the chosen language.",
"Settings.profile.form.section.experience.title": "Experience",
@ -127,7 +129,7 @@
"Settings.roles.create.title": "Create a role",
"Settings.roles.created": "Role created",
"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.description": "Name and description of the role",
"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 {
BaselineAlignment,
CheckPagePermissions,
Form,
LoadingIndicatorPage,
request,
useTracking,
useNotification,
useOverlayBlocker,
useTracking,
} 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 adminPermissions from '../../../../../admin/src/permissions';
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 styled from 'styled-components';
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';
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 toggleNotification = useNotification();
const { lockApp, unlockApp } = useOverlayBlocker();
const { formatMessage } = useIntl();
const [isSubmiting, setIsSubmiting] = useState(false);
const [isSubmitting, setIsSubmiting] = useState(false);
const { replace } = useHistory();
const permissionsRef = useRef();
const { trackUsage } = useTracking();
@ -39,31 +57,6 @@ const CreatePage = () => {
const { isLoading: isLayoutLoading, data: permissionsLayout } = useFetchPermissionsLayout();
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 => {
lockApp();
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({
id: 'Settings.roles.form.created',
})} ${moment().format('LL')}`;
return (
<>
<Main labelledBy="title">
<PageTitle name="Roles" />
<Formik
initialValues={{ name: '', description: defaultDescription }}
@ -141,84 +125,125 @@ const CreatePage = () => {
validationSchema={schema}
validateOnChange={false}
>
{({ handleSubmit, values, errors, handleReset, handleChange, handleBlur }) => (
<form onSubmit={handleSubmit}>
{({ handleSubmit, values, errors, handleReset, handleChange }) => (
<Form noValidate>
<>
<HeaderLayout
primaryAction={<Button startIcon={<AddIcon />}>Add an entry</Button>}
secondaryAction={
<Button variant="tertiary" startIcon={<EditIcon />}>
Edit
</Button>
id="title"
primaryAction={
<Stack horizontal size={2}>
<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"
subtitle="36 entries found"
as="h1"
/>
<Header
title={{
label: formatMessage({
id: 'Settings.roles.create.title',
defaultMessage: 'Create a role',
}),
}}
content={formatMessage({
title={formatMessage({
id: 'Settings.roles.create.title',
defaultMessage: 'Create a role',
})}
subtitle={formatMessage({
id: 'Settings.roles.create.description',
defaultMessage: 'Define the rights given to the role',
})}
actions={headerActions(handleSubmit, handleReset)}
isLoading={isLayoutLoading}
as="h1"
/>
<BaselineAlignment top size="3px" />
<FormCard
actions={actions}
title={formatMessage({
id: 'Settings.roles.form.title',
defaultMessage: 'Details',
})}
subtitle={formatMessage({
id: 'Settings.roles.form.description',
defaultMessage: 'Name and description of the role',
})}
>
<SizedInput
label="Settings.roles.form.input.name"
defaultMessage="Name"
name="name"
type="text"
error={errors.name ? { id: errors.name } : null}
onBlur={handleBlur}
value={values.name}
onChange={handleChange}
/>
<SizedInput
label="Settings.roles.form.input.description"
defaultMessage="Description"
name="description"
type="textarea"
onBlur={handleBlur}
value={values.description}
onChange={handleChange}
// Override the default height of the textarea
style={{ height: 115 }}
/>
</FormCard>
{!isLayoutLoading && !isRoleLoading && (
<Padded top bottom size="md">
<Permissions
isFormDisabled={false}
ref={permissionsRef}
permissions={rolePermissions}
layout={permissionsLayout}
/>
</Padded>
)}
<ContentLayout>
<Stack size={6}>
<Box background="neutral0" padding={6} shadow="filterShadow" hasRadius>
<Stack size={4}>
<Row justifyContent="space-between">
<Box>
<Box>
<Text highlighted>
{formatMessage({
id: 'Settings.roles.form.title',
defaultMessage: 'Details',
})}
</Text>
</Box>
<Box>
<Text textColor="neutral600" small>
{formatMessage({
id: 'Settings.roles.form.description',
defaultMessage: 'Name and description of the role',
})}
</Text>
</Box>
</Box>
<UsersRoleNumber>
{formatMessage(
{
id: 'Settings.roles.form.button.users-with-role',
defaultMessage:
'{number, plural, =0 {# users} one {# user} other {# users}} with this role',
},
{ number: 0 }
)}
</UsersRoleNumber>
</Row>
<Grid gap={4}>
<GridItem col={6}>
<TextInput
name="name"
error={errors.name && formatMessage({ id: errors.name })}
label={formatMessage({
id: 'Settings.roles.form.input.name',
defaultMessage: 'Name',
})}
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>
</>
</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({
name: yup.string().required(translatedErrors.required),
description: yup.string().required(translatedErrors.required),
});
export default schema;