mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-17 13:46:47 +02:00
feat: 1-3652/legal value selector visual update (#9829)
Handles the visual changes for the legal value selector widget. Before: <img width="792" alt="image" src="https://github.com/user-attachments/assets/0965d577-c4cf-4c1d-9fe7-f8f90d683988" /> After: <img width="769" alt="image" src="https://github.com/user-attachments/assets/33bdf40c-8bbb-4650-a6ba-c4b9e62f8cbd" /> 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).
This commit is contained in:
parent
b05e12d028
commit
3bb54c5a9d
@ -21,6 +21,8 @@ import {
|
|||||||
type Input,
|
type Input,
|
||||||
} from '../useConstraintInput/useConstraintInput';
|
} from '../useConstraintInput/useConstraintInput';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import { LegalValuesSelector } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector';
|
||||||
|
|
||||||
interface IResolveInputProps {
|
interface IResolveInputProps {
|
||||||
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
||||||
@ -75,25 +77,35 @@ export const ResolveInput = ({
|
|||||||
removeValue,
|
removeValue,
|
||||||
error,
|
error,
|
||||||
}: IResolveInputProps) => {
|
}: IResolveInputProps) => {
|
||||||
|
const useNewLegalValueInput = useUiFlag('addEditStrategy');
|
||||||
const resolveInput = () => {
|
const resolveInput = () => {
|
||||||
switch (input) {
|
switch (input) {
|
||||||
case IN_OPERATORS_LEGAL_VALUES:
|
case IN_OPERATORS_LEGAL_VALUES:
|
||||||
case STRING_OPERATORS_LEGAL_VALUES:
|
case STRING_OPERATORS_LEGAL_VALUES:
|
||||||
return (
|
return useNewLegalValueInput ? (
|
||||||
<>
|
<LegalValuesSelector
|
||||||
<RestrictiveLegalValues
|
data={resolveLegalValues(
|
||||||
data={resolveLegalValues(
|
constraintValues,
|
||||||
constraintValues,
|
contextDefinition.legalValues,
|
||||||
contextDefinition.legalValues,
|
)}
|
||||||
)}
|
constraintValues={constraintValues}
|
||||||
constraintValues={constraintValues}
|
values={localConstraint.values || []}
|
||||||
values={localConstraint.values || []}
|
setValuesWithRecord={setValuesWithRecord}
|
||||||
setValuesWithRecord={setValuesWithRecord}
|
setValues={setValues}
|
||||||
setValues={setValues}
|
/>
|
||||||
error={error}
|
) : (
|
||||||
setError={setError}
|
<RestrictiveLegalValues
|
||||||
/>
|
data={resolveLegalValues(
|
||||||
</>
|
constraintValues,
|
||||||
|
contextDefinition.legalValues,
|
||||||
|
)}
|
||||||
|
constraintValues={constraintValues}
|
||||||
|
values={localConstraint.values || []}
|
||||||
|
setValuesWithRecord={setValuesWithRecord}
|
||||||
|
setValues={setValues}
|
||||||
|
error={error}
|
||||||
|
setError={setError}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
case NUM_OPERATORS_LEGAL_VALUES:
|
case NUM_OPERATORS_LEGAL_VALUES:
|
||||||
return (
|
return (
|
||||||
|
@ -82,6 +82,10 @@ const ErrorText = styled('p')(({ theme }) => ({
|
|||||||
color: theme.palette.error.main,
|
color: theme.palette.error.main,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx`
|
||||||
|
* Remove with flag `addEditStrategy`
|
||||||
|
*/
|
||||||
export const RestrictiveLegalValues = ({
|
export const RestrictiveLegalValues = ({
|
||||||
data,
|
data,
|
||||||
values,
|
values,
|
||||||
|
@ -2,7 +2,9 @@ import { useState } from 'react';
|
|||||||
import { Chip, styled } from '@mui/material';
|
import { Chip, styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
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 {
|
interface IMultipleValuesProps {
|
||||||
values: string[] | undefined;
|
values: string[] | undefined;
|
||||||
@ -16,8 +18,14 @@ const StyledChip = styled(Chip)(({ theme }) => ({
|
|||||||
margin: theme.spacing(0, 1, 1, 0),
|
margin: theme.spacing(0, 1, 1, 0),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const SearchWrapper = styled('div')(({ theme }) => ({
|
||||||
|
width: '300px',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
|
||||||
export const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
export const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
||||||
const [filter, setFilter] = useState('');
|
const [filter, setFilter] = useState('');
|
||||||
|
const useNewSearchComponent = useUiFlag('addEditStrategy');
|
||||||
|
|
||||||
if (!values || values.length === 0) return null;
|
if (!values || values.length === 0) return null;
|
||||||
|
|
||||||
@ -26,10 +34,19 @@ export const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={values.length > 20}
|
condition={values.length > 20}
|
||||||
show={
|
show={
|
||||||
<ConstraintValueSearch
|
useNewSearchComponent ? (
|
||||||
filter={filter}
|
<SearchWrapper>
|
||||||
setFilter={setFilter}
|
<NewConstraintValueSearch
|
||||||
/>
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
</SearchWrapper>
|
||||||
|
) : (
|
||||||
|
<ConstraintValueSearch
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{values
|
{values
|
||||||
|
@ -7,6 +7,10 @@ interface IConstraintValueSearchProps {
|
|||||||
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx`
|
||||||
|
* Remove with flag `addEditStrategy`
|
||||||
|
*/
|
||||||
export const ConstraintValueSearch = ({
|
export const ConstraintValueSearch = ({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
@ -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<React.SetStateAction<string>>;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConstraintValueSearch = ({
|
||||||
|
filter,
|
||||||
|
setFilter,
|
||||||
|
onKeyDown,
|
||||||
|
}: IConstraintValueSearchProps) => {
|
||||||
|
const inputId = useId();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', minWidth: '120px' }}
|
||||||
|
>
|
||||||
|
<ScreenReaderOnly>
|
||||||
|
<label htmlFor={inputId}>Search</label>
|
||||||
|
</ScreenReaderOnly>
|
||||||
|
<TextField
|
||||||
|
id={inputId}
|
||||||
|
name='search'
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
placeholder='Filter values'
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
variant='outlined'
|
||||||
|
size='small'
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position='start'>
|
||||||
|
<Search />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,6 @@ import { IconButton, styled } from '@mui/material';
|
|||||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||||
import { DateSingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue';
|
import { DateSingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/DateSingleValue/DateSingleValue';
|
||||||
import { FreeTextInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/FreeTextInput/FreeTextInput';
|
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 { SingleLegalValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleLegalValue/SingleLegalValue';
|
||||||
import { SingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleValue/SingleValue';
|
import { SingleValue } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/SingleValue/SingleValue';
|
||||||
import {
|
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 { ReactComponent as CaseInsensitiveIcon } from 'assets/icons/case-insensitive.svg';
|
||||||
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
|
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
|
||||||
import { AddValuesWidget } from './AddValuesWidget';
|
import { AddValuesWidget } from './AddValuesWidget';
|
||||||
|
import { LegalValuesSelector } from './LegalValuesSelector';
|
||||||
|
|
||||||
const Container = styled('article')(({ theme }) => ({
|
const Container = styled('article')(({ theme }) => ({
|
||||||
'--padding': theme.spacing(2),
|
'--padding': theme.spacing(2),
|
||||||
@ -97,7 +97,6 @@ const ConstraintDetails = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
const InputContainer = styled('div')(({ theme }) => ({
|
const InputContainer = styled('div')(({ theme }) => ({
|
||||||
padding: 'var(--padding)',
|
padding: 'var(--padding)',
|
||||||
paddingTop: 0,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
const StyledSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||||
@ -261,20 +260,16 @@ export const EditableConstraint: FC<Props> = ({
|
|||||||
case IN_OPERATORS_LEGAL_VALUES:
|
case IN_OPERATORS_LEGAL_VALUES:
|
||||||
case STRING_OPERATORS_LEGAL_VALUES:
|
case STRING_OPERATORS_LEGAL_VALUES:
|
||||||
return (
|
return (
|
||||||
<>
|
<LegalValuesSelector
|
||||||
<RestrictiveLegalValues
|
data={resolveLegalValues(
|
||||||
data={resolveLegalValues(
|
constraintValues,
|
||||||
constraintValues,
|
contextDefinition.legalValues,
|
||||||
contextDefinition.legalValues,
|
)}
|
||||||
)}
|
constraintValues={constraintValues}
|
||||||
constraintValues={constraintValues}
|
values={localConstraint.values || []}
|
||||||
values={localConstraint.values || []}
|
setValuesWithRecord={setValuesWithRecord}
|
||||||
setValuesWithRecord={setValuesWithRecord}
|
setValues={setValues}
|
||||||
setValues={setValues}
|
/>
|
||||||
error={error}
|
|
||||||
setError={setError}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
case NUM_OPERATORS_LEGAL_VALUES:
|
case NUM_OPERATORS_LEGAL_VALUES:
|
||||||
return (
|
return (
|
||||||
|
@ -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 (
|
||||||
|
<LegalValuesSelectorWidget>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(illegalValues && illegalValues.length > 0)}
|
||||||
|
show={
|
||||||
|
<Alert severity='warning'>
|
||||||
|
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:
|
||||||
|
<ul>
|
||||||
|
{illegalValues?.map((value) => (
|
||||||
|
<li key={value}>{value}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p>Select values from a predefined set</p>
|
||||||
|
<Row>
|
||||||
|
<ConstraintValueSearch
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
|
filter={filter}
|
||||||
|
setFilter={setFilter}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
variant={'text'}
|
||||||
|
onClick={onSelectAll}
|
||||||
|
>
|
||||||
|
{isAllSelected ? 'Unselect all' : 'Select all'}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
<StyledValuesContainer>
|
||||||
|
{filteredValues.map((match) => (
|
||||||
|
<LegalValueLabel
|
||||||
|
key={match.value}
|
||||||
|
legal={match}
|
||||||
|
filter={filter}
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={Boolean(valuesMap[match.value])}
|
||||||
|
onChange={() => onChange(match.value)}
|
||||||
|
name='legal-value'
|
||||||
|
color='primary'
|
||||||
|
disabled={deletedLegalValues
|
||||||
|
.map(({ value }) => value)
|
||||||
|
.includes(match.value)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledValuesContainer>
|
||||||
|
</LegalValuesSelectorWidget>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user