mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: actions filter constraints (#6389)
https://linear.app/unleash/issue/2-1952/adapt-actions-form-ui-to-unleash-constraint-operators Implements the new action filters UI, now powered by constraint operators. This PR goes through some effort to not touch existing components too much, especially since they are critical for activation strategies. Instead, the new feature tries to adapt to the existing components and styling them appropriately, while still re-using them. We can refactor this at a later stage if needed. This UI will face some more drastic changes in the near future due to the feature rename, so I wanted to keep this PR mostly scoped to the constraint operators before proceeding with more changes.    As always, did some manual tests and it seems to be working great!
This commit is contained in:
		
							parent
							
								
									4f638a1c8d
								
							
						
					
					
						commit
						bddc508582
					
				@ -87,7 +87,7 @@ export const FreeTextInput = ({
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
        <>
 | 
			
		||||
            <ConstraintFormHeader style={{ marginBottom: 0 }}>
 | 
			
		||||
                Set values (maximum 100 char length per value)
 | 
			
		||||
            </ConstraintFormHeader>
 | 
			
		||||
@ -125,7 +125,7 @@ export const FreeTextInput = ({
 | 
			
		||||
                    removeValue={removeValue}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,8 +20,8 @@ import {
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
interface IResolveInputProps {
 | 
			
		||||
    contextDefinition: IUnleashContextDefinition;
 | 
			
		||||
    localConstraint: IConstraint;
 | 
			
		||||
    contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
 | 
			
		||||
    localConstraint: Pick<IConstraint, 'value' | 'values'>;
 | 
			
		||||
    constraintValues: string[];
 | 
			
		||||
    constraintValue: string;
 | 
			
		||||
    setValue: (value: string) => void;
 | 
			
		||||
 | 
			
		||||
@ -20,8 +20,8 @@ import {
 | 
			
		||||
import { nonEmptyArray } from 'utils/nonEmptyArray';
 | 
			
		||||
 | 
			
		||||
interface IUseConstraintInputProps {
 | 
			
		||||
    contextDefinition: IUnleashContextDefinition;
 | 
			
		||||
    localConstraint: IConstraint;
 | 
			
		||||
    contextDefinition: Pick<IUnleashContextDefinition, 'legalValues'>;
 | 
			
		||||
    localConstraint: Pick<IConstraint, 'operator' | 'value' | 'values'>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IUseConstraintOutput {
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { ConditionallyRender } from '../../../../ConditionallyRender/Conditional
 | 
			
		||||
import { IConstraint } from 'interfaces/strategy';
 | 
			
		||||
 | 
			
		||||
interface CaseSensitiveButtonProps {
 | 
			
		||||
    localConstraint: IConstraint;
 | 
			
		||||
    localConstraint: Pick<IConstraint, 'caseInsensitive'>;
 | 
			
		||||
    setCaseInsensitive: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import {
 | 
			
		||||
import { ConditionallyRender } from '../../../../ConditionallyRender/ConditionallyRender';
 | 
			
		||||
 | 
			
		||||
interface InvertedOperatorButtonProps {
 | 
			
		||||
    localConstraint: IConstraint;
 | 
			
		||||
    localConstraint: Pick<IConstraint, 'inverted'>;
 | 
			
		||||
    setInvertedOperator: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,11 +26,29 @@ export const ProjectActionsFiltersCell = ({
 | 
			
		||||
            <TooltipLink
 | 
			
		||||
                tooltip={
 | 
			
		||||
                    <>
 | 
			
		||||
                        {filters.map(([parameter, value]) => (
 | 
			
		||||
                            <StyledItem key={parameter}>
 | 
			
		||||
                                <strong>{parameter}</strong>: {value}
 | 
			
		||||
                            </StyledItem>
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {filters.map(
 | 
			
		||||
                            ([
 | 
			
		||||
                                parameter,
 | 
			
		||||
                                {
 | 
			
		||||
                                    inverted,
 | 
			
		||||
                                    operator,
 | 
			
		||||
                                    caseInsensitive,
 | 
			
		||||
                                    value,
 | 
			
		||||
                                    values,
 | 
			
		||||
                                },
 | 
			
		||||
                            ]) => (
 | 
			
		||||
                                <StyledItem key={parameter}>
 | 
			
		||||
                                    <strong>{parameter}</strong>{' '}
 | 
			
		||||
                                    {inverted ? 'NOT' : ''} {operator}{' '}
 | 
			
		||||
                                    {caseInsensitive
 | 
			
		||||
                                        ? '(case insensitive)'
 | 
			
		||||
                                        : ''}{' '}
 | 
			
		||||
                                    <strong>
 | 
			
		||||
                                        {values ? values.join(', ') : value}
 | 
			
		||||
                                    </strong>
 | 
			
		||||
                                </StyledItem>
 | 
			
		||||
                            ),
 | 
			
		||||
                        )}
 | 
			
		||||
                    </>
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,73 @@
 | 
			
		||||
import { Badge, IconButton, Tooltip, styled } from '@mui/material';
 | 
			
		||||
import { IconButton, Tooltip, styled } from '@mui/material';
 | 
			
		||||
import { ActionsFilterState } from './useProjectActionsForm';
 | 
			
		||||
import { Delete } from '@mui/icons-material';
 | 
			
		||||
import Input from 'component/common/Input/Input';
 | 
			
		||||
import { ProjectActionsFormItem } from './ProjectActionsFormItem';
 | 
			
		||||
import { ConstraintOperatorSelect } from 'component/common/ConstraintAccordion/ConstraintOperatorSelect';
 | 
			
		||||
import {
 | 
			
		||||
    Operator,
 | 
			
		||||
    allOperators,
 | 
			
		||||
    dateOperators,
 | 
			
		||||
    inOperators,
 | 
			
		||||
    stringOperators,
 | 
			
		||||
} from 'constants/operators';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { oneOf } from 'utils/oneOf';
 | 
			
		||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { CaseSensitiveButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/CaseSensitiveButton/CaseSensitiveButton';
 | 
			
		||||
import { InvertedOperatorButton } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/StyledToggleButton/InvertedOperatorButton/InvertedOperatorButton';
 | 
			
		||||
import { ResolveInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/ResolveInput/ResolveInput';
 | 
			
		||||
import { useConstraintInput } from 'component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditBody/useConstraintInput/useConstraintInput';
 | 
			
		||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
			
		||||
 | 
			
		||||
const StyledDeleteButton = styled(IconButton)({
 | 
			
		||||
    marginRight: '-6px',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledFilter = styled('div')({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    width: '100%',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledFilterHeader = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
    [theme.breakpoints.down('sm')]: {
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        alignItems: 'start',
 | 
			
		||||
        gap: theme.spacing(2),
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledOperatorOptions = styled('div')(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    display: 'inline-flex',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledOperatorSelectWrapper = styled('div')(({ theme }) => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    '&&& > div': {
 | 
			
		||||
        width: '100%',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledOperatorButtonWrapper = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'inline-flex',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
    '&&& button': {
 | 
			
		||||
        marginRight: 0,
 | 
			
		||||
        '&:not(.operator-is-active)': {
 | 
			
		||||
            backgroundColor: theme.palette.background.elevation2,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledInputContainer = styled('div')({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    flex: 1,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -12,64 +75,208 @@ const StyledInput = styled(Input)({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const StyledBadge = styled(Badge)(({ theme }) => ({
 | 
			
		||||
    margin: theme.spacing(0, 1),
 | 
			
		||||
    fontSize: theme.fontSizes.mainHeader,
 | 
			
		||||
const StyledResolveInputWrapper = styled('div')(({ theme }) => ({
 | 
			
		||||
    '& > h3': {
 | 
			
		||||
        margin: theme.spacing(1, 0, 0, 0),
 | 
			
		||||
        fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    },
 | 
			
		||||
    '& > div': {
 | 
			
		||||
        margin: 0,
 | 
			
		||||
        maxWidth: 'unset',
 | 
			
		||||
        '& > div': {
 | 
			
		||||
            flex: 1,
 | 
			
		||||
        },
 | 
			
		||||
        '& .MuiFormControl-root': {
 | 
			
		||||
            margin: theme.spacing(0.5, 0, 0, 0),
 | 
			
		||||
        },
 | 
			
		||||
        '&:not(:first-of-type)': {
 | 
			
		||||
            marginTop: theme.spacing(1),
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IProjectActionsFilterItemProps {
 | 
			
		||||
    filter: ActionsFilterState;
 | 
			
		||||
    index: number;
 | 
			
		||||
    stateChanged: (updatedFilter: ActionsFilterState) => void;
 | 
			
		||||
    onDelete: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ProjectActionsFilterItem = ({
 | 
			
		||||
    filter,
 | 
			
		||||
    index,
 | 
			
		||||
    stateChanged,
 | 
			
		||||
    onDelete,
 | 
			
		||||
}: {
 | 
			
		||||
    filter: ActionsFilterState;
 | 
			
		||||
    index: number;
 | 
			
		||||
    stateChanged: (updatedFilter: ActionsFilterState) => void;
 | 
			
		||||
    onDelete: () => void;
 | 
			
		||||
}) => {
 | 
			
		||||
    const { parameter, value } = filter;
 | 
			
		||||
}: IProjectActionsFilterItemProps) => {
 | 
			
		||||
    const { parameter, inverted, operator, caseInsensitive, value, values } =
 | 
			
		||||
        filter;
 | 
			
		||||
 | 
			
		||||
    const header = (
 | 
			
		||||
        <>
 | 
			
		||||
            <span>Filter {index + 1}</span>
 | 
			
		||||
            <div>
 | 
			
		||||
                <Tooltip title='Delete filter' arrow>
 | 
			
		||||
                    <IconButton onClick={onDelete}>
 | 
			
		||||
                    <StyledDeleteButton onClick={onDelete}>
 | 
			
		||||
                        <Delete />
 | 
			
		||||
                    </IconButton>
 | 
			
		||||
                    </StyledDeleteButton>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
            </div>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Adapted from `/frontend/src/component/common/NewConstraintAccordion/ConstraintAccordionEdit/ConstraintAccordionEditHeader/ConstraintAccordionEditHeader.tsx`
 | 
			
		||||
    const [showCaseSensitiveButton, setShowCaseSensitiveButton] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    const caseInsensitiveInOperators = useUiFlag('caseInsensitiveInOperators');
 | 
			
		||||
 | 
			
		||||
    const validOperators = allOperators.filter(
 | 
			
		||||
        (operator) => !oneOf(dateOperators, operator),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { input, validator, setError, error } = useConstraintInput({
 | 
			
		||||
        contextDefinition: { legalValues: [] },
 | 
			
		||||
        localConstraint: { operator, value, values },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const validate = () => {
 | 
			
		||||
        stateChanged({
 | 
			
		||||
            ...filter,
 | 
			
		||||
            error: undefined,
 | 
			
		||||
        });
 | 
			
		||||
        const [typeValidatorResult, err] = validator();
 | 
			
		||||
        if (!typeValidatorResult) {
 | 
			
		||||
            setError(err);
 | 
			
		||||
            stateChanged({
 | 
			
		||||
                ...filter,
 | 
			
		||||
                error: err,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        validate();
 | 
			
		||||
    }, [value, error]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (
 | 
			
		||||
            oneOf(stringOperators, operator) ||
 | 
			
		||||
            (oneOf(inOperators, operator) && caseInsensitiveInOperators)
 | 
			
		||||
        ) {
 | 
			
		||||
            setShowCaseSensitiveButton(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            setShowCaseSensitiveButton(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [operator, caseInsensitiveInOperators]);
 | 
			
		||||
 | 
			
		||||
    const onOperatorChange = (operator: Operator) => {
 | 
			
		||||
        if (
 | 
			
		||||
            oneOf(stringOperators, operator) ||
 | 
			
		||||
            (oneOf(inOperators, operator) && caseInsensitiveInOperators)
 | 
			
		||||
        ) {
 | 
			
		||||
            setShowCaseSensitiveButton(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            setShowCaseSensitiveButton(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        stateChanged({
 | 
			
		||||
            ...filter,
 | 
			
		||||
            operator,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const setValue = (value: string) => {
 | 
			
		||||
        stateChanged({
 | 
			
		||||
            ...filter,
 | 
			
		||||
            values: undefined,
 | 
			
		||||
            value,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const setValues = (values: string[]) => {
 | 
			
		||||
        stateChanged({
 | 
			
		||||
            ...filter,
 | 
			
		||||
            value: undefined,
 | 
			
		||||
            values,
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const removeValue = (index: number) => {
 | 
			
		||||
        const newValues = values?.filter((_, i) => i !== index) || [];
 | 
			
		||||
        setValues(newValues);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ProjectActionsFormItem index={index} header={header}>
 | 
			
		||||
            <StyledInputContainer>
 | 
			
		||||
                <StyledInput
 | 
			
		||||
                    label='Parameter'
 | 
			
		||||
                    value={parameter}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                        stateChanged({
 | 
			
		||||
                            ...filter,
 | 
			
		||||
                            parameter: e.target.value,
 | 
			
		||||
                        })
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </StyledInputContainer>
 | 
			
		||||
            <StyledBadge>=</StyledBadge>
 | 
			
		||||
            <StyledInputContainer>
 | 
			
		||||
                <StyledInput
 | 
			
		||||
                    label='Value'
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                        stateChanged({
 | 
			
		||||
                            ...filter,
 | 
			
		||||
                            value: e.target.value,
 | 
			
		||||
                        })
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </StyledInputContainer>
 | 
			
		||||
            <StyledFilter>
 | 
			
		||||
                <StyledFilterHeader>
 | 
			
		||||
                    <StyledInputContainer>
 | 
			
		||||
                        <StyledInput
 | 
			
		||||
                            label='Parameter'
 | 
			
		||||
                            value={parameter}
 | 
			
		||||
                            onChange={(e) =>
 | 
			
		||||
                                stateChanged({
 | 
			
		||||
                                    ...filter,
 | 
			
		||||
                                    parameter: e.target.value,
 | 
			
		||||
                                })
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    </StyledInputContainer>
 | 
			
		||||
                    <StyledOperatorOptions>
 | 
			
		||||
                        <StyledOperatorButtonWrapper>
 | 
			
		||||
                            <InvertedOperatorButton
 | 
			
		||||
                                localConstraint={{ inverted }}
 | 
			
		||||
                                setInvertedOperator={() =>
 | 
			
		||||
                                    stateChanged({
 | 
			
		||||
                                        ...filter,
 | 
			
		||||
                                        inverted: !inverted || undefined,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        </StyledOperatorButtonWrapper>
 | 
			
		||||
                        <StyledOperatorSelectWrapper>
 | 
			
		||||
                            <ConstraintOperatorSelect
 | 
			
		||||
                                options={validOperators}
 | 
			
		||||
                                value={operator}
 | 
			
		||||
                                onChange={onOperatorChange}
 | 
			
		||||
                            />
 | 
			
		||||
                        </StyledOperatorSelectWrapper>
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={showCaseSensitiveButton}
 | 
			
		||||
                            show={
 | 
			
		||||
                                <StyledOperatorButtonWrapper>
 | 
			
		||||
                                    <CaseSensitiveButton
 | 
			
		||||
                                        localConstraint={{ caseInsensitive }}
 | 
			
		||||
                                        setCaseInsensitive={() =>
 | 
			
		||||
                                            stateChanged({
 | 
			
		||||
                                                ...filter,
 | 
			
		||||
                                                caseInsensitive:
 | 
			
		||||
                                                    !caseInsensitive ||
 | 
			
		||||
                                                    undefined,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    />
 | 
			
		||||
                                </StyledOperatorButtonWrapper>
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                    </StyledOperatorOptions>
 | 
			
		||||
                </StyledFilterHeader>
 | 
			
		||||
                <StyledResolveInputWrapper>
 | 
			
		||||
                    <ResolveInput
 | 
			
		||||
                        setValues={setValues}
 | 
			
		||||
                        setValuesWithRecord={setValues}
 | 
			
		||||
                        setValue={setValue}
 | 
			
		||||
                        setError={setError}
 | 
			
		||||
                        localConstraint={{ value, values }}
 | 
			
		||||
                        constraintValues={values || []}
 | 
			
		||||
                        constraintValue={value || ''}
 | 
			
		||||
                        input={input}
 | 
			
		||||
                        error={error}
 | 
			
		||||
                        contextDefinition={{ legalValues: [] }}
 | 
			
		||||
                        removeValue={removeValue}
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledResolveInputWrapper>
 | 
			
		||||
            </StyledFilter>
 | 
			
		||||
        </ProjectActionsFormItem>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,8 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
import { ProjectActionsActionItem } from './ProjectActionsActionItem';
 | 
			
		||||
import { ProjectActionsFilterItem } from './ProjectActionsFilterItem';
 | 
			
		||||
import { ProjectActionsFormStep } from './ProjectActionsFormStep';
 | 
			
		||||
import { IN } from 'constants/operators';
 | 
			
		||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
 | 
			
		||||
 | 
			
		||||
const StyledServiceAccountAlert = styled(Alert)(({ theme }) => ({
 | 
			
		||||
    marginBottom: theme.spacing(4),
 | 
			
		||||
@ -43,15 +45,10 @@ const StyledInput = styled(Input)(() => ({
 | 
			
		||||
    width: '100%',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledSecondaryDescription = styled('p')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    color: theme.palette.text.secondary,
 | 
			
		||||
    fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
    marginBottom: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledButtonContainer = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    marginTop: theme.spacing(1),
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledDivider = styled(Divider)(({ theme }) => ({
 | 
			
		||||
@ -59,6 +56,12 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
 | 
			
		||||
    marginBottom: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const StyledTooltip = styled('div')(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'column',
 | 
			
		||||
    gap: theme.spacing(1),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
interface IProjectActionsFormProps {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
@ -111,7 +114,7 @@ export const ProjectActionsForm = ({
 | 
			
		||||
            {
 | 
			
		||||
                id,
 | 
			
		||||
                parameter: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
                operator: IN,
 | 
			
		||||
            },
 | 
			
		||||
        ]);
 | 
			
		||||
    };
 | 
			
		||||
@ -232,10 +235,6 @@ export const ProjectActionsForm = ({
 | 
			
		||||
            </ProjectActionsFormStep>
 | 
			
		||||
 | 
			
		||||
            <ProjectActionsFormStep name='When this' verticalConnector>
 | 
			
		||||
                <StyledSecondaryDescription>
 | 
			
		||||
                    If no filters are defined then the action will be triggered
 | 
			
		||||
                    every time the incoming webhook is called.
 | 
			
		||||
                </StyledSecondaryDescription>
 | 
			
		||||
                {filters.map((filter, index) => (
 | 
			
		||||
                    <ProjectActionsFilterItem
 | 
			
		||||
                        key={filter.id}
 | 
			
		||||
@ -258,11 +257,28 @@ export const ProjectActionsForm = ({
 | 
			
		||||
                    >
 | 
			
		||||
                        Add filter
 | 
			
		||||
                    </Button>
 | 
			
		||||
                    <HelpIcon
 | 
			
		||||
                        htmlTooltip
 | 
			
		||||
                        tooltip={
 | 
			
		||||
                            <StyledTooltip>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    Filters allow you to add conditions to the
 | 
			
		||||
                                    execution of the actions based on the source
 | 
			
		||||
                                    payload.
 | 
			
		||||
                                </p>
 | 
			
		||||
                                <p>
 | 
			
		||||
                                    If no filters are defined then the action
 | 
			
		||||
                                    will always be triggered from the selected
 | 
			
		||||
                                    source, no matter the payload.
 | 
			
		||||
                                </p>
 | 
			
		||||
                            </StyledTooltip>
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledButtonContainer>
 | 
			
		||||
            </ProjectActionsFormStep>
 | 
			
		||||
 | 
			
		||||
            <ProjectActionsFormStep
 | 
			
		||||
                name='Do these action(s)'
 | 
			
		||||
                name='Do these actions'
 | 
			
		||||
                verticalConnector
 | 
			
		||||
                resourceLink={
 | 
			
		||||
                    <RouterLink to='/admin/service-accounts'>
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { useActions } from 'hooks/api/getters/useActions/useActions';
 | 
			
		||||
import { IAction, IActionSet } from 'interfaces/action';
 | 
			
		||||
import { IAction, IActionSet, ParameterMatch } from 'interfaces/action';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid';
 | 
			
		||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
@ -7,14 +7,15 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
			
		||||
enum ErrorField {
 | 
			
		||||
    NAME = 'name',
 | 
			
		||||
    TRIGGER = 'trigger',
 | 
			
		||||
    FILTERS = 'filters',
 | 
			
		||||
    ACTOR = 'actor',
 | 
			
		||||
    ACTIONS = 'actions',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ActionsFilterState = {
 | 
			
		||||
export type ActionsFilterState = ParameterMatch & {
 | 
			
		||||
    id: string;
 | 
			
		||||
    parameter: string;
 | 
			
		||||
    value: string;
 | 
			
		||||
    error?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ActionsActionState = Omit<
 | 
			
		||||
@ -27,6 +28,7 @@ export type ActionsActionState = Omit<
 | 
			
		||||
const DEFAULT_PROJECT_ACTIONS_FORM_ERRORS = {
 | 
			
		||||
    [ErrorField.NAME]: undefined,
 | 
			
		||||
    [ErrorField.TRIGGER]: undefined,
 | 
			
		||||
    [ErrorField.FILTERS]: undefined,
 | 
			
		||||
    [ErrorField.ACTOR]: undefined,
 | 
			
		||||
    [ErrorField.ACTIONS]: undefined,
 | 
			
		||||
};
 | 
			
		||||
@ -50,10 +52,17 @@ export const useProjectActionsForm = (action?: IActionSet) => {
 | 
			
		||||
        setSourceId(action?.match?.sourceId ?? 0);
 | 
			
		||||
        setFilters(
 | 
			
		||||
            Object.entries(action?.match?.payload ?? {}).map(
 | 
			
		||||
                ([parameter, value]) => ({
 | 
			
		||||
                ([
 | 
			
		||||
                    parameter,
 | 
			
		||||
                    { inverted, operator, caseInsensitive, value, values },
 | 
			
		||||
                ]) => ({
 | 
			
		||||
                    id: uuidv4(),
 | 
			
		||||
                    parameter,
 | 
			
		||||
                    value: value as string,
 | 
			
		||||
                    inverted,
 | 
			
		||||
                    operator,
 | 
			
		||||
                    caseInsensitive,
 | 
			
		||||
                    value,
 | 
			
		||||
                    values,
 | 
			
		||||
                }),
 | 
			
		||||
            ),
 | 
			
		||||
        );
 | 
			
		||||
@ -87,6 +96,13 @@ export const useProjectActionsForm = (action?: IActionSet) => {
 | 
			
		||||
        setErrors((errors) => ({ ...errors, [field]: error }));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        clearError(ErrorField.FILTERS);
 | 
			
		||||
        if (filters.some(({ error }) => error)) {
 | 
			
		||||
            setError(ErrorField.FILTERS, 'One or more filters have errors.');
 | 
			
		||||
        }
 | 
			
		||||
    }, [filters]);
 | 
			
		||||
 | 
			
		||||
    const isEmpty = (value: string) => !value.length;
 | 
			
		||||
 | 
			
		||||
    const isNameNotUnique = (value: string) =>
 | 
			
		||||
 | 
			
		||||
@ -100,9 +100,25 @@ export const ProjectActionsModal = ({
 | 
			
		||||
            payload: filters
 | 
			
		||||
                .filter((f) => f.parameter.length > 0)
 | 
			
		||||
                .reduce(
 | 
			
		||||
                    (acc, filter) => ({
 | 
			
		||||
                    (
 | 
			
		||||
                        acc,
 | 
			
		||||
                        {
 | 
			
		||||
                            parameter,
 | 
			
		||||
                            inverted,
 | 
			
		||||
                            operator,
 | 
			
		||||
                            caseInsensitive,
 | 
			
		||||
                            value,
 | 
			
		||||
                            values,
 | 
			
		||||
                        },
 | 
			
		||||
                    ) => ({
 | 
			
		||||
                        ...acc,
 | 
			
		||||
                        [filter.parameter]: filter.value,
 | 
			
		||||
                        [parameter]: {
 | 
			
		||||
                            inverted,
 | 
			
		||||
                            operator,
 | 
			
		||||
                            caseInsensitive,
 | 
			
		||||
                            value,
 | 
			
		||||
                            values,
 | 
			
		||||
                        },
 | 
			
		||||
                    }),
 | 
			
		||||
                    {},
 | 
			
		||||
                ),
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,5 @@
 | 
			
		||||
import { IConstraint } from './strategy';
 | 
			
		||||
 | 
			
		||||
export interface IActionSet {
 | 
			
		||||
    id: number;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
@ -12,10 +14,15 @@ export interface IActionSet {
 | 
			
		||||
 | 
			
		||||
type MatchSource = 'incoming-webhook';
 | 
			
		||||
 | 
			
		||||
export type ParameterMatch = Pick<
 | 
			
		||||
    IConstraint,
 | 
			
		||||
    'inverted' | 'operator' | 'caseInsensitive' | 'value' | 'values'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export interface IMatch {
 | 
			
		||||
    source: MatchSource;
 | 
			
		||||
    sourceId: number;
 | 
			
		||||
    payload: Record<string, unknown>;
 | 
			
		||||
    payload: Record<string, ParameterMatch>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAction {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user