From 3bb54c5a9dd81d52cd730959b532341cf7e4a9d4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 24 Apr 2025 12:17:05 +0200 Subject: [PATCH] feat: 1-3652/legal value selector visual update (#9829) Handles the visual changes for the legal value selector widget. Before: image After: image I'm still working on improving the functionality of selecting from the search input and not losing focus when you select/deselect an item (both of these work (mostly) as expected on hosted, so we've introduced a regression somewhere). --- .../ResolveInput/ResolveInput.tsx | 42 ++-- .../RestrictiveLegalValues.tsx | 4 + .../MultipleValues/MultipleValues.tsx | 27 ++- .../ConstraintValueSearch.tsx | 4 + .../ConstraintValueSearch.tsx | 47 +++++ .../EditableConstraint.tsx | 27 +-- .../LegalValuesSelector.tsx | 199 ++++++++++++++++++ 7 files changed, 314 insertions(+), 36 deletions(-) create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintValueSearch.tsx create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx diff --git a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx index f8c6250719..a5b98af816 100644 --- a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx +++ b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx @@ -21,6 +21,8 @@ import { type Input, } from '../useConstraintInput/useConstraintInput'; import type React from 'react'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { LegalValuesSelector } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector'; interface IResolveInputProps { contextDefinition: Pick; @@ -75,25 +77,35 @@ export const ResolveInput = ({ removeValue, error, }: IResolveInputProps) => { + const useNewLegalValueInput = useUiFlag('addEditStrategy'); const resolveInput = () => { switch (input) { case IN_OPERATORS_LEGAL_VALUES: case STRING_OPERATORS_LEGAL_VALUES: - return ( - <> - - + return useNewLegalValueInput ? ( + + ) : ( + ); case NUM_OPERATORS_LEGAL_VALUES: return ( diff --git a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx index 490fe27efa..a67ec83795 100644 --- a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx +++ b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues.tsx @@ -82,6 +82,10 @@ const ErrorText = styled('p')(({ theme }) => ({ color: theme.palette.error.main, })); +/** + * @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx` + * Remove with flag `addEditStrategy` + */ export const RestrictiveLegalValues = ({ data, values, diff --git a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx index dd13705ae6..909cd7f0fe 100644 --- a/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx +++ b/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewBody/MultipleValues/MultipleValues.tsx @@ -2,7 +2,9 @@ import { useState } from 'react'; import { Chip, styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; -import { ConstraintValueSearch } from '../../../ConstraintValueSearch/ConstraintValueSearch'; +import { ConstraintValueSearch as NewConstraintValueSearch } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintValueSearch'; +import { useUiFlag } from 'hooks/useUiFlag'; +import { ConstraintValueSearch } from 'component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch'; interface IMultipleValuesProps { values: string[] | undefined; @@ -16,8 +18,14 @@ const StyledChip = styled(Chip)(({ theme }) => ({ margin: theme.spacing(0, 1, 1, 0), })); +const SearchWrapper = styled('div')(({ theme }) => ({ + width: '300px', + marginBottom: theme.spacing(2), +})); + export const MultipleValues = ({ values }: IMultipleValuesProps) => { const [filter, setFilter] = useState(''); + const useNewSearchComponent = useUiFlag('addEditStrategy'); if (!values || values.length === 0) return null; @@ -26,10 +34,19 @@ export const MultipleValues = ({ values }: IMultipleValuesProps) => { 20} show={ - + useNewSearchComponent ? ( + + + + ) : ( + + ) } /> {values diff --git a/frontend/src/component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch.tsx b/frontend/src/component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch.tsx index 85c0d12702..9b5f486ecd 100644 --- a/frontend/src/component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch.tsx +++ b/frontend/src/component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch.tsx @@ -7,6 +7,10 @@ interface IConstraintValueSearchProps { setFilter: React.Dispatch>; } +/** + * @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx` + * Remove with flag `addEditStrategy` + */ export const ConstraintValueSearch = ({ filter, setFilter, diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintValueSearch.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintValueSearch.tsx new file mode 100644 index 0000000000..2b1db9924e --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintValueSearch.tsx @@ -0,0 +1,47 @@ +import { TextField, InputAdornment } from '@mui/material'; +import Search from '@mui/icons-material/Search'; +import { useId } from 'react'; +import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly'; + +interface IConstraintValueSearchProps { + filter: string; + setFilter: React.Dispatch>; + onKeyDown?: (event: React.KeyboardEvent) => void; +} + +export const ConstraintValueSearch = ({ + filter, + setFilter, + onKeyDown, +}: IConstraintValueSearchProps) => { + const inputId = useId(); + return ( +
+ + + + setFilter(e.target.value)} + placeholder='Filter values' + onKeyDown={onKeyDown} + sx={{ + width: '100%', + }} + variant='outlined' + size='small' + InputProps={{ + startAdornment: ( + + + + ), + }} + /> +
+ ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx index 6e6c1daf12..c69ca5f1dd 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx @@ -2,7 +2,6 @@ import { IconButton, styled } from '@mui/material'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import { DateSingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue'; import { FreeTextInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/FreeTextInput/FreeTextInput'; -import { RestrictiveLegalValues } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/RestrictiveLegalValues/RestrictiveLegalValues'; import { SingleLegalValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue'; import { SingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleValue/SingleValue'; import { @@ -44,6 +43,7 @@ import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive import { ReactComponent as CaseInsensitiveIcon } from 'assets/icons/case-insensitive.svg'; import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly'; import { AddValuesWidget } from './AddValuesWidget'; +import { LegalValuesSelector } from './LegalValuesSelector'; const Container = styled('article')(({ theme }) => ({ '--padding': theme.spacing(2), @@ -97,7 +97,6 @@ const ConstraintDetails = styled('div')(({ theme }) => ({ const InputContainer = styled('div')(({ theme }) => ({ padding: 'var(--padding)', - paddingTop: 0, })); const StyledSelect = styled(GeneralSelect)(({ theme }) => ({ @@ -261,20 +260,16 @@ export const EditableConstraint: FC = ({ case IN_OPERATORS_LEGAL_VALUES: case STRING_OPERATORS_LEGAL_VALUES: return ( - <> - - + ); case NUM_OPERATORS_LEGAL_VALUES: return ( diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx new file mode 100644 index 0000000000..8170595484 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx @@ -0,0 +1,199 @@ +import { useEffect, useState } from 'react'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Alert, Button, Checkbox, styled } from '@mui/material'; +import type { ILegalValue } from 'interfaces/context'; +import { + filterLegalValues, + LegalValueLabel, +} from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel'; +import { ConstraintValueSearch } from './ConstraintValueSearch'; + +interface IRestrictiveLegalValuesProps { + data: { + legalValues: ILegalValue[]; + deletedLegalValues: ILegalValue[]; + }; + constraintValues: string[]; + values: string[]; + setValues: (values: string[]) => void; + setValuesWithRecord: (values: string[]) => void; + beforeValues?: JSX.Element; +} + +interface IValuesMap { + [key: string]: boolean; +} + +const createValuesMap = (values: string[]): IValuesMap => { + return values.reduce((result: IValuesMap, currentValue: string) => { + if (!result[currentValue]) { + result[currentValue] = true; + } + return result; + }, {}); +}; + +export const getLegalValueSet = (values: ILegalValue[]) => { + return new Set(values.map(({ value }) => value)); +}; + +export const getIllegalValues = ( + constraintValues: string[], + deletedLegalValues: ILegalValue[], +) => { + const deletedValuesSet = getLegalValueSet(deletedLegalValues); + + return constraintValues.filter( + (value) => value && deletedValuesSet.has(value), + ); +}; + +const StyledValuesContainer = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', + gap: theme.spacing(1), + maxHeight: '378px', + overflow: 'auto', +})); + +const Row = styled('div')(({ theme }) => ({ + display: 'flex', + flexFlow: 'row wrap', + alignItems: 'center', + gap: theme.spacing(1), +})); + +const LegalValuesSelectorWidget = styled('article')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), +})); + +export const LegalValuesSelector = ({ + data, + values, + setValues, + setValuesWithRecord, + constraintValues, +}: IRestrictiveLegalValuesProps) => { + const [filter, setFilter] = useState(''); + const { legalValues, deletedLegalValues } = data; + + const filteredValues = filterLegalValues(legalValues, filter); + + // Lazily initialise the values because there might be a lot of them. + const [valuesMap, setValuesMap] = useState(() => createValuesMap(values)); + + const cleanDeletedLegalValues = (constraintValues: string[]): string[] => { + const deletedValuesSet = getLegalValueSet(deletedLegalValues); + return ( + constraintValues?.filter((value) => !deletedValuesSet.has(value)) || + [] + ); + }; + + const illegalValues = getIllegalValues( + constraintValues, + deletedLegalValues, + ); + + useEffect(() => { + setValuesMap(createValuesMap(values)); + }, [values, setValuesMap, createValuesMap]); + + useEffect(() => { + if (illegalValues.length > 0) { + setValues(cleanDeletedLegalValues(values)); + } + }, []); + + const onChange = (legalValue: string) => { + if (valuesMap[legalValue]) { + const index = values.findIndex((value) => value === legalValue); + const newValues = [...values]; + newValues.splice(index, 1); + setValuesWithRecord(newValues); + return; + } + + setValuesWithRecord([...cleanDeletedLegalValues(values), legalValue]); + }; + + const isAllSelected = legalValues.every((value) => + values.includes(value.value), + ); + + const onSelectAll = () => { + if (isAllSelected) { + return setValuesWithRecord([]); + } + setValuesWithRecord([ + ...legalValues.map((legalValue) => legalValue.value), + ]); + }; + + const handleSearchKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && filteredValues.length > 0) { + const firstValue = filteredValues[0].value; + onChange(firstValue); + } + }; + + return ( + + 0)} + show={ + + This constraint is using legal values that have been + deleted as valid options. If you save changes on this + constraint and then save the strategy the following + values will be removed: +
    + {illegalValues?.map((value) => ( +
  • {value}
  • + ))} +
+
+ } + /> +

Select values from a predefined set

+ + + + + + {filteredValues.map((match) => ( + onChange(match.value)} + name='legal-value' + color='primary' + disabled={deletedLegalValues + .map(({ value }) => value) + .includes(match.value)} + /> + } + /> + ))} + +
+ ); +};