diff --git a/apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts b/apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts index f0eaeb6b01d..2503529f4ec 100644 --- a/apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts +++ b/apps/api/src/app/workflows/e2e/update-notification-template.e2e.ts @@ -381,7 +381,7 @@ describe('Update workflow by id - /workflows/:workflowId (PUT)', async () => { const createdTemplate: WorkflowResponse = body.data; expect(createdTemplate.name).to.equal(testTemplate.name); - expect(createdTemplate.steps[0].replyCallback).to.equal(undefined); + expect(createdTemplate.steps[0].replyCallback).to.deep.equal({}); const template: INotificationTemplate = body.data; diff --git a/apps/web/package.json b/apps/web/package.json index 4bd3ed28742..466d0d0107d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -84,6 +84,7 @@ "axios": "^1.6.2", "babel-plugin-import": "^1.13.3", "chart.js": "^3.7.1", + "crypto-js": "^4.2.0", "customize-cra": "^1.0.0", "date-fns": "^2.29.2", "dotenv": "^16.4.5", @@ -161,6 +162,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/testing-library__jest-dom": "^5.14.5", + "@types/crypto-js": "^4.2.2", "@types/js-cookie": "^3.0.6", "eslint-plugin-storybook": "^0.6.13", "http-server": "^0.13.0", diff --git a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx index 31205cf77ab..547d4d4cedf 100644 --- a/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx +++ b/apps/web/src/components/layout/components/v2/BridgeUpdateModal.tsx @@ -82,7 +82,7 @@ export const BridgeUpdateModal: FC = ({ isOpen, toggleOp await storeInProperLocation(url); track('Update endpoint clicked - [Bridge Modal]'); - successMessage('You have successfuly updated your Novu Endpoint configuration'); + successMessage('You have successfully updated your Novu Endpoint configuration'); toggleOpen(); } catch (error) { const err = error as Error; diff --git a/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx b/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx index 93ccc10e7f0..f5ac6b79e79 100644 --- a/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx +++ b/apps/web/src/components/layout/components/v2/SyncInfoModal.tsx @@ -1,25 +1,28 @@ +import { FC, useEffect, useState } from 'react'; +import { QueryObserverResult } from '@tanstack/react-query'; +import { showNotification } from '@mantine/notifications'; // TODO: replace with Novui Code Block when available import { Prism } from '@mantine/prism'; + // TODO: replace with Novui Modal when available import { Modal } from '@novu/design-system'; import { Button, Input, Tabs, Text, Title } from '@novu/novui'; -import { FC, useEffect, useState } from 'react'; +import { css } from '@novu/novui/css'; + import { useBridgeURL } from '../../../../studio/hooks/useBridgeURL'; import { API_ROOT, ENV } from '../../../../config'; import { useStudioState } from '../../../../studio/StudioStateProvider'; import { buildApiHttpClient } from '../../../../api'; -import { showNotification } from '@mantine/notifications'; -import { css } from '@novu/novui/css'; -import { Divider } from '@novu/novui/jsx'; export type SyncInfoModalProps = { isOpen: boolean; toggleOpen: () => void; + refetchOriginWorkflows: () => Promise>; }; const BRIDGE_ENDPOINT_PLACEHOLDER = ''; -export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => { +export const SyncInfoModal: FC = ({ isOpen, toggleOpen, refetchOriginWorkflows }) => { const { devSecretKey } = useStudioState(); const [manualUrl, setTunnelManualURl] = useState(''); @@ -42,7 +45,9 @@ export const SyncInfoModal: FC = ({ isOpen, toggleOpen }) => try { setLoadingSync(true); - const result = await api.syncBridge(manualUrl); + await api.syncBridge(manualUrl); + + refetchOriginWorkflows(); toggleOpen(); diff --git a/apps/web/src/components/layout/components/v2/SyncInfoModalTrigger.tsx b/apps/web/src/components/layout/components/v2/SyncInfoModalTrigger.tsx index ee9bdbe938d..b8fb5d25dbe 100644 --- a/apps/web/src/components/layout/components/v2/SyncInfoModalTrigger.tsx +++ b/apps/web/src/components/layout/components/v2/SyncInfoModalTrigger.tsx @@ -1,11 +1,20 @@ -import { Button } from '@novu/novui'; +import React, { useState } from 'react'; +import CryptoJS from 'crypto-js'; + +import { colors, Tooltip, useColorScheme } from '@novu/design-system'; +import { Button, Text } from '@novu/novui'; +import { css } from '@novu/novui/css'; import { IconOutlineCloudUpload } from '@novu/novui/icons'; -import { useState } from 'react'; + import { useTelemetry } from '../../../../hooks/useNovuAPI'; import { SyncInfoModal } from './SyncInfoModal'; +import { When } from '../../../utils/When'; +import { useIsSynced } from '../../../../hooks'; export function SyncInfoModalTrigger() { const [showSyncInfoModal, setShowSyncInfoModal] = useState(false); + const { isSynced, refetchOriginWorkflows } = useIsSynced(); + const track = useTelemetry(); const toggleSyncInfoModalShow = () => { @@ -15,11 +24,69 @@ export function SyncInfoModalTrigger() { return ( <> - + You have un-synced changes in your local application.} + withinPortal + > +
+ + + + +
+
+ {/** TODO: use a modal manager */} - + ); } + +function createHash(workflowDefine) { + return CryptoJS.SHA256(JSON.stringify(workflowDefine || '')).toString(CryptoJS.enc.Hex); +} + +export function Dot(props) { + const { colorScheme } = useColorScheme(); + + return ( + + + + + + + + + + ); +} diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts index a6481872d08..faa2e294023 100644 --- a/apps/web/src/hooks/index.ts +++ b/apps/web/src/hooks/index.ts @@ -31,3 +31,4 @@ export * from './useThemeChange'; export * from './useVariablesManager'; export * from './useVercelIntegration'; export * from './useVercelParams'; +export * from './useIsSynced'; diff --git a/apps/web/src/hooks/useIsSynced.ts b/apps/web/src/hooks/useIsSynced.ts new file mode 100644 index 00000000000..871eceb5426 --- /dev/null +++ b/apps/web/src/hooks/useIsSynced.ts @@ -0,0 +1,39 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; + +import { INotificationTemplate } from '@novu/shared'; + +import { getNotificationsList } from '../api/notification-templates'; +import { useDiscover } from '../studio/hooks'; +import { createHash } from '../utils/create-hash'; + +export function useIsSynced() { + const { data: bridgeDiscoverData, isLoading: isLoadingBridgeWorkflows } = useDiscover(); + const { + data: originData, + isLoading: isLoadingOriginWorkflows, + refetch, + } = useQuery( + ['origin-workflows'], + async () => { + return getNotificationsList({ page: 0, limit: 100 }); + }, + {} + ); + + const isSynced = useMemo(() => { + if (isLoadingBridgeWorkflows || isLoadingOriginWorkflows) { + return true; + } + + const bridgeDiscoverWorkflows = bridgeDiscoverData?.workflows || undefined; + const originWorkflows = originData?.data.map((workflow: INotificationTemplate) => workflow.rawData) || undefined; + + const bridgeDiscoverWorkflowsHash = createHash(JSON.stringify(bridgeDiscoverWorkflows || '')); + const storedWorkflowsHash = createHash(JSON.stringify(originWorkflows || '')); + + return storedWorkflowsHash === bridgeDiscoverWorkflowsHash; + }, [bridgeDiscoverData, originData, isLoadingBridgeWorkflows, isLoadingOriginWorkflows]); + + return { isSynced, refetchOriginWorkflows: refetch }; +} diff --git a/apps/web/src/utils/create-hash.ts b/apps/web/src/utils/create-hash.ts new file mode 100644 index 00000000000..0c58093f196 --- /dev/null +++ b/apps/web/src/utils/create-hash.ts @@ -0,0 +1,5 @@ +import CryptoJS from 'crypto-js'; + +export const createHash = (message: string) => { + return CryptoJS.SHA256(message).toString(CryptoJS.enc.Hex); +}; diff --git a/libs/dal/src/repositories/notification-template/notification-template.schema.ts b/libs/dal/src/repositories/notification-template/notification-template.schema.ts index c01bc9f7d35..51f00f959c0 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.schema.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.schema.ts @@ -217,7 +217,7 @@ const notificationTemplateSchema = new Schema( rawData: Schema.Types.Mixed, payloadSchema: Schema.Types.Mixed, }, - schemaOptions + { ...schemaOptions, minimize: false } ); notificationTemplateSchema.virtual('steps.template', { diff --git a/libs/shared/src/entities/notification-template/notification-template.interface.ts b/libs/shared/src/entities/notification-template/notification-template.interface.ts index 65454025493..358ba6c3e85 100644 --- a/libs/shared/src/entities/notification-template/notification-template.interface.ts +++ b/libs/shared/src/entities/notification-template/notification-template.interface.ts @@ -6,6 +6,7 @@ import type { TemplateVariableTypeEnum, FilterParts, WorkflowTypeEnum, + NotificationTemplateCustomData, } from '../../types'; import { IMessageTemplate } from '../message-template'; import { IPreferenceChannels } from '../subscriber-preference'; @@ -30,9 +31,12 @@ export interface INotificationTemplate { steps: INotificationTemplateStep[] | INotificationBridgeTrigger[]; triggers: INotificationTrigger[]; isBlueprint?: boolean; + blueprintId?: string; type?: WorkflowTypeEnum; // eslint-disable-next-line @typescript-eslint/no-explicit-any payloadSchema?: any; + rawData?: any; + data?: NotificationTemplateCustomData; } export class IGroupedBlueprint { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b904bdda096..95283586585 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -868,6 +868,9 @@ importers: chart.js: specifier: ^3.7.1 version: 3.9.1 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 customize-cra: specifier: ^1.0.0 version: 1.0.0 @@ -1082,6 +1085,9 @@ importers: '@testing-library/jest-dom': specifier: ^4.2.4 version: 4.2.4 + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -2917,7 +2923,7 @@ importers: version: 29.5.2 jest: specifier: ^27.1.0 - version: 27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) + version: 27.5.1(ts-node@10.9.2(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) json-schema-to-ts: specifier: ^3.0.0 version: 3.1.0 @@ -3160,7 +3166,7 @@ importers: version: 3.8.3(encoding@0.1.13) jest: specifier: ^27.0.6 - version: 27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) + version: 27.5.1(ts-node@10.9.2(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -3169,7 +3175,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(babel-jest@27.5.1(@babel/core@7.24.4))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(babel-jest@27.5.1(@babel/core@7.24.4))(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -4127,7 +4133,7 @@ importers: version: 3.8.3(encoding@0.1.13) jest: specifier: ^27.0.6 - version: 27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) + version: 27.5.1(ts-node@10.9.2(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)) npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -4142,7 +4148,7 @@ importers: version: 0.0.0 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(babel-jest@27.5.1(@babel/core@7.24.4))(jest@27.5.1(ts-node@10.9.1(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(babel-jest@27.5.1(@babel/core@7.24.4))(jest@27.5.1(ts-node@10.9.2(@swc/core@1.3.107(@swc/helpers@0.5.5))(@types/node@20.14.10)(typescript@4.9.5)))(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -14143,6 +14149,9 @@ packages: '@types/cross-spawn@6.0.3': resolution: {integrity: sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/css-font-loading-module@0.0.7': resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==} @@ -31607,7 +31616,7 @@ snapshots: '@aws-sdk/client-sso-oidc': 3.575.0 '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) '@aws-sdk/core': 3.575.0 - '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)) + '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 '@aws-sdk/middleware-logger': 3.575.0 '@aws-sdk/middleware-recursion-detection': 3.575.0 @@ -31992,7 +32001,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sts': 3.575.0 '@aws-sdk/core': 3.575.0 - '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)) + '@aws-sdk/credential-provider-node': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) '@aws-sdk/middleware-host-header': 3.575.0 '@aws-sdk/middleware-logger': 3.575.0 '@aws-sdk/middleware-recursion-detection': 3.575.0 @@ -32547,12 +32556,29 @@ snapshots: - aws-crt optional: true - '@aws-sdk/credential-provider-ini@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0)': + '@aws-sdk/credential-provider-ini@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0))': dependencies: '@aws-sdk/client-sts': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) '@aws-sdk/credential-provider-env': 3.575.0 '@aws-sdk/credential-provider-process': 3.575.0 '@aws-sdk/credential-provider-sso': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) + '@aws-sdk/credential-provider-web-identity': 3.575.0(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)) + '@aws-sdk/types': 3.575.0 + '@smithy/credential-provider-imds': 3.0.0 + '@smithy/property-provider': 3.0.0 + '@smithy/shared-ini-file-loader': 3.0.0 + '@smithy/types': 3.0.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + + '@aws-sdk/credential-provider-ini@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0)': + dependencies: + '@aws-sdk/client-sts': 3.575.0 + '@aws-sdk/credential-provider-env': 3.575.0 + '@aws-sdk/credential-provider-process': 3.575.0 + '@aws-sdk/credential-provider-sso': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) '@aws-sdk/credential-provider-web-identity': 3.575.0(@aws-sdk/client-sts@3.575.0) '@aws-sdk/types': 3.575.0 '@smithy/credential-provider-imds': 3.0.0 @@ -32618,7 +32644,7 @@ snapshots: dependencies: '@aws-sdk/credential-provider-env': 3.575.0 '@aws-sdk/credential-provider-http': 3.575.0 - '@aws-sdk/credential-provider-ini': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0) + '@aws-sdk/credential-provider-ini': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0)(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)) '@aws-sdk/credential-provider-process': 3.575.0 '@aws-sdk/credential-provider-sso': 3.575.0(@aws-sdk/client-sso-oidc@3.575.0) '@aws-sdk/credential-provider-web-identity': 3.575.0(@aws-sdk/client-sts@3.575.0(@aws-sdk/client-sso-oidc@3.575.0)) @@ -48254,6 +48280,8 @@ snapshots: dependencies: '@types/node': 20.14.10 + '@types/crypto-js@4.2.2': {} + '@types/css-font-loading-module@0.0.7': {} '@types/d3-array@3.0.4': {} @@ -53549,7 +53577,7 @@ snapshots: jest-worker: 27.5.1 postcss: 8.4.38 schema-utils: 4.0.0 - serialize-javascript: 6.0.1 + serialize-javascript: 6.0.2 source-map: 0.6.1 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.5))(esbuild@0.18.20) optionalDependencies: @@ -53561,7 +53589,7 @@ snapshots: jest-worker: 27.5.1 postcss: 8.4.38 schema-utils: 4.0.0 - serialize-javascript: 6.0.1 + serialize-javascript: 6.0.2 source-map: 0.6.1 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.5)) @@ -69019,7 +69047,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.19 jest-worker: 27.5.1 schema-utils: 3.3.0 - serialize-javascript: 6.0.1 + serialize-javascript: 6.0.2 terser: 5.22.0 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.5))(esbuild@0.18.20) optionalDependencies: @@ -69031,7 +69059,7 @@ snapshots: '@jridgewell/trace-mapping': 0.3.19 jest-worker: 27.5.1 schema-utils: 3.3.0 - serialize-javascript: 6.0.1 + serialize-javascript: 6.0.2 terser: 5.22.0 webpack: 5.78.0(@swc/core@1.3.107(@swc/helpers@0.5.5)) optionalDependencies: