mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
Chore(1 3829)/cleanup pt 3 (#10133)
This PR removes more constraint inputs and validators that are not in use anymore. Additionally, the old constraint components that are still being used by the project action filter item, have been moved to that directory. This also goes for ResolveInput which has been simplified to only the inputs and operators used by actions filter item. I've done a manual side-by-side comparison of the old and newly refactored filter item, and it appears to be working the exact same.
This commit is contained in:
parent
3db1eedb4a
commit
d7c32d688a
@ -1,64 +0,0 @@
|
||||
import { Button, styled } from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
interface IConstraintAccordionBody {
|
||||
localConstraint: IConstraint;
|
||||
setValues: (values: string[]) => void;
|
||||
triggerTransition: () => void;
|
||||
setValue: (value: string) => void;
|
||||
setAction: React.Dispatch<React.SetStateAction<string>>;
|
||||
onSubmit: () => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledInputContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing(2),
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
width: '100%',
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledInputButtonContainer = styled('div')({
|
||||
marginLeft: 'auto',
|
||||
});
|
||||
|
||||
const StyledLeftButton = styled(Button)(({ theme }) => ({
|
||||
marginRight: theme.spacing(1),
|
||||
minWidth: '125px',
|
||||
}));
|
||||
|
||||
const StyledRightButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(1),
|
||||
minWidth: '125px',
|
||||
}));
|
||||
|
||||
export const ConstraintAccordionEditBody: React.FC<
|
||||
IConstraintAccordionBody
|
||||
> = ({ localConstraint, children, triggerTransition, setAction, onSubmit }) => {
|
||||
return (
|
||||
<>
|
||||
<StyledInputContainer>{children}</StyledInputContainer>
|
||||
<StyledButtonContainer>
|
||||
<StyledInputButtonContainer>
|
||||
<StyledLeftButton
|
||||
type='button'
|
||||
onClick={onSubmit}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
data-testid='CONSTRAINT_SAVE_BUTTON'
|
||||
>
|
||||
Done
|
||||
</StyledLeftButton>
|
||||
</StyledInputButtonContainer>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import { parseDateValue } from 'component/common/util';
|
||||
|
||||
test(`Date component is able to parse midnight when it's 00`, () => {
|
||||
const f = parseDateValue('2022-03-15T12:27');
|
||||
const midnight = parseDateValue('2022-03-15T00:27');
|
||||
expect(f).toEqual('2022-03-15T12:27');
|
||||
expect(midnight).toEqual('2022-03-15T00:27');
|
||||
});
|
||||
|
||||
test(`Date component - snapshot matching`, () => {
|
||||
const midnight = '2022-03-15T00:00';
|
||||
const midday = '2022-03-15T12:00';
|
||||
const obj = {
|
||||
midnight: parseDateValue(midnight),
|
||||
midday: parseDateValue(midday),
|
||||
};
|
||||
expect(obj).toMatchSnapshot();
|
||||
});
|
@ -1,85 +0,0 @@
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { parseDateValue, parseValidDate } from 'component/common/util';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
import { getAllTimezones } from 'countries-and-timezones';
|
||||
|
||||
interface IDateSingleValueProps {
|
||||
setValue: (value: string) => void;
|
||||
value?: string;
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const StyledWrapper = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginBottom: theme.spacing(1),
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(1),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use `component/feature/FeatureStrategy/FeatureStrategyConstraints/ConstraintDateInput.tsx`
|
||||
* Remove with flag `addEditStrategy`
|
||||
*/
|
||||
export const DateSingleValue = ({
|
||||
setValue,
|
||||
value,
|
||||
error,
|
||||
setError,
|
||||
}: IDateSingleValueProps) => {
|
||||
const timezones = Object.values(getAllTimezones({ deprecated: false })).map(
|
||||
(timezone) => ({
|
||||
key: timezone.name,
|
||||
label: `${timezone.name}`,
|
||||
utcOffset: timezone.utcOffsetStr,
|
||||
}),
|
||||
);
|
||||
const { timeZone: localTimezoneName } =
|
||||
Intl.DateTimeFormat().resolvedOptions();
|
||||
const [pickedDate, setPickedDate] = useState(value || '');
|
||||
|
||||
const timezoneText = useMemo<string>(() => {
|
||||
const localTimezone = timezones.find(
|
||||
(t) => t.key === localTimezoneName,
|
||||
);
|
||||
if (localTimezone != null) {
|
||||
return `${localTimezone.key} (UTC ${localTimezone.utcOffset})`;
|
||||
} else {
|
||||
return 'The time shown is in your local time zone according to your browser.';
|
||||
}
|
||||
}, [timezones, localTimezoneName]);
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConstraintFormHeader>Select a date</ConstraintFormHeader>
|
||||
<StyledWrapper>
|
||||
<Input
|
||||
id='date'
|
||||
label='Date'
|
||||
type='datetime-local'
|
||||
value={parseDateValue(pickedDate)}
|
||||
onChange={(e) => {
|
||||
setError('');
|
||||
const parsedDate = parseValidDate(e.target.value);
|
||||
const dateString = parsedDate?.toISOString();
|
||||
dateString && setPickedDate(dateString);
|
||||
dateString && setValue(dateString);
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
error={Boolean(error)}
|
||||
errorText={error}
|
||||
required
|
||||
/>
|
||||
<p>{timezoneText}</p>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Date component - snapshot matching 1`] = `
|
||||
{
|
||||
"midday": "2022-03-15T12:00",
|
||||
"midnight": "2022-03-15T00:00",
|
||||
}
|
||||
`;
|
@ -1,198 +0,0 @@
|
||||
import type {
|
||||
ILegalValue,
|
||||
IUnleashContextDefinition,
|
||||
} from 'interfaces/context';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { DateSingleValue } from '../DateSingleValue/DateSingleValue.tsx';
|
||||
import { FreeTextInput } from '../FreeTextInput/FreeTextInput.tsx';
|
||||
import { RestrictiveLegalValues } from '../RestrictiveLegalValues/RestrictiveLegalValues.tsx';
|
||||
import { SingleLegalValue } from '../SingleLegalValue/SingleLegalValue.tsx';
|
||||
import { SingleValue } from '../SingleValue/SingleValue.tsx';
|
||||
import {
|
||||
IN_OPERATORS_LEGAL_VALUES,
|
||||
STRING_OPERATORS_FREETEXT,
|
||||
STRING_OPERATORS_LEGAL_VALUES,
|
||||
SEMVER_OPERATORS_SINGLE_VALUE,
|
||||
NUM_OPERATORS_LEGAL_VALUES,
|
||||
NUM_OPERATORS_SINGLE_VALUE,
|
||||
SEMVER_OPERATORS_LEGAL_VALUES,
|
||||
DATE_OPERATORS_SINGLE_VALUE,
|
||||
IN_OPERATORS_FREETEXT,
|
||||
type Input,
|
||||
} from '../useConstraintInput/useConstraintInput.tsx';
|
||||
import type React from 'react';
|
||||
|
||||
interface IResolveInputProps {
|
||||
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
||||
localConstraint: Pick<IConstraint, 'value' | 'values'>;
|
||||
constraintValues: string[];
|
||||
constraintValue: string;
|
||||
setValue: (value: string) => void;
|
||||
setValues: (values: string[]) => void;
|
||||
setValuesWithRecord: (values: string[]) => void;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
removeValue: (index: number) => void;
|
||||
input: Input;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const resolveLegalValues = (
|
||||
values: IConstraint['values'],
|
||||
legalValues: IUnleashContextDefinition['legalValues'],
|
||||
): { legalValues: ILegalValue[]; deletedLegalValues: ILegalValue[] } => {
|
||||
if (legalValues?.length === 0) {
|
||||
return {
|
||||
legalValues: [],
|
||||
deletedLegalValues: [],
|
||||
};
|
||||
}
|
||||
|
||||
const deletedLegalValues = (values || [])
|
||||
.filter(
|
||||
(value) =>
|
||||
!(legalValues || []).some(
|
||||
({ value: legalValue }) => legalValue === value,
|
||||
),
|
||||
)
|
||||
.map((v) => ({ value: v, description: '' }));
|
||||
|
||||
return {
|
||||
legalValues: legalValues || [],
|
||||
deletedLegalValues,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated; remove with `addEditStrategy` flag. Need an input? Prefer using specific input components.
|
||||
*
|
||||
* For the case of `ProjectActionsFilterItem.tsx`: it already excludes legal
|
||||
* values and date operators. This leaves only free text and single value
|
||||
* text/numeric operators. Alternatively, consider rewriting this component to only handle those cases.
|
||||
*/
|
||||
export const ResolveInput = ({
|
||||
input,
|
||||
contextDefinition,
|
||||
constraintValues,
|
||||
constraintValue,
|
||||
localConstraint,
|
||||
setValue,
|
||||
setValues,
|
||||
setValuesWithRecord,
|
||||
setError,
|
||||
removeValue,
|
||||
error,
|
||||
}: IResolveInputProps) => {
|
||||
const resolveInput = () => {
|
||||
switch (input) {
|
||||
case IN_OPERATORS_LEGAL_VALUES:
|
||||
case STRING_OPERATORS_LEGAL_VALUES:
|
||||
return (
|
||||
<RestrictiveLegalValues
|
||||
data={resolveLegalValues(
|
||||
constraintValues,
|
||||
contextDefinition.legalValues,
|
||||
)}
|
||||
constraintValues={constraintValues}
|
||||
values={localConstraint.values || []}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
setValues={setValues}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case NUM_OPERATORS_LEGAL_VALUES:
|
||||
return (
|
||||
<>
|
||||
<SingleLegalValue
|
||||
data={resolveLegalValues(
|
||||
[constraintValue],
|
||||
contextDefinition.legalValues,
|
||||
)}
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
constraintValue={constraintValue}
|
||||
type='number'
|
||||
legalValues={
|
||||
contextDefinition.legalValues?.filter(
|
||||
(legalValue) => Number(legalValue.value),
|
||||
) || []
|
||||
}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case SEMVER_OPERATORS_LEGAL_VALUES:
|
||||
return (
|
||||
<>
|
||||
<SingleLegalValue
|
||||
data={resolveLegalValues(
|
||||
[constraintValue],
|
||||
contextDefinition.legalValues,
|
||||
)}
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
constraintValue={constraintValue}
|
||||
type='semver'
|
||||
legalValues={contextDefinition.legalValues || []}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case DATE_OPERATORS_SINGLE_VALUE:
|
||||
return (
|
||||
<DateSingleValue
|
||||
value={localConstraint.value}
|
||||
setValue={setValue}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case IN_OPERATORS_FREETEXT:
|
||||
return (
|
||||
<FreeTextInput
|
||||
values={localConstraint.values || []}
|
||||
removeValue={removeValue}
|
||||
setValues={setValuesWithRecord}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case STRING_OPERATORS_FREETEXT:
|
||||
return (
|
||||
<>
|
||||
<FreeTextInput
|
||||
values={localConstraint.values || []}
|
||||
removeValue={removeValue}
|
||||
setValues={setValuesWithRecord}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
case NUM_OPERATORS_SINGLE_VALUE:
|
||||
return (
|
||||
<SingleValue
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
type='number'
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case SEMVER_OPERATORS_SINGLE_VALUE:
|
||||
return (
|
||||
<SingleValue
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
type='semver'
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return <>{resolveInput()}</>;
|
||||
};
|
@ -1,119 +0,0 @@
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { RestrictiveLegalValues } from './RestrictiveLegalValues.tsx';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('../../../../../../hooks/useUiFlag', () => ({
|
||||
useUiFlag: vi.fn(
|
||||
(flag: string) => flag !== 'disableShowContextFieldSelectionValues',
|
||||
),
|
||||
}));
|
||||
|
||||
test('should show alert when you have illegal legal values', async () => {
|
||||
const contextDefinitionValues = [{ value: 'value1' }, { value: 'value2' }];
|
||||
const fixedValues = ['value1', 'value2'];
|
||||
const localValues = ['value1', 'value2'];
|
||||
const deletedLegalValues = [{ value: 'value1' }];
|
||||
|
||||
render(
|
||||
<RestrictiveLegalValues
|
||||
data={{ legalValues: contextDefinitionValues, deletedLegalValues }}
|
||||
constraintValues={fixedValues}
|
||||
values={localValues}
|
||||
setValues={() => {}}
|
||||
setValuesWithRecord={() => {}}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText(
|
||||
'This constraint is currently using values that were valid in the past but have since been deleted. If you save changes on this constraint and then save the strategy the following values will be removed:',
|
||||
);
|
||||
});
|
||||
|
||||
test('Should remove illegal legal values from internal value state when mounting', () => {
|
||||
const contextDefinitionValues = [{ value: 'value1' }, { value: 'value2' }];
|
||||
const fixedValues = ['value1', 'value2'];
|
||||
let localValues = ['value1', 'value2'];
|
||||
const deletedLegalValues = [{ value: 'value1' }];
|
||||
|
||||
const setValues = (values: string[]) => {
|
||||
localValues = values;
|
||||
};
|
||||
|
||||
render(
|
||||
<RestrictiveLegalValues
|
||||
data={{
|
||||
legalValues: contextDefinitionValues,
|
||||
deletedLegalValues,
|
||||
}}
|
||||
constraintValues={fixedValues}
|
||||
values={localValues}
|
||||
setValues={setValues}
|
||||
setValuesWithRecord={() => {}}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(localValues).toEqual(['value2']);
|
||||
});
|
||||
|
||||
test('Should select all', async () => {
|
||||
const contextDefinitionValues = [{ value: 'value1' }, { value: 'value2' }];
|
||||
let localValues: string[] = [];
|
||||
|
||||
const setValuesWithRecord = (values: string[]) => {
|
||||
localValues = values;
|
||||
};
|
||||
|
||||
render(
|
||||
<RestrictiveLegalValues
|
||||
data={{
|
||||
legalValues: contextDefinitionValues,
|
||||
deletedLegalValues: [{ value: 'value3' }],
|
||||
}}
|
||||
constraintValues={[]}
|
||||
values={localValues}
|
||||
setValues={() => {}}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const selectedAllButton = await screen.findByText(/Select all/i);
|
||||
|
||||
fireEvent.click(selectedAllButton);
|
||||
expect(localValues).toEqual(['value1', 'value2']);
|
||||
});
|
||||
|
||||
test('Should unselect all', async () => {
|
||||
const contextDefinitionValues = [{ value: 'value1' }, { value: 'value2' }];
|
||||
let localValues: string[] = ['value1', 'value2'];
|
||||
|
||||
const setValuesWithRecord = (values: string[]) => {
|
||||
localValues = values;
|
||||
};
|
||||
|
||||
render(
|
||||
<RestrictiveLegalValues
|
||||
data={{
|
||||
legalValues: contextDefinitionValues,
|
||||
deletedLegalValues: [{ value: 'value3' }],
|
||||
}}
|
||||
constraintValues={[]}
|
||||
values={localValues}
|
||||
setValues={() => {}}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const selectedAllButton = await screen.findByText(/Unselect all/i);
|
||||
|
||||
fireEvent.click(selectedAllButton);
|
||||
expect(localValues).toEqual([]);
|
||||
});
|
@ -1,256 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Alert, Button, Checkbox, Chip, Stack, styled } from '@mui/material';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import {
|
||||
filterLegalValues,
|
||||
LegalValueLabel,
|
||||
} from '../LegalValueLabel/LegalValueLabel.tsx';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ConstraintValueSearch } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/ConstraintValueSearch.tsx';
|
||||
|
||||
interface IRestrictiveLegalValuesProps {
|
||||
data: {
|
||||
legalValues: ILegalValue[];
|
||||
deletedLegalValues: ILegalValue[];
|
||||
};
|
||||
constraintValues: string[];
|
||||
values: string[];
|
||||
setValues: (values: string[]) => void;
|
||||
setValuesWithRecord: (values: string[]) => void;
|
||||
beforeValues?: JSX.Element;
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
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),
|
||||
padding: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
maxHeight: '378px',
|
||||
overflow: 'auto',
|
||||
}));
|
||||
|
||||
const StyledChipList = styled('ul')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
listStyle: 'none',
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledStack = styled(Stack)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const ErrorText = styled('p')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.error.main,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx`
|
||||
* Remove with flag `addEditStrategy`
|
||||
*/
|
||||
export const RestrictiveLegalValues = ({
|
||||
data,
|
||||
values,
|
||||
setValues,
|
||||
setValuesWithRecord,
|
||||
error,
|
||||
setError,
|
||||
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 disableShowContextFieldSelectionValues = useUiFlag(
|
||||
'disableShowContextFieldSelectionValues',
|
||||
);
|
||||
|
||||
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) => {
|
||||
setError('');
|
||||
|
||||
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') {
|
||||
event.preventDefault();
|
||||
if (filteredValues.length > 0) {
|
||||
const firstValue = filteredValues[0].value;
|
||||
onChange(firstValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(illegalValues && illegalValues.length > 0)}
|
||||
show={
|
||||
<Alert severity='warning'>
|
||||
This constraint is currently using values that were
|
||||
valid in the past but have since been deleted. 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>
|
||||
}
|
||||
/>
|
||||
<StyledStack direction={'row'}>
|
||||
<ConstraintFormHeader>
|
||||
Select values from a predefined set
|
||||
</ConstraintFormHeader>
|
||||
<Button variant={'text'} onClick={onSelectAll}>
|
||||
{isAllSelected ? 'Unselect all' : 'Select all'}
|
||||
</Button>
|
||||
</StyledStack>
|
||||
<ConditionallyRender
|
||||
condition={legalValues.length > 100}
|
||||
show={
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
!disableShowContextFieldSelectionValues &&
|
||||
Boolean(values)
|
||||
}
|
||||
show={
|
||||
<StyledChipList
|
||||
sx={{ border: 0, paddingTop: 0 }}
|
||||
>
|
||||
{values.map((value) => {
|
||||
return (
|
||||
<li key={value}>
|
||||
<Chip
|
||||
label={value}
|
||||
onDelete={() =>
|
||||
onChange(value)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</StyledChipList>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div onKeyDown={handleSearchKeyDown}>
|
||||
<ConstraintValueSearch filter={filter} setFilter={setFilter} />
|
||||
</div>
|
||||
<StyledValuesContainer>
|
||||
{filteredValues.map((match) => (
|
||||
<LegalValueLabel
|
||||
key={match.value}
|
||||
legal={match}
|
||||
filter={filter}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(valuesMap[match.value])}
|
||||
onChange={() => onChange(match.value)}
|
||||
name={match.value}
|
||||
color='primary'
|
||||
disabled={deletedLegalValues
|
||||
.map(({ value }) => value)
|
||||
.includes(match.value)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledValuesContainer>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(error)}
|
||||
show={<ErrorText>{error}</ErrorText>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { SingleLegalValue } from './SingleLegalValue.tsx';
|
||||
|
||||
test('should show alert when you have illegal legal values', async () => {
|
||||
const contextDefinitionValues = [{ value: 'value1' }, { value: 'value2' }];
|
||||
const fixedValue = 'value1';
|
||||
const localValue = 'value1';
|
||||
const deletedLegalValues = [{ value: 'value1' }];
|
||||
|
||||
render(
|
||||
<SingleLegalValue
|
||||
data={{ legalValues: contextDefinitionValues, deletedLegalValues }}
|
||||
constraintValue={fixedValue}
|
||||
value={localValue}
|
||||
setValue={() => {}}
|
||||
type='number'
|
||||
legalValues={contextDefinitionValues}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText(
|
||||
'This constraint is using legal values that have been deleted as a valid option. Please select a new value from the remaining predefined legal values. The constraint will be updated with the new value when you save the strategy.',
|
||||
);
|
||||
});
|
@ -1,127 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import { FormControl, RadioGroup, Radio, Alert, styled } from '@mui/material';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useThemeStyles } from 'themes/themeStyles';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import {
|
||||
LegalValueLabel,
|
||||
filterLegalValues,
|
||||
} from '../LegalValueLabel/LegalValueLabel.tsx';
|
||||
import { getIllegalValues } from '../RestrictiveLegalValues/RestrictiveLegalValues.tsx';
|
||||
import { ConstraintValueSearch } from 'component/common/NewConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch.tsx';
|
||||
|
||||
interface ISingleLegalValueProps {
|
||||
setValue: (value: string) => void;
|
||||
value?: string;
|
||||
type: string;
|
||||
legalValues: ILegalValue[];
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
data: {
|
||||
legalValues: ILegalValue[];
|
||||
deletedLegalValues: ILegalValue[];
|
||||
};
|
||||
constraintValue: string;
|
||||
}
|
||||
|
||||
const StyledFieldsetContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(2),
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
maxHeight: '378px',
|
||||
overflow: 'auto',
|
||||
}));
|
||||
|
||||
export const SingleLegalValue = ({
|
||||
setValue,
|
||||
value,
|
||||
type,
|
||||
legalValues,
|
||||
error,
|
||||
setError,
|
||||
data,
|
||||
constraintValue,
|
||||
}: ISingleLegalValueProps) => {
|
||||
const [filter, setFilter] = useState('');
|
||||
const { classes: styles } = useThemeStyles();
|
||||
const filteredValues = filterLegalValues(legalValues, filter);
|
||||
|
||||
const { deletedLegalValues } = data;
|
||||
|
||||
const illegalValues = getIllegalValues(
|
||||
[constraintValue],
|
||||
deletedLegalValues,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(illegalValues && illegalValues.length > 0)}
|
||||
show={
|
||||
<Alert
|
||||
severity='warning'
|
||||
sx={(theme) => ({ marginTop: theme.spacing(1) })}
|
||||
>
|
||||
{' '}
|
||||
This constraint is using legal values that have been
|
||||
deleted as a valid option. Please select a new value
|
||||
from the remaining predefined legal values. The
|
||||
constraint will be updated with the new value when you
|
||||
save the strategy.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConstraintFormHeader>
|
||||
Add a single {type.toLowerCase()} value
|
||||
</ConstraintFormHeader>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(legalValues.length > 100)}
|
||||
show={
|
||||
<ConstraintValueSearch
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(legalValues.length)}
|
||||
show={
|
||||
<StyledFieldsetContainer>
|
||||
<FormControl component='fieldset'>
|
||||
<RadioGroup
|
||||
aria-label='selected-value'
|
||||
name='selected'
|
||||
value={value}
|
||||
sx={{ gap: (theme) => theme.spacing(0.5) }}
|
||||
onChange={(e) => {
|
||||
setError('');
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
>
|
||||
{filteredValues.map((match) => (
|
||||
<LegalValueLabel
|
||||
key={match.value}
|
||||
legal={match}
|
||||
control={<Radio />}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</StyledFieldsetContainer>
|
||||
}
|
||||
elseShow={
|
||||
<p>No valid legal values available for this operator.</p>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(error)}
|
||||
show={<p className={styles.error}>{error}</p>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,110 +0,0 @@
|
||||
import {
|
||||
numberValidatorGenerator,
|
||||
semVerValidatorGenerator,
|
||||
dateValidatorGenerator,
|
||||
stringValidatorGenerator,
|
||||
} from './constraintValidators.js';
|
||||
|
||||
test('numbervalidator should accept 0', () => {
|
||||
const numValidator = numberValidatorGenerator(0);
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('number validator should reject value that cannot be parsed to number', () => {
|
||||
const numValidator = numberValidatorGenerator('testa31');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a number');
|
||||
});
|
||||
|
||||
test('number validator should reject NaN', () => {
|
||||
const numValidator = numberValidatorGenerator(Number.NaN);
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a number');
|
||||
});
|
||||
|
||||
test('number validator should accept value that can be parsed to number', () => {
|
||||
const numValidator = numberValidatorGenerator('31');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('number validator should accept float values', () => {
|
||||
const numValidator = numberValidatorGenerator('31.12');
|
||||
const [result, err] = numValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('semver validator should reject prefixed values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('v1.4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value is not a valid semver. For example 1.2.4');
|
||||
});
|
||||
|
||||
test('semver validator should reject partial semver values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value is not a valid semver. For example 1.2.4');
|
||||
});
|
||||
|
||||
test('semver validator should accept semver complient values', () => {
|
||||
const semVerValidator = semVerValidatorGenerator('1.4.2');
|
||||
const [result, err] = semVerValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('date validator should reject invalid date', () => {
|
||||
const dateValidator = dateValidatorGenerator('114mydate2005');
|
||||
const [result, err] = dateValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Value must be a valid date matching RFC3339');
|
||||
});
|
||||
|
||||
test('date validator should accept valid date', () => {
|
||||
const dateValidator = dateValidatorGenerator('2022-03-03T10:15:23.262Z');
|
||||
const [result, err] = dateValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('string validator should accept a list of strings', () => {
|
||||
const stringValidator = stringValidatorGenerator(['1234', '4121']);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(err).toBe('');
|
||||
});
|
||||
|
||||
test('string validator should reject values that are not arrays', () => {
|
||||
const stringValidator = stringValidatorGenerator(4);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Values must be a list of strings');
|
||||
});
|
||||
|
||||
test('string validator should reject arrays that are not arrays of strings', () => {
|
||||
const stringValidator = stringValidatorGenerator(['test', Number.NaN, 5]);
|
||||
const [result, err] = stringValidator();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(err).toBe('Values must be a list of strings');
|
||||
});
|
@ -1,55 +0,0 @@
|
||||
import { isValid, parseISO } from 'date-fns';
|
||||
import semver from 'semver';
|
||||
|
||||
export type ConstraintValidatorOutput = [boolean, string];
|
||||
|
||||
export const numberValidatorGenerator = (value: unknown) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
const converted = Number(value);
|
||||
|
||||
if (typeof converted !== 'number' || Number.isNaN(converted)) {
|
||||
return [false, 'Value must be a number'];
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
||||
|
||||
export const stringValidatorGenerator = (values: unknown) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
const error: ConstraintValidatorOutput = [
|
||||
false,
|
||||
'Values must be a list of strings',
|
||||
];
|
||||
if (!Array.isArray(values)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!values.every((value) => typeof value === 'string')) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
||||
|
||||
export const semVerValidatorGenerator = (value: string) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
const isCleanValue = semver.clean(value) === value;
|
||||
|
||||
if (!semver.valid(value) || !isCleanValue) {
|
||||
return [false, 'Value is not a valid semver. For example 1.2.4'];
|
||||
}
|
||||
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
||||
|
||||
export const dateValidatorGenerator = (value: string) => {
|
||||
return (): ConstraintValidatorOutput => {
|
||||
if (!isValid(parseISO(value))) {
|
||||
return [false, 'Value must be a valid date matching RFC3339'];
|
||||
}
|
||||
return [true, ''];
|
||||
};
|
||||
};
|
@ -1,162 +0,0 @@
|
||||
import {
|
||||
inOperators,
|
||||
stringOperators,
|
||||
numOperators,
|
||||
semVerOperators,
|
||||
dateOperators,
|
||||
} from 'constants/operators';
|
||||
import type { IUnleashContextDefinition } from 'interfaces/context';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
|
||||
import {
|
||||
numberValidatorGenerator,
|
||||
stringValidatorGenerator,
|
||||
semVerValidatorGenerator,
|
||||
dateValidatorGenerator,
|
||||
type ConstraintValidatorOutput,
|
||||
} from './constraintValidators.ts';
|
||||
import { nonEmptyArray } from 'utils/nonEmptyArray';
|
||||
|
||||
interface IUseConstraintInputProps {
|
||||
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
|
||||
localConstraint: Pick<IConstraint, 'operator' | 'value' | 'values'>;
|
||||
}
|
||||
|
||||
interface IUseConstraintOutput {
|
||||
input: Input;
|
||||
error: string;
|
||||
validator: () => ConstraintValidatorOutput;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
export const IN_OPERATORS_LEGAL_VALUES = 'IN_OPERATORS_LEGAL_VALUES';
|
||||
export const STRING_OPERATORS_LEGAL_VALUES = 'STRING_OPERATORS_LEGAL_VALUES';
|
||||
export const NUM_OPERATORS_LEGAL_VALUES = 'NUM_OPERATORS_LEGAL_VALUES';
|
||||
export const SEMVER_OPERATORS_LEGAL_VALUES = 'SEMVER_OPERATORS_LEGAL_VALUES';
|
||||
export const DATE_OPERATORS_SINGLE_VALUE = 'DATE_OPERATORS_SINGLE_VALUE';
|
||||
export const IN_OPERATORS_FREETEXT = 'IN_OPERATORS_FREETEXT';
|
||||
export const STRING_OPERATORS_FREETEXT = 'STRING_OPERATORS_FREETEXT';
|
||||
export const NUM_OPERATORS_SINGLE_VALUE = 'NUM_OPERATORS_SINGLE_VALUE';
|
||||
export const SEMVER_OPERATORS_SINGLE_VALUE = 'SEMVER_OPERATORS_SINGLE_VALUE';
|
||||
|
||||
export type Input =
|
||||
| 'IN_OPERATORS_LEGAL_VALUES'
|
||||
| 'STRING_OPERATORS_LEGAL_VALUES'
|
||||
| 'NUM_OPERATORS_LEGAL_VALUES'
|
||||
| 'SEMVER_OPERATORS_LEGAL_VALUES'
|
||||
| 'DATE_OPERATORS_SINGLE_VALUE'
|
||||
| 'IN_OPERATORS_FREETEXT'
|
||||
| 'STRING_OPERATORS_FREETEXT'
|
||||
| 'NUM_OPERATORS_SINGLE_VALUE'
|
||||
| 'SEMVER_OPERATORS_SINGLE_VALUE';
|
||||
|
||||
const NUMBER_VALIDATOR = 'NUMBER_VALIDATOR';
|
||||
const SEMVER_VALIDATOR = 'SEMVER_VALIDATOR';
|
||||
const STRING_ARRAY_VALIDATOR = 'STRING_ARRAY_VALIDATOR';
|
||||
const DATE_VALIDATOR = 'DATE_VALIDATOR';
|
||||
|
||||
type Validator =
|
||||
| 'NUMBER_VALIDATOR'
|
||||
| 'SEMVER_VALIDATOR'
|
||||
| 'STRING_ARRAY_VALIDATOR'
|
||||
| 'DATE_VALIDATOR';
|
||||
|
||||
/**
|
||||
* @deprecated; remove with `addEditStrategy` flag. This component requires a lot of state and mixes many components. Better off using dedicated pieces where you need them.
|
||||
*/
|
||||
export const useConstraintInput = ({
|
||||
contextDefinition,
|
||||
localConstraint,
|
||||
}: IUseConstraintInputProps): IUseConstraintOutput => {
|
||||
const [input, setInput] = useState<Input>(IN_OPERATORS_FREETEXT);
|
||||
const [validator, setValidator] = useState<Validator>(
|
||||
STRING_ARRAY_VALIDATOR,
|
||||
);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const resolveInputType = useCallback(() => {
|
||||
if (
|
||||
nonEmptyArray(contextDefinition.legalValues) &&
|
||||
oneOf(inOperators, localConstraint.operator)
|
||||
) {
|
||||
setInput(IN_OPERATORS_LEGAL_VALUES);
|
||||
} else if (
|
||||
nonEmptyArray(contextDefinition.legalValues) &&
|
||||
oneOf(stringOperators, localConstraint.operator)
|
||||
) {
|
||||
setInput(STRING_OPERATORS_LEGAL_VALUES);
|
||||
} else if (
|
||||
nonEmptyArray(contextDefinition.legalValues) &&
|
||||
oneOf(numOperators, localConstraint.operator)
|
||||
) {
|
||||
setInput(NUM_OPERATORS_LEGAL_VALUES);
|
||||
} else if (
|
||||
nonEmptyArray(contextDefinition.legalValues) &&
|
||||
oneOf(semVerOperators, localConstraint.operator)
|
||||
) {
|
||||
setInput(SEMVER_OPERATORS_LEGAL_VALUES);
|
||||
} else if (oneOf(dateOperators, localConstraint.operator)) {
|
||||
setInput(DATE_OPERATORS_SINGLE_VALUE);
|
||||
} else if (oneOf(inOperators, localConstraint.operator)) {
|
||||
setInput(IN_OPERATORS_FREETEXT);
|
||||
} else if (oneOf(stringOperators, localConstraint.operator)) {
|
||||
setInput(STRING_OPERATORS_FREETEXT);
|
||||
} else if (oneOf(numOperators, localConstraint.operator)) {
|
||||
setInput(NUM_OPERATORS_SINGLE_VALUE);
|
||||
} else if (oneOf(semVerOperators, localConstraint.operator)) {
|
||||
setInput(SEMVER_OPERATORS_SINGLE_VALUE);
|
||||
}
|
||||
}, [localConstraint, contextDefinition]);
|
||||
|
||||
const resolveValidator = () => {
|
||||
switch (validator) {
|
||||
case NUMBER_VALIDATOR:
|
||||
return numberValidatorGenerator(localConstraint.value);
|
||||
case STRING_ARRAY_VALIDATOR:
|
||||
return stringValidatorGenerator(localConstraint.values || []);
|
||||
case SEMVER_VALIDATOR:
|
||||
return semVerValidatorGenerator(localConstraint.value || '');
|
||||
case DATE_VALIDATOR:
|
||||
return dateValidatorGenerator(localConstraint.value || '');
|
||||
}
|
||||
};
|
||||
|
||||
const resolveValidatorType = useCallback(
|
||||
(operator: string) => {
|
||||
if (oneOf(numOperators, operator)) {
|
||||
setValidator(NUMBER_VALIDATOR);
|
||||
}
|
||||
|
||||
if (oneOf([...stringOperators, ...inOperators], operator)) {
|
||||
setValidator(STRING_ARRAY_VALIDATOR);
|
||||
}
|
||||
|
||||
if (oneOf(semVerOperators, operator)) {
|
||||
setValidator(SEMVER_VALIDATOR);
|
||||
}
|
||||
|
||||
if (oneOf(dateOperators, operator)) {
|
||||
setValidator(DATE_VALIDATOR);
|
||||
}
|
||||
},
|
||||
[setValidator],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
resolveValidatorType(localConstraint.operator);
|
||||
}, [
|
||||
localConstraint.operator,
|
||||
localConstraint.value,
|
||||
localConstraint.values,
|
||||
resolveValidatorType,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
resolveInputType();
|
||||
}, [contextDefinition, localConstraint, resolveInputType]);
|
||||
|
||||
return { input, error, validator: resolveValidator(), setError };
|
||||
};
|
@ -96,7 +96,6 @@ export const ConstraintOperatorSelect = ({
|
||||
);
|
||||
};
|
||||
|
||||
// todo (addEditStrategy): add prop to configure the select element or style it. (currently, the chevron is different from the other select element we use). Maybe add a new component.
|
||||
return (
|
||||
<StyledFormInput variant='outlined' size='small' fullWidth>
|
||||
<InputLabel htmlFor='operator-select'>Operator</InputLabel>
|
||||
|
@ -1,54 +0,0 @@
|
||||
import { TextField, InputAdornment, Chip } from '@mui/material';
|
||||
import Search from '@mui/icons-material/Search';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface IConstraintValueSearchProps {
|
||||
filter: string;
|
||||
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `/component/feature/FeatureStrategy/FeatureStrategyConstraints/LegalValuesSelector.tsx`
|
||||
* Remove with flag `addEditStrategy`
|
||||
*/
|
||||
export const ConstraintValueSearch = ({
|
||||
filter,
|
||||
setFilter,
|
||||
}: IConstraintValueSearchProps) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '300px' }}>
|
||||
<TextField
|
||||
label='Search'
|
||||
name='search'
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder='Filter values'
|
||||
sx={(theme) => ({
|
||||
width: '100%',
|
||||
margin: theme.spacing(1, 0, 2),
|
||||
})}
|
||||
variant='outlined'
|
||||
size='small'
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position='start'>
|
||||
<Search />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(filter)}
|
||||
show={
|
||||
<Chip
|
||||
style={{ marginLeft: '1rem' }}
|
||||
label={`filter active: ${filter}`}
|
||||
onDelete={() => setFilter('')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { filterLegalValues } from './LegalValueLabel.jsx';
|
||||
import { filterLegalValues } from './LegalValueLabel.js';
|
||||
|
||||
describe('filterLegalValues function tests', () => {
|
||||
const mockLegalValues = [
|
@ -3,7 +3,7 @@ import { styled } from '@mui/material';
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { ValueChip } from './ValueList.tsx';
|
||||
import { AddValuesPopover, type OnAddActions } from './AddValuesPopover.tsx';
|
||||
import type { ConstraintValidatorOutput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/constraintValidators';
|
||||
import type { ConstraintValidatorOutput } from './ConstraintValidatorOutput.ts';
|
||||
|
||||
const StyledChip = styled(ValueChip, {
|
||||
shouldForwardProp: (prop) => prop !== 'hasValue',
|
||||
|
@ -4,7 +4,7 @@ import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { parseParameterStrings } from 'utils/parseParameter';
|
||||
import { baseChipStyles } from './ValueList.tsx';
|
||||
import { AddValuesPopover, type OnAddActions } from './AddValuesPopover.tsx';
|
||||
import type { ConstraintValidatorOutput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/constraintValidators';
|
||||
import type { ConstraintValidatorOutput } from './ConstraintValidatorOutput.ts';
|
||||
|
||||
// todo: MUI v6 / v7 upgrade: consider changing this to a Chip to align with the rest of the values and the single value selector. There was a fix introduced in v6 that makes you not lose focus on pressing esc: https://mui.com/material-ui/migration/upgrade-to-v6/#chip talk to Thomas for more info.
|
||||
const AddValuesButton = styled('button')(({ theme }) => ({
|
||||
|
@ -6,7 +6,7 @@ import { styled } from '@mui/material';
|
||||
import { getAllTimezones } from 'countries-and-timezones';
|
||||
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import type { ConstraintValidatorOutput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/constraintValidators';
|
||||
import type { ConstraintValidatorOutput } from './ConstraintValidatorOutput.ts';
|
||||
|
||||
interface IDateSingleValueProps {
|
||||
setValue: (value: string) => void;
|
||||
|
@ -0,0 +1 @@
|
||||
export type ConstraintValidatorOutput = [boolean, string];
|
@ -10,7 +10,7 @@ import {
|
||||
import {
|
||||
filterLegalValues,
|
||||
LegalValueLabel,
|
||||
} from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel';
|
||||
} from 'component/common/NewConstraintAccordion/LegalValueLabel/LegalValueLabel.tsx';
|
||||
import { ConstraintValueSearch } from './ConstraintValueSearch.tsx';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import React from 'react';
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { isValid, parseISO } from 'date-fns';
|
||||
import semver from 'semver';
|
||||
import {
|
||||
type EditableConstraint,
|
||||
isDateConstraint,
|
||||
isNumberConstraint,
|
||||
isSemVerConstraint,
|
||||
} from './editable-constraint-type.js';
|
||||
isDateOperator,
|
||||
isNumOperator,
|
||||
isSemVerOperator,
|
||||
type Operator,
|
||||
} from 'constants/operators.js';
|
||||
export type ConstraintValidationResult = [boolean, string];
|
||||
|
||||
const numberValidator = (value: string): ConstraintValidationResult => {
|
||||
@ -59,14 +59,14 @@ const dateValidator = (value: string): ConstraintValidationResult => {
|
||||
return [true, ''];
|
||||
};
|
||||
|
||||
export const constraintValidator = (constraint: EditableConstraint) => {
|
||||
if (isDateConstraint(constraint)) {
|
||||
export const constraintValidator = (operator: Operator) => {
|
||||
if (isDateOperator(operator)) {
|
||||
return dateValidator;
|
||||
}
|
||||
if (isSemVerConstraint(constraint)) {
|
||||
if (isSemVerOperator(operator)) {
|
||||
return semVerValidator;
|
||||
}
|
||||
if (isNumberConstraint(constraint)) {
|
||||
if (isNumOperator(operator)) {
|
||||
return numberValidator;
|
||||
}
|
||||
return stringListValidator;
|
||||
|
@ -76,7 +76,7 @@ export const useEditableConstraint = (
|
||||
[JSON.stringify(context), localConstraint.contextName],
|
||||
);
|
||||
|
||||
const validator = constraintValidator(localConstraint);
|
||||
const validator = constraintValidator(localConstraint.operator);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Box, Radio, RadioGroup, Typography } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { LegalValueLabel } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/LegalValueLabel/LegalValueLabel';
|
||||
import { LegalValueLabel } from 'component/common/NewConstraintAccordion/LegalValueLabel/LegalValueLabel.tsx';
|
||||
import { useState } from 'react';
|
||||
import useFeatureLifecycleApi from 'hooks/api/actions/useFeatureLifecycleApi/useFeatureLifecycleApi';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
@ -11,18 +11,19 @@ import Input from 'component/common/Input/Input';
|
||||
import { ProjectActionsFormItem } from '../ProjectActionsFormItem.tsx';
|
||||
import { ConstraintOperatorSelect } from 'component/common/NewConstraintAccordion/ConstraintOperatorSelect';
|
||||
import {
|
||||
inOperators,
|
||||
numOperators,
|
||||
type Operator,
|
||||
allOperators,
|
||||
dateOperators,
|
||||
semVerOperators,
|
||||
stringOperators,
|
||||
} from 'constants/operators';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { CaseSensitiveButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton';
|
||||
import { InvertedOperatorButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton';
|
||||
import { ResolveInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput';
|
||||
import { useConstraintInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
|
||||
import { CaseSensitiveButton } from './StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx';
|
||||
import { InvertedOperatorButton } from './StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx';
|
||||
import { constraintValidator } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/useEditableConstraint/constraint-validator.ts';
|
||||
import { ResolveInput } from './ProjectActionsFilterItemInputs/ResolveInput.tsx';
|
||||
|
||||
const StyledDeleteButton = styled(IconButton)({
|
||||
marginRight: '-6px',
|
||||
@ -130,25 +131,30 @@ export const ProjectActionsFilterItem = ({
|
||||
</>
|
||||
);
|
||||
|
||||
// Adapted from `/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader.tsx`
|
||||
const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
|
||||
useState(false);
|
||||
|
||||
const validOperators = allOperators.filter(
|
||||
(operator) => !oneOf(dateOperators, operator),
|
||||
);
|
||||
const validOperators = [
|
||||
...inOperators,
|
||||
...stringOperators,
|
||||
...numOperators,
|
||||
...semVerOperators,
|
||||
];
|
||||
|
||||
const { input, validator, setError, error } = useConstraintInput({
|
||||
contextDefinition: { legalValues: [] },
|
||||
localConstraint: { operator, value, values },
|
||||
});
|
||||
const [error, setError] = useState('');
|
||||
const validator = constraintValidator(operator);
|
||||
|
||||
const validate = () => {
|
||||
stateChanged({
|
||||
...filter,
|
||||
error: undefined,
|
||||
});
|
||||
const [typeValidatorResult, err] = validator();
|
||||
|
||||
if (value === undefined && values === undefined) {
|
||||
return;
|
||||
}
|
||||
const validatorArgs = values ? values : [value || ''];
|
||||
const [typeValidatorResult, err] = validator(...validatorArgs);
|
||||
if (!typeValidatorResult) {
|
||||
setError(err);
|
||||
stateChanged({
|
||||
@ -270,15 +276,11 @@ export const ProjectActionsFilterItem = ({
|
||||
<StyledResolveInputWrapper>
|
||||
<ResolveInput
|
||||
setValues={setValues}
|
||||
setValuesWithRecord={setValues}
|
||||
setValue={setValue}
|
||||
setError={setError}
|
||||
localConstraint={{ value, values }}
|
||||
constraintValues={values || []}
|
||||
constraintValue={value || ''}
|
||||
input={input}
|
||||
operator={operator}
|
||||
error={error}
|
||||
contextDefinition={{ legalValues: [] }}
|
||||
removeValue={removeValue}
|
||||
/>
|
||||
</StyledResolveInputWrapper>
|
||||
|
@ -0,0 +1,78 @@
|
||||
import {
|
||||
isInOperator,
|
||||
isNumOperator,
|
||||
isSemVerOperator,
|
||||
isStringOperator,
|
||||
type Operator,
|
||||
} from 'constants/operators';
|
||||
import type { IConstraint } from 'interfaces/strategy.ts';
|
||||
import { FreeTextInput } from './FreeTextInput/FreeTextInput.tsx';
|
||||
import { SingleValue } from './SingleValue/SingleValue.tsx';
|
||||
|
||||
interface IResolveInputProps {
|
||||
localConstraint: Pick<IConstraint, 'value' | 'values'>;
|
||||
setValue: (value: string) => void;
|
||||
setValues: (values: string[]) => void;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
removeValue: (index: number) => void;
|
||||
operator: Operator;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export const ResolveInput = ({
|
||||
operator,
|
||||
localConstraint,
|
||||
setValue,
|
||||
setValues,
|
||||
setError,
|
||||
removeValue,
|
||||
error,
|
||||
}: IResolveInputProps) => {
|
||||
if (isInOperator(operator)) {
|
||||
return (
|
||||
<FreeTextInput
|
||||
values={localConstraint.values || []}
|
||||
removeValue={removeValue}
|
||||
setValues={setValues}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isStringOperator(operator)) {
|
||||
return (
|
||||
<FreeTextInput
|
||||
values={localConstraint.values || []}
|
||||
removeValue={removeValue}
|
||||
setValues={setValues}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isNumOperator(operator)) {
|
||||
return (
|
||||
<SingleValue
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
type='number'
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSemVerOperator(operator)) {
|
||||
return (
|
||||
<SingleValue
|
||||
setValue={setValue}
|
||||
value={localConstraint.value}
|
||||
type='semver'
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
@ -5,8 +5,8 @@ import {
|
||||
StyledToggleButtonOff,
|
||||
StyledToggleButtonOn,
|
||||
} from '../StyledToggleButton.tsx';
|
||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender.tsx';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender.tsx';
|
||||
|
||||
interface CaseSensitiveButtonProps {
|
||||
localConstraint: Pick<IConstraint, 'caseInsensitive'>;
|
@ -6,7 +6,7 @@ import {
|
||||
StyledToggleButtonOff,
|
||||
StyledToggleButtonOn,
|
||||
} from '../StyledToggleButton.tsx';
|
||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender.tsx';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender.tsx';
|
||||
|
||||
interface InvertedOperatorButtonProps {
|
||||
localConstraint: Pick<IConstraint, 'inverted'>;
|
Loading…
Reference in New Issue
Block a user