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 ;
+};
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 (
-