mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-21 13:47:39 +02:00
delete legacy constraint accordion (#10110)
This PR continues the cleanup after removing the addEditStrategy flag (part 2 of ???). The primary purpose of this PR is to delete and remove all references to the LegacyConstraintAccordion. I've gone and updated all references to the legacy files in external components and verified manually that they still work. Most of the files in this PR are changing references. I've extracted two bits into more general constants/utils: 1. Constraint IDs are a symbol. it was exported as a const from the previous createEmptyConstraint file. I've moved it into constants. 2. formatOperatorDescription was similarly used all over the place, so I've placed it in the shared utils directory. In reviewing this, you can ignore any changes in the legacy constraint accordion folder, because that's all been deleted. Instead, focus on the changes in the other files. It's primarily just import updates, but would be good to get a second set of eyes, anyway.
This commit is contained in:
parent
fa28ee2d6a
commit
c3dda01d53
@ -24,8 +24,8 @@ import { SidebarModal } from 'component/common/SidebarModal/SidebarModal';
|
||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||
import { FeatureStrategyForm } from '../../../../feature/FeatureStrategy/FeatureStrategyForm/FeatureStrategyForm.tsx';
|
||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { constraintId } from 'constants/constraintId.ts';
|
||||
|
||||
interface IEditChangeProps {
|
||||
change: IChangeRequestAddStrategy | IChangeRequestUpdateStrategy;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { ComponentProps, FC, ReactNode } from 'react';
|
||||
import { StrategyEvaluationItem } from '../StrategyEvaluationItem/StrategyEvaluationItem.tsx';
|
||||
import type { ConstraintSchema } from 'openapi';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { StrategyEvaluationChip } from '../StrategyEvaluationChip/StrategyEvaluationChip.tsx';
|
||||
import { styled, Tooltip } from '@mui/material';
|
||||
import { Truncator } from 'component/common/Truncator/Truncator';
|
||||
@ -11,6 +10,7 @@ import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||
import { useConstraintTooltips } from './hooks/useConstraintTooltips.ts';
|
||||
import { ReactComponent as CaseSensitiveIcon } from 'assets/icons/case-sensitive.svg';
|
||||
import { isCaseSensitive } from './isCaseSensitive.ts';
|
||||
import { formatOperatorDescription } from 'utils/formatOperatorDescription.ts';
|
||||
|
||||
const Operator: FC<{
|
||||
label: ConstraintSchema['operator'];
|
||||
|
@ -1,52 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
import { ConstraintAccordionEdit } from './ConstraintAccordionEdit/ConstraintAccordionEdit.tsx';
|
||||
import { ConstraintAccordionView } from './ConstraintAccordionView/ConstraintAccordionView.tsx';
|
||||
|
||||
interface IConstraintAccordionProps {
|
||||
compact: boolean;
|
||||
editing: boolean;
|
||||
constraint: IConstraint;
|
||||
onCancel: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onSave?: (constraint: IConstraint) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `component/common/NewConstraintAccordion/NewConstraintAccordion`
|
||||
*/
|
||||
export const ConstraintAccordion = ({
|
||||
constraint,
|
||||
compact = false,
|
||||
editing,
|
||||
onEdit,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onSave,
|
||||
}: IConstraintAccordionProps) => {
|
||||
if (!constraint) return null;
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={Boolean(editing && onSave)}
|
||||
show={
|
||||
<ConstraintAccordionEdit
|
||||
constraint={constraint}
|
||||
onCancel={onCancel}
|
||||
onSave={onSave!}
|
||||
onDelete={onDelete}
|
||||
compact={compact}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<ConstraintAccordionView
|
||||
constraint={constraint}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,272 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConstraintAccordionEditBody } from './ConstraintAccordionEditBody/ConstraintAccordionEditBody.tsx';
|
||||
import { ConstraintAccordionEditHeader } from './ConstraintAccordionEditHeader/ConstraintAccordionEditHeader.tsx';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { cleanConstraint } from 'utils/cleanConstraint';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import type { IUnleashContextDefinition } from 'interfaces/context';
|
||||
import { useConstraintInput } from './ConstraintAccordionEditBody/useConstraintInput/useConstraintInput.tsx';
|
||||
import type { Operator } from 'constants/operators';
|
||||
import { ResolveInput } from './ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx';
|
||||
|
||||
interface IConstraintAccordionEditProps {
|
||||
constraint: IConstraint;
|
||||
onCancel: () => void;
|
||||
onSave: (constraint: IConstraint) => void;
|
||||
compact: boolean;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const CANCEL = 'cancel';
|
||||
export const SAVE = 'save';
|
||||
|
||||
const resolveContextDefinition = (
|
||||
context: IUnleashContextDefinition[],
|
||||
contextName: string,
|
||||
): IUnleashContextDefinition => {
|
||||
const definition = context.find(
|
||||
(contextDef) => contextDef.name === contextName,
|
||||
);
|
||||
|
||||
return (
|
||||
definition || {
|
||||
name: '',
|
||||
description: '',
|
||||
createdAt: '',
|
||||
sortOrder: 1,
|
||||
stickiness: false,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const StyledForm = styled('div')({ padding: 0, margin: 0, width: '100%' });
|
||||
|
||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
backgroundColor: theme.palette.background.elevation1,
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
'& .expanded': {
|
||||
'&:before': {
|
||||
opacity: '0 !important',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
border: 'none',
|
||||
padding: theme.spacing(0.5, 3),
|
||||
'&:hover .valuesExpandLabel': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
borderTop: `1px dashed ${theme.palette.divider}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 0,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionEdit = ({
|
||||
constraint,
|
||||
compact,
|
||||
onCancel,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: IConstraintAccordionEditProps) => {
|
||||
const [localConstraint, setLocalConstraint] = useState<IConstraint>(
|
||||
cleanConstraint(constraint),
|
||||
);
|
||||
|
||||
const { context } = useUnleashContext();
|
||||
const [contextDefinition, setContextDefinition] = useState(
|
||||
resolveContextDefinition(context, localConstraint.contextName),
|
||||
);
|
||||
const { validateConstraint } = useFeatureApi();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [action, setAction] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Setting expanded to true on mount will cause the accordion
|
||||
// animation to take effect and transition the expanded accordion in
|
||||
setExpanded(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setContextDefinition(
|
||||
resolveContextDefinition(context, localConstraint.contextName),
|
||||
);
|
||||
}, [localConstraint.contextName, context]);
|
||||
|
||||
const setContextName = useCallback((contextName: string) => {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
contextName,
|
||||
values: [],
|
||||
value: '',
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setOperator = useCallback((operator: Operator) => {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
operator,
|
||||
values: [],
|
||||
value: '',
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setValues = useCallback((values: string[]) => {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
values,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setValue = useCallback((value: string) => {
|
||||
setLocalConstraint((prev) => ({ ...prev, value }));
|
||||
}, []);
|
||||
|
||||
const setInvertedOperator = () => {
|
||||
setLocalConstraint((prev) => ({ ...prev, inverted: !prev.inverted }));
|
||||
};
|
||||
|
||||
const setCaseInsensitive = useCallback(() => {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
caseInsensitive: !prev.caseInsensitive,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const removeValue = useCallback(
|
||||
(index: number) => {
|
||||
const valueCopy = [...localConstraint.values!];
|
||||
valueCopy.splice(index, 1);
|
||||
|
||||
setValues(valueCopy);
|
||||
},
|
||||
[localConstraint, setValues],
|
||||
);
|
||||
|
||||
const triggerTransition = () => {
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const validateConstraintValues = () => {
|
||||
const hasValues =
|
||||
Array.isArray(localConstraint.values) &&
|
||||
Boolean(localConstraint.values.length > 0);
|
||||
const hasValue = Boolean(localConstraint.value);
|
||||
|
||||
if (hasValues || hasValue) {
|
||||
setError('');
|
||||
return true;
|
||||
}
|
||||
setError('You must provide a value for the constraint');
|
||||
return false;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const hasValues = validateConstraintValues();
|
||||
if (!hasValues) return;
|
||||
const [typeValidatorResult, err] = validator();
|
||||
|
||||
if (!typeValidatorResult) {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
if (typeValidatorResult) {
|
||||
try {
|
||||
await validateConstraint(localConstraint);
|
||||
setError('');
|
||||
setAction(SAVE);
|
||||
triggerTransition();
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
setError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { input, validator, setError, error } = useConstraintInput({
|
||||
contextDefinition,
|
||||
localConstraint,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setError('');
|
||||
setLocalConstraint((localConstraint) =>
|
||||
cleanConstraint(localConstraint),
|
||||
);
|
||||
}, [localConstraint.operator, localConstraint.contextName, setError]);
|
||||
|
||||
return (
|
||||
<StyledForm>
|
||||
<StyledAccordion
|
||||
expanded={expanded}
|
||||
TransitionProps={{
|
||||
onExited: () => {
|
||||
if (action === CANCEL) {
|
||||
setAction('');
|
||||
onCancel();
|
||||
} else if (action === SAVE) {
|
||||
setAction('');
|
||||
onSave(localConstraint);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledAccordionSummary>
|
||||
<ConstraintAccordionEditHeader
|
||||
localConstraint={localConstraint}
|
||||
setLocalConstraint={setLocalConstraint}
|
||||
setContextName={setContextName}
|
||||
setOperator={setOperator}
|
||||
action={action}
|
||||
compact={compact}
|
||||
setInvertedOperator={setInvertedOperator}
|
||||
setCaseInsensitive={setCaseInsensitive}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
|
||||
<StyledAccordionDetails>
|
||||
<ConstraintAccordionEditBody
|
||||
localConstraint={localConstraint}
|
||||
setValues={setValues}
|
||||
setValue={setValue}
|
||||
triggerTransition={triggerTransition}
|
||||
setAction={setAction}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ResolveInput
|
||||
setValues={setValues}
|
||||
setValue={setValue}
|
||||
setError={setError}
|
||||
localConstraint={localConstraint}
|
||||
constraintValues={constraint?.values || []}
|
||||
constraintValue={constraint?.value || ''}
|
||||
input={input}
|
||||
error={error}
|
||||
contextDefinition={contextDefinition}
|
||||
removeValue={removeValue}
|
||||
/>
|
||||
</ConstraintAccordionEditBody>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
@ -1,76 +0,0 @@
|
||||
import { Button, styled } from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { CANCEL } from '../ConstraintAccordionEdit.tsx';
|
||||
|
||||
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',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
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>
|
||||
<StyledRightButton
|
||||
onClick={() => {
|
||||
setAction(CANCEL);
|
||||
triggerTransition();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</StyledRightButton>
|
||||
</StyledInputButtonContainer>
|
||||
</StyledButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
const StyledHeader = styled('h3')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.bodySize,
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintFormHeader: React.FC<
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
> = ({ children, ...rest }) => {
|
||||
return <StyledHeader {...rest}>{children}</StyledHeader>;
|
||||
};
|
@ -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,84 +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 NewConstraintAccordion components
|
||||
*/
|
||||
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,173 +0,0 @@
|
||||
import { Button, Chip } from '@mui/material';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import { parseParameterStrings } from 'utils/parseParameter';
|
||||
|
||||
interface IFreeTextInputProps {
|
||||
values: string[];
|
||||
removeValue: (index: number) => void;
|
||||
setValues: (values: string[]) => void;
|
||||
beforeValues?: JSX.Element;
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme) => ({
|
||||
valueChip: {
|
||||
margin: '0 0.5rem 0.5rem 0',
|
||||
},
|
||||
chipValue: {
|
||||
whiteSpace: 'pre',
|
||||
},
|
||||
inputContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(700)]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
},
|
||||
inputInnerContainer: {
|
||||
minWidth: '300px',
|
||||
[theme.breakpoints.down(700)]: {
|
||||
minWidth: '100%',
|
||||
},
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
margin: '1rem 0',
|
||||
},
|
||||
button: {
|
||||
marginLeft: '1rem',
|
||||
[theme.breakpoints.down(700)]: {
|
||||
marginLeft: 0,
|
||||
marginBottom: '0.5rem',
|
||||
},
|
||||
},
|
||||
valuesContainer: { marginTop: '1rem' },
|
||||
}));
|
||||
|
||||
const ENTER = 'Enter';
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const FreeTextInput = ({
|
||||
values,
|
||||
removeValue,
|
||||
setValues,
|
||||
error,
|
||||
setError,
|
||||
}: IFreeTextInputProps) => {
|
||||
const [inputValues, setInputValues] = useState('');
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const onKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === ENTER) {
|
||||
event.preventDefault();
|
||||
addValues();
|
||||
}
|
||||
};
|
||||
|
||||
const addValues = () => {
|
||||
const newValues = uniqueValues([
|
||||
...values,
|
||||
...parseParameterStrings(inputValues),
|
||||
]);
|
||||
|
||||
if (newValues.length === 0) {
|
||||
setError('values cannot be empty');
|
||||
} else if (newValues.some((v) => v.length > 100)) {
|
||||
setError('values cannot be longer than 100 characters');
|
||||
} else {
|
||||
setError('');
|
||||
setInputValues('');
|
||||
setValues(newValues);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConstraintFormHeader style={{ marginBottom: 0 }}>
|
||||
Set values (maximum 100 char length per value)
|
||||
</ConstraintFormHeader>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputInnerContainer}>
|
||||
<Input
|
||||
onKeyPress={onKeyPress}
|
||||
label='Values'
|
||||
name='values'
|
||||
value={inputValues}
|
||||
onFocus={() => {
|
||||
setError('');
|
||||
}}
|
||||
onChange={(e) => setInputValues(e.target.value)}
|
||||
placeholder='value1, value2, value3...'
|
||||
className={styles.input}
|
||||
error={Boolean(error)}
|
||||
errorText={error}
|
||||
data-testid='CONSTRAINT_VALUES_INPUT'
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
onClick={() => addValues()}
|
||||
data-testid='CONSTRAINT_VALUES_ADD_BUTTON'
|
||||
>
|
||||
Add values
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.valuesContainer}>
|
||||
<ConstraintValueChips
|
||||
values={values}
|
||||
removeValue={removeValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IConstraintValueChipsProps {
|
||||
values: string[];
|
||||
removeValue: (index: number) => void;
|
||||
}
|
||||
|
||||
const ConstraintValueChips = ({
|
||||
values,
|
||||
removeValue,
|
||||
}: IConstraintValueChipsProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
return (
|
||||
<>
|
||||
{values.map((value, index) => {
|
||||
// Key is not ideal, but we don't have anything guaranteed to
|
||||
// be unique here.
|
||||
return (
|
||||
<Chip
|
||||
label={
|
||||
<StringTruncator
|
||||
text={value}
|
||||
maxLength={35}
|
||||
maxWidth='100'
|
||||
className={styles.chipValue}
|
||||
/>
|
||||
}
|
||||
key={`${value}-${index}`}
|
||||
onDelete={() => removeValue(index)}
|
||||
className={styles.valueChip}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const uniqueValues = <T,>(values: T[]): T[] => {
|
||||
return Array.from(new Set(values));
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export const useStyles = makeStyles()((theme) => ({
|
||||
container: {
|
||||
display: 'inline-block',
|
||||
wordBreak: 'break-word',
|
||||
padding: theme.spacing(0.5, 1),
|
||||
background: theme.palette.background.paper,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
'&:hover': {
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
},
|
||||
},
|
||||
value: {
|
||||
lineHeight: 1.33,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
},
|
||||
description: {
|
||||
lineHeight: 1.33,
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
color: theme.palette.action.active,
|
||||
},
|
||||
}));
|
@ -1,42 +0,0 @@
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import { useStyles } from './LegalValueLabel.styles';
|
||||
import type React from 'react';
|
||||
import { FormControlLabel } from '@mui/material';
|
||||
|
||||
interface ILegalValueTextProps {
|
||||
legal: ILegalValue;
|
||||
control: React.ReactElement;
|
||||
}
|
||||
|
||||
export const LegalValueLabel = ({ legal, control }: ILegalValueTextProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<FormControlLabel
|
||||
value={legal.value}
|
||||
control={control}
|
||||
label={
|
||||
<>
|
||||
<div className={styles.value}>{legal.value}</div>
|
||||
<div className={styles.description}>
|
||||
{legal.description}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const filterLegalValues = (
|
||||
legalValues: ILegalValue[],
|
||||
filter: string,
|
||||
): ILegalValue[] => {
|
||||
return legalValues.filter((legalValue) => {
|
||||
return legalValue.value.includes(filter);
|
||||
});
|
||||
};
|
@ -1,193 +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: IUnleashContextDefinition;
|
||||
localConstraint: IConstraint;
|
||||
constraintValues: string[];
|
||||
constraintValue: string;
|
||||
setValue: (value: string) => void;
|
||||
setValues: (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 use NewConstraintAccordion components
|
||||
*/
|
||||
export const ResolveInput = ({
|
||||
input,
|
||||
contextDefinition,
|
||||
constraintValues,
|
||||
constraintValue,
|
||||
localConstraint,
|
||||
setValue,
|
||||
setValues,
|
||||
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 || []}
|
||||
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={setValues}
|
||||
error={error}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
case STRING_OPERATORS_FREETEXT:
|
||||
return (
|
||||
<>
|
||||
<FreeTextInput
|
||||
values={localConstraint.values || []}
|
||||
removeValue={removeValue}
|
||||
setValues={setValues}
|
||||
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,52 +0,0 @@
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { RestrictiveLegalValues } from './RestrictiveLegalValues.tsx';
|
||||
|
||||
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={() => {}}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText(
|
||||
'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:',
|
||||
);
|
||||
});
|
||||
|
||||
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}
|
||||
error={''}
|
||||
setError={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(localValues).toEqual(['value2']);
|
||||
});
|
@ -1,177 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Alert, Checkbox, styled } from '@mui/material';
|
||||
import { useThemeStyles } from 'themes/themeStyles';
|
||||
import { ConstraintValueSearch } from 'component/common/LegacyConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import {
|
||||
filterLegalValues,
|
||||
LegalValueLabel,
|
||||
} from '../LegalValueLabel/LegalValueLabel.tsx';
|
||||
|
||||
interface IRestrictiveLegalValuesProps {
|
||||
data: {
|
||||
legalValues: ILegalValue[];
|
||||
deletedLegalValues: ILegalValue[];
|
||||
};
|
||||
constraintValues: string[];
|
||||
values: string[];
|
||||
setValues: (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) => deletedValuesSet.has(value));
|
||||
};
|
||||
|
||||
const StyledValuesContainer = 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',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const RestrictiveLegalValues = ({
|
||||
data,
|
||||
values,
|
||||
setValues,
|
||||
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 { classes: styles } = useThemeStyles();
|
||||
|
||||
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);
|
||||
setValues(newValues);
|
||||
return;
|
||||
}
|
||||
|
||||
setValues([...cleanDeletedLegalValues(values), legalValue]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
|
||||
<ConstraintFormHeader>
|
||||
Select values from a predefined set
|
||||
</ConstraintFormHeader>
|
||||
<ConditionallyRender
|
||||
condition={legalValues.length > 100}
|
||||
show={
|
||||
<ConstraintValueSearch
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<StyledValuesContainer>
|
||||
{filteredValues.map((match) => (
|
||||
<LegalValueLabel
|
||||
key={match.value}
|
||||
legal={match}
|
||||
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={<p className={styles.error}>{error}</p>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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,116 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import { FormControl, RadioGroup, Radio, Alert } from '@mui/material';
|
||||
import { ConstraintValueSearch } from 'component/common/LegacyConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
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={
|
||||
<FormControl component='fieldset'>
|
||||
<RadioGroup
|
||||
aria-label='selected-value'
|
||||
name='selected'
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setError('');
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
>
|
||||
{filteredValues.map((match) => (
|
||||
<LegalValueLabel
|
||||
key={match.value}
|
||||
legal={match}
|
||||
control={<Radio />}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
}
|
||||
elseShow={
|
||||
<p>No valid legal values available for this operator.</p>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(error)}
|
||||
show={<p className={styles.error}>{error}</p>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,55 +0,0 @@
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
|
||||
interface ISingleValueProps {
|
||||
setValue: (value: string) => void;
|
||||
value?: string;
|
||||
type: string;
|
||||
error: string;
|
||||
setError: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()((theme) => ({
|
||||
singleValueContainer: { maxWidth: '300px', marginTop: '-1rem' },
|
||||
singleValueInput: {
|
||||
width: '100%',
|
||||
margin: '1rem 0',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const SingleValue = ({
|
||||
setValue,
|
||||
value,
|
||||
type,
|
||||
error,
|
||||
setError,
|
||||
}: ISingleValueProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
return (
|
||||
<>
|
||||
<ConstraintFormHeader>
|
||||
Add a single {type.toLowerCase()} value
|
||||
</ConstraintFormHeader>
|
||||
<div className={styles.singleValueContainer}>
|
||||
<Input
|
||||
label={type}
|
||||
name='value'
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
setError('');
|
||||
setValue(e.target.value.trim());
|
||||
}}
|
||||
onFocus={() => setError('')}
|
||||
placeholder={`Enter a single ${type} value`}
|
||||
className={styles.singleValueInput}
|
||||
error={Boolean(error)}
|
||||
errorText={error}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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: IUnleashContextDefinition;
|
||||
localConstraint: IConstraint;
|
||||
}
|
||||
|
||||
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 use NewConstraintAccordion components
|
||||
*/
|
||||
export const useConstraintInput = ({
|
||||
contextDefinition,
|
||||
localConstraint,
|
||||
}: IUseConstraintInputProps): IUseConstraintOutput => {
|
||||
const [input, setInput] = useState<Input>(IN_OPERATORS_LEGAL_VALUES);
|
||||
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 };
|
||||
};
|
@ -1,212 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||
import { ConstraintIcon } from 'component/common/LegacyConstraintAccordion/ConstraintIcon';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
dateOperators,
|
||||
DATE_AFTER,
|
||||
IN,
|
||||
stringOperators,
|
||||
} from 'constants/operators';
|
||||
import { resolveText } from './helpers.ts';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Operator } from 'constants/operators';
|
||||
import { ConstraintOperatorSelect } from 'component/common/LegacyConstraintAccordion/ConstraintOperatorSelect';
|
||||
import {
|
||||
operatorsForContext,
|
||||
CURRENT_TIME_CONTEXT_FIELD,
|
||||
} from 'utils/operatorsForContext';
|
||||
import { InvertedOperatorButton } from '../StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx';
|
||||
import { CaseSensitiveButton } from '../StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx';
|
||||
import { ConstraintAccordionHeaderActions } from '../../ConstraintAccordionHeaderActions/ConstraintAccordionHeaderActions.tsx';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IConstraintAccordionViewHeader {
|
||||
localConstraint: IConstraint;
|
||||
setContextName: (contextName: string) => void;
|
||||
setOperator: (operator: Operator) => void;
|
||||
setLocalConstraint: React.Dispatch<React.SetStateAction<IConstraint>>;
|
||||
action: string;
|
||||
compact: boolean;
|
||||
onDelete?: () => void;
|
||||
setInvertedOperator: () => void;
|
||||
setCaseInsensitive: () => void;
|
||||
}
|
||||
|
||||
const StyledHeaderContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
}));
|
||||
const StyledSelectContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(770)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
const StyledBottomSelect = styled('div')(({ theme }) => ({
|
||||
[theme.breakpoints.down(770)]: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
display: 'inline-flex',
|
||||
}));
|
||||
|
||||
const StyledHeaderSelect = styled('div')(({ theme }) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
width: '200px',
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
width: '170px',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
width: '200px',
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
width: '170px',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeaderText = styled('p')(({ theme }) => ({
|
||||
maxWidth: '400px',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
[theme.breakpoints.down('xl')]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionEditHeader = ({
|
||||
compact,
|
||||
localConstraint,
|
||||
setLocalConstraint,
|
||||
setContextName,
|
||||
setOperator,
|
||||
onDelete,
|
||||
setInvertedOperator,
|
||||
setCaseInsensitive,
|
||||
}: IConstraintAccordionViewHeader) => {
|
||||
const { context } = useUnleashContext();
|
||||
const { contextName, operator } = localConstraint;
|
||||
const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
|
||||
useState(false);
|
||||
|
||||
/* We need a special case to handle the currentTime context field. Since
|
||||
this field will be the only one to allow DATE_BEFORE and DATE_AFTER operators
|
||||
this will check if the context field is the current time context field AND check
|
||||
if it is not already using one of the date operators (to not overwrite if there is existing
|
||||
data). */
|
||||
useEffect(() => {
|
||||
if (
|
||||
contextName === CURRENT_TIME_CONTEXT_FIELD &&
|
||||
!oneOf(dateOperators, operator)
|
||||
) {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
operator: DATE_AFTER,
|
||||
value: new Date().toISOString(),
|
||||
}));
|
||||
} else if (
|
||||
contextName !== CURRENT_TIME_CONTEXT_FIELD &&
|
||||
oneOf(dateOperators, operator)
|
||||
) {
|
||||
setOperator(IN);
|
||||
}
|
||||
|
||||
if (oneOf(stringOperators, operator)) {
|
||||
setShowCaseSensitiveButton(true);
|
||||
} else {
|
||||
setShowCaseSensitiveButton(false);
|
||||
}
|
||||
}, [contextName, setOperator, operator, setLocalConstraint]);
|
||||
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constraintNameOptions = context.map((context) => {
|
||||
return { key: context.name, label: context.name };
|
||||
});
|
||||
|
||||
const onOperatorChange = (operator: Operator) => {
|
||||
if (oneOf(stringOperators, operator)) {
|
||||
setShowCaseSensitiveButton(true);
|
||||
} else {
|
||||
setShowCaseSensitiveButton(false);
|
||||
}
|
||||
|
||||
if (oneOf(dateOperators, operator)) {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
operator: operator,
|
||||
value: new Date().toISOString(),
|
||||
}));
|
||||
} else {
|
||||
setOperator(operator);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderContainer>
|
||||
<ConstraintIcon />
|
||||
<StyledSelectContainer>
|
||||
<div>
|
||||
<StyledGeneralSelect
|
||||
id='context-field-select'
|
||||
name='contextName'
|
||||
label='Context Field'
|
||||
autoFocus
|
||||
options={constraintNameOptions}
|
||||
value={contextName || ''}
|
||||
onChange={setContextName}
|
||||
/>
|
||||
</div>
|
||||
<StyledBottomSelect>
|
||||
<InvertedOperatorButton
|
||||
localConstraint={localConstraint}
|
||||
setInvertedOperator={setInvertedOperator}
|
||||
/>
|
||||
<StyledHeaderSelect>
|
||||
<ConstraintOperatorSelect
|
||||
options={operatorsForContext(contextName)}
|
||||
value={operator}
|
||||
onChange={onOperatorChange}
|
||||
/>
|
||||
</StyledHeaderSelect>
|
||||
<ConditionallyRender
|
||||
condition={showCaseSensitiveButton}
|
||||
show={
|
||||
<CaseSensitiveButton
|
||||
localConstraint={localConstraint}
|
||||
setCaseInsensitive={setCaseInsensitive}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledBottomSelect>
|
||||
</StyledSelectContainer>
|
||||
<ConditionallyRender
|
||||
condition={!compact}
|
||||
show={
|
||||
<StyledHeaderText>
|
||||
{resolveText(operator, contextName)}
|
||||
</StyledHeaderText>
|
||||
}
|
||||
/>
|
||||
<ConstraintAccordionHeaderActions onDelete={onDelete} disableEdit />
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
};
|
@ -1,82 +0,0 @@
|
||||
import {
|
||||
DATE_BEFORE,
|
||||
DATE_AFTER,
|
||||
IN,
|
||||
NOT_IN,
|
||||
NUM_EQ,
|
||||
NUM_GT,
|
||||
NUM_GTE,
|
||||
NUM_LT,
|
||||
NUM_LTE,
|
||||
STR_CONTAINS,
|
||||
STR_ENDS_WITH,
|
||||
STR_STARTS_WITH,
|
||||
SEMVER_EQ,
|
||||
SEMVER_GT,
|
||||
SEMVER_LT,
|
||||
type Operator,
|
||||
} from 'constants/operators';
|
||||
|
||||
export const resolveText = (operator: Operator, contextName: string) => {
|
||||
const base = `To satisfy this constraint, values passed into the SDK as ${contextName} must`;
|
||||
|
||||
if (operator === IN) {
|
||||
return `${base} include:`;
|
||||
}
|
||||
|
||||
if (operator === NOT_IN) {
|
||||
return `${base} not include:`;
|
||||
}
|
||||
|
||||
if (operator === STR_ENDS_WITH) {
|
||||
return `${base} end with:`;
|
||||
}
|
||||
|
||||
if (operator === STR_STARTS_WITH) {
|
||||
return `${base} start with:`;
|
||||
}
|
||||
|
||||
if (operator === STR_CONTAINS) {
|
||||
return `${base} contain:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_EQ) {
|
||||
return `${base} match:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_GT) {
|
||||
return `${base} be greater than:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_GTE) {
|
||||
return `${base} be greater than or equal to:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_LT) {
|
||||
return `${base} be less than:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_LTE) {
|
||||
return `${base} be less than or equal to:`;
|
||||
}
|
||||
|
||||
if (operator === DATE_AFTER) {
|
||||
return `${base} be after the following date`;
|
||||
}
|
||||
|
||||
if (operator === DATE_BEFORE) {
|
||||
return `${base} be before the following date:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_EQ) {
|
||||
return `${base} match the following version:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_GT) {
|
||||
return `${base} be greater than the following version:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_LT) {
|
||||
return `${base} be less than the following version:`;
|
||||
}
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
import { Tooltip, Box } from '@mui/material';
|
||||
import { ReactComponent as CaseSensitive } from 'assets/icons/24_Text format.svg';
|
||||
import { ReactComponent as CaseSensitiveOff } from 'assets/icons/24_Text format off.svg';
|
||||
import {
|
||||
StyledToggleButtonOff,
|
||||
StyledToggleButtonOn,
|
||||
} from '../StyledToggleButton.tsx';
|
||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender.tsx';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
interface CaseSensitiveButtonProps {
|
||||
localConstraint: IConstraint;
|
||||
setCaseInsensitive: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const CaseSensitiveButton = ({
|
||||
localConstraint,
|
||||
setCaseInsensitive,
|
||||
}: CaseSensitiveButtonProps) => (
|
||||
<Tooltip
|
||||
title={
|
||||
localConstraint.caseInsensitive
|
||||
? 'Make it case sensitive'
|
||||
: 'Make it case insensitive'
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.caseInsensitive)}
|
||||
show={
|
||||
<StyledToggleButtonOff
|
||||
onClick={setCaseInsensitive}
|
||||
disableRipple
|
||||
>
|
||||
<CaseSensitiveOff />
|
||||
</StyledToggleButtonOff>
|
||||
}
|
||||
elseShow={
|
||||
<StyledToggleButtonOn
|
||||
className='operator-is-active'
|
||||
onClick={setCaseInsensitive}
|
||||
disableRipple
|
||||
>
|
||||
<CaseSensitive />
|
||||
</StyledToggleButtonOn>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
@ -1,50 +0,0 @@
|
||||
import { Box, Tooltip } from '@mui/material';
|
||||
import { ReactComponent as NegatedOnIcon } from 'assets/icons/not_operator_selected.svg';
|
||||
import { ReactComponent as NegatedOffIcon } from 'assets/icons/not_operator_unselected.svg';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import {
|
||||
StyledToggleButtonOff,
|
||||
StyledToggleButtonOn,
|
||||
} from '../StyledToggleButton.tsx';
|
||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender.tsx';
|
||||
|
||||
interface InvertedOperatorButtonProps {
|
||||
localConstraint: IConstraint;
|
||||
setInvertedOperator: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const InvertedOperatorButton = ({
|
||||
localConstraint,
|
||||
setInvertedOperator,
|
||||
}: InvertedOperatorButtonProps) => (
|
||||
<Tooltip
|
||||
title={localConstraint.inverted ? 'Remove negation' : 'Negate operator'}
|
||||
arrow
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(localConstraint.inverted)}
|
||||
show={
|
||||
<StyledToggleButtonOn
|
||||
className='operator-is-active'
|
||||
onClick={setInvertedOperator}
|
||||
disableRipple
|
||||
>
|
||||
<NegatedOnIcon />
|
||||
</StyledToggleButtonOn>
|
||||
}
|
||||
elseShow={
|
||||
<StyledToggleButtonOff
|
||||
onClick={setInvertedOperator}
|
||||
disableRipple
|
||||
>
|
||||
<NegatedOffIcon />
|
||||
</StyledToggleButtonOff>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
@ -1,45 +0,0 @@
|
||||
import { styled } from '@mui/system';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const StyledToggleButtonOff = styled(IconButton)(({ theme }) => ({
|
||||
width: '28px',
|
||||
minWidth: '28px',
|
||||
maxWidth: '28px',
|
||||
height: 'auto',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: '0 1px 0',
|
||||
marginRight: '1rem',
|
||||
'&:hover': {
|
||||
background: theme.palette.background.application,
|
||||
},
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
marginRight: '0.5rem',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const StyledToggleButtonOn = styled(IconButton)(({ theme }) => ({
|
||||
width: '28px',
|
||||
minWidth: '28px',
|
||||
maxWidth: '28px',
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginRight: '1rem',
|
||||
padding: '0 1px 0',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.contrastText,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
},
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
marginRight: '0.5rem',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
@ -1,77 +0,0 @@
|
||||
import type React from 'react';
|
||||
import { IconButton, styled, Tooltip } from '@mui/material';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import Edit from '@mui/icons-material/Edit';
|
||||
import { ConditionallyRender } from '../../ConditionallyRender/ConditionallyRender.tsx';
|
||||
|
||||
interface ConstraintAccordionHeaderActionsProps {
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
disableEdit?: boolean;
|
||||
disableDelete?: boolean;
|
||||
}
|
||||
|
||||
const StyledHeaderActions = styled('div')(({ theme }) => ({
|
||||
marginLeft: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionHeaderActions = ({
|
||||
onEdit,
|
||||
onDelete,
|
||||
disableDelete = false,
|
||||
disableEdit = false,
|
||||
}: ConstraintAccordionHeaderActionsProps) => {
|
||||
const onEditClick =
|
||||
onEdit &&
|
||||
((event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
onEdit();
|
||||
});
|
||||
|
||||
const onDeleteClick =
|
||||
onDelete &&
|
||||
((event: React.SyntheticEvent) => {
|
||||
event.stopPropagation();
|
||||
onDelete();
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledHeaderActions>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(onEditClick) && !disableEdit}
|
||||
show={
|
||||
<Tooltip title='Edit constraint' arrow>
|
||||
<IconButton
|
||||
type='button'
|
||||
onClick={onEditClick}
|
||||
disabled={disableEdit}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(onDeleteClick) && !disableDelete}
|
||||
show={
|
||||
<Tooltip title='Delete constraint' arrow>
|
||||
<IconButton
|
||||
type='button'
|
||||
onClick={onDeleteClick}
|
||||
disabled={disableDelete}
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</StyledHeaderActions>
|
||||
);
|
||||
};
|
@ -1,261 +0,0 @@
|
||||
import type React from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
type RefObject,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
import { Button, styled, Tooltip } from '@mui/material';
|
||||
import HelpOutline from '@mui/icons-material/HelpOutline';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConstraintAccordion } from 'component/common/LegacyConstraintAccordion/ConstraintAccordion';
|
||||
import produce from 'immer';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { type IUseWeakMap, useWeakMap } from 'hooks/useWeakMap';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { createEmptyConstraint } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
|
||||
export interface IConstraintAccordionListProps {
|
||||
constraints: IConstraint[];
|
||||
setConstraints?: React.Dispatch<React.SetStateAction<IConstraint[]>>;
|
||||
showCreateButton?: boolean;
|
||||
/* Add "constraints" title on the top - default `true` */
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
// Ref methods exposed by this component.
|
||||
export interface IConstraintAccordionListRef {
|
||||
addConstraint?: (contextName: string) => void;
|
||||
}
|
||||
|
||||
// Extra form state for each constraint.
|
||||
interface IConstraintAccordionListItemState {
|
||||
// Is the constraint new (never been saved)?
|
||||
new?: boolean;
|
||||
// Is the constraint currently being edited?
|
||||
editing?: boolean;
|
||||
}
|
||||
|
||||
export const constraintAccordionListId = 'constraintAccordionListId';
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'&.constraint-list-element': {
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHelpWrapper = styled(Tooltip)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(0.75),
|
||||
height: theme.spacing(1.5),
|
||||
}));
|
||||
|
||||
const StyledHelp = styled(HelpOutline)(({ theme }) => ({
|
||||
fill: theme.palette.action.active,
|
||||
[theme.breakpoints.down(860)]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledConstraintLabel = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.secondary,
|
||||
}));
|
||||
|
||||
const StyledAddCustomLabel = styled('div')(({ theme }) => ({
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
display: 'flex',
|
||||
}));
|
||||
|
||||
export const useConstraintAccordionList = (
|
||||
setConstraints:
|
||||
| React.Dispatch<React.SetStateAction<IConstraint[]>>
|
||||
| undefined,
|
||||
ref: React.RefObject<IConstraintAccordionListRef>,
|
||||
) => {
|
||||
const state = useWeakMap<IConstraint, IConstraintAccordionListItemState>();
|
||||
const { context } = useUnleashContext();
|
||||
|
||||
const addConstraint =
|
||||
setConstraints &&
|
||||
((contextName: string) => {
|
||||
const constraint = createEmptyConstraint(contextName);
|
||||
state.set(constraint, { editing: true, new: true });
|
||||
setConstraints((prev) => [...prev, constraint]);
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addConstraint,
|
||||
}));
|
||||
|
||||
const onAdd =
|
||||
addConstraint &&
|
||||
(() => {
|
||||
addConstraint(context[0].name);
|
||||
});
|
||||
|
||||
return { onAdd, state, context };
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionList = forwardRef<
|
||||
IConstraintAccordionListRef | undefined,
|
||||
IConstraintAccordionListProps
|
||||
>(
|
||||
(
|
||||
{ constraints, setConstraints, showCreateButton, showLabel = true },
|
||||
ref,
|
||||
) => {
|
||||
const { onAdd, state, context } = useConstraintAccordionList(
|
||||
setConstraints,
|
||||
ref as RefObject<IConstraintAccordionListRef>,
|
||||
);
|
||||
|
||||
if (context.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer id={constraintAccordionListId}>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
constraints && constraints.length > 0 && showLabel
|
||||
}
|
||||
show={
|
||||
<StyledConstraintLabel>
|
||||
Constraints
|
||||
</StyledConstraintLabel>
|
||||
}
|
||||
/>
|
||||
<ConstraintList
|
||||
ref={ref}
|
||||
setConstraints={setConstraints}
|
||||
constraints={constraints}
|
||||
state={state}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(showCreateButton && onAdd)}
|
||||
show={
|
||||
<div>
|
||||
<StyledAddCustomLabel>
|
||||
<p>Add any number of constraints</p>
|
||||
<StyledHelpWrapper
|
||||
title='View constraints documentation'
|
||||
arrow
|
||||
>
|
||||
<a
|
||||
href={
|
||||
'https://docs.getunleash.io/reference/activation-strategies#constraints'
|
||||
}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<StyledHelp />
|
||||
</a>
|
||||
</StyledHelpWrapper>
|
||||
</StyledAddCustomLabel>
|
||||
<Button
|
||||
type='button'
|
||||
onClick={onAdd}
|
||||
variant='outlined'
|
||||
color='primary'
|
||||
data-testid='ADD_CONSTRAINT_BUTTON'
|
||||
>
|
||||
Add constraint
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface IConstraintList {
|
||||
constraints: IConstraint[];
|
||||
setConstraints?: React.Dispatch<React.SetStateAction<IConstraint[]>>;
|
||||
state: IUseWeakMap<IConstraint, IConstraintAccordionListItemState>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintList = forwardRef<
|
||||
IConstraintAccordionListRef | undefined,
|
||||
IConstraintList
|
||||
>(({ constraints, setConstraints, state }, ref) => {
|
||||
const { context } = useUnleashContext();
|
||||
|
||||
const onEdit =
|
||||
setConstraints &&
|
||||
((constraint: IConstraint) => {
|
||||
state.set(constraint, { editing: true });
|
||||
});
|
||||
|
||||
const onRemove =
|
||||
setConstraints &&
|
||||
((index: number) => {
|
||||
const constraint = constraints[index];
|
||||
state.set(constraint, {});
|
||||
setConstraints(
|
||||
produce((draft) => {
|
||||
draft.splice(index, 1);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const onSave =
|
||||
setConstraints &&
|
||||
((index: number, constraint: IConstraint) => {
|
||||
state.set(constraint, {});
|
||||
setConstraints(
|
||||
produce((draft) => {
|
||||
draft[index] = constraint;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const onCancel = (index: number) => {
|
||||
const constraint = constraints[index];
|
||||
state.get(constraint)?.new && onRemove?.(index);
|
||||
state.set(constraint, {});
|
||||
};
|
||||
|
||||
if (context.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
id={constraintAccordionListId}
|
||||
className='constraint-list-element'
|
||||
>
|
||||
{constraints.map((constraint, index) => (
|
||||
<Fragment key={objectId(constraint)}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
/>
|
||||
<ConstraintAccordion
|
||||
constraint={constraint}
|
||||
onEdit={onEdit?.bind(null, constraint)}
|
||||
onCancel={onCancel.bind(null, index)}
|
||||
onDelete={onRemove?.bind(null, index)}
|
||||
onSave={onSave?.bind(null, index)}
|
||||
editing={Boolean(state.get(constraint)?.editing)}
|
||||
compact
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
});
|
@ -1,25 +0,0 @@
|
||||
import { dateOperators } from 'constants/operators';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import { operatorsForContext } from 'utils/operatorsForContext';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const constraintId = Symbol('id');
|
||||
|
||||
export const createEmptyConstraint = (contextName: string): IConstraint => {
|
||||
const operator = operatorsForContext(contextName)[0];
|
||||
|
||||
const value = oneOf(dateOperators, operator)
|
||||
? new Date().toISOString()
|
||||
: '';
|
||||
|
||||
return {
|
||||
contextName,
|
||||
operator,
|
||||
value,
|
||||
values: [],
|
||||
caseInsensitive: false,
|
||||
inverted: false,
|
||||
[constraintId]: uuidv4(),
|
||||
};
|
||||
};
|
@ -1,123 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
type SxProps,
|
||||
type Theme,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConstraintAccordionViewBody } from './ConstraintAccordionViewBody/ConstraintAccordionViewBody.tsx';
|
||||
import { ConstraintAccordionViewHeader } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionViewHeader/ConstraintAccordionViewHeader';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import {
|
||||
dateOperators,
|
||||
numOperators,
|
||||
semVerOperators,
|
||||
} from 'constants/operators';
|
||||
|
||||
interface IConstraintAccordionViewProps {
|
||||
constraint: IConstraint;
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
sx?: SxProps<Theme>;
|
||||
compact?: boolean;
|
||||
disabled?: boolean;
|
||||
renderAfter?: JSX.Element;
|
||||
}
|
||||
|
||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
backgroundColor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
'&:before': {
|
||||
opacity: '0',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
'& .root': {
|
||||
border: 'none',
|
||||
padding: theme.spacing(0.5, 3),
|
||||
'&:hover .valuesExpandLabel': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
},
|
||||
userSelect: 'auto',
|
||||
WebkitUserSelect: 'auto',
|
||||
MozUserSelect: 'auto',
|
||||
MsUserSelect: 'auto',
|
||||
}));
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
borderTop: `1px dashed ${theme.palette.divider}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionView = ({
|
||||
constraint,
|
||||
onEdit,
|
||||
onDelete,
|
||||
sx = undefined,
|
||||
compact = false,
|
||||
disabled = false,
|
||||
renderAfter,
|
||||
}: IConstraintAccordionViewProps) => {
|
||||
const [expandable, setExpandable] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const singleValue = oneOf(
|
||||
[...semVerOperators, ...numOperators, ...dateOperators],
|
||||
constraint.operator,
|
||||
);
|
||||
const handleClick = () => {
|
||||
if (expandable) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAccordion expanded={expanded} sx={sx}>
|
||||
<StyledAccordionSummary
|
||||
expandIcon={null}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
cursor: expandable ? 'pointer' : 'default!important',
|
||||
'&:hover': {
|
||||
cursor: expandable ? 'pointer' : 'default!important',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledWrapper>
|
||||
<ConstraintAccordionViewHeader
|
||||
constraint={constraint}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
singleValue={singleValue}
|
||||
allowExpand={setExpandable}
|
||||
disabled={disabled}
|
||||
expanded={expanded}
|
||||
compact={compact}
|
||||
/>
|
||||
{renderAfter}
|
||||
</StyledWrapper>
|
||||
</StyledAccordionSummary>
|
||||
|
||||
<StyledAccordionDetails>
|
||||
<ConstraintAccordionViewBody constraint={constraint} />
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
);
|
||||
};
|
@ -1,37 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { formatConstraintValue } from 'utils/formatConstraintValue';
|
||||
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||
import { MultipleValues } from './MultipleValues/MultipleValues.tsx';
|
||||
import { SingleValue } from './SingleValue/SingleValue.tsx';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IConstraintAccordionViewBodyProps {
|
||||
constraint: IConstraint;
|
||||
}
|
||||
|
||||
const StyledValueContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2, 0),
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintAccordionViewBody = ({
|
||||
constraint,
|
||||
}: IConstraintAccordionViewBodyProps) => {
|
||||
const { locationSettings } = useLocationSettings();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StyledValueContainer>
|
||||
<MultipleValues values={constraint.values} />
|
||||
<SingleValue
|
||||
value={formatConstraintValue(constraint, locationSettings)}
|
||||
operator={constraint.operator}
|
||||
/>
|
||||
</StyledValueContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
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.tsx';
|
||||
|
||||
interface IMultipleValuesProps {
|
||||
values: string[] | undefined;
|
||||
}
|
||||
|
||||
const StyledTruncator = styled(StringTruncator)({
|
||||
whiteSpace: 'pre',
|
||||
});
|
||||
|
||||
const StyledChip = styled(Chip)(({ theme }) => ({
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const MultipleValues = ({ values }: IMultipleValuesProps) => {
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
if (!values || values.length === 0) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConditionallyRender
|
||||
condition={values.length > 20}
|
||||
show={
|
||||
<ConstraintValueSearch
|
||||
filter={filter}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{values
|
||||
.filter((value) => value.includes(filter))
|
||||
.map((value, index) => (
|
||||
<StyledChip
|
||||
key={`${value}-${index}`}
|
||||
label={
|
||||
<StyledTruncator
|
||||
maxWidth='400'
|
||||
text={value}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,47 +0,0 @@
|
||||
import { Chip, styled } from '@mui/material';
|
||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||
|
||||
interface ISingleValueProps {
|
||||
value: string | undefined;
|
||||
operator: string;
|
||||
}
|
||||
|
||||
const StyledDiv = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(600)]: { flexDirection: 'column' },
|
||||
}));
|
||||
|
||||
const StyledParagraph = styled('p')(({ theme }) => ({
|
||||
marginRight: theme.spacing(1.5),
|
||||
[theme.breakpoints.down(600)]: {
|
||||
marginBottom: theme.spacing(1.5),
|
||||
marginRight: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledChip = styled(Chip)(({ theme }) => ({
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const SingleValue = ({ value, operator }: ISingleValueProps) => {
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<StyledDiv>
|
||||
<StyledParagraph>Value must be {operator}</StyledParagraph>{' '}
|
||||
<StyledChip
|
||||
label={
|
||||
<StringTruncator
|
||||
maxWidth='400'
|
||||
text={value}
|
||||
maxLength={50}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledDiv>
|
||||
);
|
||||
};
|
@ -1,40 +0,0 @@
|
||||
import type { VFC } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import TrackChanges from '@mui/icons-material/TrackChanges';
|
||||
|
||||
interface IConstraintIconProps {
|
||||
compact?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintIcon: VFC<IConstraintIconProps> = ({
|
||||
compact,
|
||||
disabled,
|
||||
}) => (
|
||||
<Box
|
||||
className='constraint-icon-container'
|
||||
sx={(theme) => ({
|
||||
backgroundColor: disabled
|
||||
? theme.palette.neutral.border
|
||||
: 'primary.light',
|
||||
p: compact ? '1px' : '2px',
|
||||
borderRadius: '50%',
|
||||
width: compact ? '18px' : '24px',
|
||||
height: compact ? '18px' : '24px',
|
||||
marginRight: '13px',
|
||||
})}
|
||||
>
|
||||
<TrackChanges
|
||||
className='constraint-icon'
|
||||
sx={(theme) => ({
|
||||
fill: theme.palette.common.white,
|
||||
display: 'block',
|
||||
width: compact ? theme.spacing(2) : theme.spacing(2.5),
|
||||
height: compact ? theme.spacing(2) : theme.spacing(2.5),
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
);
|
@ -1,56 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IConstraintOperatorProps {
|
||||
constraint: IConstraint;
|
||||
hasPrefix?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const StyledContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(0.5, 1.5),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: theme.palette.background.elevation2,
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledName = styled('p', {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled: boolean }>(({ theme, disabled }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
lineHeight: 17 / 14,
|
||||
color: disabled ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
const StyledText = styled('p', {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled: boolean }>(({ theme, disabled }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
color: disabled ? theme.palette.text.secondary : theme.palette.neutral.main,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintOperator = ({
|
||||
constraint,
|
||||
hasPrefix,
|
||||
disabled = false,
|
||||
}: IConstraintOperatorProps) => {
|
||||
const operatorName = constraint.operator;
|
||||
const operatorText = formatOperatorDescription(constraint.operator);
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
style={{
|
||||
borderTopLeftRadius: hasPrefix ? 0 : undefined,
|
||||
borderBottomLeftRadius: hasPrefix ? 0 : undefined,
|
||||
paddingLeft: hasPrefix ? 0 : undefined,
|
||||
}}
|
||||
>
|
||||
<StyledName disabled={disabled}>{operatorName}</StyledName>
|
||||
<StyledText disabled={disabled}>{operatorText}</StyledText>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,151 +0,0 @@
|
||||
import {
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
type SelectChangeEvent,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
type Operator,
|
||||
stringOperators,
|
||||
semVerOperators,
|
||||
dateOperators,
|
||||
numOperators,
|
||||
inOperators,
|
||||
} from 'constants/operators';
|
||||
import { useState } from 'react';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
|
||||
interface IConstraintOperatorSelectProps {
|
||||
options: Operator[];
|
||||
value: Operator;
|
||||
onChange: (value: Operator) => void;
|
||||
}
|
||||
|
||||
const StyledValueContainer = styled('div')(({ theme }) => ({
|
||||
lineHeight: 1.1,
|
||||
marginTop: -2,
|
||||
marginBottom: -10,
|
||||
}));
|
||||
|
||||
const StyledLabel = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
}));
|
||||
|
||||
const StyledDescription = styled('div')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
color: theme.palette.neutral.main,
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}));
|
||||
|
||||
const StyledFormInput = styled(FormControl)(({ theme }) => ({
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
width: '170px',
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledMenuItem = styled(MenuItem, {
|
||||
shouldForwardProp: (prop) => prop !== 'separator',
|
||||
})<{ separator: boolean }>(({ theme, separator }) =>
|
||||
separator
|
||||
? {
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
marginTop: theme.spacing(2),
|
||||
'&:before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: theme.spacing(-1),
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderTop: '1px solid',
|
||||
borderTopColor: theme.palette.divider,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
const StyledOptionContainer = styled('div')(({ theme }) => ({
|
||||
lineHeight: 1.2,
|
||||
}));
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintOperatorSelect = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: IConstraintOperatorSelectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onSelectChange = (event: SelectChangeEvent) => {
|
||||
onChange(event.target.value as Operator);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
return (
|
||||
<StyledValueContainer>
|
||||
<StyledLabel>{value}</StyledLabel>
|
||||
<StyledDescription>
|
||||
{formatOperatorDescription(value)}
|
||||
</StyledDescription>
|
||||
</StyledValueContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledFormInput variant='outlined' size='small' fullWidth>
|
||||
<InputLabel htmlFor='operator-select'>Operator</InputLabel>
|
||||
<Select
|
||||
id='operator-select'
|
||||
name='operator'
|
||||
label='Operator'
|
||||
value={value}
|
||||
open={open}
|
||||
onOpen={() => setOpen(true)}
|
||||
onClose={() => setOpen(false)}
|
||||
onChange={onSelectChange}
|
||||
renderValue={renderValue}
|
||||
>
|
||||
{options.map((operator) => (
|
||||
<StyledMenuItem
|
||||
key={operator}
|
||||
value={operator}
|
||||
separator={needSeparatorAbove(options, operator)}
|
||||
>
|
||||
<StyledOptionContainer>
|
||||
<StyledLabel>{operator}</StyledLabel>
|
||||
<StyledDescription>
|
||||
{formatOperatorDescription(operator)}
|
||||
</StyledDescription>
|
||||
</StyledOptionContainer>
|
||||
</StyledMenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</StyledFormInput>
|
||||
);
|
||||
};
|
||||
|
||||
const needSeparatorAbove = (options: Operator[], option: Operator): boolean => {
|
||||
if (option === options[0]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return operatorGroups.some((group) => {
|
||||
return group[0] === option;
|
||||
});
|
||||
};
|
||||
|
||||
const operatorGroups = [
|
||||
inOperators,
|
||||
stringOperators,
|
||||
numOperators,
|
||||
dateOperators,
|
||||
semVerOperators,
|
||||
];
|
@ -1,42 +0,0 @@
|
||||
import { TextField, InputAdornment } from '@mui/material';
|
||||
import Search from '@mui/icons-material/Search';
|
||||
|
||||
interface IConstraintValueSearchProps {
|
||||
filter: string;
|
||||
setFilter: React.Dispatch<React.SetStateAction<string>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use NewConstraintAccordion components
|
||||
*/
|
||||
export const ConstraintValueSearch = ({
|
||||
filter,
|
||||
setFilter,
|
||||
}: IConstraintValueSearchProps) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<TextField
|
||||
label='Search'
|
||||
name='search'
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
placeholder='Filter values'
|
||||
style={{
|
||||
width: '100%',
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
variant='outlined'
|
||||
size='small'
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position='start'>
|
||||
<Search />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,331 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { ConstraintAccordionEditBody } from './ConstraintAccordionEditBody/ConstraintAccordionEditBody.tsx';
|
||||
import { ConstraintAccordionEditHeader } from './ConstraintAccordionEditHeader/ConstraintAccordionEditHeader.tsx';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { cleanConstraint } from 'utils/cleanConstraint';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import type { IUnleashContextDefinition } from 'interfaces/context';
|
||||
import { useConstraintInput } from './ConstraintAccordionEditBody/useConstraintInput/useConstraintInput.tsx';
|
||||
import type { Operator } from 'constants/operators';
|
||||
import { ResolveInput } from './ConstraintAccordionEditBody/ResolveInput/ResolveInput.tsx';
|
||||
|
||||
interface IConstraintAccordionEditProps {
|
||||
constraint: IConstraint;
|
||||
onCancel: () => void;
|
||||
onSave: (constraint: IConstraint) => void;
|
||||
compact: boolean;
|
||||
onDelete?: () => void;
|
||||
onAutoSave?: (constraint: IConstraint) => void;
|
||||
}
|
||||
|
||||
export const CANCEL = 'cancel';
|
||||
export const SAVE = 'save';
|
||||
|
||||
const resolveContextDefinition = (
|
||||
context: IUnleashContextDefinition[],
|
||||
contextName: string,
|
||||
): IUnleashContextDefinition => {
|
||||
const definition = context.find(
|
||||
(contextDef) => contextDef.name === contextName,
|
||||
);
|
||||
|
||||
return (
|
||||
definition || {
|
||||
name: '',
|
||||
description: '',
|
||||
createdAt: '',
|
||||
sortOrder: 1,
|
||||
stickiness: false,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const StyledForm = styled('div')({ padding: 0, margin: 0, width: '100%' });
|
||||
|
||||
const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
backgroundColor: theme.palette.background.elevation1,
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
'& .expanded': {
|
||||
'&:before': {
|
||||
opacity: '0 !important',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
border: 'none',
|
||||
padding: theme.spacing(0.5, 3),
|
||||
'&:hover .valuesExpandLabel': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
borderTop: `1px dashed ${theme.palette.divider}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: 0,
|
||||
}));
|
||||
|
||||
export const ConstraintAccordionEdit = ({
|
||||
constraint,
|
||||
compact,
|
||||
onSave,
|
||||
onDelete,
|
||||
onAutoSave,
|
||||
}: IConstraintAccordionEditProps) => {
|
||||
const [localConstraint, setLocalConstraint] = useState<IConstraint>(
|
||||
cleanConstraint(constraint),
|
||||
);
|
||||
const [constraintChanges, setConstraintChanges] = useState<IConstraint[]>([
|
||||
cleanConstraint(constraint),
|
||||
]);
|
||||
|
||||
const { context } = useUnleashContext();
|
||||
const [contextDefinition, setContextDefinition] = useState(
|
||||
resolveContextDefinition(context, localConstraint.contextName),
|
||||
);
|
||||
const { validateConstraint } = useFeatureApi();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [action, setAction] = useState('');
|
||||
|
||||
const { input, validator, setError, error } = useConstraintInput({
|
||||
contextDefinition,
|
||||
localConstraint,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Setting expanded to true on mount will cause the accordion
|
||||
// animation to take effect and transition the expanded accordion in
|
||||
setExpanded(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setContextDefinition(
|
||||
resolveContextDefinition(context, localConstraint.contextName),
|
||||
);
|
||||
}, [localConstraint.contextName, context]);
|
||||
|
||||
useEffect(() => {
|
||||
setError('');
|
||||
}, [setError]);
|
||||
|
||||
const onUndo = () => {
|
||||
if (constraintChanges.length < 2) return;
|
||||
const previousChange = constraintChanges[constraintChanges.length - 2];
|
||||
|
||||
setLocalConstraint(previousChange);
|
||||
setConstraintChanges((prev) => prev.slice(0, prev.length - 1));
|
||||
autoSave(previousChange);
|
||||
};
|
||||
|
||||
const autoSave = (localConstraint: IConstraint) => {
|
||||
if (onAutoSave) {
|
||||
onAutoSave(localConstraint);
|
||||
}
|
||||
};
|
||||
|
||||
const recordChange = (localConstraint: IConstraint) => {
|
||||
setConstraintChanges((prev) => [...prev, localConstraint]);
|
||||
autoSave(localConstraint);
|
||||
};
|
||||
|
||||
const setContextName = useCallback((contextName: string) => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = cleanConstraint({
|
||||
...prev,
|
||||
contextName,
|
||||
values: [],
|
||||
value: '',
|
||||
});
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setOperator = useCallback((operator: Operator) => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = cleanConstraint({
|
||||
...prev,
|
||||
operator,
|
||||
values: [],
|
||||
value: '',
|
||||
});
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setValuesWithRecord = useCallback((values: string[]) => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = { ...prev, values };
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setValues = useCallback((values: string[]) => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = { ...prev, values };
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setValue = useCallback((value: string) => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = { ...prev, value };
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setInvertedOperator = () => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = { ...prev, inverted: !prev.inverted };
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
};
|
||||
|
||||
const setCaseInsensitive = useCallback(() => {
|
||||
setLocalConstraint((prev) => {
|
||||
const localConstraint = {
|
||||
...prev,
|
||||
caseInsensitive: !prev.caseInsensitive,
|
||||
};
|
||||
|
||||
recordChange(localConstraint);
|
||||
|
||||
return localConstraint;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeValue = useCallback(
|
||||
(index: number) => {
|
||||
const valueCopy = [...localConstraint.values!];
|
||||
valueCopy.splice(index, 1);
|
||||
|
||||
setValuesWithRecord(valueCopy);
|
||||
},
|
||||
[localConstraint, setValuesWithRecord],
|
||||
);
|
||||
|
||||
const triggerTransition = () => {
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
const validateConstraintValues = () => {
|
||||
const hasValues =
|
||||
Array.isArray(localConstraint.values) &&
|
||||
Boolean(localConstraint.values.length > 0);
|
||||
const hasValue = Boolean(localConstraint.value);
|
||||
|
||||
if (hasValues || hasValue) {
|
||||
setError('');
|
||||
return true;
|
||||
}
|
||||
setError('You must provide a value for the constraint');
|
||||
return false;
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const hasValues = validateConstraintValues();
|
||||
if (!hasValues) return;
|
||||
const [typeValidatorResult, err] = validator();
|
||||
|
||||
if (!typeValidatorResult) {
|
||||
setError(err);
|
||||
}
|
||||
|
||||
if (typeValidatorResult) {
|
||||
try {
|
||||
await validateConstraint(localConstraint);
|
||||
setError('');
|
||||
setAction(SAVE);
|
||||
triggerTransition();
|
||||
return;
|
||||
} catch (error: unknown) {
|
||||
setError(formatUnknownError(error));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledForm>
|
||||
<StyledAccordion
|
||||
expanded={expanded}
|
||||
TransitionProps={{
|
||||
onExited: () => {
|
||||
if (action === SAVE) {
|
||||
setAction('');
|
||||
onSave(localConstraint);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledAccordionSummary>
|
||||
<ConstraintAccordionEditHeader
|
||||
localConstraint={localConstraint}
|
||||
setLocalConstraint={setLocalConstraint}
|
||||
setContextName={setContextName}
|
||||
setOperator={setOperator}
|
||||
action={action}
|
||||
compact={compact}
|
||||
setInvertedOperator={setInvertedOperator}
|
||||
setCaseInsensitive={setCaseInsensitive}
|
||||
onDelete={onDelete}
|
||||
onUndo={onUndo}
|
||||
constraintChanges={constraintChanges}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
|
||||
<StyledAccordionDetails>
|
||||
<ConstraintAccordionEditBody
|
||||
localConstraint={localConstraint}
|
||||
setValues={setValues}
|
||||
setValue={setValue}
|
||||
triggerTransition={triggerTransition}
|
||||
setAction={setAction}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<ResolveInput
|
||||
setValues={setValues}
|
||||
setValuesWithRecord={setValuesWithRecord}
|
||||
setValue={setValue}
|
||||
setError={setError}
|
||||
localConstraint={localConstraint}
|
||||
constraintValues={constraint?.values || []}
|
||||
constraintValue={constraint?.value || ''}
|
||||
input={input}
|
||||
error={error}
|
||||
contextDefinition={contextDefinition}
|
||||
removeValue={removeValue}
|
||||
/>
|
||||
</ConstraintAccordionEditBody>
|
||||
</StyledAccordionDetails>
|
||||
</StyledAccordion>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Alert, Button, Checkbox, Chip, Stack, styled } from '@mui/material';
|
||||
import { ConstraintValueSearch } from 'component/common/LegacyConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
||||
import { ConstraintFormHeader } from '../ConstraintFormHeader/ConstraintFormHeader.tsx';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
import {
|
||||
@ -9,6 +8,7 @@ import {
|
||||
LegalValueLabel,
|
||||
} from '../LegalValueLabel/LegalValueLabel.tsx';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ConstraintValueSearch } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/ConstraintValueSearch.tsx';
|
||||
|
||||
interface IRestrictiveLegalValuesProps {
|
||||
data: {
|
||||
|
@ -2,7 +2,6 @@ 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 { ConstraintValueSearch } from 'component/common/LegacyConstraintAccordion/ConstraintValueSearch/ConstraintValueSearch';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { useThemeStyles } from 'themes/themeStyles';
|
||||
import type { ILegalValue } from 'interfaces/context';
|
||||
@ -11,6 +10,7 @@ import {
|
||||
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;
|
||||
|
@ -1,218 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
|
||||
import { ConstraintIcon } from 'component/common/LegacyConstraintAccordion/ConstraintIcon';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
dateOperators,
|
||||
DATE_AFTER,
|
||||
IN,
|
||||
stringOperators,
|
||||
} from 'constants/operators';
|
||||
import { resolveText } from './helpers.ts';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Operator } from 'constants/operators';
|
||||
import { ConstraintOperatorSelect } from 'component/common/LegacyConstraintAccordion/ConstraintOperatorSelect';
|
||||
import {
|
||||
operatorsForContext,
|
||||
CURRENT_TIME_CONTEXT_FIELD,
|
||||
} from 'utils/operatorsForContext';
|
||||
import { InvertedOperatorButton } from '../StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton.tsx';
|
||||
import { CaseSensitiveButton } from '../StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton.tsx';
|
||||
import { ConstraintAccordionEditActions } from '../../ConstraintAccordionEditActions/ConstraintAccordionEditActions.tsx';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IConstraintAccordionViewHeader {
|
||||
localConstraint: IConstraint;
|
||||
setContextName: (contextName: string) => void;
|
||||
setOperator: (operator: Operator) => void;
|
||||
setLocalConstraint: React.Dispatch<React.SetStateAction<IConstraint>>;
|
||||
action: string;
|
||||
compact: boolean;
|
||||
onDelete?: () => void;
|
||||
setInvertedOperator: () => void;
|
||||
setCaseInsensitive: () => void;
|
||||
onUndo: () => void;
|
||||
constraintChanges: IConstraint[];
|
||||
}
|
||||
|
||||
const StyledHeaderContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
},
|
||||
}));
|
||||
const StyledSelectContainer = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
[theme.breakpoints.down(770)]: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}));
|
||||
const StyledBottomSelect = styled('div')(({ theme }) => ({
|
||||
[theme.breakpoints.down(770)]: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
display: 'inline-flex',
|
||||
}));
|
||||
|
||||
const StyledHeaderSelect = styled('div')(({ theme }) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
width: '200px',
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
width: '170px',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledGeneralSelect = styled(GeneralSelect)(({ theme }) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
width: '200px',
|
||||
[theme.breakpoints.between(1101, 1365)]: {
|
||||
width: '170px',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeaderText = styled('p')(({ theme }) => ({
|
||||
maxWidth: '400px',
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
[theme.breakpoints.down('xl')]: {
|
||||
display: 'none',
|
||||
},
|
||||
}));
|
||||
|
||||
export const ConstraintAccordionEditHeader = ({
|
||||
compact,
|
||||
constraintChanges,
|
||||
localConstraint,
|
||||
setLocalConstraint,
|
||||
setContextName,
|
||||
setOperator,
|
||||
onDelete,
|
||||
onUndo,
|
||||
setInvertedOperator,
|
||||
setCaseInsensitive,
|
||||
}: IConstraintAccordionViewHeader) => {
|
||||
const { context } = useUnleashContext();
|
||||
const { contextName, operator } = localConstraint;
|
||||
const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
|
||||
useState(false);
|
||||
|
||||
/* We need a special case to handle the currentTime context field. Since
|
||||
this field will be the only one to allow DATE_BEFORE and DATE_AFTER operators
|
||||
this will check if the context field is the current time context field AND check
|
||||
if it is not already using one of the date operators (to not overwrite if there is existing
|
||||
data). */
|
||||
useEffect(() => {
|
||||
if (
|
||||
contextName === CURRENT_TIME_CONTEXT_FIELD &&
|
||||
!oneOf(dateOperators, operator)
|
||||
) {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
operator: DATE_AFTER,
|
||||
value: new Date().toISOString(),
|
||||
}));
|
||||
} else if (
|
||||
contextName !== CURRENT_TIME_CONTEXT_FIELD &&
|
||||
oneOf(dateOperators, operator)
|
||||
) {
|
||||
setOperator(IN);
|
||||
}
|
||||
|
||||
if (oneOf(stringOperators, operator)) {
|
||||
setShowCaseSensitiveButton(true);
|
||||
} else {
|
||||
setShowCaseSensitiveButton(false);
|
||||
}
|
||||
}, [contextName, setOperator, operator, setLocalConstraint]);
|
||||
|
||||
if (!context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constraintNameOptions = context.map((context) => {
|
||||
return { key: context.name, label: context.name };
|
||||
});
|
||||
|
||||
const onOperatorChange = (operator: Operator) => {
|
||||
if (oneOf(stringOperators, operator)) {
|
||||
setShowCaseSensitiveButton(true);
|
||||
} else {
|
||||
setShowCaseSensitiveButton(false);
|
||||
}
|
||||
|
||||
if (oneOf(dateOperators, operator)) {
|
||||
setLocalConstraint((prev) => ({
|
||||
...prev,
|
||||
operator: operator,
|
||||
value: new Date().toISOString(),
|
||||
}));
|
||||
} else {
|
||||
setOperator(operator);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderContainer>
|
||||
<ConstraintIcon />
|
||||
<StyledSelectContainer>
|
||||
<div>
|
||||
<StyledGeneralSelect
|
||||
id='context-field-select'
|
||||
name='contextName'
|
||||
label='Context Field'
|
||||
autoFocus
|
||||
options={constraintNameOptions}
|
||||
value={contextName || ''}
|
||||
onChange={setContextName}
|
||||
/>
|
||||
</div>
|
||||
<StyledBottomSelect>
|
||||
<InvertedOperatorButton
|
||||
localConstraint={localConstraint}
|
||||
setInvertedOperator={setInvertedOperator}
|
||||
/>
|
||||
<StyledHeaderSelect>
|
||||
<ConstraintOperatorSelect
|
||||
options={operatorsForContext(contextName)}
|
||||
value={operator}
|
||||
onChange={onOperatorChange}
|
||||
/>
|
||||
</StyledHeaderSelect>
|
||||
<ConditionallyRender
|
||||
condition={showCaseSensitiveButton}
|
||||
show={
|
||||
<CaseSensitiveButton
|
||||
localConstraint={localConstraint}
|
||||
setCaseInsensitive={setCaseInsensitive}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</StyledBottomSelect>
|
||||
</StyledSelectContainer>
|
||||
<ConditionallyRender
|
||||
condition={!compact}
|
||||
show={
|
||||
<StyledHeaderText>
|
||||
{resolveText(operator, contextName)}
|
||||
</StyledHeaderText>
|
||||
}
|
||||
/>
|
||||
<ConstraintAccordionEditActions
|
||||
onDelete={onDelete}
|
||||
onUndo={onUndo}
|
||||
constraintChanges={constraintChanges}
|
||||
disableEdit
|
||||
/>
|
||||
</StyledHeaderContainer>
|
||||
);
|
||||
};
|
@ -1,82 +0,0 @@
|
||||
import {
|
||||
DATE_BEFORE,
|
||||
DATE_AFTER,
|
||||
IN,
|
||||
NOT_IN,
|
||||
NUM_EQ,
|
||||
NUM_GT,
|
||||
NUM_GTE,
|
||||
NUM_LT,
|
||||
NUM_LTE,
|
||||
STR_CONTAINS,
|
||||
STR_ENDS_WITH,
|
||||
STR_STARTS_WITH,
|
||||
SEMVER_EQ,
|
||||
SEMVER_GT,
|
||||
SEMVER_LT,
|
||||
type Operator,
|
||||
} from 'constants/operators';
|
||||
|
||||
export const resolveText = (operator: Operator, contextName: string) => {
|
||||
const base = `To satisfy this constraint, values passed into the SDK as ${contextName} must`;
|
||||
|
||||
if (operator === IN) {
|
||||
return `${base} include:`;
|
||||
}
|
||||
|
||||
if (operator === NOT_IN) {
|
||||
return `${base} not include:`;
|
||||
}
|
||||
|
||||
if (operator === STR_ENDS_WITH) {
|
||||
return `${base} end with:`;
|
||||
}
|
||||
|
||||
if (operator === STR_STARTS_WITH) {
|
||||
return `${base} start with:`;
|
||||
}
|
||||
|
||||
if (operator === STR_CONTAINS) {
|
||||
return `${base} contain:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_EQ) {
|
||||
return `${base} match:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_GT) {
|
||||
return `${base} be greater than:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_GTE) {
|
||||
return `${base} be greater than or equal to:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_LT) {
|
||||
return `${base} be less than:`;
|
||||
}
|
||||
|
||||
if (operator === NUM_LTE) {
|
||||
return `${base} be less than or equal to:`;
|
||||
}
|
||||
|
||||
if (operator === DATE_AFTER) {
|
||||
return `${base} be after the following date`;
|
||||
}
|
||||
|
||||
if (operator === DATE_BEFORE) {
|
||||
return `${base} be before the following date:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_EQ) {
|
||||
return `${base} match the following version:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_GT) {
|
||||
return `${base} be greater than the following version:`;
|
||||
}
|
||||
|
||||
if (operator === SEMVER_LT) {
|
||||
return `${base} be less than the following version:`;
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { styled } from '@mui/material';
|
||||
import { formatOperatorDescription } from 'utils/formatOperatorDescription';
|
||||
|
||||
interface IConstraintOperatorProps {
|
||||
constraint: IConstraint;
|
||||
|
@ -1,23 +0,0 @@
|
||||
import type { Operator } from 'constants/operators';
|
||||
|
||||
export const formatOperatorDescription = (operator: Operator): string => {
|
||||
return constraintOperatorDescriptions[operator];
|
||||
};
|
||||
|
||||
const constraintOperatorDescriptions = {
|
||||
IN: 'is one of',
|
||||
NOT_IN: 'is not one of',
|
||||
STR_CONTAINS: 'is a string that contains',
|
||||
STR_STARTS_WITH: 'is a string that starts with',
|
||||
STR_ENDS_WITH: 'is a string that ends with',
|
||||
NUM_EQ: 'is a number equal to',
|
||||
NUM_GT: 'is a number greater than',
|
||||
NUM_GTE: 'is a number greater than or equal to',
|
||||
NUM_LT: 'is a number less than',
|
||||
NUM_LTE: 'is a number less than or equal to',
|
||||
DATE_BEFORE: 'is a date before',
|
||||
DATE_AFTER: 'is a date after',
|
||||
SEMVER_EQ: 'is a SemVer equal to',
|
||||
SEMVER_GT: 'is a SemVer greater than',
|
||||
SEMVER_LT: 'is a SemVer less than',
|
||||
};
|
@ -15,7 +15,7 @@ import {
|
||||
inOperators,
|
||||
} from 'constants/operators';
|
||||
import { useState } from 'react';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { formatOperatorDescription } from 'utils/formatOperatorDescription';
|
||||
|
||||
interface IConstraintOperatorSelectProps {
|
||||
options: Operator[];
|
||||
|
@ -5,12 +5,10 @@ import { styled } from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import produce from 'immer';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import {
|
||||
constraintId,
|
||||
createEmptyConstraint,
|
||||
} from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
|
||||
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint';
|
||||
import { createEmptyConstraint } from '../NewConstraintAccordionList/createEmptyConstraint.ts';
|
||||
import { constraintId } from 'constants/constraintId.ts';
|
||||
export interface IEditableConstraintsListRef {
|
||||
addConstraint?: (contextName: string) => void;
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { styled } from '@mui/material';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
|
||||
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
import { constraintId } from 'constants/constraintId';
|
||||
|
||||
export interface IViewableConstraintsListProps {
|
||||
constraints: IConstraint[];
|
||||
@ -27,7 +27,7 @@ export const ViewableConstraintsList = ({
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ConstraintsList>
|
||||
{constraints.map((constraint, index) => (
|
||||
{constraints.map((constraint) => (
|
||||
<ConstraintAccordionView
|
||||
key={constraint[constraintId]}
|
||||
constraint={constraint}
|
||||
|
@ -1,49 +0,0 @@
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import { ConstraintAccordionEdit } from './ConstraintAccordionEdit/ConstraintAccordionEdit.tsx';
|
||||
import { ConstraintAccordionView } from './ConstraintAccordionView/ConstraintAccordionView.tsx';
|
||||
|
||||
export interface IConstraintAccordionProps {
|
||||
compact: boolean;
|
||||
editing: boolean;
|
||||
constraint: IConstraint;
|
||||
onCancel: () => void;
|
||||
onEdit?: () => void;
|
||||
onDelete?: () => void;
|
||||
onAutoSave?: (constraint: IConstraint) => void;
|
||||
onSave?: (constraint: IConstraint) => void;
|
||||
}
|
||||
|
||||
export const NewConstraintAccordion = ({
|
||||
constraint,
|
||||
compact = false,
|
||||
editing,
|
||||
onEdit,
|
||||
onCancel,
|
||||
onDelete,
|
||||
onSave,
|
||||
onAutoSave,
|
||||
}: IConstraintAccordionProps) => {
|
||||
if (!constraint) return null;
|
||||
|
||||
if (editing && onSave) {
|
||||
return (
|
||||
<ConstraintAccordionEdit
|
||||
constraint={constraint}
|
||||
onCancel={onCancel}
|
||||
onSave={onSave!}
|
||||
onDelete={onDelete}
|
||||
onAutoSave={onAutoSave!}
|
||||
compact={compact}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConstraintAccordionView
|
||||
constraint={constraint}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
};
|
@ -5,10 +5,10 @@ import type { IConstraint } from 'interfaces/strategy';
|
||||
import produce from 'immer';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
|
||||
import type { IUseWeakMap } from 'hooks/useWeakMap';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { ConstraintsList } from 'component/common/ConstraintsList/ConstraintsList';
|
||||
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint/EditableConstraint';
|
||||
import { constraintId } from 'constants/constraintId';
|
||||
|
||||
export interface IConstraintAccordionListProps {
|
||||
constraints: IConstraint[];
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { constraintId } from 'constants/constraintId';
|
||||
import { dateOperators } from 'constants/operators';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { oneOf } from 'utils/oneOf';
|
||||
import { operatorsForContext } from 'utils/operatorsForContext';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const createEmptyConstraint = (contextName: string): IConstraint => {
|
||||
const operator = operatorsForContext(contextName)[0];
|
||||
@ -17,5 +19,6 @@ export const createEmptyConstraint = (contextName: string): IConstraint => {
|
||||
values: [],
|
||||
caseInsensitive: false,
|
||||
inverted: false,
|
||||
[constraintId]: uuidv4(),
|
||||
};
|
||||
};
|
||||
|
@ -1,141 +0,0 @@
|
||||
import { useState, type VFC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import DonutLarge from '@mui/icons-material/DonutLarge';
|
||||
import type { ISegment } from 'interfaces/segment';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Button,
|
||||
styled,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ConstraintAccordionList } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
|
||||
interface ISegmentItemProps {
|
||||
segment: Partial<ISegment>;
|
||||
isExpanded?: boolean;
|
||||
disabled?: boolean | null;
|
||||
constraintList?: JSX.Element;
|
||||
headerContent?: JSX.Element;
|
||||
}
|
||||
|
||||
const StyledAccordion = styled(Accordion, {
|
||||
shouldForwardProp: (prop) => prop !== 'isDisabled',
|
||||
})<{ isDisabled: boolean | null }>(({ theme, isDisabled }) => ({
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
'&.segment-accordion': {
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
},
|
||||
boxShadow: 'none',
|
||||
margin: 0,
|
||||
transition: 'all 0.1s ease',
|
||||
'&:before': {
|
||||
opacity: '0 !important',
|
||||
},
|
||||
'&.Mui-expanded': { backgroundColor: theme.palette.neutral.light },
|
||||
backgroundColor: isDisabled
|
||||
? theme.palette.envAccordion.disabled
|
||||
: theme.palette.background.paper,
|
||||
}));
|
||||
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
margin: theme.spacing(0, 0.5),
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
'.MuiAccordionSummary-content': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledLink = styled(Link)(({ theme }) => ({
|
||||
textDecoration: 'none',
|
||||
marginLeft: theme.spacing(1),
|
||||
'&:hover': {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledText = styled('span', {
|
||||
shouldForwardProp: (prop) => prop !== 'disabled',
|
||||
})<{ disabled: boolean | null }>(({ theme, disabled }) => ({
|
||||
color: disabled ? theme.palette.text.secondary : 'inherit',
|
||||
}));
|
||||
|
||||
export const SegmentItem: VFC<ISegmentItemProps> = ({
|
||||
segment,
|
||||
isExpanded,
|
||||
headerContent,
|
||||
constraintList,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(isExpanded || false);
|
||||
|
||||
return (
|
||||
<StyledAccordion
|
||||
className='segment-accordion'
|
||||
isDisabled={disabled}
|
||||
expanded={isOpen}
|
||||
>
|
||||
<StyledAccordionSummary id={`segment-accordion-${segment.id}`}>
|
||||
<DonutLarge
|
||||
sx={(theme) => ({
|
||||
mr: 1,
|
||||
color: disabled
|
||||
? theme.palette.neutral.border
|
||||
: theme.palette.secondary.main,
|
||||
})}
|
||||
/>
|
||||
<StyledText disabled={disabled}>Segment:</StyledText>
|
||||
<StyledLink to={`/segments/edit/${segment.id}`}>
|
||||
{segment.name}
|
||||
</StyledLink>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(headerContent)}
|
||||
show={headerContent}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={!isExpanded}
|
||||
show={
|
||||
<Button
|
||||
size='small'
|
||||
variant='outlined'
|
||||
onClick={() => setIsOpen((value) => !value)}
|
||||
sx={{
|
||||
my: 0,
|
||||
ml: 'auto',
|
||||
fontSize: (theme) =>
|
||||
theme.typography.body2.fontSize,
|
||||
}}
|
||||
>
|
||||
{isOpen ? 'Close preview' : 'Preview'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</StyledAccordionSummary>
|
||||
<AccordionDetails sx={{ pt: 0 }}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(constraintList)}
|
||||
show={constraintList}
|
||||
elseShow={
|
||||
<ConditionallyRender
|
||||
condition={(segment?.constraints?.length || 0) > 0}
|
||||
show={
|
||||
<ConstraintAccordionList
|
||||
constraints={segment!.constraints!}
|
||||
showLabel={false}
|
||||
/>
|
||||
}
|
||||
elseShow={
|
||||
<Typography>
|
||||
This segment has no constraints.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</StyledAccordion>
|
||||
);
|
||||
};
|
@ -14,10 +14,10 @@ import {
|
||||
numOperators,
|
||||
inOperators,
|
||||
} from 'constants/operators';
|
||||
import { formatOperatorDescription } from 'component/common/LegacyConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { useId } from 'react';
|
||||
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
|
||||
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||
import { formatOperatorDescription } from 'utils/formatOperatorDescription';
|
||||
|
||||
interface IConstraintOperatorSelectProps {
|
||||
options: Operator[];
|
||||
|
@ -3,7 +3,7 @@ import { render } from 'utils/testRenderer';
|
||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
||||
import { FeatureStrategyConstraintAccordionList } from './FeatureStrategyConstraintAccordionList.tsx';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { constraintId } from 'constants/constraintId.ts';
|
||||
|
||||
const server = testServerSetup();
|
||||
|
||||
|
@ -1,19 +1,18 @@
|
||||
import type React from 'react';
|
||||
import { forwardRef, type RefObject } from 'react';
|
||||
import { forwardRef, useImperativeHandle, type RefObject } from 'react';
|
||||
import { Box, Button, styled, Typography } from '@mui/material';
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import type { IConstraint } from 'interfaces/strategy';
|
||||
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
||||
import {
|
||||
type IConstraintAccordionListRef,
|
||||
useConstraintAccordionList,
|
||||
} from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||
import { EditableConstraintsList } from 'component/common/NewConstraintAccordion/ConstraintsList/EditableConstraintsList';
|
||||
import { Limit } from 'component/common/Limit/Limit';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { RecentlyUsedConstraints } from '../RecentlyUsedConstraints/RecentlyUsedConstraints.tsx';
|
||||
import { useWeakMap } from 'hooks/useWeakMap.ts';
|
||||
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext.ts';
|
||||
import { createEmptyConstraint } from 'component/common/NewConstraintAccordion/NewConstraintAccordionList/createEmptyConstraint.ts';
|
||||
|
||||
interface IConstraintAccordionListProps {
|
||||
constraints: IConstraint[];
|
||||
@ -47,11 +46,52 @@ const useConstraintLimit = (constraintsCount: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
interface IConstraintAccordionListRef {
|
||||
addConstraint?: (contextName: string) => void;
|
||||
}
|
||||
|
||||
interface IConstraintAccordionListItemState {
|
||||
// Is the constraint new (never been saved)?
|
||||
new?: boolean;
|
||||
// Is the constraint currently being edited?
|
||||
editing?: boolean;
|
||||
}
|
||||
|
||||
const useConstraintAccordionList = (
|
||||
setConstraints:
|
||||
| React.Dispatch<React.SetStateAction<IConstraint[]>>
|
||||
| undefined,
|
||||
ref: React.RefObject<IConstraintAccordionListRef>,
|
||||
) => {
|
||||
const state = useWeakMap<IConstraint, IConstraintAccordionListItemState>();
|
||||
const { context } = useUnleashContext();
|
||||
|
||||
const addConstraint =
|
||||
setConstraints &&
|
||||
((contextName: string) => {
|
||||
const constraint = createEmptyConstraint(contextName);
|
||||
state.set(constraint, { editing: true, new: true });
|
||||
setConstraints((prev) => [...prev, constraint]);
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addConstraint,
|
||||
}));
|
||||
|
||||
const onAdd =
|
||||
addConstraint &&
|
||||
(() => {
|
||||
addConstraint(context[0].name);
|
||||
});
|
||||
|
||||
return { onAdd, state, context };
|
||||
};
|
||||
|
||||
export const FeatureStrategyConstraintAccordionList = forwardRef<
|
||||
IConstraintAccordionListRef | undefined,
|
||||
IConstraintAccordionListProps
|
||||
>(({ constraints, setConstraints, showCreateButton }, ref) => {
|
||||
const { onAdd, state, context } = useConstraintAccordionList(
|
||||
const { onAdd, context } = useConstraintAccordionList(
|
||||
setConstraints,
|
||||
ref as RefObject<IConstraintAccordionListRef>,
|
||||
);
|
||||
|
@ -28,13 +28,13 @@ import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequ
|
||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||
import { FeatureStrategyForm } from '../FeatureStrategyForm/FeatureStrategyForm.tsx';
|
||||
import { NewStrategyVariants } from 'component/feature/StrategyTypes/NewStrategyVariants';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useScheduledChangeRequestsWithStrategy } from 'hooks/api/getters/useScheduledChangeRequestsWithStrategy/useScheduledChangeRequestsWithStrategy';
|
||||
import {
|
||||
getChangeRequestConflictCreatedData,
|
||||
getChangeRequestConflictCreatedDataFromScheduleData,
|
||||
} from './change-request-conflict-data.ts';
|
||||
import { constraintId } from 'constants/constraintId.ts';
|
||||
|
||||
const useTitleTracking = () => {
|
||||
const [previousTitle, setPreviousTitle] = useState<string>('');
|
||||
|
@ -5,8 +5,8 @@ import Clear from '@mui/icons-material/Clear';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { constraintAccordionListId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/ConstraintAccordionList';
|
||||
import { styled, type Theme, Tooltip } from '@mui/material';
|
||||
import { constraintAccordionListId } from 'component/common/NewConstraintAccordion/NewConstraintAccordionList/NewConstraintAccordionList';
|
||||
|
||||
interface IFeatureStrategySegmentListProps {
|
||||
segment: ISegment;
|
||||
|
@ -3,8 +3,8 @@ import { Fragment, useState } from 'react';
|
||||
import type { ISegment } from 'interfaces/segment';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/LegacySegmentItem';
|
||||
import { styled } from '@mui/material';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
|
||||
|
||||
interface IFeatureStrategySegmentListProps {
|
||||
segments: ISegment[];
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Fragment } from 'react';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/LegacySegmentItem';
|
||||
import type { ISegment } from 'interfaces/segment';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
|
||||
|
||||
interface IFeatureOverviewSegmentProps {
|
||||
segments?: ISegment[];
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Fragment, type VFC } from 'react';
|
||||
import { type FC, Fragment } from 'react';
|
||||
import type { PlaygroundConstraintSchema } from 'openapi';
|
||||
import { objectId } from 'utils/objectId';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import { styled } from '@mui/material';
|
||||
import { ConstraintAccordionView } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
import { ConstraintSeparator } from 'component/common/ConstraintsList/ConstraintSeparator/ConstraintSeparator';
|
||||
import { ConstraintAccordionView } from 'component/common/NewConstraintAccordion/ConstraintAccordionView/ConstraintAccordionView';
|
||||
|
||||
interface IConstraintExecutionWithoutResultsProps {
|
||||
constraints?: PlaygroundConstraintSchema[];
|
||||
@ -16,7 +16,7 @@ export const ConstraintExecutionWrapper = styled('div')(() => ({
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
export const ConstraintExecutionWithoutResults: VFC<
|
||||
export const ConstraintExecutionWithoutResults: FC<
|
||||
IConstraintExecutionWithoutResultsProps
|
||||
> = ({ constraints }) => {
|
||||
if (!constraints) return null;
|
||||
@ -27,7 +27,7 @@ export const ConstraintExecutionWithoutResults: VFC<
|
||||
<Fragment key={objectId(constraint)}>
|
||||
<ConditionallyRender
|
||||
condition={index > 0}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
show={<ConstraintSeparator />}
|
||||
/>
|
||||
<ConstraintAccordionView
|
||||
constraint={constraint}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Fragment, type VFC } from 'react';
|
||||
import type { PlaygroundSegmentSchema } from 'openapi';
|
||||
import { StrategySeparator } from 'component/common/StrategySeparator/LegacyStrategySeparator';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/LegacySegmentItem';
|
||||
import { ConstraintExecutionWithoutResults } from '../ConstraintExecution/ConstraintExecutionWithoutResults.tsx';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem.tsx';
|
||||
import { ConstraintSeparator } from 'component/common/ConstraintsList/ConstraintSeparator/ConstraintSeparator.tsx';
|
||||
|
||||
interface ISegmentExecutionWithoutResultProps {
|
||||
segments?: PlaygroundSegmentSchema[];
|
||||
@ -36,7 +36,7 @@ export const SegmentExecutionWithoutResult: VFC<
|
||||
// Don't add if it's the last segment item
|
||||
index !== segments.length - 1
|
||||
}
|
||||
show={<StrategySeparator text='AND' />}
|
||||
show={<ConstraintSeparator />}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
|
@ -2,7 +2,7 @@ import { styled, Typography } from '@mui/material';
|
||||
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import type { IActionSet } from 'interfaces/action';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { formatOperatorDescription } from 'component/common/NewConstraintAccordion/ConstraintOperator/formatOperatorDescription';
|
||||
import { formatOperatorDescription } from 'utils/formatOperatorDescription';
|
||||
|
||||
const StyledItem = styled(Typography)(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
|
@ -2,8 +2,8 @@ import { Fragment, useState } from 'react';
|
||||
import type { ISegment } from 'interfaces/segment';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { FeatureStrategySegmentChip } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegmentChip';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/LegacySegmentItem';
|
||||
import { styled } from '@mui/material';
|
||||
import { SegmentItem } from 'component/common/SegmentItem/SegmentItem';
|
||||
|
||||
const StyledList = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
|
1
frontend/src/constants/constraintId.ts
Normal file
1
frontend/src/constants/constraintId.ts
Normal file
@ -0,0 +1 @@
|
||||
export const constraintId = Symbol('id');
|
@ -1,6 +1,6 @@
|
||||
import type { Operator } from 'constants/operators';
|
||||
import type { IFeatureVariant } from './featureToggle.js';
|
||||
import { constraintId } from 'component/common/LegacyConstraintAccordion/ConstraintAccordionList/createEmptyConstraint';
|
||||
import { constraintId } from 'constants/constraintId.js';
|
||||
|
||||
export interface IFeatureStrategy {
|
||||
id: string;
|
||||
|
Loading…
Reference in New Issue
Block a user