From a414dcd8204ec2ec0cf8bd76c3ed61469fcd5745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Mon, 28 Aug 2023 15:38:59 +0200 Subject: [PATCH 01/50] refactor: so filter logic from message matcher to filter abstract class --- .../message-matcher.usecase.spec.ts | 24 +-- .../message-matcher.usecase.ts | 127 +++------------- .../usecases/message-matcher/types.ts | 8 - .../send-message/send-message.usecase.ts | 2 +- packages/application-generic/src/index.ts | 2 + .../src/utils}/filter-processing-details.ts | 10 +- .../application-generic/src/utils/filter.ts | 139 ++++++++++++++++++ 7 files changed, 180 insertions(+), 132 deletions(-) delete mode 100644 apps/worker/src/app/workflow/usecases/message-matcher/types.ts rename {apps/worker/src/app/workflow/usecases/message-matcher => packages/application-generic/src/utils}/filter-processing-details.ts (69%) create mode 100644 packages/application-generic/src/utils/filter.ts diff --git a/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.spec.ts b/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.spec.ts index 77c59b720aa..e716e042800 100644 --- a/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.spec.ts +++ b/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.spec.ts @@ -1053,7 +1053,7 @@ describe('Message filter matcher', function () { it('should add a passed condition', () => { const result = MessageMatcher.sumFilters( { - stepFilters: [], + filters: [], failedFilters: [], passedFilters: ['payload'], }, @@ -1069,14 +1069,14 @@ describe('Message filter matcher', function () { expect(result.passedFilters).to.contain('payload'); expect(result.passedFilters.length).to.eq(1); - expect(result.stepFilters.length).to.eq(1); - expect(result.stepFilters).to.contain('payload'); + expect(result.filters.length).to.eq(1); + expect(result.filters).to.contain('payload'); }); it('should add a failed condition', () => { const result = MessageMatcher.sumFilters( { - stepFilters: [], + filters: [], failedFilters: [], passedFilters: [], }, @@ -1092,14 +1092,14 @@ describe('Message filter matcher', function () { expect(result.failedFilters).to.contain('payload'); expect(result.failedFilters.length).to.eq(1); - expect(result.stepFilters.length).to.eq(1); - expect(result.stepFilters).to.contain('payload'); + expect(result.filters.length).to.eq(1); + expect(result.filters).to.contain('payload'); }); it('should add online for both cases of online', () => { let result = MessageMatcher.sumFilters( { - stepFilters: [], + filters: [], failedFilters: [], passedFilters: [], }, @@ -1115,12 +1115,12 @@ describe('Message filter matcher', function () { expect(result.passedFilters).to.contain('online'); expect(result.passedFilters.length).to.eq(1); - expect(result.stepFilters.length).to.eq(1); - expect(result.stepFilters).to.contain('online'); + expect(result.filters.length).to.eq(1); + expect(result.filters).to.contain('online'); result = MessageMatcher.sumFilters( { - stepFilters: [], + filters: [], failedFilters: [], passedFilters: [], }, @@ -1136,8 +1136,8 @@ describe('Message filter matcher', function () { expect(result.passedFilters).to.contain('online'); expect(result.passedFilters.length).to.eq(1); - expect(result.stepFilters.length).to.eq(1); - expect(result.stepFilters).to.contain('online'); + expect(result.filters.length).to.eq(1); + expect(result.filters).to.contain('online'); }); }); }); diff --git a/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.ts b/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.ts index 4608ed5da2e..58cb50536cf 100644 --- a/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/message-matcher/message-matcher.usecase.ts @@ -3,7 +3,6 @@ import axios from 'axios'; import { Injectable } from '@nestjs/common'; import { parseISO, differenceInMinutes, differenceInHours, differenceInDays } from 'date-fns'; import { - IBaseFieldFilterPart, FilterParts, IWebhookFilterPart, IRealtimeOnlineFilterPart, @@ -27,11 +26,16 @@ import { MessageRepository, JobRepository, } from '@novu/dal'; -import { DetailEnum, CreateExecutionDetails, CreateExecutionDetailsCommand } from '@novu/application-generic'; +import { + DetailEnum, + CreateExecutionDetails, + CreateExecutionDetailsCommand, + Filter, + IFilterVariables, + FilterProcessingDetails, +} from '@novu/application-generic'; import { EmailEventStatusEnum } from '@novu/stateless'; -import { IFilterVariables } from './types'; -import { FilterProcessingDetails } from './filter-processing-details'; import { SendMessageCommand } from '../send-message/send-message.command'; import { EXCEPTION_MESSAGE_ON_WEBHOOK_FILTER, createHash, PlatformException } from '../../../shared/utils'; @@ -48,7 +52,7 @@ const differenceIn = (currentDate: Date, lastDate: Date, timeOperator: TimeOpera }; @Injectable() -export class MessageMatcher { +export class MessageMatcher extends Filter { constructor( private subscriberRepository: SubscriberRepository, private createExecutionDetails: CreateExecutionDetails, @@ -56,7 +60,9 @@ export class MessageMatcher { private executionDetailsRepository: ExecutionDetailsRepository, private messageRepository: MessageRepository, private jobRepository: JobRepository - ) {} + ) { + super(); + } public async filter( command: SendMessageCommand, @@ -75,7 +81,7 @@ export class MessageMatcher { if (step.filters?.length) { const details: FilterProcessingDetails[] = []; - const foundFilter = await findAsync(step.filters, async (filter) => { + const foundFilter = await this.findAsync(step.filters, async (filter) => { const filterProcessingDetails = new FilterProcessingDetails(); filterProcessingDetails.addFilter(filter, variables); @@ -141,7 +147,7 @@ export class MessageMatcher { public static sumFilters( summary: { - stepFilters: string[]; + filters: string[]; failedFilters: string[]; passedFilters: string[]; }, @@ -153,19 +159,7 @@ export class MessageMatcher { type = 'online'; } - if (condition.passed && !summary.passedFilters.includes(type)) { - summary.passedFilters.push(type); - } - - if (!condition.passed && !summary.failedFilters.includes(type)) { - summary.failedFilters.push(type); - } - - if (!summary.stepFilters.includes(type)) { - summary.stepFilters.push(type); - } - - return summary; + return Filter.sumFilters(summary, condition, type); } private async handleGroupFilters( @@ -201,14 +195,14 @@ export class MessageMatcher { ): Promise { const { webhookFilters, otherFilters } = this.splitFilters(filter); - const matchedOtherFilters = await filterAsync(otherFilters, (i) => + const matchedOtherFilters = await this.filterAsync(otherFilters, (i) => this.processFilter(variables, i, command, filterProcessingDetails) ); if (otherFilters.length !== matchedOtherFilters.length) { return false; } - const matchedWebhookFilters = await filterAsync(webhookFilters, (i) => + const matchedWebhookFilters = await this.filterAsync(webhookFilters, (i) => this.processFilter(variables, i, command, filterProcessingDetails) ); @@ -223,14 +217,14 @@ export class MessageMatcher { ): Promise { const { webhookFilters, otherFilters } = this.splitFilters(filter); - const foundFilter = await findAsync(otherFilters, (i) => + const foundFilter = await this.findAsync(otherFilters, (i) => this.processFilter(variables, i, command, filterProcessingDetails) ); if (foundFilter) { return true; } - return !!(await findAsync(webhookFilters, (i) => + return !!(await this.findAsync(webhookFilters, (i) => this.processFilter(variables, i, command, filterProcessingDetails) )); } @@ -373,56 +367,6 @@ export class MessageMatcher { return result; } - private processFilterEquality( - variables: IFilterVariables, - fieldFilter: IBaseFieldFilterPart, - filterProcessingDetails: FilterProcessingDetails - ): boolean { - const actualValue = _.get(variables, `${fieldFilter.on}.${fieldFilter.field}`); - const filterValue = this.parseValue(actualValue, fieldFilter.value); - let result = false; - - if (fieldFilter.operator === 'EQUAL') { - result = actualValue === filterValue; - } - if (fieldFilter.operator === 'NOT_EQUAL') { - result = actualValue !== filterValue; - } - if (fieldFilter.operator === 'LARGER') { - result = actualValue > filterValue; - } - if (fieldFilter.operator === 'SMALLER') { - result = actualValue < filterValue; - } - if (fieldFilter.operator === 'LARGER_EQUAL') { - result = actualValue >= filterValue; - } - if (fieldFilter.operator === 'SMALLER_EQUAL') { - result = actualValue <= filterValue; - } - if (fieldFilter.operator === 'NOT_IN') { - result = !actualValue.includes(filterValue); - } - if (fieldFilter.operator === 'IN') { - result = actualValue.includes(filterValue); - } - if (fieldFilter.operator === 'IS_DEFINED') { - result = actualValue !== undefined; - } - const actualValueString: string = Array.isArray(actualValue) ? JSON.stringify(actualValue) : `${actualValue ?? ''}`; - - filterProcessingDetails.addCondition({ - filter: FILTER_TO_LABEL[fieldFilter.on], - field: fieldFilter.field, - expected: `${filterValue}`, - actual: `${actualValueString}`, - operator: `${fieldFilter.operator}`, - passed: result, - }); - - return result; - } - private async getWebhookResponse( child: IWebhookFilterPart, variables: IFilterVariables, @@ -529,37 +473,4 @@ export class MessageMatcher { return passed; } - - private parseValue(originValue, parsingValue) { - switch (typeof originValue) { - case 'number': - return Number(parsingValue); - case 'string': - return String(parsingValue); - case 'boolean': - return parsingValue === 'true'; - case 'bigint': - return Number(parsingValue); - default: - return parsingValue; - } - } -} - -async function findAsync(array: T[], predicate: (t: T) => Promise): Promise { - for (const t of array) { - if (await predicate(t)) { - return t; - } - } - - return undefined; -} - -async function filterAsync(arr: T[], callback: (item: T) => Promise): Promise { - const fail = Symbol('Filter Async failure'); - - return (await Promise.all(arr.map(async (item) => ((await callback(item)) ? item : fail)))).filter( - (i) => i !== fail - ) as T[]; } diff --git a/apps/worker/src/app/workflow/usecases/message-matcher/types.ts b/apps/worker/src/app/workflow/usecases/message-matcher/types.ts deleted file mode 100644 index d159545a1e7..00000000000 --- a/apps/worker/src/app/workflow/usecases/message-matcher/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ITriggerPayload } from '@novu/shared'; -import type { SubscriberEntity } from '@novu/dal'; - -export interface IFilterVariables { - payload?: ITriggerPayload; - subscriber?: SubscriberEntity; - webhook?: Record; -} diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts index d0ebed96513..802b25da123 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts @@ -67,7 +67,7 @@ export class SendMessage { if (!command.payload?.$on_boarding_trigger) { const usedFilters = shouldRun.conditions.reduce(MessageMatcher.sumFilters, { - stepFilters: [], + filters: [], failedFilters: [], passedFilters: [], }); diff --git a/packages/application-generic/src/index.ts b/packages/application-generic/src/index.ts index e3bac91c1fd..20d0d728d8c 100644 --- a/packages/application-generic/src/index.ts +++ b/packages/application-generic/src/index.ts @@ -7,4 +7,6 @@ export * from './logging/index'; export * from './usecases'; export * from './instrumentation/index'; export * from './utils/subscriber'; +export * from './utils/filter'; +export * from './utils/filter-processing-details'; export * from './resilience'; diff --git a/apps/worker/src/app/workflow/usecases/message-matcher/filter-processing-details.ts b/packages/application-generic/src/utils/filter-processing-details.ts similarity index 69% rename from apps/worker/src/app/workflow/usecases/message-matcher/filter-processing-details.ts rename to packages/application-generic/src/utils/filter-processing-details.ts index 1ccef11d3ae..8bfb2bbe2a0 100644 --- a/apps/worker/src/app/workflow/usecases/message-matcher/filter-processing-details.ts +++ b/packages/application-generic/src/utils/filter-processing-details.ts @@ -1,7 +1,11 @@ -import { StepFilter } from '@novu/dal'; -import { ICondition } from '@novu/shared'; +import { StepFilter, SubscriberEntity } from '@novu/dal'; +import { ICondition, ITriggerPayload } from '@novu/shared'; -import { IFilterVariables } from './types'; +export interface IFilterVariables { + payload?: ITriggerPayload; + subscriber?: SubscriberEntity; + webhook?: Record; +} export class FilterProcessingDetails { private conditions: ICondition[] = []; diff --git a/packages/application-generic/src/utils/filter.ts b/packages/application-generic/src/utils/filter.ts new file mode 100644 index 00000000000..066df2643e9 --- /dev/null +++ b/packages/application-generic/src/utils/filter.ts @@ -0,0 +1,139 @@ +import * as _ from 'lodash'; +import { + IBaseFieldFilterPart, + FILTER_TO_LABEL, + ICondition, +} from '@novu/shared'; + +import { + FilterProcessingDetails, + IFilterVariables, +} from './filter-processing-details'; + +export abstract class Filter { + protected processFilterEquality( + variables: IFilterVariables, + fieldFilter: IBaseFieldFilterPart, + filterProcessingDetails: FilterProcessingDetails + ): boolean { + const actualValue = _.get( + variables, + `${fieldFilter.on}.${fieldFilter.field}` + ); + const filterValue = this.parseValue(actualValue, fieldFilter.value); + let result = false; + + if (fieldFilter.operator === 'EQUAL') { + result = actualValue === filterValue; + } + if (fieldFilter.operator === 'NOT_EQUAL') { + result = actualValue !== filterValue; + } + if (fieldFilter.operator === 'LARGER') { + result = actualValue > filterValue; + } + if (fieldFilter.operator === 'SMALLER') { + result = actualValue < filterValue; + } + if (fieldFilter.operator === 'LARGER_EQUAL') { + result = actualValue >= filterValue; + } + if (fieldFilter.operator === 'SMALLER_EQUAL') { + result = actualValue <= filterValue; + } + if (fieldFilter.operator === 'NOT_IN') { + result = !actualValue.includes(filterValue); + } + if (fieldFilter.operator === 'IN') { + result = actualValue.includes(filterValue); + } + if (fieldFilter.operator === 'IS_DEFINED') { + result = actualValue !== undefined; + } + const actualValueString: string = Array.isArray(actualValue) + ? JSON.stringify(actualValue) + : `${actualValue ?? ''}`; + + filterProcessingDetails.addCondition({ + filter: FILTER_TO_LABEL[fieldFilter.on], + field: fieldFilter.field, + expected: `${filterValue}`, + actual: `${actualValueString}`, + operator: `${fieldFilter.operator}`, + passed: result, + }); + + return result; + } + + public static sumFilters( + summary: { + filters: string[]; + failedFilters: string[]; + passedFilters: string[]; + }, + condition: ICondition, + type?: string + ) { + if (!type) { + type = condition.filter; + } + + type = type?.toLowerCase(); + + if (condition.passed && !summary.passedFilters.includes(type)) { + summary.passedFilters.push(type); + } + + if (!condition.passed && !summary.failedFilters.includes(type)) { + summary.failedFilters.push(type); + } + + if (!summary.filters.includes(type)) { + summary.filters.push(type); + } + + return summary; + } + + private parseValue(originValue, parsingValue) { + switch (typeof originValue) { + case 'number': + return Number(parsingValue); + case 'string': + return String(parsingValue); + case 'boolean': + return parsingValue === 'true'; + case 'bigint': + return Number(parsingValue); + default: + return parsingValue; + } + } + + protected async findAsync( + array: T[], + predicate: (t: T) => Promise + ): Promise { + for (const t of array) { + if (await predicate(t)) { + return t; + } + } + + return undefined; + } + + protected async filterAsync( + arr: T[], + callback: (item: T) => Promise + ): Promise { + const fail = Symbol('Filter Async failure'); + + return ( + await Promise.all( + arr.map(async (item) => ((await callback(item)) ? item : fail)) + ) + ).filter((i) => i !== fail) as T[]; + } +} From 5ac009c80eabcca72ee986519e9c0257b6c3472e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 29 Aug 2023 11:53:57 +0200 Subject: [PATCH 02/50] feat: add mongoose schema and usecase for conditions --- apps/api/src/app/shared/dtos/step-filter.ts | 8 +- .../integration/integration.entity.ts | 12 +- .../integration/integration.schema.ts | 17 ++ libs/shared/src/types/builder/filter.types.ts | 8 +- .../conditions-filter.command.ts | 22 +++ .../conditions-filter.usecase.ts | 151 ++++++++++++++++++ .../src/usecases/conditions-filter/index.ts | 2 + .../application-generic/src/usecases/index.ts | 1 + 8 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts create mode 100644 packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts create mode 100644 packages/application-generic/src/usecases/conditions-filter/index.ts diff --git a/apps/api/src/app/shared/dtos/step-filter.ts b/apps/api/src/app/shared/dtos/step-filter.ts index 449b5c9d8a7..096f43c10ea 100644 --- a/apps/api/src/app/shared/dtos/step-filter.ts +++ b/apps/api/src/app/shared/dtos/step-filter.ts @@ -9,7 +9,13 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; class BaseFilterPart { - on: FilterPartTypeEnum; + on: + | FilterPartTypeEnum.IS_ONLINE + | FilterPartTypeEnum.IS_ONLINE_IN_LAST + | FilterPartTypeEnum.PAYLOAD + | FilterPartTypeEnum.PREVIOUS_STEP + | FilterPartTypeEnum.SUBSCRIBER + | FilterPartTypeEnum.WEBHOOK; } class BaseFieldFilterPart extends BaseFilterPart { diff --git a/libs/dal/src/repositories/integration/integration.entity.ts b/libs/dal/src/repositories/integration/integration.entity.ts index afe29c4245b..0eae6a4267b 100644 --- a/libs/dal/src/repositories/integration/integration.entity.ts +++ b/libs/dal/src/repositories/integration/integration.entity.ts @@ -1,4 +1,4 @@ -import { ChannelTypeEnum, ICredentials } from '@novu/shared'; +import { BuilderFieldType, BuilderGroupValues, ChannelTypeEnum, FilterParts, ICredentials } from '@novu/shared'; import type { EnvironmentId } from '../environment'; import type { OrganizationId } from '../organization'; @@ -32,6 +32,16 @@ export class IntegrationEntity { deletedAt: string; deletedBy: string; + + conditions?: { + isNegated: boolean; + + type: BuilderFieldType; + + value: BuilderGroupValues; + + children: FilterParts[]; + }[]; } export type ICredentialsEntity = ICredentials; diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts index 6fdff2d34fd..2789b1d21a9 100644 --- a/libs/dal/src/repositories/integration/integration.schema.ts +++ b/libs/dal/src/repositories/integration/integration.schema.ts @@ -61,6 +61,23 @@ const integrationSchema = new Schema( type: Schema.Types.Boolean, default: false, }, + conditions: [ + { + isNegated: Schema.Types.Boolean, + type: { + type: Schema.Types.String, + }, + value: Schema.Types.String, + children: [ + { + field: Schema.Types.String, + value: Schema.Types.Mixed, + operator: Schema.Types.String, + on: Schema.Types.String, + }, + ], + }, + ], }, schemaOptions ); diff --git a/libs/shared/src/types/builder/filter.types.ts b/libs/shared/src/types/builder/filter.types.ts index 75469227408..6faee7c3375 100644 --- a/libs/shared/src/types/builder/filter.types.ts +++ b/libs/shared/src/types/builder/filter.types.ts @@ -13,6 +13,7 @@ export enum FilterPartTypeEnum { IS_ONLINE = 'isOnline', IS_ONLINE_IN_LAST = 'isOnlineInLast', PREVIOUS_STEP = 'previousStep', + TENANT = 'tenant', } export enum PreviousStepTypeEnum { @@ -51,6 +52,10 @@ export interface IWebhookFilterPart extends IBaseFieldFilterPart { webhookUrl: string; } +export interface ITenantFilterPart extends IBaseFieldFilterPart { + on: FilterPartTypeEnum.TENANT; +} + export interface IRealtimeOnlineFilterPart extends IBaseFilterPart { on: FilterPartTypeEnum.IS_ONLINE; value: boolean; @@ -67,7 +72,8 @@ export type FilterParts = | IWebhookFilterPart | IRealtimeOnlineFilterPart | IOnlineInLastFilterPart - | IPreviousStepFilterPart; + | IPreviousStepFilterPart + | ITenantFilterPart; export type Operator = BuilderFieldOperator | TimeOperatorEnum; diff --git a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts new file mode 100644 index 00000000000..ba0f7aa12be --- /dev/null +++ b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts @@ -0,0 +1,22 @@ +import { + BuilderFieldType, + BuilderGroupValues, + FilterParts, +} from '@novu/shared'; +import { IsDefined } from 'class-validator'; +import { EnvironmentWithUserCommand } from '../../commands'; + +export interface IFilter { + isNegated: boolean; + + type: BuilderFieldType; + + value: BuilderGroupValues; + + children: FilterParts[]; +} + +export class ConditionsFilterCommand extends EnvironmentWithUserCommand { + @IsDefined() + filters: IFilter[]; +} diff --git a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts new file mode 100644 index 00000000000..54ea39826c8 --- /dev/null +++ b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts @@ -0,0 +1,151 @@ +import { Injectable } from '@nestjs/common'; +import { FilterParts, FilterPartTypeEnum, ICondition } from '@novu/shared'; +import { Filter } from '../../utils/filter'; +import { + FilterProcessingDetails, + IFilterVariables, +} from '../../utils/filter-processing-details'; +import { ConditionsFilterCommand, IFilter } from './conditions-filter.command'; + +@Injectable() +export class ConditionsFilter extends Filter { + public async filter( + command: ConditionsFilterCommand, + variables: IFilterVariables + ): Promise<{ + passed: boolean; + conditions: ICondition[]; + }> { + const { filters } = command; + if (!filters || !Array.isArray(filters)) { + return { + passed: true, + conditions: [], + }; + } + if (filters?.length) { + const details: FilterProcessingDetails[] = []; + + const foundFilter = await this.findAsync(filters, async (filter) => { + const filterProcessingDetails = new FilterProcessingDetails(); + filterProcessingDetails.addFilter(filter, variables); + + const children = filter.children; + const noRules = + !children || (Array.isArray(children) && children.length === 0); + if (noRules) { + return true; + } + + const singleRule = + !children || (Array.isArray(children) && children.length === 1); + if (singleRule) { + const result = await this.processFilter( + variables, + children[0], + filterProcessingDetails + ); + + details.push(filterProcessingDetails); + + return result; + } + + const result = await this.handleGroupFilters( + filter, + variables, + + filterProcessingDetails + ); + + details.push(filterProcessingDetails); + + return result; + }); + + const conditions = details + .map((detail) => detail.toObject().conditions) + .reduce( + (conditionsArray, collection) => [...collection, ...conditionsArray], + [] + ); + + return { + passed: !!foundFilter, + conditions: conditions, + }; + } + + return { + passed: true, + conditions: [], + }; + } + + private async handleGroupFilters( + filter: IFilter, + variables: IFilterVariables, + filterProcessingDetails: FilterProcessingDetails + ): Promise { + if (filter.value === 'OR') { + return await this.handleOrFilters( + filter, + variables, + + filterProcessingDetails + ); + } + + if (filter.value === 'AND') { + return await this.handleAndFilters( + filter, + variables, + filterProcessingDetails + ); + } + + return false; + } + + private async handleAndFilters( + filter: IFilter, + variables: IFilterVariables, + filterProcessingDetails: FilterProcessingDetails + ): Promise { + const matchedOtherFilters = await this.filterAsync(filter.children, (i) => + this.processFilter(variables, i, filterProcessingDetails) + ); + + return filter.children.length === matchedOtherFilters.length; + } + + private async handleOrFilters( + filter: IFilter, + variables: IFilterVariables, + filterProcessingDetails: FilterProcessingDetails + ): Promise { + const foundFilter = await this.findAsync(filter.children, (i) => + this.processFilter(variables, i, filterProcessingDetails) + ); + + return foundFilter ? true : false; + } + + private async processFilter( + variables: IFilterVariables, + child: FilterParts, + filterProcessingDetails: FilterProcessingDetails + ): Promise { + let passed = false; + + if (child.on === FilterPartTypeEnum.TENANT) { + passed = this.processFilterEquality( + variables, + child, + filterProcessingDetails + ); + } + + return passed; + } +} diff --git a/packages/application-generic/src/usecases/conditions-filter/index.ts b/packages/application-generic/src/usecases/conditions-filter/index.ts new file mode 100644 index 00000000000..c62412cb49d --- /dev/null +++ b/packages/application-generic/src/usecases/conditions-filter/index.ts @@ -0,0 +1,2 @@ +export * from './conditions-filter.command'; +export * from './conditions-filter.usecase'; diff --git a/packages/application-generic/src/usecases/index.ts b/packages/application-generic/src/usecases/index.ts index aa1a815df3c..76aaf8cee1e 100644 --- a/packages/application-generic/src/usecases/index.ts +++ b/packages/application-generic/src/usecases/index.ts @@ -24,3 +24,4 @@ export * from './update-tenant'; export * from './create-tenant'; export * from './get-tenant'; export * from './process-tenant'; +export * from './conditions-filter'; From 399547c387c67a68da9891267e52a503225545d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 29 Aug 2023 14:35:39 +0200 Subject: [PATCH 03/50] feat: add conditions filter for integrations --- apps/api/src/app/shared/dtos/step-filter.ts | 4 +- .../initialize-session.usecase.ts | 1 + .../send-message/send-message-chat.usecase.ts | 3 + .../send-message-email.usecase.ts | 3 + .../send-message-in-app.usecase.ts | 3 + .../send-message/send-message-push.usecase.ts | 3 + .../send-message/send-message-sms.usecase.ts | 3 + .../conditions-filter.usecase.ts | 86 +++++++++---------- .../select-integration.command.ts | 7 +- .../select-integration.spec.ts | 7 +- .../select-integration.usecase.ts | 64 +++++++++++--- .../send-test-email.usecase.ts | 1 + .../src/utils/filter-processing-details.ts | 3 +- 13 files changed, 126 insertions(+), 62 deletions(-) diff --git a/apps/api/src/app/shared/dtos/step-filter.ts b/apps/api/src/app/shared/dtos/step-filter.ts index 096f43c10ea..e3f1375aad3 100644 --- a/apps/api/src/app/shared/dtos/step-filter.ts +++ b/apps/api/src/app/shared/dtos/step-filter.ts @@ -7,6 +7,7 @@ import { TimeOperatorEnum, } from '@novu/shared'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ITenantFilterPart } from '@novu/shared'; class BaseFilterPart { on: @@ -108,7 +109,8 @@ type FilterParts = | WebhookFilterPart | RealtimeOnlineFilterPart | OnlineInLastFilterPart - | PreviousStepFilterPart; + | PreviousStepFilterPart + | ITenantFilterPart; export class StepFilter { @ApiProperty() diff --git a/apps/api/src/app/widgets/usecases/initialize-session/initialize-session.usecase.ts b/apps/api/src/app/widgets/usecases/initialize-session/initialize-session.usecase.ts index cd8bf2a5aad..9682d8a53da 100644 --- a/apps/api/src/app/widgets/usecases/initialize-session/initialize-session.usecase.ts +++ b/apps/api/src/app/widgets/usecases/initialize-session/initialize-session.usecase.ts @@ -42,6 +42,7 @@ export class InitializeSession { userId: command.subscriberId, channelType: ChannelTypeEnum.IN_APP, providerId: InAppProviderIdEnum.Novu, + filterData: {}, }) ); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts index a71596608e9..f87d400bd9b 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-chat.usecase.ts @@ -162,6 +162,9 @@ export class SendMessageChat extends SendMessageBase { providerId: subscriberChannel.providerId, channelType: ChannelTypeEnum.CHAT, userId: command.userId, + filterData: { + tenant: command.job.tenant, + }, }); const chatWebhookUrl = command.payload.webhookUrl || subscriberChannel.credentials?.webhookUrl; diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts index cef3dfe4c5a..225e4531d8d 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts @@ -81,6 +81,9 @@ export class SendMessageEmail extends SendMessageBase { channelType: ChannelTypeEnum.EMAIL, userId: command.userId, identifier: overrideSelectedIntegration as string, + filterData: { + tenant: command.job.tenant, + }, }); } catch (e) { await this.createExecutionDetails.execute( diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts index 3f7824cc7be..88d62fdbac3 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-in-app.usecase.ts @@ -86,6 +86,9 @@ export class SendMessageInApp extends SendMessageBase { environmentId: command.environmentId, channelType: ChannelTypeEnum.IN_APP, userId: command.userId, + filterData: { + tenant: command.job.tenant, + }, }); if (!integration) { diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts index bad354f6476..69185daa7fd 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-push.usecase.ts @@ -145,6 +145,9 @@ export class SendMessagePush extends SendMessageBase { channelType: ChannelTypeEnum.PUSH, providerId: channel.providerId, userId: command.userId, + filterData: { + tenant: command.job.tenant, + }, }); if (!integration) { diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts index a24ea970613..ae77c9fdf94 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-sms.usecase.ts @@ -67,6 +67,9 @@ export class SendMessageSms extends SendMessageBase { channelType: ChannelTypeEnum.SMS, userId: command.userId, identifier: overrideSelectedIntegration as string, + filterData: { + tenant: command.job.tenant, + }, }); Sentry.addBreadcrumb({ diff --git a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts index 54ea39826c8..8529e2be005 100644 --- a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts +++ b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts @@ -17,68 +17,62 @@ export class ConditionsFilter extends Filter { conditions: ICondition[]; }> { const { filters } = command; - if (!filters || !Array.isArray(filters)) { + if (!filters || !Array.isArray(filters) || filters.length === 0) { return { passed: true, conditions: [], }; } - if (filters?.length) { - const details: FilterProcessingDetails[] = []; - - const foundFilter = await this.findAsync(filters, async (filter) => { - const filterProcessingDetails = new FilterProcessingDetails(); - filterProcessingDetails.addFilter(filter, variables); - - const children = filter.children; - const noRules = - !children || (Array.isArray(children) && children.length === 0); - if (noRules) { - return true; - } - - const singleRule = - !children || (Array.isArray(children) && children.length === 1); - if (singleRule) { - const result = await this.processFilter( - variables, - children[0], - filterProcessingDetails - ); - - details.push(filterProcessingDetails); - - return result; - } - - const result = await this.handleGroupFilters( - filter, - variables, + const details: FilterProcessingDetails[] = []; + + const foundFilter = await this.findAsync(filters, async (filter) => { + const filterProcessingDetails = new FilterProcessingDetails(); + filterProcessingDetails.addFilter(filter, variables); + + const children = filter.children; + const noRules = + !children || (Array.isArray(children) && children.length === 0); + if (noRules) { + return true; + } + + const singleRule = + !children || (Array.isArray(children) && children.length === 1); + if (singleRule) { + const result = await this.processFilter( + variables, + children[0], filterProcessingDetails ); details.push(filterProcessingDetails); return result; - }); + } - const conditions = details - .map((detail) => detail.toObject().conditions) - .reduce( - (conditionsArray, collection) => [...collection, ...conditionsArray], - [] - ); + const result = await this.handleGroupFilters( + filter, + variables, - return { - passed: !!foundFilter, - conditions: conditions, - }; - } + filterProcessingDetails + ); + + details.push(filterProcessingDetails); + + return result; + }); + + const conditions = details + .map((detail) => detail.toObject().conditions) + .reduce( + (conditionsArray, collection) => [...collection, ...conditionsArray], + [] + ); return { - passed: true, - conditions: [], + passed: !!foundFilter, + conditions: conditions, }; } diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.command.ts b/packages/application-generic/src/usecases/select-integration/select-integration.command.ts index c4a47c76bf0..27b1f65464f 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.command.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.command.ts @@ -1,5 +1,5 @@ import { IsDefined, IsMongoId, IsOptional } from 'class-validator'; -import { ChannelTypeEnum, ProvidersIdEnum } from '@novu/shared'; +import { ChannelTypeEnum, ITenantDefine, ProvidersIdEnum } from '@novu/shared'; import { EnvironmentWithUserCommand } from '../../commands/project.command'; @@ -16,4 +16,9 @@ export class SelectIntegrationCommand extends EnvironmentWithUserCommand { @IsOptional() providerId?: ProvidersIdEnum; + + @IsDefined() + filterData: { + tenant?: ITenantDefine; + }; } diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts b/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts index 10c19cf07c2..6cdcbc35e75 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts @@ -6,6 +6,7 @@ import { SelectIntegration } from './select-integration.usecase'; import { SelectIntegrationCommand } from './select-integration.command'; import { GetFeatureFlag } from '../get-feature-flag'; import { GetDecryptedIntegrations } from '../get-decrypted-integrations'; +import { ConditionsFilter } from '../conditions-filter'; const testIntegration: IntegrationEntity = { _environmentId: 'env-test-123', @@ -92,7 +93,8 @@ describe('select integration', function () { // @ts-ignore new GetFeatureFlag(), // @ts-ignore - new GetDecryptedIntegrations() + new GetDecryptedIntegrations(), + new ConditionsFilter() ); jest.clearAllMocks(); }); @@ -104,6 +106,7 @@ describe('select integration', function () { environmentId: 'environmentId', organizationId: 'organizationId', userId: 'userId', + filterData: {}, }) ); @@ -120,6 +123,7 @@ describe('select integration', function () { environmentId: 'environmentId', organizationId: 'organizationId', userId: 'userId', + filterData: {}, }) ); @@ -151,6 +155,7 @@ describe('select integration', function () { environmentId, organizationId, userId, + filterData: {}, }) ); diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts index 234e72926b1..c52ac564333 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts @@ -5,17 +5,20 @@ import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; import { SelectIntegrationCommand } from './select-integration.command'; import { buildIntegrationKey, CachedQuery } from '../../services'; import { FeatureFlagCommand, GetFeatureFlag } from '../get-feature-flag'; +import { ConditionsFilter } from '../conditions-filter/conditions-filter.usecase'; import { GetDecryptedIntegrations, GetDecryptedIntegrationsCommand, } from '../get-decrypted-integrations'; +import { ConditionsFilterCommand } from '../conditions-filter'; @Injectable() export class SelectIntegration { constructor( private integrationRepository: IntegrationRepository, protected getFeatureFlag: GetFeatureFlag, - protected getDecryptedIntegrationsUsecase: GetDecryptedIntegrations + protected getDecryptedIntegrationsUsecase: GetDecryptedIntegrations, + protected conditionsFilter: ConditionsFilter ) {} @CachedQuery({ @@ -52,6 +55,51 @@ export class SelectIntegration { return integrations[0]; } + let integration: IntegrationEntity | null = null; + + if (command.filterData.tenant) { + const query: Partial & { _organizationId: string } = { + ...(command.id ? { id: command.id } : {}), + _organizationId: command.organizationId, + _environmentId: command.environmentId, + channel: command.channelType, + ...(command.providerId ? { providerId: command.providerId } : {}), + active: true, + }; + + const integrations = await this.integrationRepository.find(query); + + for (const currentIntegration of integrations) { + const { passed } = await this.conditionsFilter.filter( + ConditionsFilterCommand.create({ + filters: currentIntegration.conditions, + environmentId: command.environmentId, + organizationId: command.organizationId, + userId: command.userId, + }), + command.filterData + ); + if (passed) { + integration = currentIntegration; + break; + } + } + } + + if (!integration) { + integration = await this.getPrimaryIntegration(command); + } + + if (!integration) { + return; + } + + return GetDecryptedIntegrations.getDecryptedCredentials(integration); + } + + private async getPrimaryIntegration( + command: SelectIntegrationCommand + ): Promise { const isChannelSupportsPrimary = CHANNELS_WITH_PRIMARY.includes( command.channelType ); @@ -77,16 +125,8 @@ export class SelectIntegration { }; } - const integration = await this.integrationRepository.findOne( - query, - undefined, - { query: { sort: { createdAt: -1 } } } - ); - - if (!integration) { - return; - } - - return GetDecryptedIntegrations.getDecryptedCredentials(integration); + return await this.integrationRepository.findOne(query, undefined, { + query: { sort: { createdAt: -1 } }, + }); } } diff --git a/packages/application-generic/src/usecases/send-test-email/send-test-email.usecase.ts b/packages/application-generic/src/usecases/send-test-email/send-test-email.usecase.ts index 3f1e5d7864f..864667d43b0 100644 --- a/packages/application-generic/src/usecases/send-test-email/send-test-email.usecase.ts +++ b/packages/application-generic/src/usecases/send-test-email/send-test-email.usecase.ts @@ -49,6 +49,7 @@ export class SendTestEmail { environmentId: command.environmentId, channelType: ChannelTypeEnum.EMAIL, userId: command.userId, + filterData: {}, }) ); diff --git a/packages/application-generic/src/utils/filter-processing-details.ts b/packages/application-generic/src/utils/filter-processing-details.ts index 8bfb2bbe2a0..52594edcc00 100644 --- a/packages/application-generic/src/utils/filter-processing-details.ts +++ b/packages/application-generic/src/utils/filter-processing-details.ts @@ -1,10 +1,11 @@ import { StepFilter, SubscriberEntity } from '@novu/dal'; -import { ICondition, ITriggerPayload } from '@novu/shared'; +import { ICondition, ITenantDefine, ITriggerPayload } from '@novu/shared'; export interface IFilterVariables { payload?: ITriggerPayload; subscriber?: SubscriberEntity; webhook?: Record; + tenant?: ITenantDefine; } export class FilterProcessingDetails { From 06fe8ff2805fd04dbcbf4d28dbbc68176d4ed807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 29 Aug 2023 15:00:43 +0200 Subject: [PATCH 04/50] fix: so conditions filter usecase is injected --- apps/api/src/app/integrations/usecases/index.ts | 8 +++++++- apps/worker/src/app/workflow/workflow.module.ts | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/integrations/usecases/index.ts b/apps/api/src/app/integrations/usecases/index.ts index d04e0dabab5..3a236504c10 100644 --- a/apps/api/src/app/integrations/usecases/index.ts +++ b/apps/api/src/app/integrations/usecases/index.ts @@ -1,4 +1,9 @@ -import { SelectIntegration, GetDecryptedIntegrations, CalculateLimitNovuIntegration } from '@novu/application-generic'; +import { + SelectIntegration, + GetDecryptedIntegrations, + CalculateLimitNovuIntegration, + ConditionsFilter, +} from '@novu/application-generic'; import { GetWebhookSupportStatus } from './get-webhook-support-status/get-webhook-support-status.usecase'; import { CreateIntegration } from './create-integration/create-integration.usecase'; @@ -18,6 +23,7 @@ export const USE_CASES = [ GetInAppActivated, GetWebhookSupportStatus, CreateIntegration, + ConditionsFilter, GetIntegrations, GetActiveIntegrations, SelectIntegration, diff --git a/apps/worker/src/app/workflow/workflow.module.ts b/apps/worker/src/app/workflow/workflow.module.ts index 296bc3eaa50..7b10ab568f0 100644 --- a/apps/worker/src/app/workflow/workflow.module.ts +++ b/apps/worker/src/app/workflow/workflow.module.ts @@ -36,6 +36,7 @@ import { GetTenant, CreateTenant, ProcessTenant, + ConditionsFilter, } from '@novu/application-generic'; import { JobRepository } from '@novu/dal'; @@ -70,6 +71,7 @@ const USE_CASES = [ AddDigestJob, CalculateLimitNovuIntegration, CreateExecutionDetails, + ConditionsFilter, BulkCreateExecutionDetails, GetDecryptedIntegrations, SelectIntegration, From e75fd7a33c66dd06c13b8c5f8a484ae83e5cc27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 30 Aug 2023 08:05:23 +0200 Subject: [PATCH 05/50] fix: after pr comments --- .../integration/integration.entity.ts | 11 ++------- .../conditions-filter.command.ts | 13 ++-------- .../conditions-filter.usecase.ts | 11 +++++---- .../select-integration.spec.ts | 9 +++++-- .../select-integration.usecase.ts | 24 +++++++++++++++---- .../src/utils/filter-processing-details.ts | 4 ++-- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/libs/dal/src/repositories/integration/integration.entity.ts b/libs/dal/src/repositories/integration/integration.entity.ts index 0eae6a4267b..64273b240d4 100644 --- a/libs/dal/src/repositories/integration/integration.entity.ts +++ b/libs/dal/src/repositories/integration/integration.entity.ts @@ -3,6 +3,7 @@ import { BuilderFieldType, BuilderGroupValues, ChannelTypeEnum, FilterParts, ICr import type { EnvironmentId } from '../environment'; import type { OrganizationId } from '../organization'; import { ChangePropsValueType } from '../../types/helpers'; +import { StepFilter } from '../notification-template'; export class IntegrationEntity { _id: string; @@ -33,15 +34,7 @@ export class IntegrationEntity { deletedBy: string; - conditions?: { - isNegated: boolean; - - type: BuilderFieldType; - - value: BuilderGroupValues; - - children: FilterParts[]; - }[]; + conditions?: StepFilter[]; } export type ICredentialsEntity = ICredentials; diff --git a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts index ba0f7aa12be..2ea1e94a2d1 100644 --- a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts +++ b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.command.ts @@ -1,3 +1,4 @@ +import { StepFilter } from '@novu/dal'; import { BuilderFieldType, BuilderGroupValues, @@ -6,17 +7,7 @@ import { import { IsDefined } from 'class-validator'; import { EnvironmentWithUserCommand } from '../../commands'; -export interface IFilter { - isNegated: boolean; - - type: BuilderFieldType; - - value: BuilderGroupValues; - - children: FilterParts[]; -} - export class ConditionsFilterCommand extends EnvironmentWithUserCommand { @IsDefined() - filters: IFilter[]; + filters: StepFilter[]; } diff --git a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts index 8529e2be005..876891081d5 100644 --- a/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts +++ b/packages/application-generic/src/usecases/conditions-filter/conditions-filter.usecase.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; +import { StepFilter } from '@novu/dal'; import { FilterParts, FilterPartTypeEnum, ICondition } from '@novu/shared'; import { Filter } from '../../utils/filter'; import { FilterProcessingDetails, IFilterVariables, } from '../../utils/filter-processing-details'; -import { ConditionsFilterCommand, IFilter } from './conditions-filter.command'; +import { ConditionsFilterCommand } from './conditions-filter.command'; @Injectable() export class ConditionsFilter extends Filter { @@ -77,7 +78,7 @@ export class ConditionsFilter extends Filter { } private async handleGroupFilters( - filter: IFilter, + filter: StepFilter, variables: IFilterVariables, filterProcessingDetails: FilterProcessingDetails ): Promise { @@ -102,7 +103,7 @@ export class ConditionsFilter extends Filter { } private async handleAndFilters( - filter: IFilter, + filter: StepFilter, variables: IFilterVariables, filterProcessingDetails: FilterProcessingDetails ): Promise { @@ -114,7 +115,7 @@ export class ConditionsFilter extends Filter { } private async handleOrFilters( - filter: IFilter, + filter: StepFilter, variables: IFilterVariables, filterProcessingDetails: FilterProcessingDetails ): Promise { @@ -122,7 +123,7 @@ export class ConditionsFilter extends Filter { this.processFilter(variables, i, filterProcessingDetails) ); - return foundFilter ? true : false; + return !!foundFilter; } private async processFilter( diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts b/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts index 6cdcbc35e75..e098cca392b 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.spec.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { ChannelTypeEnum, EmailProviderIdEnum } from '@novu/shared'; -import { IntegrationEntity, IntegrationRepository } from '@novu/dal'; +import { + IntegrationEntity, + IntegrationRepository, + TenantRepository, +} from '@novu/dal'; import { SelectIntegration } from './select-integration.usecase'; import { SelectIntegrationCommand } from './select-integration.command'; @@ -94,7 +98,8 @@ describe('select integration', function () { new GetFeatureFlag(), // @ts-ignore new GetDecryptedIntegrations(), - new ConditionsFilter() + new ConditionsFilter(), + new TenantRepository() ); jest.clearAllMocks(); }); diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts index c52ac564333..35db9f92a13 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts @@ -1,5 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { IntegrationEntity, IntegrationRepository } from '@novu/dal'; +import { + IntegrationEntity, + IntegrationRepository, + TenantEntity, + TenantRepository, +} from '@novu/dal'; import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; import { SelectIntegrationCommand } from './select-integration.command'; @@ -18,7 +23,8 @@ export class SelectIntegration { private integrationRepository: IntegrationRepository, protected getFeatureFlag: GetFeatureFlag, protected getDecryptedIntegrationsUsecase: GetDecryptedIntegrations, - protected conditionsFilter: ConditionsFilter + protected conditionsFilter: ConditionsFilter, + private tenantRepository: TenantRepository ) {} @CachedQuery({ @@ -57,7 +63,7 @@ export class SelectIntegration { let integration: IntegrationEntity | null = null; - if (command.filterData.tenant) { + if (command.identifier) { const query: Partial & { _organizationId: string } = { ...(command.id ? { id: command.id } : {}), _organizationId: command.organizationId, @@ -67,6 +73,14 @@ export class SelectIntegration { active: true, }; + let tenant: TenantEntity | null = null; + + if (command.filterData.tenant.identifier) { + tenant = await this.tenantRepository.findOne({ + identifier: command.filterData.tenant.identifier, + }); + } + const integrations = await this.integrationRepository.find(query); for (const currentIntegration of integrations) { @@ -77,7 +91,9 @@ export class SelectIntegration { organizationId: command.organizationId, userId: command.userId, }), - command.filterData + { + tenant, + } ); if (passed) { integration = currentIntegration; diff --git a/packages/application-generic/src/utils/filter-processing-details.ts b/packages/application-generic/src/utils/filter-processing-details.ts index 52594edcc00..c234ffd9a78 100644 --- a/packages/application-generic/src/utils/filter-processing-details.ts +++ b/packages/application-generic/src/utils/filter-processing-details.ts @@ -1,11 +1,11 @@ -import { StepFilter, SubscriberEntity } from '@novu/dal'; +import { StepFilter, SubscriberEntity, TenantEntity } from '@novu/dal'; import { ICondition, ITenantDefine, ITriggerPayload } from '@novu/shared'; export interface IFilterVariables { payload?: ITriggerPayload; subscriber?: SubscriberEntity; webhook?: Record; - tenant?: ITenantDefine; + tenant?: TenantEntity; } export class FilterProcessingDetails { From 74a13ab06e01ec2098535532d3b176e0e9a89e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 30 Aug 2023 10:36:44 +0200 Subject: [PATCH 06/50] feat: add api to add conditions on create and update integration --- .../dtos/create-integration-request.dto.ts | 6 ++++++ .../dtos/integration-response.dto.ts | 8 ++++++++ .../dtos/update-integration.dto.ts | 6 ++++++ .../integrations/integrations.controller.ts | 2 ++ .../create-integration.command.ts | 4 ++++ .../create-integration.usecase.ts | 5 +++++ .../update-integration.command.ts | 4 ++++ .../update-integration.usecase.ts | 4 +++- apps/api/src/app/shared/dtos/step-filter.ts | 20 ++++++++++--------- 9 files changed, 49 insertions(+), 10 deletions(-) diff --git a/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts b/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts index 8e1bb5aba3d..bad4f31c898 100644 --- a/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts +++ b/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts @@ -4,6 +4,7 @@ import { Type } from 'class-transformer'; import { ChannelTypeEnum, ICreateIntegrationBodyDto } from '@novu/shared'; import { CredentialsDto } from './credentials.dto'; +import { StepFilter } from '../../shared/dtos/step-filter'; export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto { @ApiPropertyOptional({ type: String }) @@ -53,4 +54,9 @@ export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto { @IsOptional() @IsBoolean() check?: boolean; + + @ApiPropertyOptional({ + type: [StepFilter], + }) + conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/dtos/integration-response.dto.ts b/apps/api/src/app/integrations/dtos/integration-response.dto.ts index a3a4551a387..6312328cb4e 100644 --- a/apps/api/src/app/integrations/dtos/integration-response.dto.ts +++ b/apps/api/src/app/integrations/dtos/integration-response.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ChannelTypeEnum } from '@novu/shared'; +import { ValidateNested } from 'class-validator'; +import { StepFilter } from '../../shared/dtos/step-filter'; import { CredentialsDto } from './credentials.dto'; export class IntegrationResponseDto { @@ -45,4 +47,10 @@ export class IntegrationResponseDto { @ApiProperty() primary: boolean; + + @ApiPropertyOptional({ + type: [StepFilter], + }) + @ValidateNested({ each: true }) + conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/dtos/update-integration.dto.ts b/apps/api/src/app/integrations/dtos/update-integration.dto.ts index bdb7ee1a052..ead32018f69 100644 --- a/apps/api/src/app/integrations/dtos/update-integration.dto.ts +++ b/apps/api/src/app/integrations/dtos/update-integration.dto.ts @@ -3,6 +3,7 @@ import { IUpdateIntegrationBodyDto } from '@novu/shared'; import { IsBoolean, IsMongoId, IsOptional, IsString, ValidateNested } from 'class-validator'; import { CredentialsDto } from './credentials.dto'; import { Type } from 'class-transformer'; +import { StepFilter } from '../../shared/dtos/step-filter'; export class UpdateIntegrationRequestDto implements IUpdateIntegrationBodyDto { @ApiPropertyOptional({ type: String }) @@ -40,4 +41,9 @@ export class UpdateIntegrationRequestDto implements IUpdateIntegrationBodyDto { @IsOptional() @IsBoolean() check?: boolean; + + @ApiPropertyOptional({ + type: [StepFilter], + }) + conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/integrations.controller.ts b/apps/api/src/app/integrations/integrations.controller.ts index 3f8b0c984ce..0d654dc269e 100644 --- a/apps/api/src/app/integrations/integrations.controller.ts +++ b/apps/api/src/app/integrations/integrations.controller.ts @@ -147,6 +147,7 @@ export class IntegrationsController { credentials: body.credentials, active: body.active ?? false, check: body.check ?? true, + conditions: body.conditions, }) ); } @@ -178,6 +179,7 @@ export class IntegrationsController { credentials: body.credentials, active: body.active, check: body.check ?? true, + conditions: body.conditions, }) ); } diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts index 90c00c11105..7a652b18100 100644 --- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts +++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts @@ -2,6 +2,7 @@ import { IsDefined, IsEnum, IsOptional, IsString } from 'class-validator'; import { ChannelTypeEnum, ICredentialsDto } from '@novu/shared'; import { EnvironmentCommand } from '../../../shared/commands/project.command'; +import { StepFilter } from '@novu/dal'; export class CreateIntegrationCommand extends EnvironmentCommand { @IsOptional() @@ -31,4 +32,7 @@ export class CreateIntegrationCommand extends EnvironmentCommand { @IsDefined() userId: string; + + @IsOptional() + conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts index b62b47c3d35..7b6c7af7214 100644 --- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts @@ -75,6 +75,10 @@ export class CreateIntegration { channel: command.channel, }); + if (command.conditions && command.conditions.length > 0) { + return result; + } + if (activeIntegrationsCount === 0) { result.primary = true; } @@ -176,6 +180,7 @@ export class CreateIntegration { channel: command.channel, credentials: encryptCredentials(command.credentials ?? {}), active: command.active, + conditions: command.conditions, }; const isActiveAndChannelSupportsPrimary = command.active && CHANNELS_WITH_PRIMARY.includes(command.channel); diff --git a/apps/api/src/app/integrations/usecases/update-integration/update-integration.command.ts b/apps/api/src/app/integrations/usecases/update-integration/update-integration.command.ts index 69211eebaa2..e43a1d02aa7 100644 --- a/apps/api/src/app/integrations/usecases/update-integration/update-integration.command.ts +++ b/apps/api/src/app/integrations/usecases/update-integration/update-integration.command.ts @@ -2,6 +2,7 @@ import { IsDefined, IsMongoId, IsOptional, IsString } from 'class-validator'; import { ICredentialsDto } from '@novu/shared'; import { OrganizationCommand } from '../../../shared/commands/organization.command'; +import { StepFilter } from '../../../shared/dtos/step-filter'; export class UpdateIntegrationCommand extends OrganizationCommand { @IsOptional() @@ -31,4 +32,7 @@ export class UpdateIntegrationCommand extends OrganizationCommand { @IsOptional() check?: boolean; + + @IsOptional() + conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts index 62adbb8bff8..e6667a76260 100644 --- a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts @@ -182,6 +182,8 @@ export class UpdateIntegration { updatePayload.credentials = encryptCredentials(command.credentials); } + updatePayload.conditions = command.conditions; + if (!Object.keys(updatePayload).length) { throw new BadRequestException('No properties found for update'); } @@ -201,7 +203,7 @@ export class UpdateIntegration { active: !!command.active, }); - updatePayload.primary = primary; + updatePayload.primary = command.conditions && command.conditions.length > 0 ? false : primary; updatePayload.priority = priority; } diff --git a/apps/api/src/app/shared/dtos/step-filter.ts b/apps/api/src/app/shared/dtos/step-filter.ts index e3f1375aad3..cd06682db44 100644 --- a/apps/api/src/app/shared/dtos/step-filter.ts +++ b/apps/api/src/app/shared/dtos/step-filter.ts @@ -7,16 +7,9 @@ import { TimeOperatorEnum, } from '@novu/shared'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ITenantFilterPart } from '@novu/shared'; class BaseFilterPart { - on: - | FilterPartTypeEnum.IS_ONLINE - | FilterPartTypeEnum.IS_ONLINE_IN_LAST - | FilterPartTypeEnum.PAYLOAD - | FilterPartTypeEnum.PREVIOUS_STEP - | FilterPartTypeEnum.SUBSCRIBER - | FilterPartTypeEnum.WEBHOOK; + on: FilterPartTypeEnum; } class BaseFieldFilterPart extends BaseFilterPart { @@ -104,13 +97,21 @@ class PreviousStepFilterPart extends BaseFilterPart { stepType: PreviousStepTypeEnum; } +class TenantFilterPart extends BaseFieldFilterPart { + @ApiProperty({ + enum: [FilterPartTypeEnum.TENANT], + description: 'Only on integrations right now', + }) + on: FilterPartTypeEnum.TENANT; +} + type FilterParts = | FieldFilterPart | WebhookFilterPart | RealtimeOnlineFilterPart | OnlineInLastFilterPart | PreviousStepFilterPart - | ITenantFilterPart; + | TenantFilterPart; export class StepFilter { @ApiProperty() @@ -133,6 +134,7 @@ export class StepFilter { RealtimeOnlineFilterPart, OnlineInLastFilterPart, PreviousStepFilterPart, + TenantFilterPart, ], }) children: FilterParts[]; From e06e6ef41664b68867df9590947c2dd5a3d87e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 30 Aug 2023 10:43:27 +0200 Subject: [PATCH 07/50] fix: after pr comments --- .../select-integration/select-integration.usecase.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts index 35db9f92a13..db7534b358a 100644 --- a/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts +++ b/packages/application-generic/src/usecases/select-integration/select-integration.usecase.ts @@ -61,9 +61,10 @@ export class SelectIntegration { return integrations[0]; } - let integration: IntegrationEntity | null = null; + let integration: IntegrationEntity | null = + await this.getPrimaryIntegration(command); - if (command.identifier) { + if (!command.identifier && command.filterData.tenant) { const query: Partial & { _organizationId: string } = { ...(command.id ? { id: command.id } : {}), _organizationId: command.organizationId, @@ -102,10 +103,6 @@ export class SelectIntegration { } } - if (!integration) { - integration = await this.getPrimaryIntegration(command); - } - if (!integration) { return; } From 280c2ad2d511e9984575ea0f87e6a4afa4266742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 30 Aug 2023 12:32:12 +0200 Subject: [PATCH 08/50] feat: fix so conditions are emptied when integration are set as primary --- .../set-integration-as-primary.usecase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/api/src/app/integrations/usecases/set-integration-as-primary/set-integration-as-primary.usecase.ts b/apps/api/src/app/integrations/usecases/set-integration-as-primary/set-integration-as-primary.usecase.ts index d28ccce68c7..1d17a57af25 100644 --- a/apps/api/src/app/integrations/usecases/set-integration-as-primary/set-integration-as-primary.usecase.ts +++ b/apps/api/src/app/integrations/usecases/set-integration-as-primary/set-integration-as-primary.usecase.ts @@ -46,6 +46,7 @@ export class SetIntegrationAsPrimary { $set: { active: true, primary: true, + conditions: [], }, } ); From 2e25e86048f92354c56103c6a33835ae7a245908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Wed, 30 Aug 2023 18:01:12 +0200 Subject: [PATCH 09/50] fix: after pr comments --- .../integrations/dtos/create-integration-request.dto.ts | 1 + .../src/app/integrations/dtos/integration-response.dto.ts | 2 -- .../src/app/integrations/dtos/update-integration.dto.ts | 1 + .../create-integration/create-integration.command.ts | 2 +- .../create-integration/create-integration.usecase.ts | 8 ++++---- .../update-integration/update-integration.usecase.ts | 4 +++- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts b/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts index bad4f31c898..a407877d889 100644 --- a/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts +++ b/apps/api/src/app/integrations/dtos/create-integration-request.dto.ts @@ -58,5 +58,6 @@ export class CreateIntegrationRequestDto implements ICreateIntegrationBodyDto { @ApiPropertyOptional({ type: [StepFilter], }) + @ValidateNested({ each: true }) conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/dtos/integration-response.dto.ts b/apps/api/src/app/integrations/dtos/integration-response.dto.ts index 6312328cb4e..5c3e0fe95c1 100644 --- a/apps/api/src/app/integrations/dtos/integration-response.dto.ts +++ b/apps/api/src/app/integrations/dtos/integration-response.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ChannelTypeEnum } from '@novu/shared'; -import { ValidateNested } from 'class-validator'; import { StepFilter } from '../../shared/dtos/step-filter'; import { CredentialsDto } from './credentials.dto'; @@ -51,6 +50,5 @@ export class IntegrationResponseDto { @ApiPropertyOptional({ type: [StepFilter], }) - @ValidateNested({ each: true }) conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/dtos/update-integration.dto.ts b/apps/api/src/app/integrations/dtos/update-integration.dto.ts index ead32018f69..a26ee3a7290 100644 --- a/apps/api/src/app/integrations/dtos/update-integration.dto.ts +++ b/apps/api/src/app/integrations/dtos/update-integration.dto.ts @@ -45,5 +45,6 @@ export class UpdateIntegrationRequestDto implements IUpdateIntegrationBodyDto { @ApiPropertyOptional({ type: [StepFilter], }) + @ValidateNested({ each: true }) conditions?: StepFilter[]; } diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts index 7a652b18100..af2fb5c048a 100644 --- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts +++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.command.ts @@ -2,7 +2,7 @@ import { IsDefined, IsEnum, IsOptional, IsString } from 'class-validator'; import { ChannelTypeEnum, ICredentialsDto } from '@novu/shared'; import { EnvironmentCommand } from '../../../shared/commands/project.command'; -import { StepFilter } from '@novu/dal'; +import { StepFilter } from '../../../shared/dtos/step-filter'; export class CreateIntegrationCommand extends EnvironmentCommand { @IsOptional() diff --git a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts index 7b6c7af7214..1b0d559f591 100644 --- a/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/create-integration/create-integration.usecase.ts @@ -69,16 +69,16 @@ export class CreateIntegration { result.priority = highestPriorityIntegration ? highestPriorityIntegration.priority + 1 : 1; } + if (command.conditions && command.conditions.length > 0) { + return result; + } + const activeIntegrationsCount = await this.integrationRepository.countActiveExcludingNovu({ _organizationId: command.organizationId, _environmentId: command.environmentId, channel: command.channel, }); - if (command.conditions && command.conditions.length > 0) { - return result; - } - if (activeIntegrationsCount === 0) { result.primary = true; } diff --git a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts index e6667a76260..5ea8d07b71a 100644 --- a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts @@ -182,7 +182,9 @@ export class UpdateIntegration { updatePayload.credentials = encryptCredentials(command.credentials); } - updatePayload.conditions = command.conditions; + if (command.conditions) { + updatePayload.conditions = command.conditions; + } if (!Object.keys(updatePayload).length) { throw new BadRequestException('No properties found for update'); From 64dcdda40dc2a7ccb0b0ce35cd65067373fbf115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Thu, 31 Aug 2023 11:17:35 +0200 Subject: [PATCH 10/50] fix: after pr comments --- .../update-integration.usecase.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts index 5ea8d07b71a..ef3956b8c54 100644 --- a/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/update-integration/update-integration.usecase.ts @@ -31,8 +31,10 @@ export class UpdateIntegration { private async calculatePriorityAndPrimaryForActive({ existingIntegration, + conditionsApplied = false, }: { existingIntegration: IntegrationEntity; + conditionsApplied?: boolean; }) { const result: { primary: boolean; priority: number } = { primary: existingIntegration.primary, @@ -74,15 +76,21 @@ export class UpdateIntegration { result.primary = true; } + if (conditionsApplied) { + result.primary = false; + } + return result; } private async calculatePriorityAndPrimary({ existingIntegration, active, + conditionsApplied = false, }: { existingIntegration: IntegrationEntity; active: boolean; + conditionsApplied?: boolean; }) { let result: { primary: boolean; priority: number } = { primary: existingIntegration.primary, @@ -92,6 +100,7 @@ export class UpdateIntegration { if (active) { result = await this.calculatePriorityAndPrimaryForActive({ existingIntegration, + conditionsApplied, }); } else { await this.integrationRepository.recalculatePriorityForAllActive({ @@ -198,17 +207,26 @@ export class UpdateIntegration { }) ); + const haveConditions = updatePayload.conditions && updatePayload.conditions?.length > 0; + const isChannelSupportsPrimary = CHANNELS_WITH_PRIMARY.includes(existingIntegration.channel); if (isMultiProviderConfigurationEnabled && isActiveChanged && isChannelSupportsPrimary) { const { primary, priority } = await this.calculatePriorityAndPrimary({ existingIntegration, active: !!command.active, + conditionsApplied: haveConditions, }); - updatePayload.primary = command.conditions && command.conditions.length > 0 ? false : primary; + updatePayload.primary = primary; updatePayload.priority = priority; } + const shouldRemovePrimary = haveConditions && existingIntegration.primary; + + if (shouldRemovePrimary) { + updatePayload.primary = false; + } + await this.integrationRepository.update( { _id: existingIntegration._id, @@ -219,6 +237,15 @@ export class UpdateIntegration { } ); + if (shouldRemovePrimary) { + await this.integrationRepository.recalculatePriorityForAllActive({ + _id: existingIntegration._id, + _organizationId: existingIntegration._organizationId, + _environmentId: existingIntegration._organizationId, + channel: existingIntegration.channel, + }); + } + if ( !isMultiProviderConfigurationEnabled && command.active && From 93124b89496a5bc0fe5a0ff6190e1eec113f167b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Thu, 31 Aug 2023 11:20:17 +0200 Subject: [PATCH 11/50] fix: add logs for debugging --- apps/api/src/app/events/e2e/trigger-event.e2e.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/api/src/app/events/e2e/trigger-event.e2e.ts b/apps/api/src/app/events/e2e/trigger-event.e2e.ts index cfb48d3c77e..34149ea59cb 100644 --- a/apps/api/src/app/events/e2e/trigger-event.e2e.ts +++ b/apps/api/src/app/events/e2e/trigger-event.e2e.ts @@ -473,7 +473,13 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { let expireAt = new Date(message?.expireAt as string); let createdAt = new Date(message?.createdAt as string); + console.log('expireAt', expireAt); + console.log('createdAt', createdAt); + let subExpireMonths = subMonths(expireAt, IN_APP_MESSAGE_EXPIRE_MONTHS); + + console.log('subExpireMonths', subExpireMonths); + let diff = differenceInMilliseconds(subExpireMonths, createdAt); expect(diff).to.approximately(0, 100); From 36c0b5340fccf83904217d8fcfe0c928efb0201b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Thu, 31 Aug 2023 14:14:38 +0200 Subject: [PATCH 12/50] feat: add condition icon in integration table --- .../design-system/icons/general/Condition.tsx | 27 +++++++++++++ apps/web/src/design-system/icons/index.ts | 1 + .../pages/integrations/IntegrationsList.tsx | 8 ++++ .../integrations/components/ConditionCell.tsx | 38 +++++++++++++++++++ apps/web/src/pages/integrations/types.ts | 1 + 5 files changed, 75 insertions(+) create mode 100644 apps/web/src/design-system/icons/general/Condition.tsx create mode 100644 apps/web/src/pages/integrations/components/ConditionCell.tsx diff --git a/apps/web/src/design-system/icons/general/Condition.tsx b/apps/web/src/design-system/icons/general/Condition.tsx new file mode 100644 index 00000000000..1c901a9e138 --- /dev/null +++ b/apps/web/src/design-system/icons/general/Condition.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +/* eslint-disable */ +export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/design-system/icons/index.ts b/apps/web/src/design-system/icons/index.ts index 1cfb85528f3..dcc7189b1c6 100644 --- a/apps/web/src/design-system/icons/index.ts +++ b/apps/web/src/design-system/icons/index.ts @@ -81,6 +81,7 @@ export { Translate } from './general/Translate'; export { UserAccess } from './general/UserAccess'; export { SSO } from './general/SSO'; export { Cloud } from './general/Cloud'; +export { Condition } from './general/Condition'; export { Copy } from './actions/Copy'; export { Close } from './actions/Close'; diff --git a/apps/web/src/pages/integrations/IntegrationsList.tsx b/apps/web/src/pages/integrations/IntegrationsList.tsx index 721ce176bd1..daff3f5f1ac 100644 --- a/apps/web/src/pages/integrations/IntegrationsList.tsx +++ b/apps/web/src/pages/integrations/IntegrationsList.tsx @@ -18,6 +18,7 @@ import { IntegrationStatusCell } from './components/IntegrationStatusCell'; import { When } from '../../components/utils/When'; import { IntegrationsListNoData } from './components/IntegrationsListNoData'; import { mapToTableIntegration } from './utils'; +import { ConditionCell } from './components/ConditionCell'; const columns: IExtendedColumn[] = [ { @@ -49,6 +50,13 @@ const columns: IExtendedColumn[] = [ Header: 'Environment', Cell: IntegrationEnvironmentCell, }, + { + accessor: 'conditions', + Header: 'Condition', + width: 100, + maxWidth: 100, + Cell: ConditionCell, + }, { accessor: 'active', Header: 'Status', diff --git a/apps/web/src/pages/integrations/components/ConditionCell.tsx b/apps/web/src/pages/integrations/components/ConditionCell.tsx new file mode 100644 index 00000000000..9bd7215ae85 --- /dev/null +++ b/apps/web/src/pages/integrations/components/ConditionCell.tsx @@ -0,0 +1,38 @@ +import { Group, useMantineColorScheme } from '@mantine/core'; +import { colors, IExtendedCellProps, withCellLoading } from '../../../design-system'; +import { Condition } from '../../../design-system/icons'; +import type { ITableIntegration } from '../types'; + +const ConditionCellBase = ({ row: { original } }: IExtendedCellProps) => { + const { colorScheme } = useMantineColorScheme(); + + if (original.conditions) { + return ( +
+ - +
+ ); + } + + return ( + + +
{3}
+
+ ); +}; + +export const ConditionCell = withCellLoading(ConditionCellBase); diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 0c1f6d72375..3439cc75579 100644 --- a/apps/web/src/pages/integrations/types.ts +++ b/apps/web/src/pages/integrations/types.ts @@ -20,6 +20,7 @@ export interface ITableIntegration { environment: string; active: boolean; logoFileName: IProviderConfig['logoFileName']; + conditions?: any[]; } export interface IIntegratedProvider { From 3ce87435e197f04f5b06b20df15b1ebf969e4526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Thu, 31 Aug 2023 14:16:20 +0200 Subject: [PATCH 13/50] fix: condition cell --- apps/web/src/pages/integrations/components/ConditionCell.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/integrations/components/ConditionCell.tsx b/apps/web/src/pages/integrations/components/ConditionCell.tsx index 9bd7215ae85..3e07e6e308e 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) { return (
-
{3}
+
{original.conditions.length}
); }; From d7db6330bc6db9ddcc09be42386ced9b1c6deb70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Fri, 1 Sep 2023 09:26:06 +0200 Subject: [PATCH 14/50] feat: add logic for select primary integration modal --- .../components/UpdateIntegrationSidebarHeader.tsx | 10 ++++++++-- .../multi-provider/SelectPrimaryIntegrationModal.tsx | 11 +++++++---- .../multi-provider/UpdateProviderSidebar.tsx | 8 ++++++-- apps/web/src/pages/integrations/types.ts | 2 ++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index 412ff4977bb..4784d062a33 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -6,7 +6,7 @@ import { CHANNELS_WITH_PRIMARY, NOVU_PROVIDERS } from '@novu/shared'; import { Button, colors, Dropdown, Modal, NameInput, Text, Title } from '../../../design-system'; import { useFetchEnvironments } from '../../../hooks/useFetchEnvironments'; import { ProviderImage } from './multi-provider/SelectProviderSidebar'; -import type { IIntegratedProvider } from '../types'; +import type { IIntegratedProvider, IntegrationEntity } from '../types'; import { useProviders } from '../useProviders'; import { useDeleteIntegration } from '../../../api/hooks'; import { errorMessage, successMessage } from '../../../utils/notifications'; @@ -63,7 +63,13 @@ export const UpdateIntegrationSidebarHeader = ({ openModal({ environmentId: provider.environmentId, channelType: provider.channel, - exclude: [provider.integrationId], + exclude: (el: IntegrationEntity) => { + if (el._id === provider.integrationId || (el.conditions && el.conditions.length > 0)) { + return true; + } + + return false; + }, onClose: () => { deleteIntegration({ id: provider.integrationId, diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx index 4c74643e7c7..acfd6cc2d38 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -20,7 +20,7 @@ import { IntegrationEnvironmentPill } from '../IntegrationEnvironmentPill'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels'; import { useIntegrations } from '../../../../hooks'; -import { ITableIntegration } from '../../types'; +import { IntegrationEntity, ITableIntegration } from '../../types'; import { mapToTableIntegration } from '../../utils'; import { IntegrationStatusCell } from '../IntegrationStatusCell'; import { IntegrationNameCell } from '../IntegrationNameCell'; @@ -114,7 +114,7 @@ export interface ISelectPrimaryIntegrationModalProps { isOpened: boolean; environmentId?: string; channelType?: ChannelTypeEnum; - exclude?: string[]; + exclude?: (integration: IntegrationEntity) => boolean; onClose: () => void; } @@ -143,7 +143,10 @@ export const SelectPrimaryIntegrationModal = ({ }); const integrationsByEnvAndChannel = useMemo(() => { const filteredIntegrations = (integrations ?? []).filter((el) => { - const isNotExcluded = !exclude?.includes(el._id ?? ''); + let isNotExcluded = true; + if (exclude) { + isNotExcluded = !exclude(el); + } if (environmentId) { return el.channel === channelType && el._environmentId === environmentId && isNotExcluded; @@ -152,7 +155,7 @@ export const SelectPrimaryIntegrationModal = ({ return el.channel === channelType && isNotExcluded; }); - return filteredIntegrations.map((el) => mapToTableIntegration(el, environments)); + return filteredIntegrations.filter((el) => el.active).map((el) => mapToTableIntegration(el, environments)); }, [integrations, environments, channelType, environmentId, exclude]); const initialSelectedIndex = useMemo(() => { 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..76cd81ffbfc 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -167,22 +167,26 @@ export function UpdateProviderSidebar({ return; } - const { channel: selectedChannel, environmentId, primary } = selectedProvider; + const { channel: selectedChannel, environmentId, primary, conditions } = selectedProvider; const isActiveFieldChanged = dirtyFields.active; const hasSameChannelActiveIntegration = !!providers .filter((el) => !NOVU_PROVIDERS.includes(el.providerId) && el.integrationId !== selectedProvider.integrationId) .find((el) => el.active && el.channel === selectedChannel && el.environmentId === environmentId); const isChannelSupportPrimary = CHANNELS_WITH_PRIMARY.includes(selectedChannel); + const hasAddedCondition = primary && conditions && conditions.length > 0; // show modal + const hasNotAddedCondtionOnActive = !(isActive && conditions && conditions.length > 0); // show modal if ( isActiveFieldChanged && isChannelSupportPrimary && + hasAddedCondition && + hasNotAddedCondtionOnActive && ((isActive && hasSameChannelActiveIntegration) || (!isActive && primary && hasSameChannelActiveIntegration)) ) { openSelectPrimaryIntegrationModal({ environmentId: selectedProvider?.environmentId, channelType: selectedProvider?.channel, - exclude: !isActive ? [selectedProvider.integrationId] : undefined, + exclude: !isActive ? (el) => el._id === selectedProvider.integrationId : undefined, onClose: () => { updateIntegration(data); }, diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 3439cc75579..2dbe03c52c5 100644 --- a/apps/web/src/pages/integrations/types.ts +++ b/apps/web/src/pages/integrations/types.ts @@ -40,6 +40,7 @@ export interface IIntegratedProvider { name?: string; identifier?: string; primary: boolean; + conditions?: any[]; } export interface IntegrationEntity { @@ -57,4 +58,5 @@ export interface IntegrationEntity { primary: boolean; deletedAt: string; deletedBy: string; + conditions?: any[]; } From b89d10f8802e616ff04ddc0f8d6200d1f8e077ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Fri, 1 Sep 2023 09:29:24 +0200 Subject: [PATCH 15/50] fix: cspell errors --- .../components/multi-provider/UpdateProviderSidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 76cd81ffbfc..8bbe4b5f7b7 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -174,13 +174,13 @@ export function UpdateProviderSidebar({ .find((el) => el.active && el.channel === selectedChannel && el.environmentId === environmentId); const isChannelSupportPrimary = CHANNELS_WITH_PRIMARY.includes(selectedChannel); const hasAddedCondition = primary && conditions && conditions.length > 0; // show modal - const hasNotAddedCondtionOnActive = !(isActive && conditions && conditions.length > 0); // show modal + const hasNotAddedConditionOnActive = !(isActive && conditions && conditions.length > 0); // show modal if ( isActiveFieldChanged && isChannelSupportPrimary && hasAddedCondition && - hasNotAddedCondtionOnActive && + hasNotAddedConditionOnActive && ((isActive && hasSameChannelActiveIntegration) || (!isActive && primary && hasSameChannelActiveIntegration)) ) { openSelectPrimaryIntegrationModal({ From 5fbf16523a32221b668554e1f951992f84bb87f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Sat, 2 Sep 2023 05:59:56 +0200 Subject: [PATCH 16/50] fix: after pr comment --- apps/web/src/design-system/icons/general/Condition.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/design-system/icons/general/Condition.tsx b/apps/web/src/design-system/icons/general/Condition.tsx index 1c901a9e138..73ec7b5f743 100644 --- a/apps/web/src/design-system/icons/general/Condition.tsx +++ b/apps/web/src/design-system/icons/general/Condition.tsx @@ -3,7 +3,7 @@ import React from 'react'; export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { return ( - + From 6bc1abc2154418c3d23dda8331e5aa39ed32e78b Mon Sep 17 00:00:00 2001 From: ainouzgali Date: Mon, 4 Sep 2023 12:38:57 +0300 Subject: [PATCH 17/50] feat: wip conditions component --- .../src/components/conditions/Conditions.tsx | 767 ++++++++++++++++++ .../icons/actions/ConditionPlus.tsx | 29 + .../design-system/icons/actions/Duplicate.tsx | 12 + .../icons/actions/PlusFilled.tsx | 10 +- .../design-system/icons/general/Condition.tsx | 4 +- apps/web/src/design-system/icons/index.ts | 2 + .../web/src/design-system/sidebar/Sidebar.tsx | 8 +- .../CreateProviderInstanceSidebar.tsx | 65 +- .../multi-provider/UpdateProviderSidebar.tsx | 31 + apps/web/src/pages/integrations/types.ts | 15 + .../src/pages/integrations/useProviders.ts | 16 + .../templates/editor/TemplateEditorPage.tsx | 10 +- .../templates/workflow/SideBar/Sidebar.tsx | 2 +- .../workflow/SideBar/StepSettings.tsx | 73 +- .../templates/workflow/WorkflowEditor.tsx | 1 + libs/shared/src/consts/filters/filters.ts | 1 + .../construct-integration.interface.ts | 7 + 17 files changed, 1016 insertions(+), 37 deletions(-) create mode 100644 apps/web/src/components/conditions/Conditions.tsx create mode 100644 apps/web/src/design-system/icons/actions/ConditionPlus.tsx create mode 100644 apps/web/src/design-system/icons/actions/Duplicate.tsx diff --git a/apps/web/src/components/conditions/Conditions.tsx b/apps/web/src/components/conditions/Conditions.tsx new file mode 100644 index 00000000000..f83df40a908 --- /dev/null +++ b/apps/web/src/components/conditions/Conditions.tsx @@ -0,0 +1,767 @@ +import { Button, colors, Dropdown, Input, Select, Sidebar, Text, Title, Tooltip } from '../../design-system'; +import { Grid, Group, ActionIcon, Center } from '@mantine/core'; +import { FILTER_TO_LABEL, FilterPartTypeEnum } from '@novu/shared'; +import { ConditionPlus, DotsHorizontal, Duplicate, Trash, Condition, ErrorIcon } from '../../design-system/icons'; +import { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form'; +import styled from '@emotion/styled'; +import { useEffect } from 'react'; + +export function Conditions({ + isOpened, + conditions, + onClose, + setConditions, +}: { + isOpened: boolean; + onClose: () => void; + setConditions: (data: any) => void; + conditions: any; +}) { + const { + control, + setValue, + getValues, + handleSubmit: handleSubmit1, + watch, + } = useForm({ + defaultValues: { conditions }, + }); + const { fields, append, update, remove, insert } = useFieldArray({ + control, + name: `conditions.0.children`, + }); + + const watchConditions = watch(`conditions.0.children`); + + const FilterPartTypeList = [ + { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, + { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, + ]; + function handleOnChildOnChange(index: number) { + return (data) => { + const newField = Object.assign({}, fields[index], { on: data }); + update(index, newField); + }; + } + + useEffect(() => { + console.log(watchConditions); + }, [watchConditions]); + // console.log('conditions', conditions); + + function updateConditions(data) { + console.log('data 1', data); + setConditions(data); + } + + return ( + { + e.stopPropagation(); + e.preventDefault(); + console.log(e); + handleSubmit1(updateConditions)(e); + onClose(); + + // e.stopPropagation(); + }} + customHeader={ +
+ + + Condition for + +
+ } + customFooter={ + + + + + } + > + {fields.map((item, index) => { + const filterFieldOn = (fields[index] as any).on; + console.log('item', item); + + return ( +
+ + + {index > 0 ? ( + + { + return ( + + ); + }} + /> + + + { + return ( + + ); + }} + /> + + + { + return ( + +
+ +
+ + } + required + disabled={field.value === 'IS_DEFINED'} + error={fieldState.error?.message} + 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 + + + +
+
+ ); + })} + + + + + + ); +} +export function Conditions1({ + isOpened, + conditions, + onClose, + setConditions, +}: { + isOpened: boolean; + onClose: () => void; + setConditions: (data: any) => void; + conditions: any; +}) { + const { + control, + setValue, + getValues, + handleSubmit: handleSubmit1, + watch, + } = useForm({ + defaultValues: { conditions }, + }); + const { fields, append, update, remove, insert } = useFieldArray({ + control, + name: `conditions.0.children`, + }); + + const watchConditions = watch(`conditions.0.children`); + + const FilterPartTypeList = [ + { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, + { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, + ]; + function handleOnChildOnChange(index: number) { + return (data) => { + const newField = Object.assign({}, fields[index], { on: data }); + update(index, newField); + }; + } + + useEffect(() => { + console.log(watchConditions); + }, [watchConditions]); + // console.log('conditions', conditions); + + function updateConditions(data) { + console.log('data 1', data); + setConditions(data); + } + + return ( + { + e.preventDefault(); + console.log(e); + handleSubmit1(updateConditions)(e); + onClose(); + + // e.stopPropagation(); + }} + customHeader={ +
+ + + Condition for + +
+ } + customFooter={ + + + + + } + > + {fields.map((item, index) => { + const filterFieldOn = (fields[index] as any).on; + console.log('item', item); + + return ( +
+ + + {index > 0 ? ( + + { + return ( + + ); + }} + /> + + + { + return ( + + ); + }} + /> + + + { + return ( + +
+ +
+ + } + required + disabled={field.value === 'IS_DEFINED'} + error={fieldState.error?.message} + 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 + + + +
+
+ ); + })} + + + + +
+ ); +} +export function Conditions2({ + isOpened, + onClose, + control, + setValue, + getValues, +}: { + isOpened: boolean; + onClose: () => void; + control: any; + setValue: any; + getValues: any; +}) { + const { fields, append, update, remove, insert } = useFieldArray({ + control, + name: `conditions.0.children`, + }); + + const FilterPartTypeList = [ + { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, + { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, + ]; + console.log(28 / 4); + function handleOnChildOnChange(index: number) { + return (data) => { + const newField = Object.assign({}, fields[index], { on: data }); + update(index, newField); + }; + } + + return ( + + + + Condition for + + + } + customFooter={ + + + + + } + > + {fields.map((item, index) => { + const filterFieldOn = (fields[index] as any).on; + + return ( +
+ + + {index > 0 ? ( + + { + return ( + + ); + }} + /> + + + { + return ( + + ); + }} + /> + + + { + return ( + + ); + }} + /> + + + + + + } + middlewares={{ flip: false, shift: false }} + position="bottom-end" + > + { + insert(index + 1, getValues(`conditions.0.children.${index}`)); + }} + icon={} + > + Duplicate + + { + remove(index); + }} + icon={} + > + Delete + + + + +
+ ); + })} + + + + +
+ ); +} + +const ItemName = () => { + return
bla
; +}; + +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; + } + } + } +`; 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..20f7f4485ad 100644 --- a/apps/web/src/design-system/sidebar/Sidebar.tsx +++ b/apps/web/src/design-system/sidebar/Sidebar.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { ActionIcon, createStyles, Drawer, Loader, MantineTheme, Stack } from '@mantine/core'; +import { ActionIcon, Box, createStyles, Drawer, Loader, MantineTheme, Stack } from '@mantine/core'; import { ReactNode } from 'react'; import { HEADER_HEIGHT } from '../../components/layout/constants'; @@ -46,10 +46,11 @@ const useDrawerStyles = createStyles((theme: MantineTheme) => { return { root: { position: 'absolute', - zIndex: 1, + // zIndex: 1, }, drawer: { position: 'fixed', + // zIndex: 9999, top: `${INTEGRATION_SETTING_TOP}px`, right: 0, bottom: 0, @@ -122,11 +123,12 @@ export const Sidebar = ({ closeOnEscape={false} withinPortal={false} trapFocus={false} + zIndex={999} data-expanded={isExpanded} >
- {isExpanded && ( + {isExpanded && onBack && ( 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..ebd17b528a1 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,21 @@ 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 { + BuilderFieldType, + BuilderGroupValues, + ChannelTypeEnum, + FilterParts, + FilterPartTypeEnum, + 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'; @@ -19,10 +28,17 @@ import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels'; import type { 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: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; } export function CreateProviderInstanceSidebar({ @@ -42,6 +58,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,15 +74,17 @@ 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: [], }, }); const selectedEnvironmentId = watch('environmentId'); + const conditions = watch('conditions'); const showInAppErrorMessage = useMemo(() => { if (!provider || integrations.length === 0 || provider.id !== InAppProviderIdEnum.Novu) { @@ -86,7 +105,8 @@ export function CreateProviderInstanceSidebar({ } const { channel: selectedChannel } = provider; - const { environmentId } = data; + const { environmentId, conditions: cond } = data; + console.log('data', cond); const { _id: integrationId } = await createIntegrationApi({ providerId: provider.id, @@ -95,6 +115,7 @@ export function CreateProviderInstanceSidebar({ credentials: {}, active: provider.channel === ChannelTypeEnum.IN_APP ? true : false, check: false, + conditions: cond, _environmentId: environmentId, }); @@ -124,6 +145,21 @@ export function CreateProviderInstanceSidebar({ reset({ name: provider?.displayName ?? '', environmentId: environments.find((env) => env.name === 'Development')?._id || '', + conditions: [ + { + isNegated: false, + type: 'GROUP', + value: 'AND', + children: [ + { + on: FilterPartTypeEnum.TENANT, + field: 'identifier', + value: 'pawan', + operator: 'EQUAL', + }, + ], + }, + ], }); }, [environments, provider]); @@ -131,6 +167,21 @@ export function CreateProviderInstanceSidebar({ return null; } + console.log(conditions); + + if (openConditions) { + return ( + { + setValue('conditions', data.conditions); + }} + 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..4b32a8089e5 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -5,9 +5,12 @@ import slugify from 'slugify'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { useIntercom } from 'react-use-intercom'; import { + BuilderFieldType, + BuilderGroupValues, CHANNELS_WITH_PRIMARY, CredentialsKeyEnum, EmailProviderIdEnum, + FilterParts, IConfigCredentials, IConstructIntegrationDto, ICredentialsDto, @@ -36,12 +39,20 @@ 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: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; } enum SidebarStateEnum { @@ -62,6 +73,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 +91,7 @@ export function UpdateProviderSidebar({ credentials: {}, active: false, identifier: '', + conditions: [], }, }); const { @@ -87,6 +100,7 @@ export function UpdateProviderSidebar({ reset, watch, setValue, + getValues, formState: { errors, isDirty, dirtyFields }, } = methods; @@ -138,6 +152,7 @@ export function UpdateProviderSidebar({ return prev; }, {} as any), + conditions: foundProvider.conditions, active: foundProvider.active, }); }, [integrationId, providers]); @@ -206,6 +221,19 @@ export function UpdateProviderSidebar({ name: `credentials.${CredentialsKeyEnum.Hmac}`, }); + if (openConditions) { + return ( + { + setValue('conditions', data.conditions); + }} + onClose={() => setOpenConditions(false)} + /> + ); + } + if ( SmsProviderIdEnum.Novu === selectedProvider?.providerId || EmailProviderIdEnum.Novu === selectedProvider?.providerId @@ -326,6 +354,9 @@ export function UpdateProviderSidebar({ + diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 3439cc75579..8ee50d1fc59 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, @@ -33,6 +36,12 @@ export interface IIntegratedProvider { comingSoon: boolean; active: boolean; connected: boolean; + conditions?: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; logoFileName: ILogoFileName; betaVersion: boolean; novu?: boolean; @@ -51,6 +60,12 @@ export interface IntegrationEntity { providerId: ProvidersIdEnum; channel: ChannelTypeEnum; credentials: ICredentials; + conditions?: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; active: boolean; deleted: boolean; order: number; diff --git a/apps/web/src/pages/integrations/useProviders.ts b/apps/web/src/pages/integrations/useProviders.ts index 923970872df..64ecbb4d9a4 100644 --- a/apps/web/src/pages/integrations/useProviders.ts +++ b/apps/web/src/pages/integrations/useProviders.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import * as cloneDeep from 'lodash.clonedeep'; import { ChannelTypeEnum, + FilterPartTypeEnum, IConfigCredentials, IProviderConfig, NOVU_SMS_EMAIL_PROVIDERS, @@ -116,6 +117,21 @@ function initializeProvidersByIntegration(integrations: IntegrationEntity[]): II name: integrationItem?.name, identifier: integrationItem?.identifier, primary: integrationItem?.primary ?? false, + conditions: integrationItem?.conditions ?? [ + { + isNegated: false, + type: 'GROUP', + value: 'AND', + children: [ + { + on: FilterPartTypeEnum.TENANT, + field: 'identifier', + value: 'pawan', + operator: 'EQUAL', + }, + ], + }, + ], }; }); } diff --git a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx index 63555093811..4a62f010910 100644 --- a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx +++ b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx @@ -82,11 +82,11 @@ function BaseTemplateEditorPage() { - + {/**/} ); } diff --git a/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx index 8b4a57b6e31..d60a41667ca 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx @@ -40,5 +40,5 @@ const SideBarWrapper = styled.div<{ dark: boolean }>` background: transparent; height: 100%; right: 8px; - z-index: 9999; + z-index: 5; `; diff --git a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx index aec4e688048..c27c95a7fb8 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx @@ -1,5 +1,5 @@ import { Group } from '@mantine/core'; -import { useFormContext } from 'react-hook-form'; +import { useFieldArray, useFormContext } from 'react-hook-form'; import { Button } from '../../../../design-system'; import type { IForm } from '../../components/formTypes'; @@ -7,7 +7,7 @@ import { StepActiveSwitch } from '../StepActiveSwitch'; import { useEnvController } from '../../../../hooks'; import { ShouldStopOnFailSwitch } from '../ShouldStopOnFailSwitch'; import { ReplyCallback, ReplyCallbackSwitch } from '../ReplyCallback'; -import { useParams } from 'react-router-dom'; +import { useParams, Outlet } from 'react-router-dom'; import { StepTypeEnum } from '@novu/shared'; import { When } from '../../../../components/utils/When'; import { FilterModal } from '../../filter/FilterModal'; @@ -15,18 +15,43 @@ import { useState } from 'react'; import { Filter } from '../../../../design-system/icons/actions/Filter'; import { FilterGradient } from '../../../../design-system/icons/gradient/FilterGradient'; import { FilterOutlined } from '../../../../design-system/icons/gradient/FilterOutlined'; +import { Conditions } from '../../../../components/conditions/Conditions'; export function StepSettings({ index }: { index: number }) { const { readonly } = useEnvController(); - const { control, watch, setValue } = useFormContext(); + const { control, watch, setValue, getValues } = useFormContext(); const [filterOpen, setFilterOpen] = useState(false); const { channel } = useParams<{ channel: StepTypeEnum; }>(); const [filterHover, setFilterHover] = useState(false); - + const { fields, replace, update, remove } = useFieldArray({ + control, + name: `steps.${index}.filters.0.children`, + }); const filters = watch(`steps.${index}.filters.0.children`); + console.log('fields', fields); + + /* + * if (filterOpen) { + * return ( + * { + * setFilterOpen(false); + * }} + * setConditions={(data) => { + * console.log(data); + * replace(data.conditions[0].children); + * setValue(`steps.${index}.filters.0.children`, fields); + * }} + * conditions={getValues(`steps.${index}.filters`)} + * /> + * ); + * } + */ + return ( <> @@ -79,18 +104,34 @@ export function StepSettings({ index }: { index: number }) { - { - setFilterOpen(false); - }} - confirm={() => { - setFilterOpen(false); - }} - control={control} - stepIndex={index} - setValue={setValue} - /> + {filterOpen && ( + <> + { + setFilterOpen(false); + }} + setConditions={(data) => { + console.log(data); + replace(data.conditions[0].children); + setValue(`steps.${index}.filters.0.children`, fields); + }} + conditions={getValues(`steps.${index}.filters`)} + /> + + )} + {/* {*/} + {/* setFilterOpen(false);*/} + {/* }}*/} + {/* confirm={() => {*/} + {/* setFilterOpen(false);*/} + {/* }}*/} + {/* control={control}*/} + {/* stepIndex={index}*/} + {/* setValue={setValue}*/} + {/*/>*/} ); } diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index bc055ab8fc4..1b5472f720c 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -61,6 +61,7 @@ const WorkflowEditor = () => { event.preventDefault(); if (node.type === 'channelNode') { + console.log('node.data.uuid', node.data.uuid); navigate(basePath + `/${node.data.channelType}/${node.data.uuid}`); } if (node.type === 'triggerNode') { 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[]; + }[]; } From c259fbd412e837b89469a406a7209f3e66349d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Mon, 4 Sep 2023 15:30:12 +0200 Subject: [PATCH 18/50] fix: after pr comments --- .../UpdateIntegrationSidebarHeader.tsx | 6 +----- .../SelectPrimaryIntegrationModal.tsx | 2 +- apps/web/src/pages/integrations/types.ts | 17 +++++++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index 4784d062a33..58de40fb612 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -64,11 +64,7 @@ export const UpdateIntegrationSidebarHeader = ({ environmentId: provider.environmentId, channelType: provider.channel, exclude: (el: IntegrationEntity) => { - if (el._id === provider.integrationId || (el.conditions && el.conditions.length > 0)) { - return true; - } - - return false; + return el._id === provider.integrationId; }, onClose: () => { deleteIntegration({ diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx index acfd6cc2d38..4f40d12f44e 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -155,7 +155,7 @@ export const SelectPrimaryIntegrationModal = ({ return el.channel === channelType && isNotExcluded; }); - return filteredIntegrations.filter((el) => el.active).map((el) => mapToTableIntegration(el, environments)); + return filteredIntegrations.map((el) => mapToTableIntegration(el, environments)); }, [integrations, environments, channelType, environmentId, exclude]); const initialSelectedIndex = useMemo(() => { diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 2dbe03c52c5..3bb25f6814a 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, @@ -40,7 +43,12 @@ export interface IIntegratedProvider { name?: string; identifier?: string; primary: boolean; - conditions?: any[]; + conditions?: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; } export interface IntegrationEntity { @@ -58,5 +66,10 @@ export interface IntegrationEntity { primary: boolean; deletedAt: string; deletedBy: string; - conditions?: any[]; + conditions?: { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; + }[]; } From 1de9293045c62c6fbb2464dce6f453d0308aa7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Mon, 4 Sep 2023 15:34:31 +0200 Subject: [PATCH 19/50] fix: after pr comment --- .../multi-provider/UpdateProviderSidebar.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 8bbe4b5f7b7..bfb59903fe6 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -173,16 +173,18 @@ export function UpdateProviderSidebar({ .filter((el) => !NOVU_PROVIDERS.includes(el.providerId) && el.integrationId !== selectedProvider.integrationId) .find((el) => el.active && el.channel === selectedChannel && el.environmentId === environmentId); const isChannelSupportPrimary = CHANNELS_WITH_PRIMARY.includes(selectedChannel); - const hasAddedCondition = primary && conditions && conditions.length > 0; // show modal - const hasNotAddedConditionOnActive = !(isActive && conditions && conditions.length > 0); // show modal - - if ( - isActiveFieldChanged && - isChannelSupportPrimary && - hasAddedCondition && - hasNotAddedConditionOnActive && - ((isActive && hasSameChannelActiveIntegration) || (!isActive && primary && hasSameChannelActiveIntegration)) - ) { + + const isChangedToActive = + isActiveFieldChanged && isChannelSupportPrimary && isActive && hasSameChannelActiveIntegration; + + const isChangedToInactiveAndIsPrimary = + isActiveFieldChanged && isChannelSupportPrimary && !isActive && primary && hasSameChannelActiveIntegration; + + const isPrimaryAndHasConditionsApplied = primary && conditions && conditions.length > 0; + + const hasNoConditions = !conditions || conditions.length === 0; + + if ((hasNoConditions && isChangedToActive) || isChangedToInactiveAndIsPrimary || isPrimaryAndHasConditionsApplied) { openSelectPrimaryIntegrationModal({ environmentId: selectedProvider?.environmentId, channelType: selectedProvider?.channel, From 7d8d0987e7110b4fd2df6cd06e18dd64d562822d Mon Sep 17 00:00:00 2001 From: ainouzgali Date: Mon, 4 Sep 2023 18:37:00 +0300 Subject: [PATCH 20/50] feat(wip): conditions component --- .../src/components/conditions/Conditions.tsx | 651 +++--------------- .../web/src/design-system/sidebar/Sidebar.tsx | 8 +- .../integrations/components/ConditionCell.tsx | 6 +- .../CreateProviderInstanceSidebar.tsx | 47 +- .../multi-provider/UpdateProviderSidebar.tsx | 15 +- apps/web/src/pages/integrations/types.ts | 23 +- .../src/pages/integrations/useProviders.ts | 17 +- apps/web/src/pages/integrations/utils.ts | 1 + .../templates/editor/TemplateEditorPage.tsx | 10 +- .../templates/workflow/SideBar/Sidebar.tsx | 2 +- .../workflow/SideBar/StepSettings.tsx | 81 +-- .../templates/workflow/WorkflowEditor.tsx | 1 - 12 files changed, 159 insertions(+), 703 deletions(-) diff --git a/apps/web/src/components/conditions/Conditions.tsx b/apps/web/src/components/conditions/Conditions.tsx index f83df40a908..fb9e52c7a7c 100644 --- a/apps/web/src/components/conditions/Conditions.tsx +++ b/apps/web/src/components/conditions/Conditions.tsx @@ -1,42 +1,47 @@ -import { Button, colors, Dropdown, Input, Select, Sidebar, Text, Title, Tooltip } from '../../design-system'; 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 { Controller, useFieldArray, useForm, useWatch } from 'react-hook-form'; -import styled from '@emotion/styled'; -import { useEffect } from 'react'; +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: any) => void; - conditions: any; + setConditions: (data: IConditions[]) => void; + conditions?: IConditions[]; + name: string; }) { const { control, setValue, getValues, - handleSubmit: handleSubmit1, - watch, + 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 watchConditions = watch(`conditions.0.children`); + const FilterPartTypeList = [{ value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }]; - const FilterPartTypeList = [ - { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, - { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, - ]; function handleOnChildOnChange(index: number) { return (data) => { const newField = Object.assign({}, fields[index], { on: data }); @@ -44,14 +49,9 @@ export function Conditions({ }; } - useEffect(() => { - console.log(watchConditions); - }, [watchConditions]); - // console.log('conditions', conditions); - function updateConditions(data) { - console.log('data 1', data); - setConditions(data); + setConditions(data.conditions); + onClose(); } return ( @@ -59,22 +59,11 @@ export function Conditions({ isOpened={isOpened} onClose={onClose} isExpanded - // onSubmit={handleSubmit(updateConditions)} - - onSubmit={(e) => { - e.stopPropagation(); - e.preventDefault(); - console.log(e); - handleSubmit1(updateConditions)(e); - onClose(); - - // e.stopPropagation(); - }} customHeader={
- Condition for + Condition for {name} provider instance
} @@ -83,276 +72,31 @@ export function Conditions({ - - - } - > - {fields.map((item, index) => { - const filterFieldOn = (fields[index] as any).on; - console.log('item', item); - - return ( -
- - - {index > 0 ? ( - - { - return ( - - ); + + 0} + label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'} + > +
+
- ); - })} - - - - - - ); -} -export function Conditions1({ - isOpened, - conditions, - onClose, - setConditions, -}: { - isOpened: boolean; - onClose: () => void; - setConditions: (data: any) => void; - conditions: any; -}) { - const { - control, - setValue, - getValues, - handleSubmit: handleSubmit1, - watch, - } = useForm({ - defaultValues: { conditions }, - }); - const { fields, append, update, remove, insert } = useFieldArray({ - control, - name: `conditions.0.children`, - }); - - const watchConditions = watch(`conditions.0.children`); - - const FilterPartTypeList = [ - { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, - { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, - ]; - function handleOnChildOnChange(index: number) { - return (data) => { - const newField = Object.assign({}, fields[index], { on: data }); - update(index, newField); - }; - } - - useEffect(() => { - console.log(watchConditions); - }, [watchConditions]); - // console.log('conditions', conditions); - - function updateConditions(data) { - console.log('data 1', data); - setConditions(data); - } - - return ( - { - e.preventDefault(); - console.log(e); - handleSubmit1(updateConditions)(e); - onClose(); - - // e.stopPropagation(); - }} - customHeader={ -
- - - Condition for - -
- } - customFooter={ - - - + Apply conditions + +
+ + } > {fields.map((item, index) => { - const filterFieldOn = (fields[index] as any).on; - console.log('item', item); - return (
@@ -388,7 +132,7 @@ export function Conditions1({ { return ( - ); - }} - /> - - - { return ( -
- -
- - } - required - disabled={field.value === 'IS_DEFINED'} - error={fieldState.error?.message} - 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 - - - -
-
- ); - })} - - - - - - ); -} -export function Conditions2({ - isOpened, - onClose, - control, - setValue, - getValues, -}: { - isOpened: boolean; - onClose: () => void; - control: any; - setValue: any; - getValues: any; -}) { - const { fields, append, update, remove, insert } = useFieldArray({ - control, - name: `conditions.0.children`, - }); - - const FilterPartTypeList = [ - { value: FilterPartTypeEnum.TENANT, label: FILTER_TO_LABEL[FilterPartTypeEnum.TENANT] }, - { value: FilterPartTypeEnum.SUBSCRIBER, label: FILTER_TO_LABEL[FilterPartTypeEnum.SUBSCRIBER] }, - ]; - console.log(28 / 4); - function handleOnChildOnChange(index: number) { - return (data) => { - const newField = Object.assign({}, fields[index], { on: data }); - update(index, newField); - }; - } - - return ( - - - - Condition for - - - } - customFooter={ - - - - - } - > - {fields.map((item, index) => { - const filterFieldOn = (fields[index] as any).on; - - return ( -
- - - {index > 0 ? ( - - { - return ( - - ); - }} - /> - - - { - return ( - ); }} @@ -678,22 +196,38 @@ export function Conditions2({ /> - { - return ( - - ); - }} - /> + {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" + /> + ); + }} + /> + )} { append({ operator: 'EQUAL', - on: 'tenant', + on: FilterPartTypeEnum.TENANT, + field: 'identifier', + value: '', }); }} icon={} @@ -748,10 +284,6 @@ export function Conditions2({ ); } -const ItemName = () => { - return
bla
; -}; - const Wrapper = styled.div` .mantine-Select-wrapper:not(:hover) { .mantine-Select-input { @@ -765,3 +297,24 @@ 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`}; + } +`; diff --git a/apps/web/src/design-system/sidebar/Sidebar.tsx b/apps/web/src/design-system/sidebar/Sidebar.tsx index 20f7f4485ad..274fc147e2c 100644 --- a/apps/web/src/design-system/sidebar/Sidebar.tsx +++ b/apps/web/src/design-system/sidebar/Sidebar.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { ActionIcon, Box, createStyles, Drawer, Loader, MantineTheme, Stack } from '@mantine/core'; +import { ActionIcon, createStyles, Drawer, Loader, MantineTheme, Stack } from '@mantine/core'; import { ReactNode } from 'react'; import { HEADER_HEIGHT } from '../../components/layout/constants'; @@ -46,11 +46,10 @@ const useDrawerStyles = createStyles((theme: MantineTheme) => { return { root: { position: 'absolute', - // zIndex: 1, + zIndex: 1, }, drawer: { position: 'fixed', - // zIndex: 9999, top: `${INTEGRATION_SETTING_TOP}px`, right: 0, bottom: 0, @@ -123,10 +122,9 @@ export const Sidebar = ({ closeOnEscape={false} withinPortal={false} trapFocus={false} - zIndex={999} data-expanded={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 ebd17b528a1..f6d667a753d 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -3,16 +3,7 @@ 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 { - BuilderFieldType, - BuilderGroupValues, - ChannelTypeEnum, - FilterParts, - FilterPartTypeEnum, - ICreateIntegrationBodyDto, - InAppProviderIdEnum, - providers, -} from '@novu/shared'; +import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared'; import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; import { ArrowLeft, ConditionPlus } from '../../../../design-system/icons'; @@ -25,7 +16,7 @@ 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'; @@ -33,12 +24,7 @@ import { Conditions } from '../../../../components/conditions/Conditions'; interface ICreateProviderInstanceForm { name: string; environmentId: string; - conditions: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; + conditions: IConditions[]; } export function CreateProviderInstanceSidebar({ @@ -84,7 +70,6 @@ export function CreateProviderInstanceSidebar({ }); const selectedEnvironmentId = watch('environmentId'); - const conditions = watch('conditions'); const showInAppErrorMessage = useMemo(() => { if (!provider || integrations.length === 0 || provider.id !== InAppProviderIdEnum.Novu) { @@ -105,8 +90,7 @@ export function CreateProviderInstanceSidebar({ } const { channel: selectedChannel } = provider; - const { environmentId, conditions: cond } = data; - console.log('data', cond); + const { environmentId, conditions } = data; const { _id: integrationId } = await createIntegrationApi({ providerId: provider.id, @@ -115,7 +99,7 @@ export function CreateProviderInstanceSidebar({ credentials: {}, active: provider.channel === ChannelTypeEnum.IN_APP ? true : false, check: false, - conditions: cond, + conditions, _environmentId: environmentId, }); @@ -145,21 +129,7 @@ export function CreateProviderInstanceSidebar({ reset({ name: provider?.displayName ?? '', environmentId: environments.find((env) => env.name === 'Development')?._id || '', - conditions: [ - { - isNegated: false, - type: 'GROUP', - value: 'AND', - children: [ - { - on: FilterPartTypeEnum.TENANT, - field: 'identifier', - value: 'pawan', - operator: 'EQUAL', - }, - ], - }, - ], + conditions: [], }); }, [environments, provider]); @@ -167,15 +137,14 @@ export function CreateProviderInstanceSidebar({ return null; } - console.log(conditions); - if (openConditions) { return ( { - setValue('conditions', data.conditions); + setValue('conditions', data, { shouldDirty: true }); }} onClose={() => setOpenConditions(false)} /> 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 4b32a8089e5..cbe036ca79e 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -5,12 +5,9 @@ import slugify from 'slugify'; import { Controller, FormProvider, useForm, useWatch } from 'react-hook-form'; import { useIntercom } from 'react-use-intercom'; import { - BuilderFieldType, - BuilderGroupValues, CHANNELS_WITH_PRIMARY, CredentialsKeyEnum, EmailProviderIdEnum, - FilterParts, IConfigCredentials, IConstructIntegrationDto, ICredentialsDto, @@ -21,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'; @@ -47,12 +44,7 @@ interface IProviderForm { credentials: ICredentialsDto; active: boolean; identifier: string; - conditions: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; + conditions: IConditions[]; } enum SidebarStateEnum { @@ -225,9 +217,10 @@ export function UpdateProviderSidebar({ return ( { - setValue('conditions', data.conditions); + setValue('conditions', data, { shouldDirty: true }); }} onClose={() => setOpenConditions(false)} /> diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index 8ee50d1fc59..a4c11479f00 100644 --- a/apps/web/src/pages/integrations/types.ts +++ b/apps/web/src/pages/integrations/types.ts @@ -23,7 +23,7 @@ export interface ITableIntegration { environment: string; active: boolean; logoFileName: IProviderConfig['logoFileName']; - conditions?: any[]; + conditions?: IConditions[]; } export interface IIntegratedProvider { @@ -36,12 +36,7 @@ export interface IIntegratedProvider { comingSoon: boolean; active: boolean; connected: boolean; - conditions?: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; + conditions?: IConditions[]; logoFileName: ILogoFileName; betaVersion: boolean; novu?: boolean; @@ -60,12 +55,7 @@ export interface IntegrationEntity { providerId: ProvidersIdEnum; channel: ChannelTypeEnum; credentials: ICredentials; - conditions?: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; + conditions?: IConditions[]; active: boolean; deleted: boolean; order: number; @@ -73,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 64ecbb4d9a4..d4b6e2faff6 100644 --- a/apps/web/src/pages/integrations/useProviders.ts +++ b/apps/web/src/pages/integrations/useProviders.ts @@ -2,7 +2,6 @@ import { useMemo } from 'react'; import * as cloneDeep from 'lodash.clonedeep'; import { ChannelTypeEnum, - FilterPartTypeEnum, IConfigCredentials, IProviderConfig, NOVU_SMS_EMAIL_PROVIDERS, @@ -117,21 +116,7 @@ function initializeProvidersByIntegration(integrations: IntegrationEntity[]): II name: integrationItem?.name, identifier: integrationItem?.identifier, primary: integrationItem?.primary ?? false, - conditions: integrationItem?.conditions ?? [ - { - isNegated: false, - type: 'GROUP', - value: 'AND', - children: [ - { - on: FilterPartTypeEnum.TENANT, - field: 'identifier', - value: 'pawan', - operator: 'EQUAL', - }, - ], - }, - ], + 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/editor/TemplateEditorPage.tsx b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx index 4a62f010910..63555093811 100644 --- a/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx +++ b/apps/web/src/pages/templates/editor/TemplateEditorPage.tsx @@ -82,11 +82,11 @@ function BaseTemplateEditorPage() { - {/**/} + ); } diff --git a/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx index d60a41667ca..8b4a57b6e31 100644 --- a/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx +++ b/apps/web/src/pages/templates/workflow/SideBar/Sidebar.tsx @@ -40,5 +40,5 @@ const SideBarWrapper = styled.div<{ dark: boolean }>` background: transparent; height: 100%; right: 8px; - z-index: 5; + z-index: 9999; `; diff --git a/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx b/apps/web/src/pages/templates/workflow/SideBar/StepSettings.tsx index c27c95a7fb8..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 { useFieldArray, useFormContext } from 'react-hook-form'; +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,50 +11,21 @@ import { StepActiveSwitch } from '../StepActiveSwitch'; import { useEnvController } from '../../../../hooks'; import { ShouldStopOnFailSwitch } from '../ShouldStopOnFailSwitch'; import { ReplyCallback, ReplyCallbackSwitch } from '../ReplyCallback'; -import { useParams, Outlet } 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'; -import { Conditions } from '../../../../components/conditions/Conditions'; export function StepSettings({ index }: { index: number }) { const { readonly } = useEnvController(); - const { control, watch, setValue, getValues } = useFormContext(); + const { control, watch, setValue } = useFormContext(); const [filterOpen, setFilterOpen] = useState(false); const { channel } = useParams<{ channel: StepTypeEnum; }>(); const [filterHover, setFilterHover] = useState(false); - const { fields, replace, update, remove } = useFieldArray({ - control, - name: `steps.${index}.filters.0.children`, - }); - const filters = watch(`steps.${index}.filters.0.children`); - console.log('fields', fields); - - /* - * if (filterOpen) { - * return ( - * { - * setFilterOpen(false); - * }} - * setConditions={(data) => { - * console.log(data); - * replace(data.conditions[0].children); - * setValue(`steps.${index}.filters.0.children`, fields); - * }} - * conditions={getValues(`steps.${index}.filters`)} - * /> - * ); - * } - */ + const filters = watch(`steps.${index}.filters.0.children`); return ( <> @@ -104,34 +79,18 @@ export function StepSettings({ index }: { index: number }) { - {filterOpen && ( - <> - { - setFilterOpen(false); - }} - setConditions={(data) => { - console.log(data); - replace(data.conditions[0].children); - setValue(`steps.${index}.filters.0.children`, fields); - }} - conditions={getValues(`steps.${index}.filters`)} - /> - - )} - {/* {*/} - {/* setFilterOpen(false);*/} - {/* }}*/} - {/* confirm={() => {*/} - {/* setFilterOpen(false);*/} - {/* }}*/} - {/* control={control}*/} - {/* stepIndex={index}*/} - {/* setValue={setValue}*/} - {/*/>*/} + { + setFilterOpen(false); + }} + confirm={() => { + setFilterOpen(false); + }} + control={control} + stepIndex={index} + setValue={setValue} + /> ); } diff --git a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx index 1b5472f720c..bc055ab8fc4 100644 --- a/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx +++ b/apps/web/src/pages/templates/workflow/WorkflowEditor.tsx @@ -61,7 +61,6 @@ const WorkflowEditor = () => { event.preventDefault(); if (node.type === 'channelNode') { - console.log('node.data.uuid', node.data.uuid); navigate(basePath + `/${node.data.channelType}/${node.data.uuid}`); } if (node.type === 'triggerNode') { From b8fea49b22d36d052bdd0e157ee6f77d0c4b357e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 5 Sep 2023 06:27:46 +0200 Subject: [PATCH 21/50] fix: after pr comment --- .../components/multi-provider/UpdateProviderSidebar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 bfb59903fe6..99c437ee150 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -180,7 +180,8 @@ export function UpdateProviderSidebar({ const isChangedToInactiveAndIsPrimary = isActiveFieldChanged && isChannelSupportPrimary && !isActive && primary && hasSameChannelActiveIntegration; - const isPrimaryAndHasConditionsApplied = primary && conditions && conditions.length > 0; + const isPrimaryAndHasConditionsApplied = + primary && conditions && conditions.length > 0 && hasSameChannelActiveIntegration; const hasNoConditions = !conditions || conditions.length === 0; From 7cc4c4eae71b5848c757608f3b9df7f83eb4c977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Mon, 4 Sep 2023 12:33:36 +0200 Subject: [PATCH 22/50] feat: add condition and primary icon buttons --- .../icons/general/AddCondition.tsx | 13 ++ .../icons/general/RemoveCondition.tsx | 12 ++ .../design-system/icons/general/StarEmpty.tsx | 2 +- .../design-system/icons/general/Warning.tsx | 12 ++ apps/web/src/design-system/icons/index.ts | 3 + .../components/ConditionIconButton.tsx | 66 +++++++++ .../components/PrimaryIconButton.tsx | 133 ++++++++++++++++++ .../UpdateIntegrationSidebarHeader.tsx | 12 +- .../CreateProviderInstanceSidebar.tsx | 6 +- apps/web/src/pages/integrations/types.ts | 12 -- 10 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/design-system/icons/general/AddCondition.tsx create mode 100644 apps/web/src/design-system/icons/general/RemoveCondition.tsx create mode 100644 apps/web/src/design-system/icons/general/Warning.tsx create mode 100644 apps/web/src/pages/integrations/components/ConditionIconButton.tsx create mode 100644 apps/web/src/pages/integrations/components/PrimaryIconButton.tsx diff --git a/apps/web/src/design-system/icons/general/AddCondition.tsx b/apps/web/src/design-system/icons/general/AddCondition.tsx new file mode 100644 index 00000000000..7641faae6e9 --- /dev/null +++ b/apps/web/src/design-system/icons/general/AddCondition.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +/* eslint-disable */ +export function AddCondition(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + + ); +} diff --git a/apps/web/src/design-system/icons/general/RemoveCondition.tsx b/apps/web/src/design-system/icons/general/RemoveCondition.tsx new file mode 100644 index 00000000000..8c61a4abe27 --- /dev/null +++ b/apps/web/src/design-system/icons/general/RemoveCondition.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +/* eslint-disable */ +export function RemoveCondition(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + ); +} diff --git a/apps/web/src/design-system/icons/general/StarEmpty.tsx b/apps/web/src/design-system/icons/general/StarEmpty.tsx index c0a3d3b062f..b64ae38ae9d 100644 --- a/apps/web/src/design-system/icons/general/StarEmpty.tsx +++ b/apps/web/src/design-system/icons/general/StarEmpty.tsx @@ -4,7 +4,7 @@ export const StarEmpty = (props: React.ComponentPropsWithoutRef<'svg'>) => { return ( diff --git a/apps/web/src/design-system/icons/general/Warning.tsx b/apps/web/src/design-system/icons/general/Warning.tsx new file mode 100644 index 00000000000..5fa4fa4c0a4 --- /dev/null +++ b/apps/web/src/design-system/icons/general/Warning.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +/* eslint-disable */ +export function Warning(props: React.ComponentPropsWithoutRef<'svg'>) { + return ( + + + + ); +} diff --git a/apps/web/src/design-system/icons/index.ts b/apps/web/src/design-system/icons/index.ts index a209a84f806..29a00931e40 100644 --- a/apps/web/src/design-system/icons/index.ts +++ b/apps/web/src/design-system/icons/index.ts @@ -82,6 +82,9 @@ export { UserAccess } from './general/UserAccess'; export { SSO } from './general/SSO'; export { Cloud } from './general/Cloud'; export { Condition } from './general/Condition'; +export { AddCondition } from './general/AddCondition'; +export { RemoveCondition } from './general/RemoveCondition'; +export { Warning } from './general/Warning'; export { Copy } from './actions/Copy'; export { Close } from './actions/Close'; diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx new file mode 100644 index 00000000000..c50dc22935e --- /dev/null +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -0,0 +1,66 @@ +import styled from '@emotion/styled'; +import { Group, UnstyledButton, Text } from '@mantine/core'; +import { colors } from '@novu/notification-center'; +import { When } from '../../../components/utils/When'; +import { Tooltip } from '../../../design-system'; +import { AddCondition, Condition } from '../../../design-system/icons'; + +const Button = styled(Group)` + text-align: center; + border-radius: 8px; + width: 32px; + height: 32px; + color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B60 : colors.B30)}; + + &:hover { + background: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B30 : colors.B85)}; + color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.white : colors.B30)}; + } +`; + +const RemovesPrimary = () => { + return ( + + This action replaces +
the primary provider +
+ Learn more...{' '} +
+ ); +}; + +export const ConditionIconButton = ({ + conditions, + primary = false, + onClick, +}: { + conditions?: any[]; + primary?: boolean; + onClick: () => void; +}) => { + return ( + + Add Conditions + + + + + } + position="bottom" + > + + + + + ); +}; diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx new file mode 100644 index 00000000000..b123f304dea --- /dev/null +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -0,0 +1,133 @@ +import styled from '@emotion/styled'; +import { Group, UnstyledButton, Text, Title, useMantineTheme, Modal } from '@mantine/core'; +import { colors } from '@novu/notification-center'; +import { useState } from 'react'; +import { When } from '../../../components/utils/When'; +import { shadows, Tooltip, Button } from '../../../design-system'; +import { RemoveCondition, StarEmpty, Warning } from '../../../design-system/icons'; + +const IconButton = styled(Group)` + text-align: center; + border-radius: 8px; + width: 32px; + height: 32px; + color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B60 : colors.B30)}; + + &:hover { + background: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B30 : colors.B85)}; + color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.white : colors.B30)}; + } +`; + +const RemovesCondition = () => { + return ( + + This action remove +
applied conditions +
+ Learn more... +
+ ); +}; + +export const PrimaryIconButton = ({ + conditions, + primary = false, + onClick, +}: { + conditions?: any[]; + primary?: boolean; + onClick: () => void; +}) => { + const [modalOpen, setModalOpen] = useState(false); + const theme = useMantineTheme(); + + if (primary) { + return null; + } + + return ( + <> + + Mark as Primary + 0}> + + + + } + position="bottom" + > + { + if (conditions && conditions.length > 0) { + setModalOpen(true); + + return; + } + onClick(); + }} + > + + + + + + + + Conditions will be removed + + } + sx={{ backdropFilter: 'blur(10px)' }} + shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} + radius="md" + size="lg" + onClose={() => { + setModalOpen(false); + }} + centered + overflow="inside" + > + + Marking this instance as primary will remove all conditions since primary instances cannot have any + conditions. + + + + + + + + ); +}; diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index 58de40fb612..3f8b48cce00 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -14,6 +14,8 @@ import { DotsHorizontal, StarEmpty, Trash } from '../../../design-system/icons'; import { ProviderInfo } from './multi-provider/ProviderInfo'; import { useSelectPrimaryIntegrationModal } from './multi-provider/useSelectPrimaryIntegrationModal'; import { useMakePrimaryIntegration } from '../../../api/hooks/useMakePrimaryIntegration'; +import { ConditionIconButton } from './ConditionIconButton'; +import { PrimaryIconButton } from './PrimaryIconButton'; export const UpdateIntegrationSidebarHeader = ({ provider, @@ -107,6 +109,14 @@ export const UpdateIntegrationSidebarHeader = ({ /> {children} + { + makePrimaryIntegration({ id: provider.integrationId }); + }} + conditions={provider.conditions} + /> + {}} conditions={provider.conditions} />
{ makePrimaryIntegration({ id: provider.integrationId }); }} - icon={} + icon={} disabled={isLoading || isMarkingPrimary} > Mark as primary 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 f6d667a753d..b95080de512 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -20,6 +20,7 @@ import type { IConditions, IntegrationEntity } from '../../types'; import { useProviders } from '../../useProviders'; import { When } from '../../../../components/utils/When'; import { Conditions } from '../../../../components/conditions/Conditions'; +import { ConditionIconButton } from '../ConditionIconButton'; interface ICreateProviderInstanceForm { name: string; @@ -161,7 +162,7 @@ export function CreateProviderInstanceSidebar({ }} onClose={onClose} customHeader={ - + @@ -182,6 +183,9 @@ export function CreateProviderInstanceSidebar({ ); }} /> + + {}} /> + } customFooter={ diff --git a/apps/web/src/pages/integrations/types.ts b/apps/web/src/pages/integrations/types.ts index bfa3461a15d..a4c11479f00 100644 --- a/apps/web/src/pages/integrations/types.ts +++ b/apps/web/src/pages/integrations/types.ts @@ -44,12 +44,6 @@ export interface IIntegratedProvider { name?: string; identifier?: string; primary: boolean; - conditions?: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; } export interface IntegrationEntity { @@ -68,12 +62,6 @@ export interface IntegrationEntity { primary: boolean; deletedAt: string; deletedBy: string; - conditions?: { - isNegated?: boolean; - type?: BuilderFieldType; - value?: BuilderGroupValues; - children?: FilterParts[]; - }[]; } export interface IConditions { From 121601ab3013c29aa8c93eaae7037212320b5de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Mon, 4 Sep 2023 15:22:38 +0200 Subject: [PATCH 23/50] feat: Add conditions button for create integration --- .../design-system/icons/general/Condition.tsx | 12 ++++++++++-- .../components/ConditionIconButton.tsx | 2 +- .../CreateProviderInstanceSidebar.tsx | 16 +++++++++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/web/src/design-system/icons/general/Condition.tsx b/apps/web/src/design-system/icons/general/Condition.tsx index 1593e16d602..cfa890652db 100644 --- a/apps/web/src/design-system/icons/general/Condition.tsx +++ b/apps/web/src/design-system/icons/general/Condition.tsx @@ -2,7 +2,15 @@ import React from 'react'; export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { return ( - + @@ -12,7 +20,7 @@ export function Condition(props: React.ComponentPropsWithoutRef<'svg'>) { diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index c50dc22935e..b1fa58be328 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -42,7 +42,7 @@ export const ConditionIconButton = ({ - Add Conditions + {conditions && conditions.length > 0 ? 'Edit' : 'Add'} Conditions 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 b95080de512..d860c95b019 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -6,7 +6,7 @@ import styled from '@emotion/styled'; import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared'; import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; -import { ArrowLeft, ConditionPlus } from '../../../../design-system/icons'; +import { AddCondition, ConditionPlus, ArrowLeft, Condition } from '../../../../design-system/icons'; import { inputStyles } from '../../../../design-system/config/inputs.styles'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; import { useSegment } from '../../../../components/providers/SegmentProvider'; @@ -264,6 +264,20 @@ export function CreateProviderInstanceSidebar({ You can only create one {provider.displayName} per environment. + + + ); } From 1a11fb0bd1f52392fc53690fd7682cbe7f8688e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 5 Sep 2023 09:34:38 +0200 Subject: [PATCH 24/50] feat: add some nodes missing provider modal --- .../SelectPrimaryIntegrationModal.tsx | 19 +++--- .../multi-provider/UpdateProviderSidebar.tsx | 63 ++++++++++++++++++- .../useSelectPrimaryIntegrationModal.tsx | 11 ++-- 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx index 4f40d12f44e..d6a97bd9d49 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -115,7 +115,7 @@ export interface ISelectPrimaryIntegrationModalProps { environmentId?: string; channelType?: ChannelTypeEnum; exclude?: (integration: IntegrationEntity) => boolean; - onClose: () => void; + onClose: (cancel?: boolean) => void; } export const SelectPrimaryIntegrationModal = ({ @@ -132,14 +132,17 @@ export const SelectPrimaryIntegrationModal = ({ const { environments, isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const environmentName = environments?.find((el) => el._id === environmentId)?.name ?? ''; - const onCloseCallback = useCallback(() => { - setSelectedState(initialState); - onClose(); - }, [onClose]); + const onCloseCallback = useCallback( + (cancel?: boolean) => { + setSelectedState(initialState); + onClose(cancel); + }, + [onClose] + ); const { integrations, loading: areIntegrationsLoading } = useIntegrations(); const { makePrimaryIntegration, isLoading: isMarkingPrimaryIntegration } = useMakePrimaryIntegration({ - onSuccess: onCloseCallback, + onSuccess: () => onCloseCallback(), }); const integrationsByEnvAndChannel = useMemo(() => { const filteredIntegrations = (integrations ?? []).filter((el) => { @@ -209,7 +212,7 @@ export const SelectPrimaryIntegrationModal = ({ shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} radius="md" size="lg" - onClose={onCloseCallback} + onClose={() => onCloseCallback()} > @@ -249,7 +252,7 @@ export const SelectPrimaryIntegrationModal = ({ The selected provider instance will be activated as the primary provider cannot be disabled. )} - + + + Some nodes missing provider + + } + sx={{ backdropFilter: 'blur(10px)' }} + shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} + radius="md" + size="lg" + onClose={() => { + setModalOpen(false); + }} + centered + overflow="inside" + > + + Conditions applied to all instances designate them to specific nodes, while nodes lacking conditions remain + provider-less. Add a provider instance without conditions to cover all nodes. + + + + + + ); } diff --git a/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx index d8f655a7664..8a83027030d 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx @@ -12,10 +12,13 @@ export const useSelectPrimaryIntegrationModal = () => { const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const isOpened = opened && isMultiProviderConfigurationEnabled; - const onCloseCallback = useCallback(() => { - close(); - onClose?.(); - }, [close, onClose]); + const onCloseCallback = useCallback( + (cancel?: boolean) => { + close(); + onClose?.(cancel); + }, + [close, onClose] + ); const Component = useInlineComponent(SelectPrimaryIntegrationModal, { isOpened, From 33d70ce9d8832bee85757c5c1ac321410e053673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 5 Sep 2023 10:24:09 +0200 Subject: [PATCH 25/50] fix: after merging feature branch to this branch --- .../icons/general/AddCondition.tsx | 13 ----- apps/web/src/design-system/icons/index.ts | 1 - .../components/ConditionIconButton.tsx | 4 +- .../UpdateIntegrationSidebarHeader.tsx | 4 +- .../CreateProviderInstanceSidebar.tsx | 58 ++++++++++++------- .../multi-provider/UpdateProviderSidebar.tsx | 35 ++++++----- 6 files changed, 63 insertions(+), 52 deletions(-) delete mode 100644 apps/web/src/design-system/icons/general/AddCondition.tsx diff --git a/apps/web/src/design-system/icons/general/AddCondition.tsx b/apps/web/src/design-system/icons/general/AddCondition.tsx deleted file mode 100644 index 7641faae6e9..00000000000 --- a/apps/web/src/design-system/icons/general/AddCondition.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -/* eslint-disable */ -export function AddCondition(props: React.ComponentPropsWithoutRef<'svg'>) { - return ( - - - - - ); -} diff --git a/apps/web/src/design-system/icons/index.ts b/apps/web/src/design-system/icons/index.ts index 29a00931e40..beaf524bec2 100644 --- a/apps/web/src/design-system/icons/index.ts +++ b/apps/web/src/design-system/icons/index.ts @@ -82,7 +82,6 @@ export { UserAccess } from './general/UserAccess'; export { SSO } from './general/SSO'; export { Cloud } from './general/Cloud'; export { Condition } from './general/Condition'; -export { AddCondition } from './general/AddCondition'; export { RemoveCondition } from './general/RemoveCondition'; export { Warning } from './general/Warning'; diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index b1fa58be328..d897abc4829 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -3,7 +3,7 @@ import { Group, UnstyledButton, Text } from '@mantine/core'; import { colors } from '@novu/notification-center'; import { When } from '../../../components/utils/When'; import { Tooltip } from '../../../design-system'; -import { AddCondition, Condition } from '../../../design-system/icons'; +import { Condition, ConditionPlus } from '../../../design-system/icons'; const Button = styled(Group)` text-align: center; @@ -53,7 +53,7 @@ export const ConditionIconButton = ({ + + + Conditions + + (optional) + + + + } + description="Add a condition if you want to apply the provider instance to a specific tenant, subscriber, workflow, etc." + styles={inputStyles} + > + + + + 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 627775df018..847cde76d14 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -37,8 +37,9 @@ 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'; +import { useNavigate } from 'react-router-dom'; import { Warning } from '../../../../design-system/icons'; +import { ROUTES } from '../../../../constants/routes.enum'; interface IProviderForm { name: string; @@ -62,7 +63,7 @@ export function UpdateProviderSidebar({ integrationId?: string; onClose: () => void; }) { - const [modalOpen, setModalOpen] = useState(false); + const [missingProviderModalOpen, setMissingProviderModalOpen] = useState(true); const theme = useMantineTheme(); const { update } = useIntercom(); const { isLoading: areEnvironmentsLoading } = useFetchEnvironments(); @@ -72,7 +73,7 @@ export function UpdateProviderSidebar({ const [framework, setFramework] = useState(null); const { providers, isLoading: areProvidersLoading } = useProviders(); const isNovuInAppProvider = selectedProvider?.providerId === InAppProviderIdEnum.Novu; - + const navigate = useNavigate(); const { openModal: openSelectPrimaryIntegrationModal, SelectPrimaryIntegrationModal } = useSelectPrimaryIntegrationModal(); @@ -201,8 +202,8 @@ export function UpdateProviderSidebar({ channelType: selectedProvider?.channel, exclude: !isActive ? (el) => el._id === selectedProvider.integrationId : undefined, onClose: (cancel?: boolean) => { - if (cancel === true) { - setModalOpen(true); + if (cancel === true && primary && conditions && conditions?.length > 0) { + setMissingProviderModalOpen(true); } updateIntegration(data); }, @@ -252,7 +253,11 @@ export function UpdateProviderSidebar({ onClose={onSidebarClose} onSubmit={onSubmit} customHeader={ - + setOpenConditions(true)} + provider={selectedProvider} + onSuccessDelete={onSidebarClose} + > Test Provider } @@ -288,7 +293,11 @@ export function UpdateProviderSidebar({ onBack={onBack} customHeader={ sidebarState === SidebarStateEnum.NORMAL ? ( - + setOpenConditions(true)} + provider={selectedProvider} + onSuccessDelete={onSidebarClose} + /> ) : ( <> @@ -360,13 +369,10 @@ export function UpdateProviderSidebar({ - { - setModalOpen(false); + setMissingProviderModalOpen(false); }} centered overflow="inside" @@ -402,14 +408,15 @@ export function UpdateProviderSidebar({ - + ); }; diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index b123f304dea..ff2635cf54c 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -1,9 +1,8 @@ import styled from '@emotion/styled'; -import { Group, UnstyledButton, Text, Title, useMantineTheme, Modal } from '@mantine/core'; -import { colors } from '@novu/notification-center'; +import { Group, ActionIcon, Text, Title } from '@mantine/core'; import { useState } from 'react'; import { When } from '../../../components/utils/When'; -import { shadows, Tooltip, Button } from '../../../design-system'; +import { Tooltip, Button, colors, Modal } from '../../../design-system'; import { RemoveCondition, StarEmpty, Warning } from '../../../design-system/icons'; const IconButton = styled(Group)` @@ -24,8 +23,6 @@ const RemovesCondition = () => { This action remove
applied conditions -
- Learn more...
); }; @@ -39,8 +36,7 @@ export const PrimaryIconButton = ({ primary?: boolean; onClick: () => void; }) => { - const [modalOpen, setModalOpen] = useState(false); - const theme = useMantineTheme(); + const [modalOpen, setModalOpen] = useState(true); if (primary) { return null; @@ -59,7 +55,7 @@ export const PrimaryIconButton = ({ } position="bottom" > - { if (conditions && conditions.length > 0) { setModalOpen(true); @@ -68,40 +64,25 @@ export const PrimaryIconButton = ({ } onClick(); }} + variant="transparent" > - + Conditions will be removed } - sx={{ backdropFilter: 'blur(10px)' }} - shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} - radius="md" size="lg" onClose={() => { setModalOpen(false); }} - centered - overflow="inside" > Marking this instance as primary will remove all conditions since primary instances cannot have any diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index 47f697e20fe..7f172ee9dec 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -1,5 +1,5 @@ import { ReactNode, useMemo, useState } from 'react'; -import { Group } from '@mantine/core'; +import { Group, useMantineTheme } from '@mantine/core'; import { Controller, useFormContext } from 'react-hook-form'; import { CHANNELS_WITH_PRIMARY, NOVU_PROVIDERS } from '@novu/shared'; @@ -31,6 +31,7 @@ export const UpdateIntegrationSidebarHeader = ({ const [isModalOpened, setModalIsOpened] = useState(false); const { control } = useFormContext(); const { environments } = useFetchEnvironments(); + const { colorScheme } = useMantineTheme(); const { providers, isLoading } = useProviders(); const canMarkAsPrimary = provider && !provider.primary && CHANNELS_WITH_PRIMARY.includes(provider.channel); const { openModal, SelectPrimaryIntegrationModal } = useSelectPrimaryIntegrationModal(); @@ -136,7 +137,7 @@ export const UpdateIntegrationSidebarHeader = ({ onClick={() => { makePrimaryIntegration({ id: provider.integrationId }); }} - icon={} + icon={} disabled={isLoading || isMarkingPrimary} > Mark as primary 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 7a8fc7285ab..0d659bafb32 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Group, Radio, Text, Input } from '@mantine/core'; +import { ActionIcon, Group, Radio, Text, Input, useMantineTheme } from '@mantine/core'; import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Controller, useForm } from 'react-hook-form'; @@ -42,6 +42,7 @@ export function CreateProviderInstanceSidebar({ onGoBack: () => void; onIntegrationCreated: (id: string) => void; }) { + const { colorScheme } = useMantineTheme(); const { environments, isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const { isLoading: areIntegrationsLoading, providers: integrations } = useProviders(); const [openConditions, setOpenConditions] = useState(false); @@ -70,6 +71,13 @@ export function CreateProviderInstanceSidebar({ }); const watchedConditions = watch('conditions'); + const numOfConditions: number = useMemo(() => { + if (watchedConditions && watchedConditions[0] && watchedConditions[0].children) { + return watchedConditions[0].children.length; + } + + return 0; + }, [watchedConditions]); const selectedEnvironmentId = watch('environmentId'); const showInAppErrorMessage = useMemo(() => { @@ -267,25 +275,30 @@ export function CreateProviderInstanceSidebar({ } - description="Add a condition if you want to apply the provider instance to a specific tenant, subscriber, workflow, etc." + description="Add a condition if you want to apply the provider instance to a specific tenant." styles={inputStyles} > - 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 847cde76d14..83b39f404a0 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -63,7 +63,7 @@ export function UpdateProviderSidebar({ integrationId?: string; onClose: () => void; }) { - const [missingProviderModalOpen, setMissingProviderModalOpen] = useState(true); + const [missingProviderModalOpen, setMissingProviderModalOpen] = useState(false); const theme = useMantineTheme(); const { update } = useIntercom(); const { isLoading: areEnvironmentsLoading } = useFetchEnvironments(); @@ -416,7 +416,7 @@ export function UpdateProviderSidebar({ - - + <> + + {numOfConditions > 0 ? 'Edit' : 'Add'} Conditions + + + + + } + position="bottom" + > + { + if (primary) { + setModalOpen(true); + + return; + } + onClick(); + }} + variant="transparent" + > + + + + + 0}> + +
{numOfConditions}
+
+
+
+
+ + + Primary will be removed + + } + size="lg" + onClose={() => { + setModalOpen(false); + }} + > + + Adding conditions to this instance will remove it as primary since primary instances cannot have any + conditions. + + + + + + + ); }; diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index ff2635cf54c..b11030aa934 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -36,7 +36,7 @@ export const PrimaryIconButton = ({ primary?: boolean; onClick: () => void; }) => { - const [modalOpen, setModalOpen] = useState(true); + const [modalOpen, setModalOpen] = useState(false); if (primary) { return null; diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx index d6a97bd9d49..a99dd5e0020 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -115,7 +115,7 @@ export interface ISelectPrimaryIntegrationModalProps { environmentId?: string; channelType?: ChannelTypeEnum; exclude?: (integration: IntegrationEntity) => boolean; - onClose: (cancel?: boolean) => void; + onClose: () => void; } export const SelectPrimaryIntegrationModal = ({ @@ -132,13 +132,10 @@ export const SelectPrimaryIntegrationModal = ({ const { environments, isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const environmentName = environments?.find((el) => el._id === environmentId)?.name ?? ''; - const onCloseCallback = useCallback( - (cancel?: boolean) => { - setSelectedState(initialState); - onClose(cancel); - }, - [onClose] - ); + const onCloseCallback = useCallback(() => { + setSelectedState(initialState); + onClose(); + }, [onClose]); const { integrations, loading: areIntegrationsLoading } = useIntegrations(); const { makePrimaryIntegration, isLoading: isMarkingPrimaryIntegration } = useMakePrimaryIntegration({ @@ -252,7 +249,7 @@ export const SelectPrimaryIntegrationModal = ({ The selected provider instance will be activated as the primary provider cannot be disabled.
)} - void; }) { - const [missingProviderModalOpen, setMissingProviderModalOpen] = useState(false); - const theme = useMantineTheme(); const { update } = useIntercom(); const { isLoading: areEnvironmentsLoading } = useFetchEnvironments(); const [selectedProvider, setSelectedProvider] = useState(null); @@ -73,7 +68,6 @@ export function UpdateProviderSidebar({ const [framework, setFramework] = useState(null); const { providers, isLoading: areProvidersLoading } = useProviders(); const isNovuInAppProvider = selectedProvider?.providerId === InAppProviderIdEnum.Novu; - const navigate = useNavigate(); const { openModal: openSelectPrimaryIntegrationModal, SelectPrimaryIntegrationModal } = useSelectPrimaryIntegrationModal(); @@ -201,10 +195,7 @@ export function UpdateProviderSidebar({ environmentId: selectedProvider?.environmentId, channelType: selectedProvider?.channel, exclude: !isActive ? (el) => el._id === selectedProvider.integrationId : undefined, - onClose: (cancel?: boolean) => { - if (cancel === true && primary && conditions && conditions?.length > 0) { - setMissingProviderModalOpen(true); - } + onClose: () => { updateIntegration(data); }, }); @@ -371,58 +362,6 @@ export function UpdateProviderSidebar({ - - - Some nodes missing provider - - } - sx={{ backdropFilter: 'blur(10px)' }} - shadow={theme.colorScheme === 'dark' ? shadows.dark : shadows.medium} - radius="md" - size="lg" - onClose={() => { - setMissingProviderModalOpen(false); - }} - centered - overflow="inside" - > - - Conditions applied to all instances designate them to specific nodes, while nodes lacking conditions remain - provider-less. Add a provider instance without conditions to cover all nodes. - - - - - - ); } diff --git a/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx index 8a83027030d..d8f655a7664 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx @@ -12,13 +12,10 @@ export const useSelectPrimaryIntegrationModal = () => { const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const isOpened = opened && isMultiProviderConfigurationEnabled; - const onCloseCallback = useCallback( - (cancel?: boolean) => { - close(); - onClose?.(cancel); - }, - [close, onClose] - ); + const onCloseCallback = useCallback(() => { + close(); + onClose?.(); + }, [close, onClose]); const Component = useInlineComponent(SelectPrimaryIntegrationModal, { isOpened, From 00199f3aa641e52c0efd956e1942228c8c4f27d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 5 Sep 2023 12:37:27 +0200 Subject: [PATCH 28/50] fix: so modal are not always shown --- .../src/pages/integrations/components/ConditionIconButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index c2d909a4071..c4bd088fff7 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -37,7 +37,7 @@ export const ConditionIconButton = ({ primary?: boolean; onClick: () => void; }) => { - const [modalOpen, setModalOpen] = useState(true); + const [modalOpen, setModalOpen] = useState(false); const numOfConditions: number = useMemo(() => { if (conditions && conditions[0] && conditions[0].children) { return conditions[0].children.length; From 51b0507c07420a3c943fa61cc3e9b14bfdda5441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Tue, 5 Sep 2023 13:11:05 +0200 Subject: [PATCH 29/50] fix: after pr comments --- .../pages/integrations/components/ConditionIconButton.tsx | 8 +++++--- .../pages/integrations/components/PrimaryIconButton.tsx | 2 +- .../multi-provider/CreateProviderInstanceSidebar.tsx | 6 +++--- .../multi-provider/SelectPrimaryIntegrationModal.tsx | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index c4bd088fff7..a11e175190e 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -75,8 +75,10 @@ export const ConditionIconButton = ({ 0}> - -
{numOfConditions}
+ + +
{numOfConditions}
+
@@ -95,7 +97,7 @@ export const ConditionIconButton = ({ }} > - Adding conditions to this instance will remove it as primary since primary instances cannot have any + Adding conditions to this instance will remove it as primary since primary instances can not have any conditions. diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index b11030aa934..0f0d2daa6a0 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -85,7 +85,7 @@ export const PrimaryIconButton = ({ }} > - Marking this instance as primary will remove all conditions since primary instances cannot have any + Marking this instance as primary will remove all conditions since primary instances can not have any conditions. 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 0d659bafb32..db62d530cb8 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -286,12 +286,12 @@ export function CreateProviderInstanceSidebar({ <> - Add conditions + 0}> - - + + {numOfConditions} diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx index a99dd5e0020..53ebc5dd8d4 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -246,7 +246,7 @@ export const SelectPrimaryIntegrationModal = ({ {!isActive && !isInitialProviderSelected && ( - The selected provider instance will be activated as the primary provider cannot be disabled. + The selected provider instance will be activated as the primary provider can not be disabled. )} - - 0} - label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'} - > -
- -
-
-
+ 0} + label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'} + > +
+ +
+
} > @@ -110,13 +132,12 @@ export function Conditions({ render={({ field }) => { return ( - ); - }} - /> - - - { - return ( - - - - - - - - - - } - required - disabled={getValues(`conditions.0.children.${index}.operator`) === 'IS_DEFINED'} - error={!!fieldState.error} - placeholder="Value" - data-test-id="filter-value-input" - /> - ); - }} - /> - )} - + - { - insert(index + 1, getValues(`conditions.0.children.${index}`)); - }} - icon={} - > + handleDuplicate(index)} icon={}> Duplicate - { - remove(index); - }} - icon={} - > + handleDelete(index)} icon={}> Delete @@ -284,6 +213,93 @@ export function Conditions({ ); } +function EqualityForm({ control, index }: { control: Control; index: number }) { + const operator = useWatch({ + control, + name: `conditions.0.children.${index}.operator`, + }); + + return ( + <> + + { + return ( + + ); + }} + /> + + + + {operator !== 'IS_DEFINED' && ( + { + return ( + + + + + + + + } + required + error={!!fieldState.error} + placeholder="Value" + data-test-id="conditions-form-value-input" + /> + ); + }} + /> + )} + + + ); +} + const Wrapper = styled.div` .mantine-Select-wrapper:not(:hover) { .mantine-Select-input { @@ -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`}; - } -`; diff --git a/apps/web/src/components/conditions/index.ts b/apps/web/src/components/conditions/index.ts new file mode 100644 index 00000000000..a4dfe570c58 --- /dev/null +++ b/apps/web/src/components/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './Conditions'; +export * from './types'; diff --git a/apps/web/src/components/conditions/types.ts b/apps/web/src/components/conditions/types.ts new file mode 100644 index 00000000000..fc8556e2ed5 --- /dev/null +++ b/apps/web/src/components/conditions/types.ts @@ -0,0 +1,19 @@ +import { BuilderFieldType, BuilderGroupValues, FilterParts, FilterPartTypeEnum } from '@novu/shared'; + +export interface IConditions { + isNegated?: boolean; + type?: BuilderFieldType; + value?: BuilderGroupValues; + children?: FilterParts[]; +} + +export enum ConditionsContextEnum { + INTEGRATIONS = 'INTEGRATIONS', +} + +export const ConditionsContextFields = { + [ConditionsContextEnum.INTEGRATIONS]: { + label: 'provider instance', + filterPartsList: [FilterPartTypeEnum.TENANT], + }, +}; diff --git a/apps/web/src/design-system/tooltip/Tooltip.styles.ts b/apps/web/src/design-system/tooltip/Tooltip.styles.ts index aab5ac085da..ed0bd07b040 100644 --- a/apps/web/src/design-system/tooltip/Tooltip.styles.ts +++ b/apps/web/src/design-system/tooltip/Tooltip.styles.ts @@ -1,20 +1,27 @@ import { createStyles, MantineTheme } from '@mantine/core'; import { colors, shadows } from '../config'; +import { getGradient } from '../config/helper'; -export default createStyles((theme: MantineTheme) => { +export default createStyles((theme: MantineTheme, { error }: { error: boolean }) => { const dark = theme.colorScheme === 'dark'; + const opacityErrorColor = theme.fn.rgba(colors.error, 0.2); + const errorGradient = getGradient(opacityErrorColor); + const backgroundErrorColor = dark ? colors.B17 : colors.white; + const backgroundColor = dark ? colors.B20 : colors.white; + const background = error ? `${errorGradient}, ${backgroundErrorColor}` : backgroundColor; + const color = error ? colors.error : colors.B60; return { tooltip: { - backgroundColor: dark ? colors.B20 : theme.white, - color: colors.B60, + background, + color, boxShadow: dark ? shadows.dark : shadows.medium, padding: '12px 15px', fontSize: '14px', fontWeight: 400, }, arrow: { - backgroundColor: dark ? colors.B20 : theme.white, + background, }, }; }); diff --git a/apps/web/src/design-system/tooltip/Tooltip.tsx b/apps/web/src/design-system/tooltip/Tooltip.tsx index bd268add1d6..53d854a1c26 100644 --- a/apps/web/src/design-system/tooltip/Tooltip.tsx +++ b/apps/web/src/design-system/tooltip/Tooltip.tsx @@ -2,30 +2,29 @@ import { Tooltip as MantineTooltip, TooltipProps } from '@mantine/core'; import useStyles from './Tooltip.styles'; +interface ITooltipProps + extends Pick< + TooltipProps, + | 'multiline' + | 'width' + | 'label' + | 'opened' + | 'position' + | 'disabled' + | 'children' + | 'sx' + | 'withinPortal' + | 'offset' + | 'classNames' + > { + error?: boolean; +} /** * Tooltip component * */ -export function Tooltip({ - children, - label, - opened = undefined, - ...props -}: Pick< - TooltipProps, - | 'multiline' - | 'width' - | 'label' - | 'opened' - | 'position' - | 'disabled' - | 'children' - | 'sx' - | 'withinPortal' - | 'offset' - | 'classNames' ->) { - const { classes } = useStyles(); +export function Tooltip({ children, label, opened = undefined, error = false, ...props }: ITooltipProps) { + const { classes } = useStyles({ error }); return ( 0}> - +
{numOfConditions}
- +
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 db62d530cb8..61bd0f96827 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -1,8 +1,9 @@ import { ActionIcon, Group, Radio, Text, Input, useMantineTheme } from '@mantine/core'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { Controller, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; +import { useDisclosure } from '@mantine/hooks'; import { ChannelTypeEnum, ICreateIntegrationBodyDto, InAppProviderIdEnum, providers } from '@novu/shared'; import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; import { ConditionPlus, ArrowLeft, Condition } from '../../../../design-system/icons'; @@ -15,10 +16,10 @@ 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 { IConditions, IntegrationEntity } from '../../types'; +import type { IntegrationEntity } from '../../types'; import { useProviders } from '../../useProviders'; import { When } from '../../../../components/utils/When'; -import { Conditions } from '../../../../components/conditions/Conditions'; +import { Conditions, IConditions } from '../../../../components/conditions'; import { ConditionIconButton } from '../ConditionIconButton'; interface ICreateProviderInstanceForm { @@ -45,10 +46,10 @@ export function CreateProviderInstanceSidebar({ const { colorScheme } = useMantineTheme(); 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(); + const [conditionsFormOpened, { close: closeConditionsForm, open: openConditionsForm }] = useDisclosure(false); const provider = useMemo( () => providers.find((el) => el.channel === channel && el.id === providerId), @@ -145,17 +146,20 @@ export function CreateProviderInstanceSidebar({ if (!provider) { return null; } + const updateConditions = (conditions: IConditions[]) => { + setValue('conditions', conditions, { shouldDirty: true }); + }; + + if (conditionsFormOpened) { + const [conditions, name] = getValues(['conditions', 'name']); - if (openConditions) { return ( { - setValue('conditions', data, { shouldDirty: true }); - }} - onClose={() => setOpenConditions(false)} + conditions={conditions} + name={name} + isOpened={conditionsFormOpened} + setConditions={updateConditions} + onClose={closeConditionsForm} /> ); } @@ -192,7 +196,7 @@ export function CreateProviderInstanceSidebar({ }} /> - setOpenConditions(true)} /> +
} @@ -281,7 +285,7 @@ export function CreateProviderInstanceSidebar({ - 0} - label={!isValid ? 'Some conditions are missing values' : 'Add at least one condition'} - > +
-
@@ -160,7 +150,6 @@ export function Conditions({ placeholder="On" data={FilterPartTypeList} {...field} - onChange={handleOnChildOnChange(index)} data-test-id="conditions-form-on-dropdown" /> ); @@ -286,7 +275,6 @@ function EqualityForm({ control, index }: { control: Control; i
} - required error={!!fieldState.error} placeholder="Value" data-test-id="conditions-form-value-input" diff --git a/apps/web/src/design-system/typography/title/Title.tsx b/apps/web/src/design-system/typography/title/Title.tsx index 23383af3a2e..284ac551161 100644 --- a/apps/web/src/design-system/typography/title/Title.tsx +++ b/apps/web/src/design-system/typography/title/Title.tsx @@ -1,23 +1,31 @@ -import { Title as MantineTitle } from '@mantine/core'; +import { MantineColor, Title as MantineTitle, useMantineTheme } from '@mantine/core'; import { colors } from '../../config'; import { SpacingProps } from '../../shared/spacing.props'; interface ITitleProps extends JSX.ElementChildrenAttribute, SpacingProps { size?: 1 | 2; + color?: MantineColor; } /** * Use Title to create headers. * */ -export function Title({ size = 1, children, ...rest }: ITitleProps) { +export function Title({ size = 1, children, ...props }: ITitleProps) { + const { colorScheme } = useMantineTheme(); + + let textColor = props.color; + if (!textColor) { + textColor = colorScheme === 'dark' ? colors.white : colors.B40; + } + return ( ({ + sx={{ fontWeight: size === 1 ? 800 : 700, - color: theme.colorScheme === 'dark' ? colors.white : colors.B40, - })} + }} order={size} - {...rest} + color={textColor} + {...props} > {children} diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index 5f4208ff537..830791b39c2 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -1,10 +1,9 @@ -import { useMemo, useState } from 'react'; +import { useState } from 'react'; import styled from '@emotion/styled'; -import { Group, ActionIcon, Title, Center } from '@mantine/core'; +import { Group, ActionIcon, Center } from '@mantine/core'; import { When } from '../../../components/utils/When'; -import { colors, Tooltip, Text, Modal, Button } from '../../../design-system'; +import { colors, Tooltip, Text, Modal, Button, Title } from '../../../design-system'; import { Condition, ConditionPlus, Warning } from '../../../design-system/icons'; -import { IConditions } from '../../../components/conditions'; const IconButton = styled(Group)` text-align: center; @@ -29,29 +28,22 @@ const RemovesPrimary = () => { }; export const ConditionIconButton = ({ - conditions, + conditions = 0, primary = false, onClick, }: { - conditions?: IConditions[]; + conditions?: number; primary?: boolean; onClick: () => void; }) => { const [modalOpen, setModalOpen] = useState(false); - const numOfConditions: number = useMemo(() => { - if (conditions && conditions[0] && conditions[0].children) { - return conditions[0].children.length; - } - - return 0; - }, [conditions]); return ( <> - {numOfConditions > 0 ? 'Edit' : 'Add'} Conditions + {conditions > 0 ? 'Edit' : 'Add'} Conditions @@ -71,13 +63,13 @@ export const ConditionIconButton = ({ variant="transparent" > - + - 0}> + 0}>
-
{numOfConditions}
+
{conditions}
@@ -88,7 +80,9 @@ export const ConditionIconButton = ({ title={ - Primary will be removed + + Primary will be removed + } size="lg" diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index 0f0d2daa6a0..e81a4ab0640 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { Group, ActionIcon, Text, Title } from '@mantine/core'; +import { Group, ActionIcon, Text } from '@mantine/core'; import { useState } from 'react'; import { When } from '../../../components/utils/When'; -import { Tooltip, Button, colors, Modal } from '../../../design-system'; +import { Tooltip, Button, colors, Modal, Title } from '../../../design-system'; import { RemoveCondition, StarEmpty, Warning } from '../../../design-system/icons'; const IconButton = styled(Group)` @@ -28,11 +28,11 @@ const RemovesCondition = () => { }; export const PrimaryIconButton = ({ - conditions, + conditions = 0, primary = false, onClick, }: { - conditions?: any[]; + conditions?: number; primary?: boolean; onClick: () => void; }) => { @@ -48,7 +48,7 @@ export const PrimaryIconButton = ({ label={ <> Mark as Primary - 0}> + 0}> @@ -57,7 +57,7 @@ export const PrimaryIconButton = ({ > { - if (conditions && conditions.length > 0) { + if (conditions > 0) { setModalOpen(true); return; diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index 1b223899d60..bf50ae76243 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -1,6 +1,6 @@ import { ReactNode, useMemo, useState } from 'react'; import { Group, useMantineTheme } from '@mantine/core'; -import { Controller, useFormContext } from 'react-hook-form'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; import { Button, colors, Dropdown, Modal, NameInput, Text, Title } from '../../../design-system'; @@ -36,6 +36,15 @@ export const UpdateIntegrationSidebarHeader = ({ const canMarkAsPrimary = provider && !provider.primary && CHANNELS_WITH_PRIMARY.includes(provider.channel); const { openModal, SelectPrimaryIntegrationModal } = useSelectPrimaryIntegrationModal(); + const watchedConditions = useWatch({ control, name: 'conditions' }); + const numOfConditions: number = useMemo(() => { + if (watchedConditions && watchedConditions[0] && watchedConditions[0].children) { + return watchedConditions[0].children.length; + } + + return 0; + }, [watchedConditions]); + const shouldSetNewPrimary = useMemo(() => { if (!provider) return false; @@ -117,9 +126,9 @@ export const UpdateIntegrationSidebarHeader = ({ onClick={() => { makePrimaryIntegration({ id: provider.integrationId }); }} - conditions={provider.conditions} + conditions={numOfConditions} /> - +
Date: Thu, 7 Sep 2023 10:34:16 +0200 Subject: [PATCH 41/50] fix: so remove primary on condition modal is not shown as often as before --- .../integrations/components/ConditionIconButton.tsx | 10 +++++----- .../multi-provider/UpdateProviderSidebar.tsx | 13 +++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index 830791b39c2..c39ce861b73 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -53,7 +53,7 @@ export const ConditionIconButton = ({ > { - if (primary) { + if (primary && conditions === 0) { setModalOpen(true); return; @@ -81,7 +81,7 @@ export const ConditionIconButton = ({ - Primary will be removed + Primary flag will be removed } @@ -91,8 +91,8 @@ export const ConditionIconButton = ({ }} > - Adding conditions to this instance will remove it as primary since primary instances can not have any - conditions. + Adding conditions to the primary provider instance removes its primary status on update, potentially causing + notification failures for the steps that were using the primary provider. 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 7c298816478..f4c706a2cb6 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -191,11 +191,20 @@ export function UpdateProviderSidebar({ const hasNoConditions = !conditions || conditions.length === 0; - if ((hasNoConditions && isChangedToActive) || isChangedToInactiveAndIsPrimary || isPrimaryAndHasConditionsApplied) { + const hasUpdatedConditions = data.conditions && data.conditions.length > 0; + + const hasConditionsAndIsPrimary = hasUpdatedConditions && primary && dirtyFields.conditions; + + if ( + (hasNoConditions && isChangedToActive) || + isChangedToInactiveAndIsPrimary || + isPrimaryAndHasConditionsApplied || + hasConditionsAndIsPrimary + ) { openSelectPrimaryIntegrationModal({ environmentId: selectedProvider?.environmentId, channelType: selectedProvider?.channel, - exclude: !isActive ? (el) => el._id === selectedProvider.integrationId : undefined, + exclude: !isActive || hasConditionsAndIsPrimary ? (el) => el._id === selectedProvider.integrationId : undefined, onClose: () => { updateIntegration(data); }, From 043f78b47563b97bb20fe7e8ef4968f1ee88795c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=B6derberg?= Date: Thu, 7 Sep 2023 13:51:58 +0200 Subject: [PATCH 42/50] fix: copy for primary flag will be removed modal --- .../integrations/components/ConditionIconButton.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index c39ce861b73..b926b5ecc2b 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -21,8 +21,9 @@ const IconButton = styled(Group)` const RemovesPrimary = () => { return ( - This action replaces -
the primary provider + This action replaces the +
+ primary provider flag
); }; @@ -91,8 +92,9 @@ export const ConditionIconButton = ({ }} > - Adding conditions to the primary provider instance removes its primary status on update, potentially causing - notification failures for the steps that were using the primary provider. + Adding conditions to the primary provider instance removes its primary status when a user applies changes by + clicking the Update button. This can potentially cause notification failures for the steps that were using the + primary provider. @@ -224,7 +228,7 @@ function EqualityForm({ control, index }: { control: Control; i { value: 'identifier', label: 'Identifier' }, ]} {...field} - data-test-id="conditions-form-field-dropdown" + data-test-id="conditions-form-key" /> ); }} @@ -247,7 +251,7 @@ function EqualityForm({ control, index }: { control: Control; i { value: 'IS_DEFINED', label: 'Is defined' }, ]} {...field} - data-test-id="conditions-form-operator-dropdown" + data-test-id="conditions-form-operator" /> ); }} @@ -268,16 +272,23 @@ function EqualityForm({ control, index }: { control: Control; i value={field.value as string} rightSection={ - + - + } error={!!fieldState.error} placeholder="Value" - data-test-id="conditions-form-value-input" + data-test-id="conditions-form-value" /> ); }} diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx index c47f61b64c2..5a6e0009e9f 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx @@ -12,7 +12,7 @@ const condition: ICondition = { }; describe('Execution Details Condition Component', function () { - it('should render ExecutionDetailsCondtions properly', function () { + it('should render ExecutionDetailsConditions properly', function () { cy.mount( diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx index d6cdc2c55b6..dab60f4c9d5 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx @@ -39,7 +39,7 @@ const conditions: ICondition[] = [ ]; describe('Execution Details Condition Component', function () { - it('should render ExecutionDetailsCondtions properly', function () { + it('should render ExecutionDetailsConditions properly', function () { cy.mount( diff --git a/apps/web/src/pages/integrations/components/ConditionCell.tsx b/apps/web/src/pages/integrations/components/ConditionCell.tsx index 70c50525366..eabd3fc969f 100644 --- a/apps/web/src/pages/integrations/components/ConditionCell.tsx +++ b/apps/web/src/pages/integrations/components/ConditionCell.tsx @@ -9,6 +9,7 @@ const ConditionCellBase = ({ row: { original } }: IExtendedCellProps { if (primary && conditions === 0) { setModalOpen(true); @@ -78,6 +79,7 @@ export const ConditionIconButton = ({ diff --git a/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx b/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx index e0b65da5227..14dafb26ede 100644 --- a/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx @@ -93,7 +93,7 @@ export const IntegrationNameCell = ({ row: { original }, isLoading }: IExtendedC target={ setPopoverOpened(true)} onMouseLeave={() => setPopoverOpened(false)}> {original.name} - {original.primary && } + {original.primary && } } /> diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index e81a4ab0640..250807840a7 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -56,6 +56,7 @@ export const PrimaryIconButton = ({ position="bottom" > { if (conditions > 0) { setModalOpen(true); @@ -72,6 +73,7 @@ export const PrimaryIconButton = ({ 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 61bd0f96827..79cede5bee3 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -196,7 +196,7 @@ export function CreateProviderInstanceSidebar({ }} /> - + } @@ -285,6 +285,7 @@ export function CreateProviderInstanceSidebar({