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

View File

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

View File

@ -9,6 +9,7 @@ import ListItem from './ListItem';
function SelectMany({
addRelation,
components,
mainField,
name,
hasDraftAndPublish,
@ -57,6 +58,8 @@ function SelectMany({
return (
<>
<Select
components={components}
hasDraftAndPublish={hasDraftAndPublish}
isDisabled={isDisabled}
id={name}
filterOption={(candidate, input) => {
@ -116,12 +119,14 @@ function SelectMany({
}
SelectMany.defaultProps = {
components: {},
move: () => {},
value: null,
};
SelectMany.propTypes = {
addRelation: PropTypes.func.isRequired,
components: PropTypes.object,
hasDraftAndPublish: PropTypes.bool.isRequired,
isDisabled: 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 PropTypes from 'prop-types';
import { get, isNull } from 'lodash';
import Select from 'react-select';
import SingleValue from './SingleValue';
function SelectOne({
components,
mainField,
name,
hasDraftAndPublish,
isDisabled,
isLoading,
onChange,
@ -20,6 +22,8 @@ function SelectOne({
}) {
return (
<Select
hasDraftAndPublish={hasDraftAndPublish}
components={{ ...components, SingleValue }}
id={name}
isClearable
isDisabled={isDisabled}
@ -37,10 +41,13 @@ function SelectOne({
}
SelectOne.defaultProps = {
components: {},
value: null,
};
SelectOne.propTypes = {
components: PropTypes.object,
hasDraftAndPublish: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.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 { Text } from '@buffetjs/core';
const Wrapper = styled.div`
position: relative;
margin-bottom: 27px;
label {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.3rem;
font-weight: 500;
}
const BaselineAlignment = styled.div`
padding-top: 1px;
`;
nav + div {
height: 34px;
background-color: white;
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 A = styled(Text)`
&:hover {
text-decoration: underline;
}
`;
const Nav = styled.nav`
> 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 };
export { A, BaselineAlignment };

View File

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