From cb8add5c301b80bdbb80d6cc2396ad9a0ebea2a3 Mon Sep 17 00:00:00 2001 From: olav Date: Tue, 19 Apr 2022 15:20:01 +0200 Subject: [PATCH] feat: add context value descriptions (#874) * feat: add context value descriptions * refcator: use ConditionallyRender for ...conditional render * refactor: fix context form enter behaviour * refactor: decrease margin between inputs * refactor: show error on missing value * refactor: disable add button on error * refactor: avoid clearing value error on name focus --- .../LegalValueLabel/LegalValueLabel.styles.ts | 16 ++ .../LegalValueLabel/LegalValueLabel.tsx | 39 +++++ .../ResolveInput/ResolveInput.tsx | 3 +- .../RestrictiveLegalValues.tsx | 72 +++------ .../SingleLegalValue/SingleLegalValue.tsx | 60 ++------ .../ContectFormChip/ContextFormChip.styles.ts | 36 +++++ .../ContectFormChip/ContextFormChip.tsx | 34 +++++ .../ContextFormChipList.styles.ts | 13 ++ .../ContectFormChip/ContextFormChipList.tsx | 8 + .../context/ContextForm/ContextForm.styles.ts | 13 +- .../context/ContextForm/ContextForm.tsx | 143 +++++++++++------- .../component/context/hooks/useContextForm.ts | 27 ++-- frontend/src/interfaces/context.ts | 7 +- 13 files changed, 302 insertions(+), 169 deletions(-) create mode 100644 frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.styles.ts create mode 100644 frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.tsx create mode 100644 frontend/src/component/context/ContectFormChip/ContextFormChip.styles.ts create mode 100644 frontend/src/component/context/ContectFormChip/ContextFormChip.tsx create mode 100644 frontend/src/component/context/ContectFormChip/ContextFormChipList.styles.ts create mode 100644 frontend/src/component/context/ContectFormChip/ContextFormChipList.tsx diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.styles.ts b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.styles.ts new file mode 100644 index 0000000000..01a2281332 --- /dev/null +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.styles.ts @@ -0,0 +1,16 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + container: { + display: 'inline-block', + }, + value: { + lineHeight: 1.33, + fontSize: theme.fontSizes.smallBody, + }, + description: { + lineHeight: 1.33, + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.grey[700], + }, +})); diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.tsx new file mode 100644 index 0000000000..258ea59394 --- /dev/null +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel.tsx @@ -0,0 +1,39 @@ +import { ILegalValue } from 'interfaces/context'; +import { useStyles } from './LegalValueLabel.styles'; +import React from 'react'; +import { FormControlLabel } from '@material-ui/core'; + +interface ILegalValueTextProps { + legal: ILegalValue; + control: React.ReactElement; +} + +export const LegalValueLabel = ({ legal, control }: ILegalValueTextProps) => { + const styles = useStyles(); + + return ( +
+ +
{legal.value}
+
+ {legal.description} +
+ + } + /> +
+ ); +}; + +export const filterLegalValues = ( + legalValues: ILegalValue[], + filter: string +): ILegalValue[] => { + return legalValues.filter(legalValue => { + return legalValue.value.includes(filter); + }); +}; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx index 2e8d33c0b7..813ff1a112 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx @@ -18,6 +18,7 @@ import { IN_OPERATORS_FREETEXT, Input, } from '../useConstraintInput/useConstraintInput'; +import React from 'react'; interface IResolveInputProps { contextDefinition: IUnleashContextDefinition; @@ -81,7 +82,7 @@ export const ResolveInput = ({ type="number" legalValues={ contextDefinition.legalValues?.filter( - (value: string) => Number(value) + legalValue => Number(legalValue.value) ) || [] } error={error} diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx index 3e67efe2e2..398c644596 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx @@ -1,13 +1,17 @@ -import { Checkbox, FormControlLabel } from '@material-ui/core'; +import { Checkbox } from '@material-ui/core'; import { useCommonStyles } from 'themes/commonStyles'; import ConditionallyRender from 'component/common/ConditionallyRender'; -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch'; import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader'; +import { ILegalValue } from 'interfaces/context'; +import { + LegalValueLabel, + filterLegalValues, +} from '../LegalValueLabel/LegalValueLabel'; -// Parent component interface IRestrictiveLegalValuesProps { - legalValues: string[]; + legalValues: ILegalValue[]; values: string[]; setValues: (values: string[]) => void; beforeValues?: JSX.Element; @@ -36,6 +40,8 @@ export const RestrictiveLegalValues = ({ setError, }: IRestrictiveLegalValuesProps) => { const [filter, setFilter] = useState(''); + const filteredValues = filterLegalValues(legalValues, filter); + // Lazily initialise the values because there might be a lot of them. const [valuesMap, setValuesMap] = useState(() => createValuesMap(values)); const styles = useCommonStyles(); @@ -63,12 +69,20 @@ export const RestrictiveLegalValues = ({ Select values from a predefined set - + {filteredValues.map(match => ( + onChange(match.value)} + name={match.value} + color="primary" + /> + } + /> + ))} {error}

} @@ -76,41 +90,3 @@ export const RestrictiveLegalValues = ({ ); }; - -// Child component -interface ILegalValueOptionsProps { - legalValues: string[]; - filter: string; - onChange: (legalValue: string) => void; - valuesMap: IValuesMap; -} - -const LegalValueOptions = ({ - legalValues, - filter, - onChange, - valuesMap, -}: ILegalValueOptionsProps) => { - return ( - <> - {legalValues - .filter(legalValue => legalValue.includes(filter)) - .map(legalValue => { - return ( - onChange(legalValue)} - color="primary" - name={legalValue} - /> - } - label={legalValue} - /> - ); - })} - - ); -}; diff --git a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue.tsx b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue.tsx index 9b769eefd5..2191b52bf6 100644 --- a/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue.tsx +++ b/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue.tsx @@ -1,23 +1,20 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader'; -import { - FormControl, - FormLabel, - FormControlLabel, - RadioGroup, - Radio, -} from '@material-ui/core'; +import { FormControl, RadioGroup, Radio } from '@material-ui/core'; import { ConstraintValueSearch } from 'component/common/ConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch'; import ConditionallyRender from 'component/common/ConditionallyRender'; import { useCommonStyles } from 'themes/commonStyles'; - -// Parent component +import { ILegalValue } from 'interfaces/context'; +import { + LegalValueLabel, + filterLegalValues, +} from '../LegalValueLabel/LegalValueLabel'; interface ISingleLegalValueProps { setValue: (value: string) => void; value?: string; type: string; - legalValues: string[]; + legalValues: ILegalValue[]; error: string; setError: React.Dispatch>; } @@ -32,21 +29,18 @@ export const SingleLegalValue = ({ }: ISingleLegalValueProps) => { const [filter, setFilter] = useState(''); const styles = useCommonStyles(); + const filteredValues = filterLegalValues(legalValues, filter); return ( <> Add a single {type.toLowerCase()} value - - - Available values - - + {filteredValues.map(match => ( + } + /> + ))} } @@ -74,28 +71,3 @@ export const SingleLegalValue = ({ ); }; - -// Child components -interface IRadioOptionsProps { - legalValues: string[]; - filter: string; -} - -const RadioOptions = ({ legalValues, filter }: IRadioOptionsProps) => { - return ( - <> - {legalValues - .filter(legalValue => legalValue.includes(filter)) - .map((value, index) => { - return ( - } - label={value} - /> - ); - })} - - ); -}; diff --git a/frontend/src/component/context/ContectFormChip/ContextFormChip.styles.ts b/frontend/src/component/context/ContectFormChip/ContextFormChip.styles.ts new file mode 100644 index 0000000000..b3bad0653a --- /dev/null +++ b/frontend/src/component/context/ContectFormChip/ContextFormChip.styles.ts @@ -0,0 +1,36 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + container: { + display: 'grid', + lineHeight: 1.25, + gridTemplateColumns: '1fr auto', + alignSelf: 'start', + alignItems: 'start', + gap: '0.5rem', + padding: '0.5rem', + background: theme.palette.grey[200], + borderRadius: theme.borders.radius.main, + }, + label: { + fontSize: theme.fontSizes.smallBody, + }, + description: { + fontSize: theme.fontSizes.smallerBody, + color: theme.palette.grey[700], + }, + button: { + all: 'unset', + lineHeight: 0.1, + paddingTop: 1, + display: 'block', + cursor: 'pointer', + '& svg': { + fontSize: '1rem', + opacity: 0.5, + }, + '&:hover svg, &:focus-visible svg': { + opacity: 0.75, + }, + }, +})); diff --git a/frontend/src/component/context/ContectFormChip/ContextFormChip.tsx b/frontend/src/component/context/ContectFormChip/ContextFormChip.tsx new file mode 100644 index 0000000000..d71d2530dc --- /dev/null +++ b/frontend/src/component/context/ContectFormChip/ContextFormChip.tsx @@ -0,0 +1,34 @@ +import { useStyles } from 'component/context/ContectFormChip/ContextFormChip.styles'; +import { Cancel } from '@material-ui/icons'; +import ConditionallyRender from 'component/common/ConditionallyRender'; + +interface IContextFormChipProps { + label: string; + description?: string; + onRemove: () => void; +} + +export const ContextFormChip = ({ + label, + description, + onRemove, +}: IContextFormChipProps) => { + const styles = useStyles(); + + return ( +
  • +
    +
    {label}
    + ( +
    {description}
    + )} + /> +
    + +
  • + ); +}; diff --git a/frontend/src/component/context/ContectFormChip/ContextFormChipList.styles.ts b/frontend/src/component/context/ContectFormChip/ContextFormChipList.styles.ts new file mode 100644 index 0000000000..afba30d196 --- /dev/null +++ b/frontend/src/component/context/ContectFormChip/ContextFormChipList.styles.ts @@ -0,0 +1,13 @@ +import { makeStyles } from '@material-ui/core/styles'; + +export const useStyles = makeStyles(theme => ({ + container: { + listStyleType: 'none', + display: 'flex', + flexWrap: 'wrap', + gap: '0.5rem', + padding: 0, + margin: 0, + marginBottom: '1rem !important', + }, +})); diff --git a/frontend/src/component/context/ContectFormChip/ContextFormChipList.tsx b/frontend/src/component/context/ContectFormChip/ContextFormChipList.tsx new file mode 100644 index 0000000000..2a310f2cce --- /dev/null +++ b/frontend/src/component/context/ContectFormChip/ContextFormChipList.tsx @@ -0,0 +1,8 @@ +import { useStyles } from 'component/context/ContectFormChip/ContextFormChipList.styles'; +import React from 'react'; + +export const ContextFormChipList: React.FC = ({ children }) => { + const styles = useStyles(); + + return
      {children}
    ; +}; diff --git a/frontend/src/component/context/ContextForm/ContextForm.styles.ts b/frontend/src/component/context/ContextForm/ContextForm.styles.ts index e22d298ddf..eafb8b5f87 100644 --- a/frontend/src/component/context/ContextForm/ContextForm.styles.ts +++ b/frontend/src/component/context/ContextForm/ContextForm.styles.ts @@ -20,17 +20,16 @@ export const useStyles = makeStyles(theme => ({ }, }, tagContainer: { - display: 'flex', - alignItems: 'flex-start', + display: 'grid', + gridTemplateColumns: '1fr auto', + gap: '0.5rem', marginBottom: '1rem', }, tagInput: { - width: '75%', - marginRight: 'auto', + gridColumn: 1, }, - tagValue: { - marginRight: '3px', - marginBottom: '1rem', + tagButton: { + gridColumn: 2, }, buttonContainer: { marginTop: 'auto', diff --git a/frontend/src/component/context/ContextForm/ContextForm.tsx b/frontend/src/component/context/ContextForm/ContextForm.tsx index 1fe9385619..0bb41baeb5 100644 --- a/frontend/src/component/context/ContextForm/ContextForm.tsx +++ b/frontend/src/component/context/ContextForm/ContextForm.tsx @@ -1,24 +1,26 @@ import Input from 'component/common/Input/Input'; -import { TextField, Button, Switch, Chip, Typography } from '@material-ui/core'; +import { TextField, Button, Switch, Typography } from '@material-ui/core'; import { useStyles } from './ContextForm.styles'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Add } from '@material-ui/icons'; -import { trim } from 'component/common/util'; +import { ILegalValue } from 'interfaces/context'; +import { ContextFormChip } from 'component/context/ContectFormChip/ContextFormChip'; +import { ContextFormChipList } from 'component/context/ContectFormChip/ContextFormChipList'; interface IContextForm { contextName: string; contextDesc: string; - legalValues: Array; + legalValues: ILegalValue[]; stickiness: boolean; setContextName: React.Dispatch>; setContextDesc: React.Dispatch>; setStickiness: React.Dispatch>; - setLegalValues: React.Dispatch>; + setLegalValues: React.Dispatch>; handleSubmit: (e: any) => void; onCancel: () => void; errors: { [key: string]: string }; mode: 'Create' | 'Edit'; - clearErrors: () => void; + clearErrors: (key?: string) => void; validateContext?: () => void; setErrors: React.Dispatch>; } @@ -45,54 +47,64 @@ export const ContextForm: React.FC = ({ }) => { const styles = useStyles(); const [value, setValue] = useState(''); - const [focused, setFocused] = useState(false); + const [valueDesc, setValueDesc] = useState(''); + const [valueFocused, setValueFocused] = useState(false); - const submit = (event: React.SyntheticEvent) => { + const isMissingValue = valueDesc.trim() && !value.trim(); + + const isDuplicateValue = legalValues.some(legalValue => { + return legalValue.value.trim() === value.trim(); + }); + + useEffect(() => { + setErrors(prev => ({ + ...prev, + tag: isMissingValue + ? 'Value cannot be empty' + : isDuplicateValue + ? 'Duplicate value' + : undefined, + })); + }, [setErrors, isMissingValue, isDuplicateValue]); + + const onSubmit = (event: React.SyntheticEvent) => { event.preventDefault(); - if (focused) return; handleSubmit(event); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === ENTER && focused) { - addLegalValue(); - return; - } else if (event.key === ENTER) { - handleSubmit(event); + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === ENTER) { + event.preventDefault(); + if (valueFocused) { + addLegalValue(); + } else { + handleSubmit(event); + } } }; - const sortIgnoreCase = (a: string, b: string) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a === b) return 0; - if (a > b) return 1; - return -1; + const sortLegalValues = (a: ILegalValue, b: ILegalValue) => { + return a.value.toLowerCase().localeCompare(b.value.toLowerCase()); }; const addLegalValue = () => { - clearErrors(); - if (!value) { - return; + const next: ILegalValue = { + value: value.trim(), + description: valueDesc.trim(), + }; + if (next.value && !isDuplicateValue) { + setValue(''); + setValueDesc(''); + setLegalValues(prev => [...prev, next].sort(sortLegalValues)); } - - if (legalValues.indexOf(value) !== -1) { - setErrors(prev => ({ - ...prev, - tag: 'Duplicate legal value', - })); - return; - } - setLegalValues(prev => [...prev, trim(value)].sort(sortIgnoreCase)); - setValue(''); }; - const removeLegalValue = (index: number) => { - const filteredValues = legalValues.filter((_, i) => i !== index); - setLegalValues([...filteredValues]); + + const removeLegalValue = (value: ILegalValue) => { + setLegalValues(prev => prev.filter(p => p.value !== value.value)); }; return ( -
    +

    What is your context name? @@ -102,10 +114,10 @@ export const ContextForm: React.FC = ({ label="Context name" value={contextName} disabled={mode === 'Edit'} - onChange={e => setContextName(trim(e.target.value))} + onChange={e => setContextName(e.target.value.trim())} error={Boolean(errors.name)} errorText={errors.name} - onFocus={() => clearErrors()} + onFocus={() => clearErrors('name')} onBlur={validateContext} autoFocus /> @@ -119,25 +131,15 @@ export const ContextForm: React.FC = ({ multiline maxRows={4} value={contextDesc} + size="small" onChange={e => setContextDesc(e.target.value)} />

    Which values do you want to allow?

    - {legalValues.map((value, index) => { - return ( - removeLegalValue(index)} - title="Remove value" - /> - ); - })}
    = ({ helperText={errors.tag} variant="outlined" size="small" - onChange={e => setValue(trim(e.target.value))} - onKeyPress={e => handleKeyDown(e)} - onBlur={e => setFocused(false)} - onFocus={e => setFocused(true)} + onChange={e => setValue(e.target.value)} + onKeyPress={e => onKeyDown(e)} + onBlur={() => setValueFocused(false)} + onFocus={() => setValueFocused(true)} + inputProps={{ maxLength: 100 }} + /> + setValueDesc(e.target.value)} + onKeyPress={e => onKeyDown(e)} + onBlur={() => setValueFocused(false)} + onFocus={() => setValueFocused(true)} + inputProps={{ maxLength: 100 }} />
    + + {legalValues.map(legalValue => { + return ( + removeLegalValue(legalValue)} + /> + ); + })} +

    Custom stickiness

    By enabling stickiness on this context field you can use it diff --git a/frontend/src/component/context/hooks/useContextForm.ts b/frontend/src/component/context/hooks/useContextForm.ts index a146a07e59..e29c551f10 100644 --- a/frontend/src/component/context/hooks/useContextForm.ts +++ b/frontend/src/component/context/hooks/useContextForm.ts @@ -1,26 +1,27 @@ import { useEffect, useState } from 'react'; import useContextsApi from 'hooks/api/actions/useContextsApi/useContextsApi'; +import { ILegalValue } from 'interfaces/context'; export const useContextForm = ( - initialcontextName = '', - initialcontextDesc = '', - initialLegalValues = [] as string[], + initialContextName = '', + initialContextDesc = '', + initialLegalValues = [] as ILegalValue[], initialStickiness = false ) => { - const [contextName, setContextName] = useState(initialcontextName); - const [contextDesc, setContextDesc] = useState(initialcontextDesc); + const [contextName, setContextName] = useState(initialContextName); + const [contextDesc, setContextDesc] = useState(initialContextDesc); const [legalValues, setLegalValues] = useState(initialLegalValues); const [stickiness, setStickiness] = useState(initialStickiness); const [errors, setErrors] = useState({}); const { validateContextName } = useContextsApi(); useEffect(() => { - setContextName(initialcontextName); - }, [initialcontextName]); + setContextName(initialContextName); + }, [initialContextName]); useEffect(() => { - setContextDesc(initialcontextDesc); - }, [initialcontextDesc]); + setContextDesc(initialContextDesc); + }, [initialContextDesc]); useEffect(() => { setLegalValues(initialLegalValues); @@ -66,8 +67,12 @@ export const useContextForm = ( } }; - const clearErrors = () => { - setErrors({}); + const clearErrors = (key?: string) => { + if (key) { + setErrors(prev => ({ ...prev, [key]: undefined })); + } else { + setErrors({}); + } }; return { diff --git a/frontend/src/interfaces/context.ts b/frontend/src/interfaces/context.ts index fdc8802a73..7b76312029 100644 --- a/frontend/src/interfaces/context.ts +++ b/frontend/src/interfaces/context.ts @@ -4,5 +4,10 @@ export interface IUnleashContextDefinition { createdAt: string; sortOrder: number; stickiness: boolean; - legalValues?: string[]; + legalValues?: ILegalValue[]; +} + +export interface ILegalValue { + value: string; + description?: string; }