Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: conditions component #4105

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 146 additions & 151 deletions apps/web/src/components/conditions/Conditions.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import { Grid, Group, ActionIcon, Center } from '@mantine/core';
import { Grid, Group, ActionIcon, Center, useMantineTheme } from '@mantine/core';
import styled from '@emotion/styled';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useMemo } from 'react';
import { Control, Controller, useFieldArray, useForm, useWatch } from 'react-hook-form';

import { FILTER_TO_LABEL, FilterPartTypeEnum } from '@novu/shared';

import { Button, colors, Dropdown, Input, Select, Sidebar, Text, Title, Tooltip } from '../../design-system';
import { ConditionPlus, DotsHorizontal, Duplicate, Trash, Condition, ErrorIcon } from '../../design-system/icons';
import { When } from '../utils/When';
import { IConditions } from '../../pages/integrations/types';
import { ConditionsContextEnum, ConditionsContextFields, IConditions } from './types';

interface IConditionsForm {
conditions: IConditions[];
}
export function Conditions({
isOpened,
conditions,
onClose,
setConditions,
name,
context = ConditionsContextEnum.INTEGRATIONS,
}: {
isOpened: boolean;
onClose: () => void;
setConditions: (data: IConditions[]) => void;
conditions?: IConditions[];
name: string;
context?: ConditionsContextEnum;
}) {
const { colorScheme } = useMantineTheme();

const {
control,
setValue,
getValues,
trigger,
formState: { errors, isValid },
} = useForm({
} = useForm<IConditionsForm>({
defaultValues: { conditions },
shouldUseNativeValidation: false,
mode: 'onChange',
Expand All @@ -40,7 +47,16 @@ export function Conditions({
name: `conditions.0.children`,
});

const FilterPartTypeList = [{ value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }];
const { label, filterPartsList } = ConditionsContextFields[context];

const FilterPartTypeList = useMemo(() => {
return filterPartsList.map((filterType) => {
return {
value: filterType,
label: FILTER_TO_LABEL[filterType],
};
});
}, [context]);

function handleOnChildOnChange(index: number) {
return (data) => {
Expand All @@ -49,8 +65,23 @@ export function Conditions({
};
}

function handleDuplicate(index: number) {
insert(index + 1, getValues(`conditions.0.children.${index}`));
}

function handleDelete(index: number) {
remove(index);
}

const onApplyConditions = async () => {
await trigger('conditions');
if (!errors.conditions && fields.length > 0) {
updateConditions(getValues('conditions'));
}
};

function updateConditions(data) {
setConditions(data.conditions);
setConditions(data);
onClose();
}

Expand All @@ -61,38 +92,29 @@ export function Conditions({
isExpanded
customHeader={
<Center inline>
<Condition />
<Title ml={8} size={2}>
Condition for {name} provider instance
<Condition color={colorScheme === 'dark' ? colors.white : colors.B30} />
<Title ml={8} size={2} data-test-id="conditions-form-title">
Conditions for {name} {label}
</Title>
</Center>
}
customFooter={
<Group ml="auto">
<Button variant="outline" onClick={onClose} data-test-id="create-provider-instance-sidebar-cancel">
<Button variant="outline" onClick={onClose} data-test-id="conditions-form-cancel-btn">
Cancel
</Button>
<TooltipContainer>
<Tooltip
position={'top'}
disabled={isValid && fields.length > 0}
label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'}
>
<div>
<Button
onClick={async () => {
await trigger('conditions');
if (!errors.conditions && fields.length > 0) {
updateConditions(getValues());
}
}}
data-test-id="create-provider-instance-sidebar-create"
>
Apply conditions
</Button>
</div>
</Tooltip>
</TooltipContainer>
<Tooltip
position="top"
error
disabled={isValid && fields.length > 0}
label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'}
>
<div>
<Button onClick={onApplyConditions} data-test-id="apply-conditions-btn">
Apply conditions
</Button>
</div>
</Tooltip>
</Group>
}
>
Expand All @@ -110,13 +132,12 @@ export function Conditions({
render={({ field }) => {
return (
<Select
placeholder="How to group rules?"
data={[
{ value: 'AND', label: 'And' },
{ value: 'OR', label: 'Or' },
]}
{...field}
data-test-id="group-rules-dropdown"
data-test-id="conditions-form-value-dropdown"
/>
);
}}
Expand All @@ -140,95 +161,13 @@ export function Conditions({
data={FilterPartTypeList}
{...field}
onChange={handleOnChildOnChange(index)}
data-test-id="filter-on-dropdown"
data-test-id="conditions-form-on-dropdown"
/>
);
}}
/>
</Grid.Col>
<Grid.Col span={5}>
<Controller
control={control}
name={`conditions.0.children.${index}.field`}
defaultValue="identifier"
render={({ field }) => {
return (
<Select
placeholder="Key"
data={[
{ value: 'name', label: 'Name' },
{ value: 'identifier', label: 'Identifier' },
]}
{...field}
data-test-id="group-rules-dropdown"
/>
);
}}
/>
</Grid.Col>
<Grid.Col span={3}>
<Controller
control={control}
name={`conditions.0.children.${index}.operator`}
defaultValue="EQUAL"
render={({ field }) => {
return (
<Select
placeholder="Operator"
data={[
{ value: 'EQUAL', label: 'Equal' },
{ value: 'NOT_EQUAL', label: 'Does not equal' },
{ value: 'IN', label: 'Contains' },
{ value: 'NOT_IN', label: 'Does not contain' },
{ value: 'IS_DEFINED', label: 'Is not empty' },
]}
{...field}
data-test-id="filter-operator-dropdown"
onChange={(value) => {
field.onChange(value);
if (value === 'IS_DEFINED') {
setValue(`conditions.0.children.${index}.value`, '');
}
}}
/>
);
}}
/>
</Grid.Col>
<Grid.Col span={6}>
{getValues(`conditions.0.children.${index}.operator`) !== 'IS_DEFINED' && (
<Controller
control={control}
name={`conditions.0.children.${index}.value`}
defaultValue=""
rules={{ required: true }}
render={({ field, fieldState }) => {
return (
<Input
{...field}
value={field.value as string}
rightSection={
<When truthy={!!fieldState.error}>
<TooltipContainer>
<Tooltip position="top" offset={15} label={'Value is missing'}>
<span>
<ErrorIcon color={colors.error} />
</span>
</Tooltip>
</TooltipContainer>
</When>
}
required
disabled={getValues(`conditions.0.children.${index}.operator`) === 'IS_DEFINED'}
error={!!fieldState.error}
placeholder="Value"
data-test-id="filter-value-input"
/>
);
}}
/>
)}
</Grid.Col>
<EqualityForm control={control} index={index} />
<Grid.Col span={1}>
<Dropdown
withArrow={false}
Expand All @@ -241,20 +180,10 @@ export function Conditions({
middlewares={{ flip: false, shift: false }}
position="bottom-end"
>
<Dropdown.Item
onClick={() => {
insert(index + 1, getValues(`conditions.0.children.${index}`));
}}
icon={<Duplicate />}
>
<Dropdown.Item onClick={() => handleDuplicate(index)} icon={<Duplicate />}>
Duplicate
</Dropdown.Item>
<Dropdown.Item
onClick={() => {
remove(index);
}}
icon={<Trash />}
>
<Dropdown.Item onClick={() => handleDelete(index)} icon={<Trash />}>
Delete
</Dropdown.Item>
</Dropdown>
Expand Down Expand Up @@ -284,6 +213,93 @@ export function Conditions({
);
}

function EqualityForm({ control, index }: { control: Control<IConditionsForm>; index: number }) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems long enough to be in its own file?

const operator = useWatch({
control,
name: `conditions.0.children.${index}.operator`,
});

return (
<>
<Grid.Col span={5}>
<Controller
control={control}
name={`conditions.0.children.${index}.field`}
defaultValue="identifier"
render={({ field }) => {
return (
<Select
placeholder="Key"
data={[
{ value: 'name', label: 'Name' },
{ value: 'identifier', label: 'Identifier' },
]}
{...field}
data-test-id="conditions-form-field-dropdown"
/>
);
}}
/>
</Grid.Col>
<Grid.Col span={3}>
<Controller
control={control}
name={`conditions.0.children.${index}.operator`}
defaultValue="EQUAL"
render={({ field }) => {
return (
<Select
placeholder="Operator"
data={[
{ value: 'EQUAL', label: 'Equal' },
{ value: 'NOT_EQUAL', label: 'Does not equal' },
{ value: 'IN', label: 'Contains' },
{ value: 'NOT_IN', label: 'Does not contain' },
{ value: 'IS_DEFINED', label: 'Is defined' },
]}
{...field}
data-test-id="conditions-form-operator-dropdown"
/>
);
}}
/>
</Grid.Col>

<Grid.Col span={6}>
{operator !== 'IS_DEFINED' && (
<Controller
control={control}
name={`conditions.0.children.${index}.value`}
defaultValue=""
rules={{ required: true }}
render={({ field, fieldState }) => {
return (
<Input
{...field}
value={field.value as string}
rightSection={
<When truthy={!!fieldState.error}>
<Tooltip error position="top" offset={15} label={'Value is missing'}>
<span>
<ErrorIcon color={colors.error} />
</span>
</Tooltip>
</When>
}
required
error={!!fieldState.error}
placeholder="Value"
data-test-id="conditions-form-value-input"
/>
);
}}
/>
)}
</Grid.Col>
</>
);
}

const Wrapper = styled.div`
.mantine-Select-wrapper:not(:hover) {
.mantine-Select-input {
Expand All @@ -297,24 +313,3 @@ const Wrapper = styled.div`
}
}
`;

const TooltipContainer = styled.div`
& .mantine-Tooltip-tooltip {
color: ${colors.error};
padding: 16px;
font-size: 14px;
font-weight: 400;
border-radius: 8px;
background: ${({ theme }) =>
`linear-gradient(0deg, rgba(229, 69, 69, 0.2) 0%, rgba(229, 69, 69, 0.2) 100%), ${
theme.colorScheme === 'dark' ? '#23232b' : colors.white
} !important`};
}

& .mantine-Tooltip-arrow {
background: ${({ theme }) =>
`linear-gradient(0deg, rgba(229, 69, 69, 0.2) 0%, rgba(229, 69, 69, 0.2) 100%), ${
theme.colorScheme === 'dark' ? '#23232b' : colors.white
} !important`};
}
`;
Loading