1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-13 13:48:59 +02:00

Initial impl with reducer

This commit is contained in:
Thomas Heartman 2025-05-07 10:14:36 +02:00
parent 10e5554de3
commit 544072e40d
4 changed files with 251 additions and 189 deletions

View File

@ -4,22 +4,12 @@ import {
useConstraintInput,
type Input,
} from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
import {
DATE_AFTER,
dateOperators,
IN,
stringOperators,
type Operator,
} from 'constants/operators';
import { stringOperators, type Operator } from 'constants/operators';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import type { IUnleashContextDefinition } from 'interfaces/context';
import type { IConstraint } from 'interfaces/strategy';
import { useEffect, useMemo, useRef, useState, type FC } from 'react';
import { oneOf } from 'utils/oneOf';
import {
CURRENT_TIME_CONTEXT_FIELD,
operatorsForContext,
} from 'utils/operatorsForContext';
import { useMemo, useRef, type FC } from 'react';
import { operatorsForContext } from 'utils/operatorsForContext';
import { ConstraintOperatorSelect } from './ConstraintOperatorSelect';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
import Delete from '@mui/icons-material/Delete';
@ -136,6 +126,8 @@ const ButtonPlaceholder = styled('div')(({ theme }) => ({
// screens), but still retain necessary space for the button when it's all
// on one line.
width: theme.spacing(2),
// fontSize: theme.typography.body1.fontSize
fontSize: theme.fontSizes.extraLargeHeader,
}));
const StyledCaseInsensitiveIcon = styled(CaseInsensitiveIcon)(({ theme }) => ({
@ -175,36 +167,48 @@ const getInputType = (input: Input): InputType => {
}
};
type ConstraintUpdateAction =
| { type: 'add value(s)'; payload: string[] }
| { type: 'set value'; payload: string }
| { type: 'clear values' }
| { type: 'remove value from list'; payload: string }
| { type: 'set context field'; payload: string }
| { type: 'set operator'; payload: Operator }
| { type: 'toggle case sensitivity' }
| { type: 'toggle inverted operator' };
type Props = {
localConstraint: IConstraint;
setContextName: (contextName: string) => void;
setOperator: (operator: Operator) => void;
setLocalConstraint: React.Dispatch<React.SetStateAction<IConstraint>>;
// setContextName: (contextName: string) => void;
// setOperator: (operator: Operator) => void;
// setLocalConstraint: React.Dispatch<React.SetStateAction<IConstraint>>;
onDelete?: () => void;
toggleInvertedOperator: () => void;
toggleCaseSensitivity: () => void;
// toggleInvertedOperator: () => void;
// toggleCaseSensitivity: () => void;
contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
constraintValues: string[];
constraintValue: string;
setValue: (value: string) => void;
setValues: (values: string[]) => void;
removeValue: (index: number) => void;
// setValue: (value: string) => void;
// setValues: (values: string[]) => void;
// removeValue: (index: number) => void;
updateConstraint: (action: ConstraintUpdateAction) => void;
};
export const EditableConstraint: FC<Props> = ({
localConstraint,
setLocalConstraint,
setContextName,
setOperator,
// setLocalConstraint,
// setContextName,
// setOperator,
onDelete,
toggleInvertedOperator,
toggleCaseSensitivity,
// toggleInvertedOperator,
// toggleCaseSensitivity,
contextDefinition,
constraintValues,
constraintValue,
setValue,
setValues,
removeValue,
// setValue,
// setValues,
// removeValue,
updateConstraint,
}) => {
const { input } = useConstraintInput({
contextDefinition,
@ -213,8 +217,7 @@ export const EditableConstraint: FC<Props> = ({
const { context } = useUnleashContext();
const { contextName, operator } = localConstraint;
const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
useState(false);
const showCaseSensitiveButton = stringOperators.includes(operator);
const deleteButtonRef = useRef<HTMLButtonElement>(null);
const addValuesButtonRef = useRef<HTMLButtonElement>(null);
const inputType = getInputType(input);
@ -224,29 +227,28 @@ export const EditableConstraint: FC<Props> = ({
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]);
// 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;
@ -257,21 +259,21 @@ export const EditableConstraint: FC<Props> = ({
});
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);
}
updateConstraint({ type: 'set operator', payload: 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);
// }
};
const validator = useMemo(() => constraintValidator(input), [input]);
@ -280,7 +282,12 @@ export const EditableConstraint: FC<Props> = ({
case 'date':
return (
<ConstraintDateInput
setValue={setValue}
setValue={(value: string) =>
updateConstraint({
type: 'set value',
payload: value,
})
}
value={localConstraint.value}
validator={validator}
/>
@ -290,9 +297,14 @@ export const EditableConstraint: FC<Props> = ({
<AddSingleValueWidget
validator={validator}
onAddValue={(newValue) => {
setValue(newValue);
updateConstraint({
type: 'set value',
payload: newValue,
});
}}
removeValue={() => setValue('')}
removeValue={() =>
updateConstraint({ type: 'clear values' })
}
currentValue={localConstraint.value}
helpText={
inputType.type === 'number'
@ -311,12 +323,10 @@ export const EditableConstraint: FC<Props> = ({
helpText='Maximum 100 char length per value'
ref={addValuesButtonRef}
onAddValues={(newValues) => {
// todo (`addEditStrategy`): move deduplication logic higher up in the context handling
const combinedValues = new Set([
...(localConstraint.values || []),
...newValues,
]);
setValues(Array.from(combinedValues));
updateConstraint({
type: 'add value(s)',
payload: newValues,
});
}}
/>
);
@ -338,14 +348,23 @@ export const EditableConstraint: FC<Props> = ({
autoFocus
options={constraintNameOptions}
value={contextName || ''}
onChange={setContextName}
onChange={(contextField) =>
updateConstraint({
type: 'set context field',
payload: contextField,
})
}
variant='standard'
/>
<OperatorOptions>
<StyledButton
type='button'
onClick={toggleInvertedOperator}
onClick={() =>
updateConstraint({
type: 'toggle inverted operator',
})
}
>
{localConstraint.inverted ? (
<StyledNotEqualsIcon aria-label='The constraint operator is exclusive.' />
@ -370,7 +389,11 @@ export const EditableConstraint: FC<Props> = ({
{showCaseSensitiveButton ? (
<StyledButton
type='button'
onClick={toggleCaseSensitivity}
onClick={() =>
updateConstraint({
type: 'toggle case sensitivity',
})
}
>
{localConstraint.caseInsensitive ? (
<StyledCaseInsensitiveIcon aria-label='The match is not case sensitive.' />
@ -390,8 +413,12 @@ export const EditableConstraint: FC<Props> = ({
</ConstraintOptions>
<ValueList
values={localConstraint.values}
removeValue={removeValue}
setValues={setValues}
removeValue={(value) =>
updateConstraint({
type: 'remove value from list',
payload: value,
})
}
getExternalFocusTarget={() =>
addValuesButtonRef.current ??
deleteButtonRef.current
@ -422,8 +449,23 @@ export const EditableConstraint: FC<Props> = ({
)}
constraintValues={constraintValues}
values={localConstraint.values || []}
setValuesWithRecord={setValues}
setValues={setValues}
clearAll={() =>
updateConstraint({
type: 'clear values',
})
}
addValues={(newValues) =>
updateConstraint({
type: 'add value(s)',
payload: newValues,
})
}
removeValue={(value) =>
updateConstraint({
type: 'remove value from list',
payload: value,
})
}
/>
</LegalValuesContainer>
) : null}

View File

@ -1,10 +1,16 @@
import { useCallback, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import type { IConstraint } from 'interfaces/strategy';
import { cleanConstraint } from 'utils/cleanConstraint';
import useUnleashContext from 'hooks/api/getters/useUnleashContext/useUnleashContext';
import type { IUnleashContextDefinition } from 'interfaces/context';
import type { Operator } from 'constants/operators';
import {
DATE_AFTER,
dateOperators,
IN,
type Operator,
} from 'constants/operators';
import { EditableConstraint } from 'component/feature/FeatureStrategy/FeatureStrategyConstraints/EditableConstraint';
import { CURRENT_TIME_CONTEXT_FIELD } from 'utils/operatorsForContext';
interface IConstraintAccordionEditProps {
constraint: IConstraint;
@ -34,15 +40,102 @@ const resolveContextDefinition = (
);
};
type ConstraintUpdateAction =
| { type: 'add value(s)'; payload: string[] }
| { type: 'set value'; payload: string }
| { type: 'clear values' }
| { type: 'remove value from list'; payload: string }
| { type: 'set context field'; payload: string }
| { type: 'set operator'; payload: Operator }
| { type: 'toggle case sensitivity' }
| { type: 'toggle inverted operator' };
export const EditableConstraintWrapper = ({
constraint,
onDelete,
onAutoSave,
}: IConstraintAccordionEditProps) => {
const [localConstraint, setLocalConstraint] = useState<IConstraint>(
const constraintReducer = (
state: IConstraint,
action: ConstraintUpdateAction,
): IConstraint => {
switch (action.type) {
case 'set context field':
if (
action.payload === CURRENT_TIME_CONTEXT_FIELD &&
!dateOperators.includes(state.operator)
) {
return cleanConstraint({
...state,
operator: DATE_AFTER,
values: [],
value: new Date().toISOString(),
});
} else if (
action.payload !== CURRENT_TIME_CONTEXT_FIELD &&
dateOperators.includes(state.operator)
) {
return cleanConstraint({
...state,
operator: IN,
values: [],
value: '',
});
}
return cleanConstraint({
...state,
contextName: action.payload,
values: [],
value: '',
});
case 'set operator':
return cleanConstraint({
...state,
operator: action.payload,
values: [],
value: '',
});
case 'add value(s)': {
const combinedValues = new Set([
...(state.values || []),
...action.payload,
]);
return { ...state, values: Array.from(combinedValues) };
}
case 'set value':
return { ...state, value: action.payload };
case 'toggle inverted operator':
return { ...state, inverted: !state.inverted };
case 'toggle case sensitivity':
return { ...state, caseInsensitive: !state.inverted };
case 'remove value from list':
return {
...state,
values: (state.values ?? []).filter(
(value) => value !== action.payload,
),
};
case 'clear values':
return cleanConstraint({ ...state, values: [], value: '' });
}
};
// const [state, dispatch] = useReducer(
// constraintReducer,
// cleanConstraint(constraint),
// );
const [localConstraint, setLocalConstraint] = useState(
cleanConstraint(constraint),
);
const updateConstraint = (action: ConstraintUpdateAction) => {
const nextState = constraintReducer(localConstraint, action);
setLocalConstraint(nextState);
onAutoSave(nextState);
};
const { context } = useUnleashContext();
const [contextDefinition, setContextDefinition] = useState(
resolveContextDefinition(context, localConstraint.contextName),
@ -54,97 +147,14 @@ export const EditableConstraintWrapper = ({
);
}, [localConstraint.contextName, context]);
const setContextName = useCallback((contextName: string) => {
setLocalConstraint((prev) => {
const localConstraint = cleanConstraint({
...prev,
contextName,
values: [],
value: '',
});
onAutoSave(localConstraint);
return localConstraint;
});
}, []);
const setOperator = useCallback((operator: Operator) => {
setLocalConstraint((prev) => {
const localConstraint = cleanConstraint({
...prev,
operator,
values: [],
value: '',
});
onAutoSave(localConstraint);
return localConstraint;
});
}, []);
const setValues = useCallback((values: string[]) => {
setLocalConstraint((prev) => {
const localConstraint = { ...prev, values };
onAutoSave(localConstraint);
return localConstraint;
});
}, []);
const setValue = useCallback((value: string) => {
setLocalConstraint((prev) => {
const localConstraint = { ...prev, value };
onAutoSave(localConstraint);
return localConstraint;
});
}, []);
const setInvertedOperator = () => {
setLocalConstraint((prev) => {
const localConstraint = { ...prev, inverted: !prev.inverted };
onAutoSave(localConstraint);
return localConstraint;
});
};
const setCaseInsensitive = useCallback(() => {
setLocalConstraint((prev) => {
const localConstraint = {
...prev,
caseInsensitive: !prev.caseInsensitive,
};
onAutoSave(localConstraint);
return localConstraint;
});
}, []);
const removeValue = useCallback(
(index: number) => {
const valueCopy = [...localConstraint.values!];
valueCopy.splice(index, 1);
setValues(valueCopy);
},
[localConstraint],
);
return (
<EditableConstraint
localConstraint={localConstraint}
setLocalConstraint={setLocalConstraint}
setContextName={setContextName}
setOperator={setOperator}
toggleInvertedOperator={setInvertedOperator}
toggleCaseSensitivity={setCaseInsensitive}
onDelete={onDelete}
setValues={setValues}
setValue={setValue}
constraintValues={constraint?.values || []}
constraintValue={constraint?.value || ''}
contextDefinition={contextDefinition}
removeValue={removeValue}
updateConstraint={updateConstraint}
/>
);
};

View File

@ -15,8 +15,9 @@ interface IRestrictiveLegalValuesProps {
};
constraintValues: string[];
values: string[];
setValues: (values: string[]) => void;
setValuesWithRecord: (values: string[]) => void;
addValues: (values: string[]) => void;
removeValue: (values: string) => void;
clearAll: () => void;
beforeValues?: JSX.Element;
}
@ -72,8 +73,11 @@ const LegalValuesSelectorWidget = styled('article')(({ theme }) => ({
export const LegalValuesSelector = ({
data,
values,
setValues,
setValuesWithRecord,
// setValues,
addValues,
removeValue,
clearAll,
// setValuesWithRecord,
constraintValues,
}: IRestrictiveLegalValuesProps) => {
const [filter, setFilter] = useState('');
@ -103,20 +107,23 @@ export const LegalValuesSelector = ({
useEffect(() => {
if (illegalValues.length > 0) {
setValues(cleanDeletedLegalValues(values));
console.log('would clean deleted values here');
// setValues(cleanDeletedLegalValues(values));
}
}, []);
const onChange = (legalValue: string) => {
if (valuesMap[legalValue]) {
const index = values.findIndex((value) => value === legalValue);
const newValues = [...values];
newValues.splice(index, 1);
setValuesWithRecord(newValues);
removeValue(legalValue);
// const index = values.findIndex((value) => value === legalValue);
// const newValues = [...values];
// newValues.splice(index, 1);
// setValuesWithRecord(newValues);
return;
}
setValuesWithRecord([...cleanDeletedLegalValues(values), legalValue]);
addValues([legalValue]);
// setValuesWithRecord([...cleanDeletedLegalValues(values), legalValue]);
};
const isAllSelected = legalValues.every((value) =>
@ -125,11 +132,15 @@ export const LegalValuesSelector = ({
const onSelectAll = () => {
if (isAllSelected) {
return setValuesWithRecord([]);
clearAll();
return;
// return setValuesWithRecord([]);
} else {
addValues(legalValues.map(({ value }) => value));
}
setValuesWithRecord([
...legalValues.map((legalValue) => legalValue.value),
]);
// setValuesWithRecord([
// ...legalValues.map((legalValue) => legalValue.value),
// ]);
};
const handleSearchKeyDown = (event: React.KeyboardEvent) => {

View File

@ -55,8 +55,7 @@ export const ValueChip = styled(
type Props = {
values: string[] | undefined;
removeValue: (index: number) => void;
setValues: (values: string[]) => void;
removeValue: (value: string) => void;
// the element that should receive focus when all value chips are deleted
getExternalFocusTarget: () => HTMLElement | null;
};
@ -102,7 +101,7 @@ export const ValueList: FC<PropsWithChildren<Props>> = ({
label={value}
onDelete={() => {
nextFocusTarget(index)?.focus();
removeValue(index);
removeValue(value);
}}
/>
</li>