Design relations for D&P

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2020-09-01 15:17:50 +02:00 committed by Pierre Noël
parent d3082807d1
commit 76c662d4e7
13 changed files with 364 additions and 196 deletions

View File

@ -1,21 +1,28 @@
import styled from 'styled-components'; import styled from 'styled-components';
const RelationDPState = styled.div` const RelationDPState = styled.div`
overflow: hidden; margin: auto;
text-overflow: ellipsis;
white-space: nowrap;
&:before { &:before {
content: ''; content: '';
display: flex; display: flex;
width: 6px; width: 6px;
height: 6px; height: 6px;
margin-bottom: 1px; margin-top: ${({ marginTop }) => marginTop};
margin-left: 10px; margin-left: ${({ marginLeft }) => marginLeft};
margin-bottom: ${({ marginBottom }) => marginBottom};
margin-right: ${({ marginRight }) => marginRight};
border-radius: 50%; border-radius: 50%;
background-color: ${({ theme, isDraft }) => background-color: ${({ theme, isDraft }) =>
isDraft ? theme.main.colors.mediumBlue : theme.main.colors.green}; isDraft ? theme.main.colors.mediumBlue : theme.main.colors.green};
} }
`; `;
RelationDPState.defaultProps = {
marginLeft: '10px',
marginRight: '0',
marginTop: '0',
marginBottom: '1px',
};
export default RelationDPState; export default RelationDPState;

View File

@ -92,6 +92,7 @@ const Li = styled.li`
> div { > div {
width: 90%; width: 90%;
> a { > a {
flex-grow: 2;
max-width: 100%; max-width: 100%;
color: rgb(35, 56, 77); color: rgb(35, 56, 77);
} }
@ -143,7 +144,7 @@ const Li = styled.li`
const Span = styled.span` const Span = styled.span`
display: block; display: block;
max-width: 100%; max-width: calc(100% - 10px);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

View File

@ -9,6 +9,7 @@ import ListItem from './ListItem';
function SelectMany({ function SelectMany({
addRelation, addRelation,
components,
mainField, mainField,
name, name,
hasDraftAndPublish, hasDraftAndPublish,
@ -57,6 +58,8 @@ function SelectMany({
return ( return (
<> <>
<Select <Select
components={components}
hasDraftAndPublish={hasDraftAndPublish}
isDisabled={isDisabled} isDisabled={isDisabled}
id={name} id={name}
filterOption={(candidate, input) => { filterOption={(candidate, input) => {
@ -116,12 +119,14 @@ function SelectMany({
} }
SelectMany.defaultProps = { SelectMany.defaultProps = {
components: {},
move: () => {}, move: () => {},
value: null, value: null,
}; };
SelectMany.propTypes = { SelectMany.propTypes = {
addRelation: PropTypes.func.isRequired, addRelation: PropTypes.func.isRequired,
components: PropTypes.object,
hasDraftAndPublish: PropTypes.bool.isRequired, hasDraftAndPublish: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,

View File

@ -0,0 +1,52 @@
import React from 'react';
import { components } from 'react-select';
import PropTypes from 'prop-types';
import { get, isEmpty } from 'lodash';
import { Flex, Padded, Text } from '@buffetjs/core';
import RelationDPState from '../RelationDPState';
const SingleValue = props => {
const Component = components.SingleValue;
const hasDraftAndPublish = props.selectProps.hasDraftAndPublish;
const isDraft = isEmpty(get(props, 'data.value.published_at'));
const value = props.selectProps.value.label;
if (hasDraftAndPublish) {
return (
<Component {...props}>
<Padded left size="sm" right>
<Flex>
<RelationDPState
marginLeft="0"
marginTop="1px"
marginRight="10px"
isDraft={isDraft}
marginBottom="0"
/>
<div>
<Text ellipsis>{value}</Text>
</div>
</Flex>
</Padded>
</Component>
);
}
return (
<Component {...props}>
<Padded left right size="sm">
{value}
</Padded>
</Component>
);
};
SingleValue.propTypes = {
data: PropTypes.object.isRequired,
selectProps: PropTypes.shape({
hasDraftAndPublish: PropTypes.bool,
value: PropTypes.object,
}).isRequired,
};
export default SingleValue;

View File

@ -1,12 +1,14 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { get, isNull } from 'lodash'; import { get, isNull } from 'lodash';
import Select from 'react-select'; import Select from 'react-select';
import SingleValue from './SingleValue';
function SelectOne({ function SelectOne({
components,
mainField, mainField,
name, name,
hasDraftAndPublish,
isDisabled, isDisabled,
isLoading, isLoading,
onChange, onChange,
@ -20,6 +22,8 @@ function SelectOne({
}) { }) {
return ( return (
<Select <Select
hasDraftAndPublish={hasDraftAndPublish}
components={{ ...components, SingleValue }}
id={name} id={name}
isClearable isClearable
isDisabled={isDisabled} isDisabled={isDisabled}
@ -37,10 +41,13 @@ function SelectOne({
} }
SelectOne.defaultProps = { SelectOne.defaultProps = {
components: {},
value: null, value: null,
}; };
SelectOne.propTypes = { SelectOne.propTypes = {
components: PropTypes.object,
hasDraftAndPublish: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired,
mainField: PropTypes.string.isRequired, mainField: PropTypes.string.isRequired,

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,52 @@
import React from 'react';
import styled from 'styled-components';
import { components } from 'react-select';
import PropTypes from 'prop-types';
import { get, isEmpty } from 'lodash';
import { Flex, Text } from '@buffetjs/core';
import RelationDPState from '../RelationDPState';
const TextGrow = styled(Text)`
flex-grow: 2;
`;
const Option = props => {
const Component = components.Option;
const hasDraftAndPublish = props.selectProps.hasDraftAndPublish;
const isDraft = isEmpty(get(props, 'data.value.published_at'));
if (hasDraftAndPublish) {
return (
<Component {...props}>
<Flex>
<RelationDPState
marginLeft="0"
marginTop="1px"
marginRight="10px"
isDraft={isDraft}
marginBottom="0"
/>
<TextGrow ellipsis as="div">
{props.label}
</TextGrow>
</Flex>
</Component>
);
}
return (
<Component {...props}>
<Text ellipsis>{props.label}</Text>
</Component>
);
};
Option.propTypes = {
label: PropTypes.string.isRequired,
selectProps: PropTypes.shape({
hasDraftAndPublish: PropTypes.bool,
}).isRequired,
};
export default Option;

View File

@ -1,132 +1,14 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Text } from '@buffetjs/core';
const Wrapper = styled.div` const BaselineAlignment = styled.div`
position: relative; padding-top: 1px;
margin-bottom: 27px; `;
label {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.3rem;
font-weight: 500;
}
nav + div { const A = styled(Text)`
height: 34px; &:hover {
background-color: white; text-decoration: underline;
margin-top: 5px;
> div {
min-height: 34px;
height: 100%;
border: 1px solid #e3e9f3;
border-radius: 3px;
box-shadow: 0 1px 1px 0 rgba(104, 118, 142, 0.05);
flex-wrap: initial;
padding: 0 10px;
/* Arrow */
&:before {
content: '\f0d7';
position: absolute;
top: 5px;
right: 10px;
font-family: 'FontAwesome';
font-size: 14px;
font-weight: 800;
color: #aaa;
}
> div {
padding: 0;
&:first-of-type {
/* Placeholder */
> div span {
color: #aaa;
}
}
}
div:last-of-type {
span {
display: none;
& + div {
display: none;
}
}
svg {
width: 15px;
margin-right: 6px;
}
}
span {
font-size: 13px;
line-height: 34px;
color: #333740;
}
:hover {
cursor: pointer;
border-color: #e3e9f3;
&:before {
color: #666;
}
}
}
span[aria-live='polite'] + div {
&:before {
transform: rotate(180deg);
top: 4px;
}
& + div {
z-index: 2;
height: fit-content;
padding: 0;
margin-top: -2px;
border-top-left-radius: 0;
border-top-right-radius: 0;
&:before {
content: '';
}
div {
width: 100%;
}
> div {
max-height: 200px;
height: fit-content;
div {
height: 36px;
cursor: pointer;
}
}
}
}
} }
`; `;
const Nav = styled.nav` export { A, BaselineAlignment };
> div {
display: flex;
justify-content: space-between;
a {
color: #007eff !important;
font-size: 1.3rem;
&:hover {
text-decoration: underline !important;
cursor: pointer;
}
}
}
.description {
color: #9ea7b8;
font-family: 'Lato';
font-size: 1.2rem;
margin-top: -5px;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
`;
export { Nav, Wrapper };

View File

@ -5,6 +5,7 @@ import { FormattedMessage } from 'react-intl';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { cloneDeep, findIndex, get, isArray, isEmpty, set, has } from 'lodash'; import { cloneDeep, findIndex, get, isArray, isEmpty, set, has } from 'lodash';
import { request } from 'strapi-helper-plugin'; import { request } from 'strapi-helper-plugin';
import { Flex, Text, Padded } from '@buffetjs/core';
import pluginId from '../../pluginId'; import pluginId from '../../pluginId';
import useDataManager from '../../hooks/useDataManager'; import useDataManager from '../../hooks/useDataManager';
import useEditView from '../../hooks/useEditView'; import useEditView from '../../hooks/useEditView';
@ -12,8 +13,12 @@ import { getFieldName } from '../../utils';
import NotAllowedInput from '../NotAllowedInput'; import NotAllowedInput from '../NotAllowedInput';
import SelectOne from '../SelectOne'; import SelectOne from '../SelectOne';
import SelectMany from '../SelectMany'; import SelectMany from '../SelectMany';
import { Nav, Wrapper } from './components'; import ClearIndicator from './ClearIndicator';
import { connect, select } from './utils'; import DropdownIndicator from './DropdownIndicator';
import IndicatorSeparator from './IndicatorSeparator';
import Option from './Option';
import { A, BaselineAlignment } from './components';
import { connect, select, styles } from './utils';
function SelectWrapper({ function SelectWrapper({
componentUid, componentUid,
@ -189,24 +194,14 @@ function SelectWrapper({
targetModel targetModel
) ? null : ( ) ? null : (
<Link to={{ pathname: to, state: { from: pathname } }}> <Link to={{ pathname: to, state: { from: pathname } }}>
<FormattedMessage id="content-manager.containers.Edit.seeDetails" /> <FormattedMessage id="content-manager.containers.Edit.seeDetails">
{msg => <A color="mediumBlue">{msg}</A>}
</FormattedMessage>
</Link> </Link>
); );
const Component = isSingle ? SelectOne : SelectMany; const Component = isSingle ? SelectOne : SelectMany;
const associationsLength = isArray(value) ? value.length : 0; const associationsLength = isArray(value) ? value.length : 0;
const customStyles = {
option: provided => {
return {
...provided,
maxWidth: '100% !important',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
},
};
const isDisabled = useMemo(() => { const isDisabled = useMemo(() => {
if (isMorph) { if (isMorph) {
return true; return true;
@ -228,54 +223,63 @@ function SelectWrapper({
} }
return ( return (
<Wrapper className="form-group"> <Padded>
<Nav> <BaselineAlignment />
<div> <Flex justifyContent="space-between">
<label htmlFor={name}> <Text fontWeight="semiBold">
{label} {label}
{!isSingle && ( {!isSingle && ` (${associationsLength})`}
<span style={{ fontWeight: 400, fontSize: 12 }}>&nbsp;({associationsLength})</span> </Text>
)} {isSingle && link}
</label> </Flex>
{isSingle && link} {!isEmpty(description) && (
</div> <Padded top size="xs">
{!isEmpty(description) && <p className="description">{description}</p>} <BaselineAlignment />
</Nav> <Text fontSize="sm" color="grey" lineHeight="12px" ellipsis>
<Component {description}
addRelation={value => { </Text>
addRelation({ target: { name, value } }); </Padded>
}} )}
hasDraftAndPublish={hasDraftAndPublish} <Padded top size="sm">
id={name} <BaselineAlignment />
isDisabled={isDisabled}
isLoading={isLoading} <Component
isClearable addRelation={value => {
mainField={mainField} addRelation({ target: { name, value } });
move={moveRelation} }}
name={name} components={{ ClearIndicator, DropdownIndicator, IndicatorSeparator, Option }}
options={filteredOptions} hasDraftAndPublish={hasDraftAndPublish}
onChange={value => { id={name}
onChange({ target: { name, value: value ? value.value : value } }); isDisabled={isDisabled}
}} isLoading={isLoading}
onInputChange={onInputChange} isClearable
onMenuClose={() => { mainField={mainField}
setState(prevState => ({ ...prevState, _contains: '' })); move={moveRelation}
}} name={name}
onMenuScrollToBottom={onMenuScrollToBottom} options={filteredOptions}
onRemove={onRemoveRelation} onChange={value => {
placeholder={ onChange({ target: { name, value: value ? value.value : value } });
isEmpty(placeholder) ? ( }}
<FormattedMessage id={`${pluginId}.containers.Edit.addAnItem`} /> onInputChange={onInputChange}
) : ( onMenuClose={() => {
placeholder setState(prevState => ({ ...prevState, _contains: '' }));
) }}
} onMenuScrollToBottom={onMenuScrollToBottom}
styles={customStyles} onRemove={onRemoveRelation}
targetModel={targetModel} placeholder={
value={value} isEmpty(placeholder) ? (
/> <FormattedMessage id={`${pluginId}.containers.Edit.addAnItem`} />
<div style={{ marginBottom: 18 }} /> ) : (
</Wrapper> placeholder
)
}
styles={styles}
targetModel={targetModel}
value={value}
/>
</Padded>
<div style={{ marginBottom: 28 }} />
</Padded>
); );
} }

View File

@ -1,2 +1,3 @@
export { default as connect } from './connect'; export { default as connect } from './connect';
export { default as select } from './select'; export { default as select } from './select';
export { default as styles } from './styles';

View File

@ -0,0 +1,100 @@
/* eslint-disable indent */
/* eslint-disable no-nested-ternary */
const styles = {
container: base => ({ ...base, background: '#ffffff' }),
control: (base, state) => {
const borderRadiusStyle = state.selectProps.menuIsOpen
? {
borderBottomLeftRadius: '0 !important',
borderBottomRightRadius: '0 !important',
}
: {};
const {
selectProps: { error, value },
} = state;
let border;
let borderBottom;
let backgroundColor;
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';
}
if (state.isDisabled) {
backgroundColor = '#fafafb !important';
}
return {
...base,
fontSize: 13,
height: 34,
minHeight: 34,
border,
outline: 0,
boxShadow: 0,
borderRadius: '2px !important',
...borderRadiusStyle,
borderBottom,
backgroundColor,
};
},
input: base => ({ ...base, marginLeft: 10 }),
menu: base => {
return {
...base,
width: '100%',
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,
maxHeight: '112px',
paddingTop: 2,
}),
option: (base, state) => {
return {
...base,
height: 36,
backgroundColor: state.isSelected ? '#fff' : base.backgroundColor,
color: state.isSelected ? '#007eff' : '#333740',
fontWeight: state.isSelected ? '600' : '400',
cursor: 'pointer',
};
},
placeholder: base => ({
...base,
marginTop: 0,
marginLeft: 10,
color: '#aaa',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
maxWidth: 'calc(100% - 32px)',
}),
valueContainer: base => ({
...base,
padding: '2px 0px 4px 0px',
lineHeight: '18px',
}),
};
export default styles;