1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-24 17:51:14 +02:00
unleash.unleash/frontend/src/component/common/ConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEdit.tsx
Youssef Khedher eeda7ab5e4 feat: add segments (#780)
* 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>
2022-03-29 09:30:57 +02:00

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>
);
};