Created conditions modal

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2021-02-16 15:16:16 +01:00
parent 5a53675b8f
commit 95eeed0dec
21 changed files with 784 additions and 13 deletions

View File

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

View File

@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Text, Padded, Flex } from '@buffetjs/core';
import { useIntl } from 'react-intl';
// import { usePermissionsDataManager } from '../contexts/PermissionsDataManagerContext';
import ConditionsSelect from '../ConditionsSelect';
import Wrapper from './Wrapper';
const ActionRow = ({
arrayOfOptionsGroupedByCategory,
isGrey,
label,
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
>
{label}
</Text>
<Text
lineHeight="19px"
color="grey"
fontSize="xs"
fontWeight="bold"
textTransform="uppercase"
>
&nbsp;
{formatMessage({
id: 'Settings.permissions.conditions.when',
})}
</Text>
</Flex>
</Padded>
<ConditionsSelect
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
name={name}
onCategoryChange={onCategoryChange}
onChange={onChange}
value={value}
/>
</Wrapper>
);
};
ActionRow.propTypes = {
arrayOfOptionsGroupedByCategory: PropTypes.array.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,
};
export default ActionRow;

View File

@ -0,0 +1,15 @@
import React from 'react';
import { Remove } from '@buffetjs/icons';
import { components } from 'react-select';
const ClearIndicator = props => {
const Component = components.ClearIndicator;
return (
<Component {...props}>
<Remove width="11px" height="11px" fill="#9EA7B8" />
</Component>
);
};
export default ClearIndicator;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Flex } from '@buffetjs/core';
import styled from 'styled-components';
import PropTypes from 'prop-types';
const Wrapper = styled(Flex)`
height: 100%;
width: 32px;
background: #fafafb;
> svg {
align-self: center;
font-size: 11px;
color: #b3b5b9;
}
`;
const DropdownIndicator = ({ selectProps: { menuIsOpen } }) => {
const icon = menuIsOpen ? 'caret-up' : 'caret-down';
return (
<Wrapper>
<FontAwesomeIcon icon={icon} />
</Wrapper>
);
};
DropdownIndicator.propTypes = {
selectProps: PropTypes.shape({
menuIsOpen: PropTypes.bool.isRequired,
}).isRequired,
};
Wrapper.defaultProps = {
flexDirection: 'column',
justifyContent: 'center',
};
export default DropdownIndicator;

View File

@ -0,0 +1,3 @@
const IndicatorSeparator = () => null;
export default IndicatorSeparator;

View File

@ -0,0 +1,48 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { Collapse } from 'reactstrap';
const ToggleUl = styled(Collapse)`
font-size: 13px;
padding: 12px 15px 0 15px;
list-style: none;
background-color: #fff;
> li {
padding-top: 5px;
label {
cursor: pointer;
}
.check-wrapper {
z-index: 9;
> input {
z-index: 1;
}
}
}
> li:not(:last-child) {
padding-bottom: 12px;
}
`;
const SubUl = ({ children, isOpen }) => {
return (
<ToggleUl tag="ul" isOpen={isOpen}>
{children}
</ToggleUl>
);
};
SubUl.defaultProps = {
children: null,
isOpen: false,
};
SubUl.propTypes = {
children: PropTypes.node,
isOpen: PropTypes.bool,
};
export default SubUl;

View File

@ -0,0 +1,80 @@
/* eslint-disable indent */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import styled from 'styled-components';
const Ul = styled.ul`
max-height: 150px;
font-size: 13px;
padding: 0 15px;
margin-bottom: 0px;
list-style: none;
background-color: #fff;
> li {
label {
flex-shrink: 1;
width: fit-content !important;
cursor: pointer;
margin-bottom: 0px;
}
.check-wrapper {
z-index: 9;
> input {
z-index: 1;
}
}
.chevron {
margin: auto;
font-size: 11px;
color: #919bae;
}
}
.li-multi-menu {
margin-bottom: -3px;
}
.li {
line-height: 27px;
position: relative;
> p {
margin: 0;
}
&:hover {
> p::after {
content: attr(datadescr);
position: absolute;
left: 0;
color: #007eff;
font-weight: 700;
z-index: 100;
}
&::after {
content: '';
position: absolute;
z-index: 1;
top: 0;
left: -30px;
right: -30px;
bottom: 0;
background-color: #e6f0fb;
}
}
}
${({ disabled, theme }) =>
disabled &&
`
label {
cursor: not-allowed !important;
}
input[type='checkbox'] {
cursor: not-allowed;
&:after {
cursor: not-allowed;
color: ${theme.main.colors.grey};
}
}
`}
`;
export default Ul;

View File

@ -0,0 +1,6 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { upperFirst } from 'lodash';
const UpperFirst = ({ content }) => upperFirst(content);
export default UpperFirst;

View File

@ -0,0 +1,139 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { useState, useMemo } from 'react';
import PropTypes from 'prop-types';
import { components } from 'react-select';
import { get } from 'lodash';
import { Checkbox, Flex } from '@buffetjs/core';
import { Label } from '@buffetjs/styles';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SubUl from './SubUl';
import Ul from './Ul';
import UpperFirst from './UpperFirst';
import { getCheckboxState } from '../../../utils';
import createCollapsesObject from './utils/createCollapsesObject';
/* eslint-disable jsx-a11y/no-static-element-interactions */
const MenuList = ({ selectProps, ...rest }) => {
const Component = components.MenuList;
const { arrayOfOptionsGroupedByCategory } = selectProps;
const initCollapses = useMemo(() => createCollapsesObject(arrayOfOptionsGroupedByCategory), [
arrayOfOptionsGroupedByCategory,
]);
const [collapses, setCollapses] = useState(initCollapses);
const toggleCollapse = collapseName => {
setCollapses(prevState => ({ ...prevState, [collapseName]: !collapses[collapseName] }));
};
return (
<Component {...rest}>
<Ul
// TODO
disabled={false}
>
{arrayOfOptionsGroupedByCategory.map((category, index) => {
const [categoryName, conditions] = category;
const checkboxName = `${selectProps.name}..${categoryName}`;
const {
hasAllActionsSelected: hasAllConditionsSelected,
hasSomeActionsSelected: hasSomeConditionsSelected,
} = getCheckboxState(selectProps.value);
return (
<li key={categoryName}>
<div>
<Flex justifyContent="space-between">
<Label
htmlFor="overrideReactSelectBehavior"
onClick={() =>
selectProps.onCategoryChange({
keys: [selectProps.name, categoryName],
value: !hasAllConditionsSelected,
})}
>
<Flex>
<Checkbox
// TODO
disabled={false}
id="checkCategory"
// TODO
name={checkboxName}
onChange={() => {}}
someChecked={hasSomeConditionsSelected}
value={hasAllConditionsSelected}
/>
<UpperFirst content={categoryName} />
</Flex>
</Label>
<div
style={{ flex: 1, textAlign: 'end', cursor: 'pointer' }}
onClick={() => toggleCollapse(categoryName)}
>
<FontAwesomeIcon
style={{
margin: 'auto',
fontSize: '11px',
color: '#919bae',
}}
icon={collapses[categoryName] ? 'chevron-up' : 'chevron-down'}
/>
</div>
</Flex>
</div>
<SubUl tag="ul" isOpen={collapses[categoryName]}>
{conditions.map(condition => {
const checkboxValue = get(selectProps.value, [categoryName, condition.id], false);
return (
<li key={condition.id}>
<Flex>
<Label
htmlFor={condition.id}
message={condition.displayName}
onClick={() => {
selectProps.onChange({
keys: [selectProps.name, categoryName, condition.id],
value: !checkboxValue,
});
}}
>
<Flex>
<Checkbox
id="check"
name={condition.id}
// Remove the handler
onChange={() => {}}
value={checkboxValue}
/>
{condition.displayName}
</Flex>
</Label>
</Flex>
</li>
);
})}
</SubUl>
{index + 1 < arrayOfOptionsGroupedByCategory.length.length && (
<div style={{ paddingTop: '17px' }} />
)}
</li>
);
})}
</Ul>
</Component>
);
};
MenuList.propTypes = {
selectProps: PropTypes.shape({
arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired,
name: PropTypes.string.isRequired,
onCategoryChange: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.object.isRequired,
}).isRequired,
};
export default MenuList;

View File

@ -0,0 +1,8 @@
const createCollapsesObject = arrayOfCategories =>
arrayOfCategories.reduce((acc, current, index) => {
acc[current[0]] = index === 0;
return acc;
}, {});
export default createCollapsesObject;

View File

@ -0,0 +1,46 @@
/* eslint-disable indent */
import React from 'react';
import PropTypes from 'prop-types';
import { components } from 'react-select';
import { Text } from '@buffetjs/core';
import { useIntl } from 'react-intl';
import { createArrayOfValues } from '../../utils';
const Value = ({ children, selectProps, ...props }) => {
const { formatMessage } = useIntl();
const SingleValue = components.SingleValue;
const valuesArray = createArrayOfValues(selectProps.value).filter(val => val);
return (
<SingleValue {...props}>
<Text style={{ paddingTop: 1 }}>
{valuesArray.length === 0
? 'Anytime'
: formatMessage(
{
id: `Settings.permissions.conditions.selected.${
selectProps.value.length > 1 ? 'plural' : 'singular'
}`,
},
{ number: valuesArray.length }
)}
</Text>
</SingleValue>
);
};
Value.defaultProps = {
children: null,
selectProps: {
value: [],
},
};
Value.propTypes = {
children: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
selectProps: PropTypes.shape({
value: PropTypes.object,
}),
};
export default Value;

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
import { Option } from '@buffetjs/core';
const StyledOption = styled(Option)`
> span {
display: block !important;
color: #007eff !important;
}
`;
export default StyledOption;

View File

@ -0,0 +1,69 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import Select from 'react-select';
import { useIntl } from 'react-intl';
import MenuList from './MenuList';
import ClearIndicator from './ClearIndicator';
import DropdownIndicator from './CustomDropdownIndicator';
import IndicatorSeparator from './IndicatorSeparator';
import SingleValue from './SingleValue';
import selectStyle from '../utils/selectStyle';
const Wrapper = styled.div`
padding-left: 30px;
width: 60%;
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'default')};
`;
const ConditionsSelect = ({
arrayOfOptionsGroupedByCategory,
name,
onCategoryChange,
onChange,
value,
}) => {
const { formatMessage } = useIntl();
return (
<Wrapper
// TODO
disabled={false}
>
<Select
components={{
ClearIndicator,
DropdownIndicator,
IndicatorSeparator,
SingleValue,
MenuList,
}}
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
// TODO
isDisabled={false}
name={name}
onChange={onChange}
isClearable={false}
isLoading={false}
closeMenuOnSelect={false}
isSearchable={false}
hideSelectedOptions={false}
placeholder={formatMessage({ id: 'Settings.permissions.conditions.anytime' })}
onCategoryChange={onCategoryChange}
options={[]}
styles={selectStyle}
value={value}
/>
</Wrapper>
);
};
ConditionsSelect.propTypes = {
arrayOfOptionsGroupedByCategory: PropTypes.array.isRequired,
name: PropTypes.string.isRequired,
onCategoryChange: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.object.isRequired,
};
export default ConditionsSelect;

View File

@ -1,12 +1,59 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
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 { useIntl } from 'react-intl';
import createDefaultConditionsForm from './utils/createDefaultConditionsForm';
import ActionRow from './ActionRow';
import Separator from './Separator';
import { usePermissionsDataManager } from '../contexts/PermissionsDataManagerContext';
import updateValues from '../Permissions/utils/updateValues';
const ConditionsModal = ({ headerBreadCrumbs, isOpen, onClosed, onToggle }) => {
const ConditionsModal = ({ actions, headerBreadCrumbs, isOpen, onClosed, onToggle }) => {
const { formatMessage } = useIntl();
const { availableConditions, modifiedData } = usePermissionsDataManager();
const arrayOfOptionsGroupedByCategory = useMemo(() => {
return Object.entries(groupBy(availableConditions, 'category'));
}, [availableConditions]);
const actionsToDisplay = actions.filter(
({ isDisplayed, hasSomeActionsSelected, hasAllActionsSelected }) =>
isDisplayed && (hasSomeActionsSelected || hasAllActionsSelected)
);
const initState = useMemo(() => {
return createDefaultConditionsForm(
actionsToDisplay,
modifiedData,
arrayOfOptionsGroupedByCategory
);
}, [actionsToDisplay, modifiedData, arrayOfOptionsGroupedByCategory]);
const [state, setState] = useState(initState);
const handleCategoryChange = ({ keys, value }) => {
setState(prevState => {
const updatedState = cloneDeep(prevState);
const objToUpdate = get(prevState, keys, {});
const updatedValues = updateValues(objToUpdate, value);
set(updatedState, keys, updatedValues);
return updatedState;
});
};
const handleChange = ({ keys, value }) => {
setState(prevState => {
const updatedState = cloneDeep(prevState);
set(updatedState, keys, value);
return updatedState;
});
};
return (
<Modal withoverflow="true" onClosed={onClosed} isOpen={isOpen} onToggle={onToggle}>
@ -18,20 +65,27 @@ const ConditionsModal = ({ headerBreadCrumbs, isOpen, onClosed, onToggle }) => {
})}
</Text>
<Separator />
{/* {actions.length === 0 && (
{actionsToDisplay.length === 0 && (
<Text fontSize="md" color="grey">
{formatMessage({ id: 'Settings.permissions.conditions.no-actions' })}
</Text>
)}
{actions.map((action, index) => (
{actionsToDisplay.map(({ actionId, label, pathToConditionsObject }, index) => {
const name = pathToConditionsObject.join('..');
return (
<ActionRow
key={action.id}
action={action}
key={actionId}
arrayOfOptionsGroupedByCategory={arrayOfOptionsGroupedByCategory}
label={label}
isGrey={index % 2 === 0}
value={conditions[action.id]}
onChange={val => handleSelectChange(action.id, val)}
name={name}
onCategoryChange={handleCategoryChange}
onChange={handleChange}
value={get(state, name, {})}
/>
))} */}
);
})}
</Padded>
<ModalFooter>
<section>
@ -51,6 +105,16 @@ const ConditionsModal = ({ headerBreadCrumbs, isOpen, onClosed, onToggle }) => {
};
ConditionsModal.propTypes = {
actions: PropTypes.arrayOf(
PropTypes.shape({
actionId: PropTypes.string.isRequired,
checkboxName: PropTypes.string,
hasSomeActionsSelected: PropTypes.bool.isRequired,
hasAllActionsSelected: PropTypes.bool,
isDisplayed: PropTypes.bool.isRequired,
label: PropTypes.string,
})
).isRequired,
headerBreadCrumbs: PropTypes.arrayOf(PropTypes.string).isRequired,
isOpen: PropTypes.bool.isRequired,
onClosed: PropTypes.func.isRequired,

View File

@ -0,0 +1,46 @@
import { get } from 'lodash';
const createConditionsForm = (conditions, valueObject) => {
return conditions.reduce((acc, current) => {
acc[current.id] = get(valueObject, current.id, true);
return acc;
}, {});
};
const createCategoryForm = (arrayOfOptions, valueObject) => {
return arrayOfOptions.reduce((acc, current) => {
const [categoryName, relatedConditions] = current;
const conditionsForm = createConditionsForm(relatedConditions, valueObject);
acc[categoryName] = conditionsForm;
return acc;
}, {});
};
const createDefaultConditionsForm = (
actionsToDisplay,
modifiedData,
arrayOfOptionsGroupedByCategory
) => {
return actionsToDisplay.reduce((acc, current) => {
const valueFromModifiedData = get(
modifiedData,
[...current.pathToConditionsObject, 'conditions'],
{}
);
const categoryDefaultForm = createCategoryForm(
arrayOfOptionsGroupedByCategory,
valueFromModifiedData
);
acc[current.pathToConditionsObject.join('..')] = categoryDefaultForm;
return acc;
}, {});
};
export default createDefaultConditionsForm;

View File

@ -0,0 +1,95 @@
/* eslint-disable indent */
/* eslint-disable no-nested-ternary */
const selectStyle = {
container: base => ({
...base,
width: '70%',
alignItems: 'center',
height: '36px',
}),
menu: base => ({
...base,
margin: '0',
paddingTop: 0,
borderRadius: '2px !important',
borderTopLeftRadius: '0 !important',
borderTopRightRadius: '0 !important',
border: '1px solid #78caff !important',
boxShadow: 0,
borderTop: '0 !important',
fontSize: '13px',
}),
menuList: base => ({
...base,
paddingBottom: 9,
paddingTop: 10,
}),
multiValue: base => ({
...base,
backgroundColor: 'none',
color: '#333740',
}),
multiValueLabel: base => ({
...base,
fontSize: '13px',
}),
multiValueRemove: base => ({
...base,
display: 'none',
}),
control: (base, state) => {
const borderRadiusStyle = state.selectProps.menuIsOpen
? {
borderBottomLeftRadius: '0 !important',
borderBottomRightRadius: '0 !important',
}
: {};
const {
selectProps: { error, value },
} = state;
let border;
let borderBottom;
if (state.isFocused) {
border = '1px solid #78caff !important';
} else if (error && !value.length) {
border = '1px solid #f64d0a !important';
} else {
border = '1px solid #e3e9f3 !important';
}
if (state.menuIsOpen === true) {
borderBottom = '1px solid #e3e9f3 !important';
}
return {
...base,
fontSize: 13,
minHeight: 34,
top: '1px',
border,
outline: 0,
boxShadow: 0,
borderRadius: '2px !important',
...borderRadiusStyle,
borderBottom,
};
},
valueContainer: base => ({
...base,
padding: '2px 4px 4px 10px',
lineHeight: '18px',
minWidth: 200,
}),
placeholder: base => ({
...base,
paddingTop: 1,
color: 'black',
}),
};
export default selectStyle;

View File

@ -108,6 +108,7 @@ const Collapse = ({ availableActions, isActive, isGrey, label, onClickToggle, pa
{modalState.isMounted && (
<ConditionsModal
headerBreadCrumbs={[label, 'app.components.LeftMenuLinkContainer.settings']}
actions={checkboxesActions}
isOpen={modalState.isOpen}
onClosed={handleModalClose}
onToggle={handleToggleModalIsOpen}

View File

@ -2,7 +2,7 @@ import { get, isEmpty } from 'lodash';
import { createArrayOfValues, getCheckboxState } from '../../../utils';
const generateCheckboxesActions = (availableActions, modifiedData, pathToData) => {
return availableActions.map(({ actionId, isDisplayed, applyToProperties }) => {
return availableActions.map(({ actionId, isDisplayed, applyToProperties, label }) => {
if (!isDisplayed) {
return { actionId, hasSomeActionsSelected: false, isDisplayed };
}
@ -29,6 +29,8 @@ const generateCheckboxesActions = (availableActions, modifiedData, pathToData) =
hasSomeActionsSelected: value,
isDisplayed,
isParentCheckbox: false,
label,
pathToConditionsObject: baseCheckboxNameArray,
};
}
@ -44,6 +46,8 @@ const generateCheckboxesActions = (availableActions, modifiedData, pathToData) =
hasSomeActionsSelected,
isDisplayed,
isParentCheckbox: true,
label,
pathToConditionsObject: baseCheckboxNameArray,
};
});
};

View File

@ -68,6 +68,7 @@ const Permissions = forwardRef(({ layout }, ref) => {
return (
<PermissionsDataManagerProvider
value={{
availableConditions: layout.conditions,
modifiedData,
onChangeSimpleCheckbox: handleChangeSimpleCheckbox,
onChangeParentCheckbox: handleChangeParentCheckbox,

View File

@ -16,6 +16,7 @@ const usePermissionsDataManager = () => useContext(PermissionsDataManagerContext
PermissionsDataManagerProvider.propTypes = {
children: PropTypes.node.isRequired,
value: PropTypes.exact({
availableConditions: PropTypes.array.isRequired,
modifiedData: PropTypes.object.isRequired,
onChangeCollectionTypeLeftActionRowCheckbox: PropTypes.func.isRequired,
onChangeSimpleCheckbox: PropTypes.func.isRequired,

View File

@ -424,6 +424,7 @@ const data = {
label: 'Publish',
actionId: 'content-manager.explorer.publish',
subjects: ['restaurant'],
applyToProperties: ['locales'],
},
],
},