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,
|
||||
} 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<IUnleashContextDefinition, 'legalValues'>;
|
||||
@ -75,12 +77,23 @@ 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 ? (
|
||||
<LegalValuesSelector
|
||||
data={resolveLegalValues(
|
||||
constraintValues,
|
||||
contextDefinition.legalValues,
|
||||
)}
|
||||
constraintValues={constraintValues}
|
||||
values={localConstraint.values || []}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
setValues={setValues}
|
||||
/>
|
||||
) : (
|
||||
<RestrictiveLegalValues
|
||||
data={resolveLegalValues(
|
||||
constraintValues,
|
||||
@ -93,7 +106,6 @@ export const ResolveInput = ({
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case NUM_OPERATORS_LEGAL_VALUES:
|
||||
return (
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
<ConditionallyRender
|
||||
condition={values.length > 20}
|
||||
show={
|
||||
useNewSearchComponent ? (
|
||||
<SearchWrapper>
|
||||
<NewConstraintValueSearch
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
</SearchWrapper>
|
||||
) : (
|
||||
<ConstraintValueSearch
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{values
|
||||
|
@ -7,6 +7,10 @@ interface IConstraintValueSearchProps {
|
||||
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx`
|
||||
* Remove with flag `addEditStrategy`
|
||||
*/
|
||||
export const ConstraintValueSearch = ({
|
||||
filter,
|
||||
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 { 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,8 +260,7 @@ export const EditableConstraint: FC<Props> = ({
|
||||
case IN_OPERATORS_LEGAL_VALUES:
|
||||
case STRING_OPERATORS_LEGAL_VALUES:
|
||||
return (
|
||||
<>
|
||||
<RestrictiveLegalValues
|
||||
<LegalValuesSelector
|
||||
data={resolveLegalValues(
|
||||
constraintValues,
|
||||
contextDefinition.legalValues,
|
||||
@ -271,10 +269,7 @@ export const EditableConstraint: FC<Props> = ({
|
||||
values={localConstraint.values || []}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
setValues={setValues}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case NUM_OPERATORS_LEGAL_VALUES:
|
||||
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