diff --git a/apps/web/src/components/conditions/Conditions.tsx b/apps/web/src/components/conditions/Conditions.tsx new file mode 100644 index 00000000000..fb9e52c7a7c --- /dev/null +++ b/apps/web/src/components/conditions/Conditions.tsx @@ -0,0 +1,320 @@ +import { Grid, Group, ActionIcon, Center } from '@mantine/core'; +import styled from '@emotion/styled'; +import { Controller, useFieldArray, useForm } 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'; + +export function Conditions({ + isOpened, + conditions, + onClose, + setConditions, + name, +}: { + isOpened: boolean; + onClose: () => void; + setConditions: (data: IConditions[]) => void; + conditions?: IConditions[]; + name: string; +}) { + const { + control, + setValue, + getValues, + trigger, + formState: { errors, isValid }, + } = useForm({ + defaultValues: { conditions }, + shouldUseNativeValidation: false, + mode: 'onChange', + reValidateMode: 'onChange', + }); + + const { fields, append, update, remove, insert } = useFieldArray({ + control, + name: `conditions.0.children`, + }); + + const FilterPartTypeList = [{ value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }]; + + function handleOnChildOnChange(index: number) { + return (data) => { + const newField = Object.assign({}, fields[index], { on: data }); + update(index, newField); + }; + } + + function updateConditions(data) { + setConditions(data.conditions); + onClose(); + } + + return ( + + + + Condition for {name} provider instance + + + } + customFooter={ + + + + 0} + label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'} + > +
+ +
+
+
+
+ } + > + {fields.map((item, index) => { + return ( +
+ + + {index > 0 ? ( + + { + return ( + + ); + }} + /> + + + { + return ( + { + field.onChange(value); + if (value === 'IS_DEFINED') { + setValue(`conditions.0.children.${index}.value`, ''); + } + }} + /> + ); + }} + /> + + + {getValues(`conditions.0.children.${index}.operator`) !== 'IS_DEFINED' && ( + { + return ( + + + + + + + + + + } + required + disabled={getValues(`conditions.0.children.${index}.operator`) === 'IS_DEFINED'} + error={!!fieldState.error} + placeholder="Value" + data-test-id="filter-value-input" + /> + ); + }} + /> + )} + + + + + + } + middlewares={{ flip: false, shift: false }} + position="bottom-end" + > + { + insert(index + 1, getValues(`conditions.0.children.${index}`)); + }} + icon={} + > + Duplicate + + { + remove(index); + }} + icon={} + > + Delete + + + + +
+ ); + })} + + + + +
+ ); +} + +const Wrapper = styled.div` + .mantine-Select-wrapper:not(:hover) { + .mantine-Select-input { + border-color: transparent; + color: ${colors.B60}; + } + .mantine-Input-rightSection.mantine-Select-rightSection { + svg { + display: none; + } + } + } +`; + +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`}; + } +`; diff --git a/apps/web/src/design-system/icons/actions/ConditionPlus.tsx b/apps/web/src/design-system/icons/actions/ConditionPlus.tsx new file mode 100644 index 00000000000..1877a995c0b --- /dev/null +++ b/apps/web/src/design-system/icons/actions/ConditionPlus.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +export function ConditionPlus(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + + + + + + + ); +} diff --git a/apps/web/src/design-system/icons/actions/Duplicate.tsx b/apps/web/src/design-system/icons/actions/Duplicate.tsx new file mode 100644 index 00000000000..10eee70d2ca --- /dev/null +++ b/apps/web/src/design-system/icons/actions/Duplicate.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +/* eslint-disable */ +export function Duplicate(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + ); +} diff --git a/apps/web/src/design-system/icons/actions/PlusFilled.tsx b/apps/web/src/design-system/icons/actions/PlusFilled.tsx index 6087e41ca02..8c1eac0e7ea 100644 --- a/apps/web/src/design-system/icons/actions/PlusFilled.tsx +++ b/apps/web/src/design-system/icons/actions/PlusFilled.tsx @@ -7,14 +7,14 @@ export function PlusFilled(props: React.ComponentPropsWithoutRef<'svg'>) { - - + + diff --git a/apps/web/src/design-system/icons/general/Condition.tsx b/apps/web/src/design-system/icons/general/Condition.tsx index 73ec7b5f743..1593e16d602 100644 --- a/apps/web/src/design-system/icons/general/Condition.tsx +++ b/apps/web/src/design-system/icons/general/Condition.tsx @@ -1,5 +1,5 @@ import React from 'react'; -/* eslint-disable */ + export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { return ( @@ -19,7 +19,7 @@ export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { - + diff --git a/apps/web/src/design-system/icons/index.ts b/apps/web/src/design-system/icons/index.ts index dcc7189b1c6..a209a84f806 100644 --- a/apps/web/src/design-system/icons/index.ts +++ b/apps/web/src/design-system/icons/index.ts @@ -94,6 +94,8 @@ export { Edit } from './actions/Edit'; export { Upload } from './actions/Upload'; export { Invite } from './actions/Invite'; export { PlusFilled } from './actions/PlusFilled'; +export { ConditionPlus } from './actions/ConditionPlus'; +export { Duplicate } from './actions/Duplicate'; export { ArrowDown } from './arrows/ArrowDown'; export { DoubleArrowRight } from './arrows/DoubleArrowRight'; diff --git a/apps/web/src/design-system/sidebar/Sidebar.tsx b/apps/web/src/design-system/sidebar/Sidebar.tsx index 59508b2f91f..274fc147e2c 100644 --- a/apps/web/src/design-system/sidebar/Sidebar.tsx +++ b/apps/web/src/design-system/sidebar/Sidebar.tsx @@ -124,9 +124,9 @@ export const Sidebar = ({ trapFocus={false} data-expanded={isExpanded} > -
+ - {isExpanded && ( + {isExpanded && onBack && ( diff --git a/apps/web/src/pages/integrations/components/ConditionCell.tsx b/apps/web/src/pages/integrations/components/ConditionCell.tsx index 3e07e6e308e..70c50525366 100644 --- a/apps/web/src/pages/integrations/components/ConditionCell.tsx +++ b/apps/web/src/pages/integrations/components/ConditionCell.tsx @@ -6,7 +6,7 @@ import type { ITableIntegration } from '../types'; const ConditionCellBase = ({ row: { original } }: IExtendedCellProps) => { const { colorScheme } = useMantineColorScheme(); - if (!original.conditions) { + if (!original.conditions || original.conditions.length < 1) { return (
-
{original.conditions.length}
+
+ {original.conditions?.[0]?.children?.length} +
); }; diff --git a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx index 39f08a02096..f6d667a753d 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -1,12 +1,12 @@ import { ActionIcon, Group, Radio, Text } from '@mantine/core'; -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Controller, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared'; import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; -import { ArrowLeft } from '../../../../design-system/icons'; +import { ArrowLeft, ConditionPlus } from '../../../../design-system/icons'; import { inputStyles } from '../../../../design-system/config/inputs.styles'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; import { useSegment } from '../../../../components/providers/SegmentProvider'; @@ -16,13 +16,15 @@ import { errorMessage, successMessage } from '../../../../utils/notifications'; import { QueryKeys } from '../../../../api/query.keys'; import { ProviderImage } from './SelectProviderSidebar'; import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels'; -import type { IntegrationEntity } from '../../types'; +import type { IConditions, IntegrationEntity } from '../../types'; import { useProviders } from '../../useProviders'; import { When } from '../../../../components/utils/When'; +import { Conditions } from '../../../../components/conditions/Conditions'; interface ICreateProviderInstanceForm { name: string; environmentId: string; + conditions: IConditions[]; } export function CreateProviderInstanceSidebar({ @@ -42,6 +44,7 @@ export function CreateProviderInstanceSidebar({ }) { const { environments, isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const { isLoading: areIntegrationsLoading, providers: integrations } = useProviders(); + const [openConditions, setOpenConditions] = useState(false); const isLoading = areEnvironmentsLoading || areIntegrationsLoading; const queryClient = useQueryClient(); const segment = useSegment(); @@ -57,11 +60,12 @@ export function CreateProviderInstanceSidebar({ ICreateIntegrationBodyDto >(createIntegration); - const { handleSubmit, control, reset, watch } = useForm({ + const { handleSubmit, control, reset, watch, setValue, getValues } = useForm({ shouldUseNativeValidation: false, defaultValues: { name: '', environmentId: '', + conditions: [], }, }); @@ -86,7 +90,7 @@ export function CreateProviderInstanceSidebar({ } const { channel: selectedChannel } = provider; - const { environmentId } = data; + const { environmentId, conditions } = data; const { _id: integrationId } = await createIntegrationApi({ providerId: provider.id, @@ -95,6 +99,7 @@ export function CreateProviderInstanceSidebar({ credentials: {}, active: provider.channel === ChannelTypeEnum.IN_APP ? true : false, check: false, + conditions, _environmentId: environmentId, }); @@ -124,6 +129,7 @@ export function CreateProviderInstanceSidebar({ reset({ name: provider?.displayName ?? '', environmentId: environments.find((env) => env.name === 'Development')?._id || '', + conditions: [], }); }, [environments, provider]); @@ -131,6 +137,20 @@ export function CreateProviderInstanceSidebar({ return null; } + if (openConditions) { + return ( + { + setValue('conditions', data, { shouldDirty: true }); + }} + onClose={() => setOpenConditions(false)} + /> + ); + } + return ( + + You can only create one {provider.displayName} per environment. diff --git a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx index 6580f2c836d..cbe036ca79e 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -18,7 +18,7 @@ import { import { Button, colors, Sidebar, Text } from '../../../../design-system'; import { useProviders } from '../../useProviders'; -import type { IIntegratedProvider } from '../../types'; +import type { IConditions, IIntegratedProvider } from '../../types'; import { IntegrationInput } from '../IntegrationInput'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; import { useUpdateIntegration } from '../../../../api/hooks/useUpdateIntegration'; @@ -36,12 +36,15 @@ import { NovuInAppSetupWarning } from '../NovuInAppSetupWarning'; import { NovuProviderSidebarContent } from './NovuProviderSidebarContent'; import { useSelectPrimaryIntegrationModal } from './useSelectPrimaryIntegrationModal'; import { ShareableUrl } from '../Modal/ConnectIntegrationForm'; +import { Conditions } from '../../../../components/conditions/Conditions'; +import { ConditionPlus } from '../../../../design-system/icons'; interface IProviderForm { name: string; credentials: ICredentialsDto; active: boolean; identifier: string; + conditions: IConditions[]; } enum SidebarStateEnum { @@ -62,6 +65,7 @@ export function UpdateProviderSidebar({ const { isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const [selectedProvider, setSelectedProvider] = useState(null); const [sidebarState, setSidebarState] = useState(SidebarStateEnum.NORMAL); + const [openConditions, setOpenConditions] = useState(false); const [framework, setFramework] = useState(null); const { providers, isLoading: areProvidersLoading } = useProviders(); const isNovuInAppProvider = selectedProvider?.providerId === InAppProviderIdEnum.Novu; @@ -79,6 +83,7 @@ export function UpdateProviderSidebar({ credentials: {}, active: false, identifier: '', + conditions: [], }, }); const { @@ -87,6 +92,7 @@ export function UpdateProviderSidebar({ reset, watch, setValue, + getValues, formState: { errors, isDirty, dirtyFields }, } = methods; @@ -138,6 +144,7 @@ export function UpdateProviderSidebar({ return prev; }, {} as any), + conditions: foundProvider.conditions, active: foundProvider.active, }); }, [integrationId, providers]); @@ -206,6 +213,20 @@ export function UpdateProviderSidebar({ name: `credentials.${CredentialsKeyEnum.Hmac}`, }); + if (openConditions) { + return ( + { + setValue('conditions', data, { shouldDirty: true }); + }} + onClose={() => setOpenConditions(false)} + /> + ); + } + if ( SmsProviderIdEnum.Novu === selectedProvider?.providerId || EmailProviderIdEnum.Novu === selectedProvider?.providerId @@ -326,6 +347,9 @@ export function UpdateProviderSidebar({ + diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 3439cc75579..a4c11479f00 100644 --- a/apps/web/src/pages/integrations/types.ts +++ b/apps/web/src/pages/integrations/types.ts @@ -1,5 +1,8 @@ import type { + BuilderFieldType, + BuilderGroupValues, ChannelTypeEnum, + FilterParts, IConfigCredentials, ICredentials, ILogoFileName, @@ -20,7 +23,7 @@ export interface ITableIntegration { environment: string; active: boolean; logoFileName: IProviderConfig['logoFileName']; - conditions?: any[]; + conditions?: IConditions[]; } export interface IIntegratedProvider { @@ -33,6 +36,7 @@ export interface IIntegratedProvider { comingSoon: boolean; active: boolean; connected: boolean; + conditions?: IConditions[]; logoFileName: ILogoFileName; betaVersion: boolean; novu?: boolean; @@ -51,6 +55,7 @@ export interface IntegrationEntity { providerId: ProvidersIdEnum; channel: ChannelTypeEnum; credentials: ICredentials; + conditions?: IConditions[]; active: boolean; deleted: boolean; order: number; @@ -58,3 +63,10 @@ export interface IntegrationEntity { deletedAt: string; deletedBy: string; } + +export interface IConditions { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; +} diff --git a/apps/web/src/pages/integrations/useProviders.ts b/apps/web/src/pages/integrations/useProviders.ts index 923970872df..d4b6e2faff6 100644 --- a/apps/web/src/pages/integrations/useProviders.ts +++ b/apps/web/src/pages/integrations/useProviders.ts @@ -116,6 +116,7 @@ function initializeProvidersByIntegration(integrations: IntegrationEntity[]): II name: integrationItem?.name, identifier: integrationItem?.identifier, primary: integrationItem?.primary ?? false, + conditions: integrationItem?.conditions ?? [], }; }); } diff --git a/apps/web/src/pages/integrations/utils.ts b/apps/web/src/pages/integrations/utils.ts index d78a54b92ce..1104e3ac571 100644 --- a/apps/web/src/pages/integrations/utils.ts +++ b/apps/web/src/pages/integrations/utils.ts @@ -29,5 +29,6 @@ export const mapToTableIntegration = ( active: integration.active, logoFileName, providerId: integration.providerId, + conditions: integration.conditions, }; }; diff --git a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx index aec4e688048..73953261279 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx @@ -1,5 +1,9 @@ import { Group } from '@mantine/core'; +import { useState } from 'react'; import { useFormContext } from 'react-hook-form'; +import { useParams } from 'react-router-dom'; + +import { StepTypeEnum } from '@novu/shared'; import { Button } from '../../../../design-system'; import type { IForm } from '../../components/formTypes'; @@ -7,13 +11,9 @@ import { StepActiveSwitch } from '../StepActiveSwitch'; import { useEnvController } from '../../../../hooks'; import { ShouldStopOnFailSwitch } from '../ShouldStopOnFailSwitch'; import { ReplyCallback, ReplyCallbackSwitch } from '../ReplyCallback'; -import { useParams } from 'react-router-dom'; -import { StepTypeEnum } from '@novu/shared'; import { When } from '../../../../components/utils/When'; import { FilterModal } from '../../filter/FilterModal'; -import { useState } from 'react'; -import { Filter } from '../../../../design-system/icons/actions/Filter'; -import { FilterGradient } from '../../../../design-system/icons/gradient/FilterGradient'; +import { FilterGradient, Filter } from '../../../../design-system/icons'; import { FilterOutlined } from '../../../../design-system/icons/gradient/FilterOutlined'; export function StepSettings({ index }: { index: number }) { diff --git a/libs/shared/src/consts/filters/filters.ts b/libs/shared/src/consts/filters/filters.ts index 9a7eac4db05..1bc00920ad3 100644 --- a/libs/shared/src/consts/filters/filters.ts +++ b/libs/shared/src/consts/filters/filters.ts @@ -2,6 +2,7 @@ import { FilterPartTypeEnum } from '../../types'; export const FILTER_TO_LABEL = { [FilterPartTypeEnum.PAYLOAD]: 'Payload', + [FilterPartTypeEnum.TENANT]: 'Tenant', [FilterPartTypeEnum.SUBSCRIBER]: 'Subscriber', [FilterPartTypeEnum.WEBHOOK]: 'Webhook', [FilterPartTypeEnum.IS_ONLINE]: 'Online right now', diff --git a/libs/shared/src/dto/integration/construct-integration.interface.ts b/libs/shared/src/dto/integration/construct-integration.interface.ts index 5754dd8dc37..8b3cc2c02ff 100644 --- a/libs/shared/src/dto/integration/construct-integration.interface.ts +++ b/libs/shared/src/dto/integration/construct-integration.interface.ts @@ -1,5 +1,6 @@ import { ICredentials } from '../../entities/integration'; import type { EnvironmentId } from '../../types'; +import { BuilderFieldType, BuilderGroupValues, FilterParts } from '../../types'; export type ICredentialsDto = ICredentials; @@ -10,4 +11,10 @@ export interface IConstructIntegrationDto { credentials?: ICredentialsDto; active?: boolean; check?: boolean; + conditions?: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; }