mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-24 17:51:14 +02:00
* feat: create segmentation structure and list * feat: remove unused deps and change route * feat: change header style and add renderNoSegments * fix: style table header * feat: create useSegments hook * feat: add segmentApi hook * feat: create segment * fix: errors * feat: add contextfields list * fix: remove user from create segment api * feat: add form structure * feat: add SegmentFormStepOne * fix: tests and routes * feat: add constraint view * feat: UI to match the sketch * feat: add constraint on context select * fix: duplication * fix adding constraints Co-authored-by: olav <mail@olav.io> * fix: input date not showing up in constraint view Co-authored-by: olav <mail@olav.io> * fix: minor bugs Co-authored-by: olav <mail@olav.io> * fix: create context modal in segment page Co-authored-by: olav <mail@olav.io> * fix: validate constraint before create segment Co-authored-by: olav <mail@olav.io> * feat: create useSegment hook Co-authored-by: olav <mail@olav.io> * feat: create edit component Co-authored-by: olav <mail@olav.io> * refactor: move constraint validation endpoint * refactor: add missing route snapshot * refactor: fix segment constraints unsaved/editing state * refactor: remove create segment from mobile header menu * refactor: update segments form description * refactor: extract SegmentFormStepList component * refactor: add an optional FormTemplate docs link label * refactor: fix update segment payload * feat: finish edit component Co-authored-by: olav <mail@olav.io> * refactor: move step list above segment form * fix: update PR based on feedback Co-authored-by: olav <mail@olav.io> * refactor: fix constraint validation endpoint path * refactor: improve constraint state field name * refactor: extract AutocompleteBox component * feat: add strategy segment selection * refactor: add strategy segment previews * refactor: fix double section separator line * feat: disable deleting a usable segment * refactor: warn about segments without constraints * refactor: update text in delete segment dialogue * refactur: improve arg names * refactor: improve index var name * refactor: clarify steps list logic * refactor: use a required prop for the segment name * refactor: use ConditionallyRender for segment deletion * refactor: fix segments refetch * refactor: improve CreateUnleashContext component names * refactor: adjust segment form styles * refactor: adjust text * refactor: fix info icon tooltip hover target * refactor: add missing aria attrs to preview button * refactor: add strat name to delete segment modal * refactor: fix segment chip text alighment * refactor: use bulk endpoint for strategy segments * refactor: fix imports after merge Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com> Co-authored-by: olav <mail@olav.io>
244 lines
7.9 KiB
TypeScript
244 lines
7.9 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import classnames from 'classnames';
|
|
import { IConstraint } from 'interfaces/strategy';
|
|
import { useStyles } from '../ConstraintAccordion.styles';
|
|
import { ConstraintAccordionEditBody } from './ConstraintAccordionEditBody/ConstraintAccordionEditBody';
|
|
import { ConstraintAccordionEditHeader } from './ConstraintAccordionEditHeader/ConstraintAccordionEditHeader';
|
|
import {
|
|
Accordion,
|
|
AccordionDetails,
|
|
AccordionSummary,
|
|
} from '@material-ui/core';
|
|
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 { IUnleashContextDefinition } from 'interfaces/context';
|
|
import { useConstraintInput } from './ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
|
|
import { Operator } from 'constants/operators';
|
|
import { ResolveInput } from './ConstraintAccordionEditBody/ResolveInput/ResolveInput';
|
|
|
|
interface IConstraintAccordionEditProps {
|
|
constraint: IConstraint;
|
|
onCancel: () => void;
|
|
onSave: (constraint: IConstraint) => void;
|
|
compact: boolean;
|
|
}
|
|
|
|
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,
|
|
}
|
|
);
|
|
};
|
|
|
|
export const ConstraintAccordionEdit = ({
|
|
constraint,
|
|
compact,
|
|
onCancel,
|
|
onSave,
|
|
}: 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('');
|
|
const styles = useStyles();
|
|
|
|
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 (
|
|
<div className={styles.form}>
|
|
<Accordion
|
|
className={classnames(styles.accordion, styles.accordionEdit)}
|
|
classes={{
|
|
expanded: styles.accordionRoot,
|
|
}}
|
|
style={{ boxShadow: 'none' }}
|
|
expanded={expanded}
|
|
TransitionProps={{
|
|
onExited: () => {
|
|
if (action === CANCEL) {
|
|
setAction('');
|
|
onCancel();
|
|
} else if (action === SAVE) {
|
|
setAction('');
|
|
onSave(localConstraint);
|
|
}
|
|
},
|
|
}}
|
|
>
|
|
<AccordionSummary className={styles.summary}>
|
|
<ConstraintAccordionEditHeader
|
|
localConstraint={localConstraint}
|
|
setLocalConstraint={setLocalConstraint}
|
|
setContextName={setContextName}
|
|
setOperator={setOperator}
|
|
action={action}
|
|
compact={compact}
|
|
/>
|
|
</AccordionSummary>
|
|
|
|
<AccordionDetails
|
|
className={styles.accordionDetails}
|
|
style={{ padding: 0 }}
|
|
>
|
|
<ConstraintAccordionEditBody
|
|
localConstraint={localConstraint}
|
|
setValues={setValues}
|
|
setValue={setValue}
|
|
setCaseInsensitive={setCaseInsensitive}
|
|
triggerTransition={triggerTransition}
|
|
setAction={setAction}
|
|
setInvertedOperator={setInvertedOperator}
|
|
onSubmit={onSubmit}
|
|
>
|
|
<ResolveInput
|
|
setValues={setValues}
|
|
setValue={setValue}
|
|
setError={setError}
|
|
localConstraint={localConstraint}
|
|
input={input}
|
|
error={error}
|
|
contextDefinition={contextDefinition}
|
|
removeValue={removeValue}
|
|
setCaseInsensitive={setCaseInsensitive}
|
|
/>
|
|
</ConstraintAccordionEditBody>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
</div>
|
|
);
|
|
};
|