diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx index 6dd89606e0..42eb97b244 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint.tsx @@ -16,7 +16,10 @@ import { ReactComponent as EqualsIcon } from 'assets/icons/constraint-equals.svg import { ReactComponent as NotEqualsIcon } from 'assets/icons/constraint-not-equals.svg'; import { AddSingleValueWidget } from './AddSingleValueWidget'; import { ConstraintDateInput } from './ConstraintDateInput'; -import { LegalValuesSelector } from './LegalValuesSelector'; +import { + LegalValuesSelector, + SingleLegalValueSelector, +} from './LegalValuesSelector'; import { useEditableConstraint } from './useEditableConstraint/useEditableConstraint'; import type { IConstraint } from 'interfaces/strategy'; import { @@ -227,9 +230,11 @@ export const EditableConstraint: FC = ({ constraint: localConstraint, updateConstraint, validator, - ...constraintMetadata + ...legalValueData } = useEditableConstraint(constraint, onAutoSave); + const isLegalValueConstraint = 'legalValues' in legalValueData; + const { context } = useUnleashContext(); const { contextName, operator } = localConstraint; const showCaseSensitiveButton = isStringOperator(operator); @@ -327,25 +332,37 @@ export const EditableConstraint: FC = ({ values={ isMultiValueConstraint(localConstraint) ? Array.from(localConstraint.values) - : undefined - } - removeValue={(value) => - updateConstraint({ - type: 'remove value from list', - payload: value, - }) + : 'legalValues' in legalValueData && + localConstraint.value + ? [localConstraint.value] + : undefined } + removeValue={(value) => { + if (isMultiValueConstraint(localConstraint)) { + updateConstraint({ + type: 'remove value from list', + payload: value, + }); + } else { + updateConstraint({ + type: 'set value', + payload: '', + }); + } + }} getExternalFocusTarget={() => addValuesButtonRef.current ?? deleteButtonRef.current } > - + {isLegalValueConstraint ? null : ( + + )} @@ -361,33 +378,47 @@ export const EditableConstraint: FC = ({ - {'legalValues' in constraintMetadata && - isMultiValueConstraint(localConstraint) ? ( + {'legalValues' in legalValueData ? ( - - updateConstraint({ - type: 'clear values', - }) - } - addValues={(newValues) => - updateConstraint({ - type: 'add value(s)', - payload: newValues, - }) - } - removeValue={(value) => - updateConstraint({ - type: 'remove value from list', - payload: value, - }) - } - deletedLegalValues={ - constraintMetadata.deletedLegalValues - } - legalValues={constraintMetadata.legalValues} - /> + {isMultiValueConstraint(localConstraint) ? ( + + updateConstraint({ + type: 'clear values', + }) + } + addValues={(newValues) => + updateConstraint({ + type: 'add value(s)', + payload: newValues, + }) + } + removeValue={(value) => + updateConstraint({ + type: 'remove value from list', + payload: value, + }) + } + {...legalValueData} + /> + ) : ( + + updateConstraint({ + type: 'clear values', + }) + } + addValue={(newValues) => + updateConstraint({ + type: 'set value', + payload: newValues, + }) + } + {...legalValueData} + /> + )} ) : null} diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx index 20d85224ff..15bc4ef08c 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react'; -import { Alert, Button, Checkbox, styled } from '@mui/material'; +import { type FC, useId, useState } from 'react'; +import { Alert, Button, Checkbox, Radio, styled } from '@mui/material'; import { filterLegalValues, LegalValueLabel, @@ -7,15 +7,6 @@ import { import { ConstraintValueSearch } from './ConstraintValueSearch'; import type { ILegalValue } from 'interfaces/context'; -type LegalValuesSelectorProps = { - values: Set; - addValues: (values: string[]) => void; - removeValue: (value: string) => void; - clearAll: () => void; - deletedLegalValues?: Set; - legalValues: ILegalValue[]; -}; - const StyledValuesContainer = styled('div')(({ theme }) => ({ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', @@ -37,36 +28,39 @@ const LegalValuesSelectorWidget = styled('article')(({ theme }) => ({ gap: theme.spacing(2), })); -export const LegalValuesSelector = ({ +type BaseProps = { + legalValues: ILegalValue[]; + onChange: (value: string) => void; + deletedLegalValues?: Set; + invalidLegalValues?: Set; + isInputSelected: (value: string) => boolean; + multiSelect?: { + selectAll: () => void; + clearAll: () => void; + values: Set; + }; +}; + +const BaseLegalValueSelector: FC = ({ legalValues, - values, - addValues, - removeValue, - clearAll, + onChange, deletedLegalValues, -}: LegalValuesSelectorProps) => { + invalidLegalValues, + isInputSelected: isInputChecked, + multiSelect, +}) => { const [filter, setFilter] = useState(''); + const labelId = useId(); + const descriptionId = useId(); + const groupNameId = useId(); + const alertId = useId(); const filteredValues = filterLegalValues(legalValues, filter); - const onChange = (legalValue: string) => { - if (values.has(legalValue)) { - removeValue(legalValue); - } else { - addValues([legalValue]); - } - }; - - const isAllSelected = legalValues.every((value) => values.has(value.value)); - - const onSelectAll = () => { - if (isAllSelected) { - clearAll(); - return; - } else { - addValues(legalValues.map(({ value }) => value)); - } - }; + const isAllSelected = + multiSelect && + legalValues.length === + multiSelect.values.size + (deletedLegalValues?.size ?? 0); const handleSearchKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { @@ -77,11 +71,12 @@ export const LegalValuesSelector = ({ } } }; + const Control = multiSelect ? Checkbox : Radio; return ( {deletedLegalValues?.size ? ( - + 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: @@ -92,36 +87,58 @@ export const LegalValuesSelector = ({ ) : null} -

Select values from a predefined set

+

+ Select values from a predefined set + {invalidLegalValues?.size ? ( + + Values that are not valid for your chosen operator have + been disabled. + + ) : null} +

- + {multiSelect ? ( + + ) : null} - + {filteredValues.map((match) => ( onChange(match.value)} - name='legal-value' + onChange(match.value)} + disabled={invalidLegalValues?.has(match.value)} /> } /> @@ -130,3 +147,78 @@ export const LegalValuesSelector = ({
); }; + +type LegalValuesSelectorProps = { + values: Set; + addValues: (values: string[]) => void; + removeValue: (value: string) => void; + clearAll: () => void; + deletedLegalValues?: Set; + invalidLegalValues?: Set; + legalValues: ILegalValue[]; +}; + +export const LegalValuesSelector = ({ + legalValues, + values, + addValues, + removeValue, + clearAll, + ...baseProps +}: LegalValuesSelectorProps) => { + const onChange = (legalValue: string) => { + if (values.has(legalValue)) { + removeValue(legalValue); + } else { + addValues([legalValue]); + } + }; + + return ( + values.has(inputValue)} + onChange={onChange} + multiSelect={{ + clearAll, + selectAll: () => { + addValues(legalValues.map(({ value }) => value)); + }, + values, + }} + {...baseProps} + /> + ); +}; + +type SingleLegalValueSelectorProps = { + value: string; + addValue: (value: string) => void; + clear: () => void; + deletedLegalValues?: Set; + legalValues: ILegalValue[]; + invalidLegalValues?: Set; +}; + +export const SingleLegalValueSelector = ({ + value, + addValue, + clear, + ...baseProps +}: SingleLegalValueSelectorProps) => { + const onChange = (newValue: string) => { + if (value === newValue) { + clear(); + } else { + addValue(newValue); + } + }; + + return ( + inputValue === value} + {...baseProps} + /> + ); +}; diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/useEditableConstraint/useEditableConstraint.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/useEditableConstraint/useEditableConstraint.tsx index e922e4e142..76c246074d 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/useEditableConstraint/useEditableConstraint.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyConstraints/useEditableConstraint/useEditableConstraint.tsx @@ -52,6 +52,7 @@ type MultiValueConstraintState = { type LegalValueData = { legalValues: ILegalValue[]; deletedLegalValues?: Set; + invalidLegalValues?: Set; }; type LegalValueConstraintState = { @@ -80,11 +81,14 @@ export const useEditableConstraint = ( resolveContextDefinition(context, localConstraint.contextName), ); + const validator = constraintValidator(localConstraint); + const deletedLegalValues = useMemo(() => { if ( contextDefinition.legalValues?.length && constraint.values?.length ) { + // todo: extract and test const currentLegalValues = new Set( contextDefinition.legalValues.map(({ value }) => value), ); @@ -101,6 +105,24 @@ export const useEditableConstraint = ( JSON.stringify(constraint.values), ]); + const invalidLegalValues = useMemo(() => { + if ( + contextDefinition.legalValues?.length && + isSingleValueConstraint(localConstraint) + ) { + // todo: extract and test + return new Set( + contextDefinition.legalValues + .filter(({ value }) => !validator(value)[0]) + .map(({ value }) => value), + ); + } + return undefined; + }, [ + JSON.stringify(contextDefinition.legalValues), + JSON.stringify(localConstraint.operator), + ]); + const updateConstraint = (action: ConstraintUpdateAction) => { const nextState = constraintReducer( localConstraint, @@ -120,26 +142,37 @@ export const useEditableConstraint = ( } }; + if (contextDefinition.legalValues?.length) { + if (isSingleValueConstraint(localConstraint)) { + return { + updateConstraint, + constraint: localConstraint, + validator, + legalValues: contextDefinition.legalValues, + invalidLegalValues, + deletedLegalValues, + }; + } + return { + updateConstraint, + constraint: localConstraint, + validator, + legalValues: contextDefinition.legalValues, + invalidLegalValues, + deletedLegalValues, + }; + } if (isSingleValueConstraint(localConstraint)) { return { updateConstraint, constraint: localConstraint, - validator: constraintValidator(localConstraint), - }; - } - if (contextDefinition.legalValues?.length) { - return { - updateConstraint, - constraint: localConstraint, - validator: constraintValidator(localConstraint), - legalValues: contextDefinition.legalValues, - deletedLegalValues, + validator, }; } return { updateConstraint, constraint: localConstraint, - validator: constraintValidator(localConstraint), + validator, }; };