diff --git a/.commitlintrc.json b/.commitlintrc.json index 7293d5af944..7185c74f33d 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -4,7 +4,7 @@ "subject-case": [ 2, "always", - ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"] + ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case", "camel-case"] ], "type-enum": [ 2, diff --git a/.cspell.json b/.cspell.json index 5d1757422e2..2aafec01243 100644 --- a/.cspell.json +++ b/.cspell.json @@ -220,6 +220,7 @@ "plivo", "Plivo", "prettierignore", + "Pushpad", "Pushwoosh", "sandboxed", "sendgrid", @@ -508,7 +509,16 @@ "autodocs", "stackalt", "mediumdark", - "Docgen" + "Docgen", + "clicksend", + "Clicksend", + "Kamil", + "Myśliwiec", + "nestframework", + "ryver", + "idempotency", + "IDEMPOTENCY", + "Idempotency" ], "flagWords": [], "patterns": [ diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 73ca2ecbca2..ed6ad8ce3f0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -22,7 +22,7 @@ RUN . /etc/os-release \ # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" # [Optional] Uncomment if you want to install more global node modules -RUN su node -c "npm install -g pnpm@7.33.4" +RUN su node -c "npm install -g pnpm@8.9.0" diff --git a/.github/labeler.yml b/.github/labeler.yml index 557afd5eb7d..21fda69818a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -12,6 +12,8 @@ - apps/webhook/**/* '@novu/dal': - libs/dal/**/* +'@novu/design-system': + - libs/design-system/**/* '@novu/shared': - libs/shared/**/* '@novu/notification-center': diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 2f443ccd820..b3f25ad9105 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -13,7 +13,7 @@ jobs: with: repo-token: "${{ secrets.GITHUB_TOKEN }}" - - uses: microsoft/PR-Metrics@v1.5.4 + - uses: microsoft/PR-Metrics@v1.5.7 name: PR Metrics env: PR_METRICS_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-api-e2e.yml b/.github/workflows/reusable-api-e2e.yml index 202805d8174..8f2ce8b56cd 100644 --- a/.github/workflows/reusable-api-e2e.yml +++ b/.github/workflows/reusable-api-e2e.yml @@ -77,6 +77,8 @@ jobs: - uses: ./.github/actions/start-localstack - uses: ./.github/actions/run-worker + with: + launch_darkly_sdk_key: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }} # Runs a single command using the runners shell - name: Build API @@ -84,6 +86,8 @@ jobs: - name: Run E2E tests + env: + LAUNCH_DARKLY_SDK_KEY: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }} run: | cd apps/api && pnpm test:e2e diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index 3112553eb25..44c089ce8bd 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -102,7 +102,7 @@ jobs: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - name: Cypress run e2e - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 env: NODE_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -113,7 +113,7 @@ jobs: with: working-directory: apps/web browser: "${{ env.BROWSER_PATH }}" - record: true + record: false parallel: false install: false config-file: cypress.config.ts @@ -150,7 +150,7 @@ jobs: run: pnpm cypress install - name: Run Component tests 🧪 - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 env: NODE_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/reusable-widget-e2e.yml b/.github/workflows/reusable-widget-e2e.yml index 84426d0255b..6434b471a8b 100644 --- a/.github/workflows/reusable-widget-e2e.yml +++ b/.github/workflows/reusable-widget-e2e.yml @@ -97,7 +97,7 @@ jobs: run: pnpm cypress install - name: Cypress run - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v6 env: NODE_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitpod.dockerfile b/.gitpod.dockerfile index 0b051589b8b..9977b434367 100644 --- a/.gitpod.dockerfile +++ b/.gitpod.dockerfile @@ -1,4 +1,4 @@ FROM gitpod/workspace-mongodb RUN sudo apt-get update && sudo apt-get install -y redis-server && sudo rm -rf /var/lib/apt/lists/* -RUN npm install -g pnpm@7.33.4 +RUN npm install -g pnpm@8.9.0 diff --git a/README.md b/README.md index 0bb1247afea..a3fceee799d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@
- + - Logo + ![Novu Logo]
@@ -12,19 +12,19 @@
The ultimate service for managing multi-channel notifications with a single API.
- +

🎉 We're participating in Hacktoberfest 2023! 🎉

-Interested in participating in Hacktoberfest? We extend a warm invitation! You also get the opportunity to win some swag 😁 +Are you interested in participating in Hacktoberfest? We extend a warm invitation! You also get the opportunity to win some swag 😁 > ⭐️ If you're new to Hacktoberfest, you can learn more and register to participate [here](https://hacktoberfest.com/participation/). Registration is from **September 26th - October 31st**. - Our Hacktoberfest kickoff event is happening on October 2, 2023. 🚀 - Check out our website for [hacktoberfest instructions](https://novu.co/hacktoberfest/). -- Join our [Discord and engage with our community](https://discord.com/invite/novu), get answers to your challenges, stay updated on events, announcements & prizes. +- Join our [Discord and engage with our community](https://discord.com/invite/novu), get answers to your challenges, and stay updated on events, announcements, and prizes. In addition to this repository, here are the other Novu repositories you can contribute to for Hacktoberfest: -- [Novu docs](https://github.com/novuhq/docs/issues) +- [Novu Docs](https://github.com/novuhq/docs/issues) - [Novu PHP SDK](https://github.com/novuhq/novu-php/issues) - [Novu Ruby SDK](https://github.com/novuhq/novu-ruby/issues) - [Novu Python SDK](https://github.com/novuhq/novu-python/issues) @@ -50,7 +50,7 @@ Your contribution, no matter its size, holds immense value. We eagerly await to · Roadmap · - Twitter + X · Notifications Directory

@@ -81,7 +81,7 @@ With Novu, you can create custom workflows and define conditions for each channe - 📦 Easy to set up and integrate - 🛡 Debug and analyze multi-channel messages in a single dashboard - 📦 Embeddable notification center with real-time updates -- 👨‍💻 Community driven +- 👨‍💻 Community-driven ## 📚 Table Of Contents @@ -100,13 +100,17 @@ With Novu, you can create custom workflows and define conditions for each channe ## 🚀 Getting Started -We are excited to launch the complete Novu API and admin panel. Want to give it a test before the official release? Here is how: +We are excited to launch the complete Novu API and admin panel. Do you want to give it a test before the official release? Here is how: ``` npx novu init ``` -After setting up your account using the cloud or docker version you can trigger the API using the `@novu/node` package. +After setting up your account using the cloud or docker version, you can trigger the API using the `@novu/node` package. + +For API documentation and reference, please visit [Novu API Reference] (https://docs.novu.co/api-reference/events/trigger-event). + +To get started with the Node.js package, you can install it using npm: ```bash npm install @novu/node @@ -140,14 +144,14 @@ await novu.trigger('', { Using the Novu API and admin panel, you can easily add a real-time notification center to your web app without building it yourself. You can use our React / Vue / Angular component or an iframe embed.
-notification-center-912bb96e009fb3a69bafec23bcde00b0 +notification-center-912bb96e009fb3a69bafec23bcde00b0 Read more about how to add a notification center to your app with the Novu API [here](https://docs.novu.co/notification-center/getting-started)

- React Component - · Vue Component - · Angular Component + React Component + · Vue Component + · Angular Component

@@ -220,7 +224,7 @@ Novu provides a single API to manage providers across multiple channels with a s ## 📋 Read Our Code Of Conduct -Before you begin coding and collaborating, please read our [Code of Conduct](https://github.com/novuhq/novu/blob/main/CODE_OF_CONDUCT.md) thoroughly to understand the standards (that you are required to adhere to) for community engagement. As part of our open-source community, we hold ourselves and other contributors to a high standard of communication. As a participant and contributor to this project, you are agreeing to abide by our [Code of Conduct](https://github.com/novuhq/novu/blob/main/CODE_OF_CONDUCT.md). +Before you begin coding and collaborating, please read our [Code of Conduct](https://github.com/novuhq/novu/blob/main/CODE_OF_CONDUCT.md) thoroughly to understand the standards (that you are required to adhere to) for community engagement. As part of our open-source community, we hold ourselves and other contributors to a high standard of communication. As a participant and contributor to this project, you agree to abide by our [Code of Conduct](https://github.com/novuhq/novu/blob/main/CODE_OF_CONDUCT.md). ## 💻 Need Help? @@ -232,7 +236,7 @@ We are more than happy to help you. If you are getting any errors or facing prob ## 🔗 Links -- [Home page](https://novu.co/) +- [Home page](https://novu.co?utm_source=github) - [Contribution Guidelines](https://github.com/novuhq/novu/blob/main/CONTRIBUTING.md) - [Run Novu Locally](https://docs.novu.co/community/run-in-local-machine) @@ -244,6 +248,6 @@ Novu is licensed under the MIT License - see the [LICENSE](https://github.com/no Thanks a lot for spending your time helping Novu grow. Keep rocking 🥂 - + Contributors diff --git a/SECURITY.md b/SECURITY.md index b8340555d38..23f3539ad56 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,58 +1,43 @@ -# Security +# Security Contact -Contact: security@novu.co +**Contact:** security@novu.co -Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt) +At Novu, we prioritize the security of our systems. While we strive to make our systems as secure as possible, vulnerabilities can still exist. If you discover a vulnerability, we kindly request your assistance in helping us enhance our security measures and protect our clients. -We place a high priority on the security of our systems at Novu. However, no matter how hard we try to make our systems secure, vulnerabilities can still exist. +## Reporting a Vulnerability -In the event that you discover a vulnerability, please let us know so we can address it as soon as possible. We would like to ask you to help us better protect our clients and our systems. +**In Scope Vulnerabilities:** -## Out of scope vulnerabilities: +- Any security issues that could jeopardize the confidentiality, integrity, or availability of our systems or data. -- Clickjacking on pages with no sensitive actions. +**Out of Scope Vulnerabilities:** +- Clickjacking on pages with no sensitive actions. - Unauthenticated/logout/login CSRF. - - Attacks requiring MITM or physical access to a user's device. - - Any activity that could lead to the disruption of our service (DoS). - -- Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS. - -- Email spoofing - -- Missing DNSSEC, CAA, CSP headers - -- Lack of Secure or HTTP only flag on non-sensitive cookies - -- Deadlinks - -## Please do the following: - -- E-mail your findings to [security@novu.co](mailto:security@novu.co). - -- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you. - -- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data, - -- Do not reveal the problem to others until it has been resolved, - -- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties, - -- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation. - -## What we promise: - -- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date, - -- If you have followed the instructions above, we will not take any legal action against you in regard to the report, - -- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission, - -- We will keep you informed of the progress towards resolving the problem, - -- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and - -- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved. - +- Content spoofing and text injection issues without showing an attack vector or the ability to modify HTML/CSS. +- Email spoofing. +- Missing DNSSEC, CAA, CSP headers. +- Lack of Secure or HTTP-only flags on non-sensitive cookies. +- Deadlinks. + +**Reporting Instructions:** + +1. Email your findings to **security@novu.co**. +2. Please refrain from running automated scanners on our infrastructure or dashboard. If you intend to do so, contact us, and we will set up a sandbox for your testing. +3. Do not exploit the vulnerability or problem you have discovered, such as downloading more data than necessary or deleting/modifying others' data. +4. Keep the problem confidential until it has been resolved. +5. Do not use attacks on physical security, social engineering, distributed denial of service, spam, or third-party applications. +6. Provide sufficient information to reproduce the problem, including the IP address or URL of the affected system and a clear description of the vulnerability. Complex vulnerabilities may require additional explanation. + +## What We Promise + +1. We will respond to your report within 3 business days, providing an evaluation of the report and an expected resolution date. +2. If you have adhered to the reporting instructions, we will not take any legal action against you in relation to the report. +3. We will maintain strict confidentiality regarding your report and will not share your personal details with third parties without your consent. +4. You will be kept informed of the progress toward resolving the problem. +5. In public disclosures about the reported problem, we will credit you as the discoverer of the issue (unless you request otherwise). +6. We are committed to resolving all issues promptly and actively participating in the public disclosure of the issue once it's resolved. + +Your contribution to enhancing our security is greatly appreciated. diff --git a/_templates/provider/new/package.ejs.t b/_templates/provider/new/package.ejs.t index aa94ddba9dc..5d6122610ec 100644 --- a/_templates/provider/new/package.ejs.t +++ b/_templates/provider/new/package.ejs.t @@ -33,10 +33,6 @@ "publishConfig": { "access": "public" }, - "engines": { - "node": ">=13.0.0 <17.0.0", - "pnpm": "^7.26.0" - }, "dependencies": { "@novu/stateless": "<%= version %>" }, diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 05b4170e517..963994c0166 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -5,7 +5,7 @@ ENV BULL_MQ_PRO_NPM_TOKEN=$BULL_MQ_PRO_TOKEN ENV NX_DAEMON=false RUN npm i pm2 -g -RUN npm --no-update-notifier --no-fund --global install pnpm@7.33.4 +RUN npm --no-update-notifier --no-fund --global install pnpm@8.9.0 RUN pnpm --version WORKDIR /usr/src/app diff --git a/apps/api/README.md b/apps/api/README.md index 25194cf0e13..f7f931edb5c 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -1,78 +1,38 @@ -

- Nest Logo -

+
+ + + + Logo + + +
-[travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=main -[travis-url]: https://travis-ci.org/nestjs/nest -[linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux -[linux-url]: https://travis-ci.org/nestjs/nest -

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

-

-NPM Version -Package License -NPM Downloads -Travis -Linux -Coverage -Gitter -Backers on Open Collective -Sponsors on Open Collective - - -

- +# @novu/api -## Description +A RESTful API for accessing the Novu platform, built using [NestJS](https://nestjs.com/). -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. +## OpenAPI (formerly Swagger) -## Installation +The Novu API utilizes the [`@nestjs/swagger`](https://github.com/nestjs/swagger) package to generate up-to-date OpenAPI specifications. -```bash -$ npm install -``` +A web interface to browse the available endpoints is available at [api.novu.co/api](https://api.novu.co/api). An OpenAPI specification can be retrieved at [api.novu.co/api-json](https://api.novu.co/api-json). -## Running the app - -```bash -# development -$ npm run start +## Running the API -# watch mode -$ npm run start:dev +See the docs for [Run in Local Machine](https://docs.novu.co/community/run-in-local-machin) to get setup. Then run: -# incremental rebuild (webpack) -$ npm run webpack -$ npm run start:hmr - -# production mode -$ npm run start:prod +```bash +# Run the API in watch mode +$ npm run start:api ``` ## Test +### Unit Tests ```bash # unit tests $ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov ``` -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](LICENSE). +### E2E tests +See the docs for [Running on Local Machine - API Tests](https://docs.novu.co/community/run-in-local-machine#api). diff --git a/apps/api/e2e/idempotency.e2e.ts b/apps/api/e2e/idempotency.e2e.ts new file mode 100644 index 00000000000..ced5688bcec --- /dev/null +++ b/apps/api/e2e/idempotency.e2e.ts @@ -0,0 +1,307 @@ +import { UserSession } from '@novu/testing'; +import { CacheService } from '@novu/application-generic'; +import { expect } from 'chai'; +describe('Idempotency Test', async () => { + let session: UserSession; + const path = '/v1/testing/idempotency'; + const HEADER_KEYS = { + IDEMPOTENCY_KEY: 'idempotency-key', + RETRY_AFTER: 'retry-after', + IDEMPOTENCY_REPLAY: 'idempotency-replay', + LINK: 'link', + }; + const DOCS_LINK = 'docs.novu.co/idempotency'; + + let cacheService: CacheService | null = null; + + describe('when enabled', () => { + before(async () => { + session = new UserSession(); + await session.initialize(); + cacheService = session.testServer?.getService(CacheService); + process.env.IS_API_IDEMPOTENCY_ENABLED = 'true'; + }); + + it('should return cached same response for duplicate requests', async () => { + const key = `1`; + const { body, headers } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + const { body: bodyDupe, headers: headerDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(body.data.number).to.equal(bodyDupe.data.number); + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_REPLAY]).to.eq('true'); + }); + it('should return cached and use correct cache key when apiKey is used', async () => { + const key = `2`; + const { body, headers } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + const cacheKey = `test-${session.organization._id}-${key}`; + session.testServer?.getHttpServer(); + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const cacheVal = JSON.stringify(JSON.parse(await cacheService?.get(cacheKey)!).data); + expect(JSON.stringify(body)).to.eq(cacheVal); + const { body: bodyDupe, headers: headerDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(body.data.number).to.equal(bodyDupe.data.number); + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_REPLAY]).to.eq('true'); + }); + it('should return cached and use correct cache key when authToken and apiKey combination is used', async () => { + const key = `3`; + const { body, headers } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', session.token) + .send({ data: 201 }) + .expect(201); + const cacheKey = `test-${session.organization._id}-${key}`; + session.testServer?.getHttpServer(); + // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + const cacheVal = JSON.stringify(JSON.parse(await cacheService?.get(cacheKey)!).data); + expect(JSON.stringify(body)).to.eq(cacheVal); + const { body: bodyDupe, headers: headerDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(body.data.number).to.equal(bodyDupe.data.number); + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_REPLAY]).to.eq('true'); + }); + it('should return conflict when concurrent requests are made', async () => { + const key = `4`; + const [{ headers, body, status }, { headers: headerDupe, body: bodyDupe, status: statusDupe }] = + await Promise.all([ + session.testAgent.post(path).set(HEADER_KEYS.IDEMPOTENCY_KEY, key).send({ data: 250 }), + session.testAgent.post(path).set(HEADER_KEYS.IDEMPOTENCY_KEY, key).send({ data: 250 }), + ]); + const oneSuccess = status === 201 || statusDupe === 201; + const oneConflict = status === 409 || statusDupe === 409; + const conflictBody = status === 201 ? bodyDupe : body; + const retryHeader = headers[HEADER_KEYS.RETRY_AFTER] || headerDupe[HEADER_KEYS.RETRY_AFTER]; + expect(oneSuccess).to.be.true; + expect(oneConflict).to.be.true; + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.LINK]).to.eq(DOCS_LINK); + expect(retryHeader).to.eq(`1`); + expect(JSON.stringify(conflictBody)).to.eq( + JSON.stringify({ + message: `Request with key "${key}" is currently being processed. Please retry after 1 second`, + error: 'Conflict', + statusCode: 409, + }) + ); + }); + it('should return conflict when different body is sent for same key', async () => { + const key = '5'; + const { headers, body, status } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 250 }); + const { + headers: headerDupe, + body: bodyDupe, + status: statusDupe, + } = await session.testAgent.post(path).set(HEADER_KEYS.IDEMPOTENCY_KEY, key).send({ data: 251 }); + + const oneSuccess = status === 201 || statusDupe === 201; + const oneConflict = status === 422 || statusDupe === 422; + const conflictBody = status === 201 ? bodyDupe : body; + expect(oneSuccess).to.be.true; + expect(oneConflict).to.be.true; + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.LINK]).to.eq(DOCS_LINK); + expect(JSON.stringify(conflictBody)).to.eq( + JSON.stringify({ + message: `Request with key "${key}" is being reused for a different body`, + error: 'Unprocessable Entity', + statusCode: 422, + }) + ); + }); + it('should return non cached response for unique requests', async () => { + const key = '6'; + const key1 = '7'; + const { body, headers } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + + const { body: bodyDupe, headers: headerDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key1) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(typeof bodyDupe.data.number === 'number').to.be.true; + expect(body.data.number).not.to.equal(bodyDupe.data.number); + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key1); + }); + it('should return non cached response for GET requests', async () => { + const key = '8'; + const { body, headers } = await session.testAgent + .get(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({}) + .expect(200); + + const { body: bodyDupe } = await session.testAgent + .get(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({}) + .expect(200); + expect(typeof body.data.number === 'number').to.be.true; + expect(typeof bodyDupe.data.number === 'number').to.be.true; + expect(body.data.number).not.to.equal(bodyDupe.data.number); + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(undefined); + }); + it('should return cached error response for duplicate requests', async () => { + const key = '9'; + const { body, headers } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 422 }) + .expect(422); + + const { body: bodyDupe, headers: headerDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 422 }) + .expect(422); + expect(JSON.stringify(body)).to.equal(JSON.stringify(bodyDupe)); + + expect(headers[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + expect(headerDupe[HEADER_KEYS.IDEMPOTENCY_KEY]).to.eq(key); + }); + it('should return 400 when key bigger than allowed limit', async () => { + const key = Array.from({ length: 256 }) + .fill(0) + .map((i) => i) + .join(''); + const { body } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 250 }) + .expect(400); + expect(JSON.stringify(body)).to.eq( + JSON.stringify({ + message: `idempotencyKey "${key}" has exceeded the maximum allowed length of 255 characters`, + error: 'Bad Request', + statusCode: 400, + }) + ); + }); + }); + + describe('when disabled', () => { + before(async () => { + session = new UserSession(); + await session.initialize(); + process.env.IS_API_IDEMPOTENCY_ENABLED = 'false'; + }); + + it('should not return cached same response for duplicate requests', async () => { + const key = '10'; + const { body } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + + const { body: bodyDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(body.data.number).not.to.equal(bodyDupe.data.number); + }); + it('should return non cached response for unique requests', async () => { + const key = '11'; + const key1 = '12'; + const { body } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: 201 }) + .expect(201); + + const { body: bodyDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key1) + .send({ data: 201 }) + .expect(201); + expect(typeof body.data.number === 'number').to.be.true; + expect(typeof bodyDupe.data.number === 'number').to.be.true; + expect(body.data.number).not.to.equal(bodyDupe.data.number); + }); + it('should return non cached response for GET requests', async () => { + const key = '13'; + const { body } = await session.testAgent.get(path).set(HEADER_KEYS.IDEMPOTENCY_KEY, key).send({}).expect(200); + + const { body: bodyDupe } = await session.testAgent + .get(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({}) + .expect(200); + expect(typeof body.data.number === 'number').to.be.true; + expect(typeof bodyDupe.data.number === 'number').to.be.true; + expect(body.data.number).not.to.equal(bodyDupe.data.number); + }); + it('should not return cached error response for duplicate requests', async () => { + const key = '14'; + const { body } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: '500' }) + .expect(500); + + const { body: bodyDupe } = await session.testAgent + .post(path) + .set(HEADER_KEYS.IDEMPOTENCY_KEY, key) + .set('authorization', `ApiKey ${session.apiKey}`) + .send({ data: '500' }) + .expect(500); + expect(JSON.stringify(body)).not.to.equal(JSON.stringify(bodyDupe)); + }); + }); +}); diff --git a/apps/api/package.json b/apps/api/package.json index 0cf8a8c596c..ae30e4e395d 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@novu/api", - "version": "0.20.0-alpha.0", + "version": "0.20.0", "description": "description", "author": "", "private": "true", @@ -38,12 +38,12 @@ "@nestjs/platform-express": "^10.2.2", "@nestjs/swagger": "^7.1.8", "@nestjs/terminus": "^10.0.1", - "@novu/application-generic": "^0.20.0-alpha.0", - "@novu/dal": "^0.20.0-alpha.0", - "@novu/node": "^0.20.0-alpha.0", - "@novu/shared": "^0.20.0-alpha.0", - "@novu/stateless": "^0.20.0-alpha.0", - "@novu/testing": "^0.20.0-alpha.0", + "@novu/application-generic": "^0.20.0", + "@novu/dal": "^0.20.0", + "@novu/node": "^0.20.0", + "@novu/shared": "^0.20.0", + "@novu/stateless": "^0.20.0", + "@novu/testing": "^0.20.0", "@sendgrid/mail": "^7.6.0", "@sentry/hub": "^7.40.0", "@sentry/node": "^7.40.0", @@ -94,15 +94,15 @@ "@types/bull": "^3.15.8", "@types/chai": "^4.2.11", "@types/express": "4.17.17", - "@types/mocha": "^8.2.3", + "@types/mocha": "^10.0.2", "@types/node": "^14.6.0", "@types/passport-github": "^1.1.5", "@types/passport-jwt": "^3.0.3", "@types/sinon": "^9.0.0", "@types/supertest": "^2.0.8", "chai": "^4.2.0", - "mocha": "^8.4.0", - "nodemon": "^2.0.3", + "mocha": "^10.2.0", + "nodemon": "^3.0.1", "sinon": "^9.2.4", "ts-loader": "~9.4.0", "ts-node": "~10.9.1", @@ -110,12 +110,7 @@ "typescript": "4.9.5" }, "optionalDependencies": { - "@novu/ee-auth": "^0.20.0-alpha.0" - }, - "nx": { - "implicitDependencies": [ - "@novu/ee-auth" - ] + "@novu/ee-auth": "^0.20.0-alpha.1" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/apps/api/src/.env.development b/apps/api/src/.env.development index 8af859ce673..410ba4c291a 100644 --- a/apps/api/src/.env.development +++ b/apps/api/src/.env.development @@ -64,3 +64,5 @@ NOVU_SMS_INTEGRATION_SENDER= INTERCOM_IDENTITY_VERIFICATION_SECRET_KEY= LAUNCH_DARKLY_SDK_KEY= + +IS_API_IDEMPOTENCY_ENABLED=false diff --git a/apps/api/src/.env.production b/apps/api/src/.env.production index a52b412d515..cb1a23d72a7 100644 --- a/apps/api/src/.env.production +++ b/apps/api/src/.env.production @@ -43,6 +43,8 @@ MAIL_SERVER_DOMAIN= MAX_NOVU_INTEGRATION_MAIL_REQUESTS=300 NOVU_EMAIL_INTEGRATION_API_KEY= +MONGO_MIN_POOL_SIZE=50 + MAX_NOVU_INTEGRATION_SMS_REQUESTS=20 NOVU_SMS_INTEGRATION_ACCOUNT_SID= NOVU_SMS_INTEGRATION_TOKEN= @@ -51,3 +53,5 @@ NOVU_SMS_INTEGRATION_SENDER= INTERCOM_IDENTITY_VERIFICATION_SECRET_KEY= LAUNCH_DARKLY_SDK_KEY= + +IS_API_IDEMPOTENCY_ENABLED=false diff --git a/apps/api/src/.env.test b/apps/api/src/.env.test index 5b43216c298..070251b2084 100644 --- a/apps/api/src/.env.test +++ b/apps/api/src/.env.test @@ -74,7 +74,6 @@ VERCEL_REDIRECT_URI=http://localhost:4200/auth/login VERCEL_BASE_URL=https://api.vercel.com FF_IS_TOPIC_NOTIFICATION_ENABLED=true -IS_MULTI_PROVIDER_CONFIGURATION_ENABLED=true STORE_NOTIFICATION_CONTENT=true @@ -90,3 +89,5 @@ MAX_NOVU_INTEGRATION_SMS_REQUESTS=20 NOVU_SMS_INTEGRATION_ACCOUNT_SID=test NOVU_SMS_INTEGRATION_TOKEN=test NOVU_SMS_INTEGRATION_SENDER=1234567890 + +IS_API_IDEMPOTENCY_ENABLED=true diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 00418026038..05373ac753e 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -31,6 +31,7 @@ import { TopicsModule } from './app/topics/topics.module'; import { InboundParseModule } from './app/inbound-parse/inbound-parse.module'; import { BlueprintModule } from './app/blueprint/blueprint.module'; import { TenantModule } from './app/tenant/tenant.module'; +import { IdempotencyInterceptor } from './app/shared/framework/idempotency.interceptor'; const enterpriseImports = (): Array | ForwardReference> => { const modules: Array | ForwardReference> = []; @@ -78,7 +79,12 @@ const enterpriseModules = enterpriseImports(); const modules = baseModules.concat(enterpriseModules); -const providers: Provider[] = []; +const providers: Provider[] = [ + { + provide: APP_INTERCEPTOR, + useClass: IdempotencyInterceptor, + }, +]; if (process.env.SENTRY_DSN) { modules.push(RavenModule); diff --git a/apps/api/src/app/auth/e2e/user-registration.e2e.ts b/apps/api/src/app/auth/e2e/user-registration.e2e.ts index 5c49c6841a5..0779569dc99 100644 --- a/apps/api/src/app/auth/e2e/user-registration.e2e.ts +++ b/apps/api/src/app/auth/e2e/user-registration.e2e.ts @@ -82,7 +82,7 @@ describe('User registration - /auth/register (POST)', async () => { // Should generate environment and api keys expect(jwtContent.environmentId).to.be.ok; - const environment = await environmentRepository.findById(jwtContent.environmentId); + const environment = await environmentRepository.findOne({ _id: jwtContent.environmentId }); expect(environment.apiKeys.length).to.equal(1); expect(environment.apiKeys[0].key).to.ok; diff --git a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts index cf619d79b55..67e4a5893f3 100644 --- a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts +++ b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts @@ -3,7 +3,14 @@ import * as sinon from 'sinon'; import { UserSession } from '@novu/testing'; import { NotificationTemplateRepository, EnvironmentRepository } from '@novu/dal'; -import { EmailBlockTypeEnum, FilterPartTypeEnum, INotificationTemplate, StepTypeEnum } from '@novu/shared'; +import { + EmailBlockTypeEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, + FilterPartTypeEnum, + INotificationTemplate, + StepTypeEnum, +} from '@novu/shared'; import { buildGroupedBlueprintsKey, CacheService, @@ -177,13 +184,13 @@ export async function createTemplateFromBlueprint({ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, diff --git a/apps/api/src/app/change/e2e/get-changes.e2e.ts b/apps/api/src/app/change/e2e/get-changes.e2e.ts index 5ac9ea173fa..7406192ba12 100644 --- a/apps/api/src/app/change/e2e/get-changes.e2e.ts +++ b/apps/api/src/app/change/e2e/get-changes.e2e.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; import { ChangeRepository } from '@novu/dal'; -import { EmailBlockTypeEnum, StepTypeEnum, FilterPartTypeEnum } from '@novu/shared'; +import { + EmailBlockTypeEnum, + StepTypeEnum, + FilterPartTypeEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, +} from '@novu/shared'; import { UserSession } from '@novu/testing'; import { CreateWorkflowRequestDto, UpdateWorkflowRequestDto } from '../../workflows/dto'; @@ -32,13 +38,13 @@ describe('Get changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, diff --git a/apps/api/src/app/change/e2e/promote-changes.e2e.ts b/apps/api/src/app/change/e2e/promote-changes.e2e.ts index 05ac9b983ca..0c7f5b029b8 100644 --- a/apps/api/src/app/change/e2e/promote-changes.e2e.ts +++ b/apps/api/src/app/change/e2e/promote-changes.e2e.ts @@ -13,6 +13,8 @@ import { ChangeEntityTypeEnum, ChannelCTATypeEnum, EmailBlockTypeEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, StepTypeEnum, FilterPartTypeEnum, TemplateVariableTypeEnum, @@ -69,13 +71,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -96,7 +98,7 @@ describe('Promote changes', () => { _parentId: notificationTemplateId, }); - expect(prodVersion._notificationGroupId).to.eq(prodGroup._id); + expect(prodVersion?._notificationGroupId).to.eq(prodGroup._id); }); it('should promote step variables default values', async () => { @@ -204,13 +206,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -242,7 +244,7 @@ describe('Promote changes', () => { _parentId: notificationTemplateId, } as any); - expect(prodVersion.steps.length).to.eq(0); + expect(prodVersion?.steps.length).to.eq(0); }); it('update active flag on notification template', async () => { @@ -274,7 +276,7 @@ describe('Promote changes', () => { _parentId: notificationTemplateId, }); - expect(prodVersion.active).to.eq(true); + expect(prodVersion?.active).to.eq(true); }); it('update existing message', async () => { @@ -295,13 +297,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -352,7 +354,7 @@ describe('Promote changes', () => { _parentId: step._templateId, }); - expect(prodVersion.name).to.eq('test'); + expect(prodVersion?.name).to.eq('test'); }); it('add one more message', async () => { @@ -373,13 +375,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -431,13 +433,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'secondName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -479,13 +481,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -521,13 +523,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -632,13 +634,13 @@ describe('Promote changes', () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -659,7 +661,7 @@ describe('Promote changes', () => { _parentId: notificationTemplateId, }); - expect(prodVersion.isBlueprint).to.equal(true); + expect(prodVersion?.isBlueprint).to.equal(true); }); it('should merge creation, and status changes to one change', async () => { @@ -724,9 +726,15 @@ describe('Promote changes', () => { }); }); - async function getProductionEnvironment() { - return await environmentRepository.findOne({ + async function getProductionEnvironment(): Promise { + const production = await environmentRepository.findOne({ _parentId: session.environment._id, }); + + if (!production) { + throw new Error('No production environment'); + } + + return production; } }); diff --git a/apps/api/src/app/environments/environments.controller.ts b/apps/api/src/app/environments/environments.controller.ts index 12014f8a6a8..8c77c127549 100644 --- a/apps/api/src/app/environments/environments.controller.ts +++ b/apps/api/src/app/environments/environments.controller.ts @@ -20,7 +20,7 @@ import { GetEnvironment, GetEnvironmentCommand } from './usecases/get-environmen import { GetMyEnvironments } from './usecases/get-my-environments/get-my-environments.usecase'; import { GetMyEnvironmentsCommand } from './usecases/get-my-environments/get-my-environments.command'; import { JwtAuthGuard } from '../auth/framework/auth.guard'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiExcludeEndpoint, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiKey } from '../shared/dtos/api-key'; import { EnvironmentResponseDto } from './dtos/environment-response.dto'; import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; @@ -99,6 +99,7 @@ export class EnvironmentsController { @ApiOperation({ summary: 'Update env by id', }) + @ApiExcludeEndpoint() @ApiResponse(EnvironmentResponseDto) async updateMyEnvironment( @UserSession() user: IJwtPayload, diff --git a/apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts b/apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts index 4eb49c31c3c..1ea631ac5db 100644 --- a/apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts +++ b/apps/api/src/app/environments/usecases/create-environment/create-environment.e2e.ts @@ -22,7 +22,7 @@ describe('Create Environment - /environments (POST)', async () => { expect(body.data.name).to.eq(demoEnvironment.name); expect(body.data._organizationId).to.eq(session.organization._id); expect(body.data.identifier).to.be.ok; - const dbApp = await environmentRepository.findById(body.data._id); + const dbApp = await environmentRepository.findOne({ _id: body.data._id }); expect(dbApp.apiKeys.length).to.equal(1); expect(dbApp.apiKeys[0].key).to.be.ok; diff --git a/apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts b/apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts index 8e5a1d3f377..d7a59bb1ed8 100644 --- a/apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts +++ b/apps/api/src/app/environments/usecases/regenerate-api-keys/regenerate-api-keys.usecase.ts @@ -12,7 +12,7 @@ export class RegenerateApiKeys { ) {} async execute(command: GetApiKeysCommand): Promise { - const environment = await this.environmentRepository.findById(command.environmentId); + const environment = await this.environmentRepository.findOne({ _id: command.environmentId }); if (!environment) { throw new ApiException(`Environment id: ${command.environmentId} not found`); diff --git a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts index 4488d4fd22a..4c31e8e8e35 100644 --- a/apps/api/src/app/events/dtos/trigger-event-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-request.dto.ts @@ -112,6 +112,9 @@ export class TriggerEventRequestDto { ], }) @IsOptional() + @ValidateIf((_, value) => typeof value !== 'string') + @ValidateNested() + @Type(() => SubscriberPayloadDto) actor?: TriggerRecipientSubscriber; @ApiProperty({ diff --git a/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts b/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts index ab518c77753..1cae7579bcb 100644 --- a/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts +++ b/apps/api/src/app/events/dtos/trigger-event-to-all-request.dto.ts @@ -58,6 +58,9 @@ export class TriggerEventToAllRequestDto { ], }) @IsOptional() + @ValidateIf((_, value) => typeof value !== 'string') + @ValidateNested() + @Type(() => SubscriberPayloadDto) actor?: TriggerRecipientSubscriber; @ApiProperty({ diff --git a/apps/api/src/app/events/e2e/delay-events.e2e.ts b/apps/api/src/app/events/e2e/delay-events.e2e.ts index 2bf1632d614..bca7d81b517 100644 --- a/apps/api/src/app/events/e2e/delay-events.e2e.ts +++ b/apps/api/src/app/events/e2e/delay-events.e2e.ts @@ -24,13 +24,13 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f let standardQueueService: StandardQueueService; const messageRepository = new MessageRepository(); - const triggerEvent = async (payload, transactionId?: string, overrides = {}) => { + const triggerEvent = async (payload, transactionId?: string, overrides = {}, to = [subscriber.subscriberId]) => { await axiosInstance.post( `${session.serverUrl}/v1/events/trigger`, { transactionId, name: template.triggers[0].identifier, - to: [subscriber.subscriberId], + to, payload, overrides, }, @@ -63,7 +63,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 0.1, + amount: 2, type: DelayTypeEnum.REGULAR, }, }, @@ -94,7 +94,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f const subExpire30Days = subDays(expireAt, 30); const diff = differenceInMilliseconds(subExpire30Days, createdAt); - expect(diff).to.approximately(100, 200); + expect(diff).to.approximately(200, 2000); const messages = await messageRepository.find({ _environmentId: session.environment._id, @@ -141,7 +141,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f customVar: 'Testing of User Name', }, id, - { delay: { amount: 1, unit: DigestUnitEnum.SECONDS } } + { delay: { amount: 2, unit: DigestUnitEnum.SECONDS } } ); await session.awaitRunningJobs(template?._id, true, 0); const messages = await messageRepository.find({ @@ -211,7 +211,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 0.1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -226,13 +226,11 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f eventNumber: '1', }); - await session.awaitRunningJobs(template?._id, true, 1); - await triggerEvent({ eventNumber: '2', }); - await session.awaitRunningJobs(template?._id, true, 1); + await session.awaitRunningJobs(template?._id, true, 0); const messages = await messageRepository.find({ _environmentId: session.environment._id, @@ -240,8 +238,8 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f channel: StepTypeEnum.SMS, }); - expect(messages[0].content).to.include('Event 1'); - expect(messages[0].content).to.include('Digested Events 1'); + expect(messages[0].content).to.include('Event '); + expect(messages[0].content).to.include('Digested Events 2'); }); it('should send a single message for same exact scheduled delay', async function () { @@ -260,7 +258,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -332,6 +330,8 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f }); it('should be able to cancel delay', async function () { + const secondSubscriber = await subscriberService.createSubscriber(); + const id = MessageRepository.createObjectId(); template = await session.createTemplate({ steps: [ @@ -344,7 +344,7 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 0.1, + amount: 5, type: DelayTypeEnum.REGULAR, }, }, @@ -359,17 +359,19 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f { customVar: 'Testing of User Name', }, - id + id, + {}, + [subscriber.subscriberId, secondSubscriber.subscriberId] ); - await session.awaitRunningJobs(template?._id, true, 1); + await session.awaitRunningJobs(template?._id, true, 2); await axiosInstance.delete(`${session.serverUrl}/v1/events/trigger/${id}`, { headers: { authorization: `ApiKey ${session.apiKey}`, }, }); - let delayedJob = await jobRepository.findOne({ + let delayedJobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, type: StepTypeEnum.DELAY, @@ -382,14 +384,15 @@ describe('Trigger event - Delay triggered events - /v1/events/trigger (POST)', f transactionId: id, }); - expect(pendingJobs).to.equal(1); + expect(pendingJobs).to.equal(2); - delayedJob = await jobRepository.findOne({ + delayedJobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, type: StepTypeEnum.DELAY, transactionId: id, }); - expect(delayedJob!.status).to.equal(JobStatusEnum.CANCELED); + expect(delayedJobs[0]!.status).to.equal(JobStatusEnum.CANCELED); + expect(delayedJobs[1]!.status).to.equal(JobStatusEnum.CANCELED); }); }); diff --git a/apps/api/src/app/events/e2e/digest-events.e2e.ts b/apps/api/src/app/events/e2e/digest-events.e2e.ts index 7dd6f3bd4c3..617c0f6424f 100644 --- a/apps/api/src/app/events/e2e/digest-events.e2e.ts +++ b/apps/api/src/app/events/e2e/digest-events.e2e.ts @@ -153,7 +153,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -168,36 +168,35 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', customVar: 'Testing of User Name', }); - await session.awaitRunningJobs(template?._id, false, 2); - await triggerEvent({ customVar: 'digest', }); - await session.awaitRunningJobs(template?._id, false, 2); + await session.awaitRunningJobs(template?._id, false, 0); const jobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, + _subscriberId: subscriber._id, type: StepTypeEnum.DIGEST, }); expect(jobs.length).to.eql(2); - const delayedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.DELAYED); - expect(delayedJobs.length).to.eql(1); - const mergedJobs = jobs.filter((elem) => elem.status !== JobStatusEnum.DELAYED); - expect(mergedJobs.length).to.eql(1); - - await session.awaitRunningJobs(template?._id, false, 0); + const completedJob = jobs.find((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedJob).to.ok; + const mergedJob = jobs.find((elem) => elem.status === JobStatusEnum.MERGED); + expect(mergedJob).to.ok; - const message = await messageRepository.find({ + const message = await messageRepository.findOne({ _environmentId: session.environment._id, _subscriberId: subscriber._id, channel: StepTypeEnum.SMS, + _notificationId: completedJob?._notificationId, + _templateId: template._id, }); - expect(message[0].content).to.include('HAS_DIGEST_PROP'); + expect(message?.content).to.include('HAS_DIGEST_PROP'); }); it('should digest based on digestKey within time interval', async function () { @@ -213,7 +212,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, digestKey: 'id', type: DigestTypeEnum.REGULAR, }, @@ -280,7 +279,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, digestKey: 'id', type: DigestTypeEnum.REGULAR, }, @@ -307,7 +306,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', id: secondDigestKey, }); - await session.awaitRunningJobs(template?._id, false, 3); + await session.awaitRunningJobs(template?._id, false, 0); const jobs = await jobRepository.find({ _environmentId: session.environment._id, @@ -317,17 +316,19 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', expect(jobs.length).to.equal(3); - const delayedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.DELAYED); - expect(delayedJobs.length).to.eql(2); - const mergedJobs = jobs.filter((elem) => elem.status !== JobStatusEnum.DELAYED); + const completedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedJobs.length).to.eql(2); + const mergedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.MERGED); expect(mergedJobs.length).to.eql(1); - await session.awaitRunningJobs(template?._id, false, 0); - const messages = await messageRepository.find({ _environmentId: session.environment._id, _subscriberId: subscriber._id, channel: StepTypeEnum.SMS, + _templateId: template._id, + _notificationId: { + $in: completedJobs.map((job) => job._notificationId), + }, }); const firstDigestKeyBatch = messages.filter((message) => (message.content as string).includes('Hello world 2')); @@ -351,7 +352,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -392,7 +393,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, digestKey: 'id', type: DigestTypeEnum.REGULAR, }, @@ -454,9 +455,8 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, - type: DigestTypeEnum.REGULAR, - backoff: true, + amount: 5, + type: DigestTypeEnum.BACKOFF, backoffUnit: DigestUnitEnum.SECONDS, backoffAmount: 1, }, @@ -468,42 +468,53 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', ], }); - await triggerEvent({ - customVar: 'Testing of User Name', - }); - - await session.awaitRunningJobs(template?._id, false, 0); + const events = [ + { customVar: 'Testing of User Name' }, + { customVar: 'digest' }, + { customVar: 'merged' }, + { customVar: 'digest' }, + { customVar: 'merged' }, + { customVar: 'digest' }, + { customVar: 'merged' }, + ]; - await triggerEvent({ - customVar: 'digest', - }); + await Promise.all(events.map((event) => triggerEvent(event))); - await session.awaitRunningJobs(template?._id, false, 1); + await session.awaitRunningJobs(template?._id, false, 0); - const delayedJobs = await jobRepository.find({ + const jobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, + _subscriberId: subscriber._id, type: StepTypeEnum.DIGEST, }); - expect(delayedJobs.length).to.eql(1); + expect(jobs.length).to.eql(7); - const pendingJobs = await jobRepository.find({ + const completedJob = jobs.find((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedJob).to.ok; + const skippedJob = jobs.find((elem) => elem.status === JobStatusEnum.SKIPPED); + expect(skippedJob).to.ok; + const mergedJob = jobs.find((elem) => elem.status === JobStatusEnum.MERGED); + expect(mergedJob).to.ok; + + const generatedMessageJob = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, - status: { - $nin: [JobStatusEnum.COMPLETED, JobStatusEnum.DELAYED, JobStatusEnum.CANCELED], - }, + _subscriberId: subscriber._id, + type: StepTypeEnum.IN_APP, }); - expect(pendingJobs.length).to.equal(1); - const pendingJob = pendingJobs[0]; + expect(generatedMessageJob.length).to.equal(7); - await session.awaitRunningJobs(template?._id, false, 0); - const job = await jobRepository.findById(pendingJob._id); + const mergedInApp = generatedMessageJob.filter((elem) => elem.status === JobStatusEnum.MERGED); + expect(mergedInApp.length).to.equal(5); + + const completedInApp = generatedMessageJob.filter((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedInApp.length).to.equal(2); - expect(job?.digest?.events?.length).to.equal(1); - expect(job?.digest?.events?.[0].customVar).to.equal('digest'); + expect(completedInApp.find((i) => i.digest?.events?.length === 6)).to.be.ok; + expect(completedInApp.find((i) => i.digest?.events?.length === 0)).to.be.ok; }); it('should create multiple digest based on different digestKeys', async function () { @@ -517,7 +528,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, digestKey: 'postId', type: DigestTypeEnum.REGULAR, }, @@ -548,7 +559,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', postId, }); - await session.awaitRunningJobs(template?._id, false, 5); + await session.awaitRunningJobs(template?._id, false, 0); const digests = await jobRepository.find({ _environmentId: session.environment._id, @@ -601,7 +612,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, digestKey: 'nested.postId', type: DigestTypeEnum.REGULAR, }, @@ -678,7 +689,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', expect(jobCount).to.equal(15); }); - it.skip('should create multiple digest based on different digestKeys with backoff', async function () { + it('should create multiple digest based on different digestKeys with backoff', async function () { const postId = MessageRepository.createObjectId(); const postId2 = MessageRepository.createObjectId(); @@ -688,11 +699,10 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', type: StepTypeEnum.DIGEST, content: '', metadata: { - unit: DigestUnitEnum.MINUTES, - amount: 5, + unit: DigestUnitEnum.SECONDS, + amount: 2, digestKey: 'postId', - type: DigestTypeEnum.REGULAR, - backoff: true, + type: DigestTypeEnum.BACKOFF, backoffUnit: DigestUnitEnum.MINUTES, backoffAmount: 5, }, @@ -704,33 +714,15 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', ], }); - await triggerEvent({ - customVar: 'first', - postId, - }); - - await triggerEvent({ - customVar: 'second', - postId, - }); - - await triggerEvent({ - customVar: 'third', - }); - - await triggerEvent({ - customVar: 'fourth', - postId: postId2, - }); - - await triggerEvent({ - customVar: 'fifth', - postId: postId2, - }); + await Promise.all([ + triggerEvent({ customVar: 'first', postId }), + triggerEvent({ customVar: 'second' }), + triggerEvent({ customVar: 'third', postId: postId2 }), + triggerEvent({ customVar: 'fourth', postId }), + triggerEvent({ customVar: 'fifth', postId: postId2 }), + triggerEvent({ customVar: 'sixth' }), + ]); - await triggerEvent({ - customVar: 'sixth', - }); await session.awaitRunningJobs(template?._id, false, 0); const digests = await jobRepository.find({ @@ -739,9 +731,22 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', type: StepTypeEnum.DIGEST, }); - expect(digests.length).to.equal(3); - expect(digests[0]?.payload.postId).not.to.equal(digests[1]?.payload.postId); - expect(digests[2]?.payload.postId).to.equal(undefined); + expect(digests.length).to.equal(6); + + const completedJobs = digests.filter((job) => job.status === JobStatusEnum.COMPLETED); + expect(completedJobs.length).to.equal(3); + + const skippedJobs = digests.filter((job) => job.status === JobStatusEnum.SKIPPED); + expect(skippedJobs.length).to.equal(3); + + const postId1Jobs = digests.filter((job) => job.payload.postId === postId); + expect(postId1Jobs.length).to.equal(2); + + const postId2Jobs = digests.filter((job) => job.payload.postId === postId2); + expect(postId2Jobs.length).to.equal(2); + + const noPostIdJobs = digests.filter((job) => !job.payload.postId); + expect(noPostIdJobs.length).to.equal(2); await session.awaitRunningJobs(template?._id, false, 0); @@ -771,14 +776,10 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', _environmentId: session.environment._id, _templateId: template._id, }); - const allJobsBackoff = await jobRepository.find({ - _environmentId: session.environment._id, - _templateId: template._id, - }); - expect(jobCount).to.equal(15); + expect(jobCount).to.equal(18); }); - it.skip('should create multiple digests based on different nested digestKeys with backoff', async function () { + it('should create multiple digests based on different nested digestKeys with backoff', async function () { const postId = MessageRepository.createObjectId(); const postId2 = MessageRepository.createObjectId(); @@ -788,11 +789,10 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', type: StepTypeEnum.DIGEST, content: '', metadata: { - unit: DigestUnitEnum.MINUTES, - amount: 5, + unit: DigestUnitEnum.SECONDS, + amount: 2, digestKey: 'nested.postId', - type: DigestTypeEnum.REGULAR, - backoff: true, + type: DigestTypeEnum.BACKOFF, backoffUnit: DigestUnitEnum.MINUTES, backoffAmount: 5, }, @@ -808,37 +808,31 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', customVar: 'first', nested: { postId: postId }, }); - await session.awaitParsingEvents(); await triggerEvent({ customVar: 'second', nested: { postId: postId }, }); - await session.awaitParsingEvents(); await triggerEvent({ customVar: 'third', }); - await session.awaitParsingEvents(); await triggerEvent({ customVar: 'fourth', nested: { postId: postId2 }, }); - await session.awaitParsingEvents(); await triggerEvent({ customVar: 'fifth', nested: { postId: postId2 }, }); - await session.awaitParsingEvents(); await triggerEvent({ customVar: 'sixth', }); - await session.awaitParsingEvents(); - await session.awaitRunningJobs(template?._id, false, 6); + await session.awaitRunningJobs(template?._id, false, 0); const digests = await jobRepository.find({ _environmentId: session.environment._id, @@ -846,10 +840,22 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', type: StepTypeEnum.DIGEST, }); - expect(digests.length).to.equal(2); - expect(digests[0].payload?.nested?.postId).not.to.equal(digests[1].payload?.nested?.postId); + expect(digests.length).to.equal(6); - await session.awaitRunningJobs(template?._id, false, 0); + const completedJobs = digests.filter((job) => job.status === JobStatusEnum.COMPLETED); + expect(completedJobs.length).to.equal(3); + + const skippedJobs = digests.filter((job) => job.status === JobStatusEnum.SKIPPED); + expect(skippedJobs.length).to.equal(3); + + const postId1Jobs = digests.filter((job) => job.payload?.nested?.postId === postId); + expect(postId1Jobs.length).to.equal(2); + + const postId2Jobs = digests.filter((job) => job.payload?.nested?.postId === postId2); + expect(postId2Jobs.length).to.equal(2); + + const noPostIdJobs = digests.filter((job) => !job.payload?.nested?.postId); + expect(noPostIdJobs.length).to.equal(2); const messages = await messageRepository.find({ _environmentId: session.environment._id, @@ -862,7 +868,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', _environmentId: session.environment._id, _templateId: template._id, }); - expect(jobCount).to.equal(14); + expect(jobCount).to.equal(18); }); it('should add a digest prop to chat template compilation', async function () { @@ -873,7 +879,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -889,36 +895,37 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', customVar: 'Testing of User Name', }); - await session.awaitRunningJobs(template?._id, false, 1); - await triggerEvent({ customVar: 'digest', }); - await session.awaitRunningJobs(template?._id, false, 2); + await session.awaitRunningJobs(template?._id, false, 0); const jobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, + _subscriberId: subscriber._id, type: StepTypeEnum.DIGEST, }); expect(jobs.length).to.eql(2); - const delayedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.DELAYED); - expect(delayedJobs.length).to.eql(1); - const mergedJobs = jobs.filter((elem) => elem.status !== JobStatusEnum.DELAYED); - expect(mergedJobs.length).to.eql(1); + const completedJob = jobs.find((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedJob).to.ok; + const mergedJob = jobs.find((elem) => elem.status === JobStatusEnum.MERGED); + expect(mergedJob).to.ok; await session.awaitRunningJobs(template?._id, false, 0); - const message = await messageRepository.find({ + const message = await messageRepository.findOne({ _environmentId: session.environment._id, _subscriberId: subscriber._id, channel: StepTypeEnum.IN_APP, + _templateId: template._id, + _notificationId: completedJob?._notificationId, }); - expect(message[0].content).to.include('HAS_DIGEST_PROP'); - expect(message[0].content).to.include('Total events in digest:2'); + expect(message?.content).to.include('HAS_DIGEST_PROP'); + expect(message?.content).to.include('Total events in digest:2'); }); it('should add a digest prop to push template compilation', async function () { @@ -929,7 +936,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -945,34 +952,35 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', customVar: 'Testing of User Name', }); - await session.awaitRunningJobs(template?._id, false, 1); - await triggerEvent({ customVar: 'digest', }); - await session.awaitRunningJobs(template?._id, false, 2); + await session.awaitRunningJobs(template?._id, false, 0); const jobs = await jobRepository.find({ _environmentId: session.environment._id, _templateId: template._id, + _subscriberId: subscriber._id, type: StepTypeEnum.DIGEST, }); - const delayedJobs = jobs.filter((elem) => elem.status === JobStatusEnum.DELAYED); - expect(delayedJobs.length).to.eql(1); - const mergedJobs = jobs.filter((elem) => elem.status !== JobStatusEnum.DELAYED); - expect(mergedJobs.length).to.eql(1); + expect(jobs.length).to.eql(2); - await session.awaitRunningJobs(template?._id, false, 0); + const completedJob = jobs.find((elem) => elem.status === JobStatusEnum.COMPLETED); + expect(completedJob).to.ok; + const mergedJob = jobs.find((elem) => elem.status === JobStatusEnum.MERGED); + expect(mergedJob).to.ok; - const message = await messageRepository.find({ + const message = await messageRepository.findOne({ _environmentId: session.environment._id, _subscriberId: subscriber._id, channel: StepTypeEnum.PUSH, + _templateId: template._id, + _notificationId: completedJob?._notificationId, }); - expect(message[0].content).to.include('HAS_DIGEST_PROP'); + expect(message?.content).to.include('HAS_DIGEST_PROP'); }); it('should merge digest events accordingly when concurrent calls', async () => { @@ -983,7 +991,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, @@ -1047,7 +1055,10 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', let delayed = delayedJobs[0]; do { - delayed = (await jobRepository.findById(delayedJobs[0]._id)) as JobEntity; + delayed = (await jobRepository.findOne({ + _id: delayedJobs[0]._id, + _environmentId: session.environment._id, + })) as JobEntity; delayedJobUpdateTime = delayed.updatedAt; await promiseTimeout(100); } while (delayed.status !== JobStatusEnum.COMPLETED); @@ -1069,7 +1080,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DigestTypeEnum.REGULAR, }, }, diff --git a/apps/api/src/app/events/e2e/send-message-push.e2e.ts b/apps/api/src/app/events/e2e/send-message-push.e2e.ts new file mode 100644 index 00000000000..18e5b0869b7 --- /dev/null +++ b/apps/api/src/app/events/e2e/send-message-push.e2e.ts @@ -0,0 +1,210 @@ +import axios from 'axios'; +import { expect } from 'chai'; +import { + ExecutionDetailsRepository, + IntegrationRepository, + MessageRepository, + NotificationTemplateEntity, +} from '@novu/dal'; +import { DetailEnum } from '@novu/application-generic'; +import { ChannelTypeEnum, PushProviderIdEnum, StepTypeEnum } from '@novu/shared'; +import { UserSession } from '@novu/testing'; + +const axiosInstance = axios.create(); + +const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; + +describe('Trigger event - Send Push Notification - /v1/events/trigger (POST)', () => { + let session: UserSession; + let template: NotificationTemplateEntity; + + const executionDetailsRepository = new ExecutionDetailsRepository(); + const integrationRepository = new IntegrationRepository(); + const messageRepository = new MessageRepository(); + + before(async () => { + process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; + + session = new UserSession(); + await session.initialize(); + + template = await session.createTemplate({ + steps: [ + { + active: true, + type: StepTypeEnum.PUSH, + title: 'Title', + content: 'Welcome to {{organizationName}}' as string, + }, + ], + }); + }); + + after(() => { + process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; + }); + + describe('Multiple providers active', () => { + before(async () => { + const payload = { + providerId: PushProviderIdEnum.EXPO, + channel: ChannelTypeEnum.PUSH, + credentials: { apiKey: '123' }, + _environmentId: session.environment._id, + active: true, + check: false, + }; + + await session.testAgent.post('/v1/integrations').send(payload); + + const integrations = await integrationRepository.find({ + _environmentId: session.environment._id, + channel: ChannelTypeEnum.PUSH, + active: true, + }); + + expect(integrations.length).to.equal(2); + }); + + afterEach(async () => { + await executionDetailsRepository.delete({ _environmentId: session.environment._id }); + }); + + it('should not create any message if subscriber has no configured channel', async () => { + await triggerEvent(session, template); + + await session.awaitRunningJobs(template._id); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _templateId: template._id, + _subscriberId: session.subscriberId, + }); + + expect(messages.length).to.equal(0); + + const executionDetails = await executionDetailsRepository.find({ + _environmentId: session.environment._id, + }); + + expect(executionDetails.length).to.equal(5); + const noActiveChannel = executionDetails.find((ex) => ex.detail === DetailEnum.SUBSCRIBER_NO_ACTIVE_CHANNEL); + expect(noActiveChannel).to.be.ok; + expect(noActiveChannel?.providerId).to.equal('fcm'); + const genericError = executionDetails.find((ex) => ex.detail === DetailEnum.NOTIFICATION_ERROR); + expect(genericError).to.be.ok; + }); + + it('should not create any message if subscriber has configured two providers without device tokens', async () => { + await updateCredentials(session, session.subscriberId, PushProviderIdEnum.FCM, []); + await updateCredentials(session, session.subscriberId, PushProviderIdEnum.EXPO, []); + + await triggerEvent(session, template); + + await session.awaitRunningJobs(template._id); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _templateId: template._id, + _subscriberId: session.subscriberId, + }); + + expect(messages.length).to.equal(0); + + const executionDetails = await executionDetailsRepository.find({ + _environmentId: session.environment._id, + }); + + expect(executionDetails.length).to.equal(6); + const fcm = executionDetails.find( + (ex) => ex.detail === DetailEnum.PUSH_MISSING_DEVICE_TOKENS && ex.providerId === PushProviderIdEnum.FCM + ); + expect(fcm).to.be.ok; + const expo = executionDetails.find( + (ex) => ex.detail === DetailEnum.PUSH_MISSING_DEVICE_TOKENS && ex.providerId === PushProviderIdEnum.EXPO + ); + expect(expo).to.be.ok; + const genericError = executionDetails.find((ex) => ex.detail === DetailEnum.NOTIFICATION_ERROR); + expect(genericError).to.be.ok; + }); + + it('should not create any message if subscriber has configured one provider without device tokens and the other has invalid device token', async () => { + await updateCredentials(session, session.subscriberId, PushProviderIdEnum.FCM, ['invalidDeviceToken']); + await updateCredentials(session, session.subscriberId, PushProviderIdEnum.EXPO, []); + + await triggerEvent(session, template); + + await session.awaitRunningJobs(template._id); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _templateId: template._id, + _subscriberId: session.subscriberId, + }); + + expect(messages.length).to.equal(0); + + const executionDetails = await executionDetailsRepository.find({ + _environmentId: session.environment._id, + }); + + expect(executionDetails.length).to.equal(8); + const fcmMessageCreated = executionDetails.find( + (ex) => + ex.detail === `${DetailEnum.MESSAGE_CREATED}: ${PushProviderIdEnum.FCM}` && + ex.providerId === PushProviderIdEnum.FCM + ); + expect(fcmMessageCreated).to.be.ok; + const fcmProviderError = executionDetails.find( + (ex) => ex.detail === DetailEnum.PROVIDER_ERROR && ex.providerId === PushProviderIdEnum.FCM + ); + expect(fcmProviderError).to.be.ok; + + const expo = executionDetails.find( + (ex) => ex.detail === DetailEnum.PUSH_MISSING_DEVICE_TOKENS && ex.providerId === PushProviderIdEnum.EXPO + ); + expect(expo).to.be.ok; + const genericError = executionDetails.find((ex) => ex.detail === DetailEnum.NOTIFICATION_ERROR); + expect(genericError).to.be.ok; + }); + }); +}); + +async function triggerEvent(session: UserSession, template: NotificationTemplateEntity) { + await axiosInstance.post( + `${session.serverUrl}/v1/events/trigger`, + { + name: template.triggers[0].identifier, + to: [{ subscriberId: session.subscriberId }], + payload: {}, + }, + { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, + } + ); +} + +async function updateCredentials( + session: UserSession, + subscriberId: string, + providerId: PushProviderIdEnum, + deviceTokens: string[] +) { + await axiosInstance.put( + `${session.serverUrl}/v1/subscribers/${subscriberId}/credentials`, + { + subscriberId, + providerId, + credentials: { + deviceTokens, + }, + }, + { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, + } + ); +} 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 37da8aaee5c..66474841a14 100644 --- a/apps/api/src/app/events/e2e/trigger-event.e2e.ts +++ b/apps/api/src/app/events/e2e/trigger-event.e2e.ts @@ -19,19 +19,23 @@ import { UserSession, SubscribersService } from '@novu/testing'; import { ChannelTypeEnum, EmailBlockTypeEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, + FilterPartTypeEnum, StepTypeEnum, IEmailBlock, ISubscribersDefine, TemplateVariableTypeEnum, EmailProviderIdEnum, SmsProviderIdEnum, - FilterPartTypeEnum, DigestUnitEnum, DelayTypeEnum, PreviousStepTypeEnum, InAppProviderIdEnum, MESSAGE_IN_APP_RETENTION_DAYS, MESSAGE_GENERIC_RETENTION_DAYS, + ActorTypeEnum, + SystemAvatarIconEnum, } from '@novu/shared'; import { EmailEventStatusEnum } from '@novu/stateless'; import { createTenant } from '../../tenant/e2e/create-tenant.e2e'; @@ -41,8 +45,6 @@ const axiosInstance = axios.create(); const eventTriggerPath = '/v1/events/trigger'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - const promiseTimeout = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { @@ -60,6 +62,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { describe(`Trigger Event - ${eventTriggerPath} (POST)`, function () { beforeEach(async () => { + process.env.LAUNCH_DARKLY_SDK_KEY = ''; session = new UserSession(); await session.initialize(); template = await session.createTemplate(); @@ -83,18 +86,18 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, filters: [ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PAYLOAD, - operator: 'IS_DEFINED', + operator: FieldOperatorEnum.IS_DEFINED, field: 'exclude', value: '', }, @@ -163,18 +166,18 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, filters: [ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PAYLOAD, - operator: 'IS_DEFINED', + operator: FieldOperatorEnum.IS_DEFINED, field: 'exclude', value: '', }, @@ -243,18 +246,18 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, filters: [ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PAYLOAD, - operator: 'IS_DEFINED', + operator: FieldOperatorEnum.IS_DEFINED, field: 'exclude', value: '', }, @@ -324,18 +327,18 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, filters: [ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PAYLOAD, - operator: 'IS_DEFINED', + operator: FieldOperatorEnum.IS_DEFINED, field: 'exclude', value: '', }, @@ -397,7 +400,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { _environmentId: session.environment._id, conditions: [ { - children: [{ field: 'identifier', value: 'test', operator: 'EQUAL', on: 'tenant' }], + children: [{ field: 'identifier', value: 'test', operator: FieldOperatorEnum.EQUAL, on: 'tenant' }], }, ], active: true, @@ -436,10 +439,10 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { _environmentId: session.environment._id, conditions: [ { - value: 'OR', + value: FieldLogicalOperatorEnum.OR, children: [ - { field: 'identifier', value: 'test3', operator: 'EQUAL', on: 'tenant' }, - { field: 'identifier', value: 'test2', operator: 'EQUAL', on: 'tenant' }, + { field: 'identifier', value: 'test3', operator: FieldOperatorEnum.EQUAL, on: 'tenant' }, + { field: 'identifier', value: 'test2', operator: FieldOperatorEnum.EQUAL, on: 'tenant' }, ], }, ], @@ -496,7 +499,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { _environmentId: session.environment._id, conditions: [ { - children: [{ field: 'identifier', value: 'test1', operator: 'EQUAL', on: 'tenant' }], + children: [{ field: 'identifier', value: 'test1', operator: FieldOperatorEnum.EQUAL, on: 'tenant' }], }, ], active: true, @@ -1176,6 +1179,44 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { expect(message!.subject).to.equal('Test email a subject nested'); }); + it('should trigger E-Mail notification with actor data', async function () { + const newSubscriberId = SubscriberRepository.createObjectId(); + const channelType = ChannelTypeEnum.EMAIL; + const actorSubscriber = await subscriberService.createSubscriber({ firstName: 'Actor' }); + + template = await session.createTemplate({ + steps: [ + { + name: 'Message Name', + subject: 'Test email', + type: StepTypeEnum.EMAIL, + content: [ + { + type: EmailBlockTypeEnum.TEXT, + content: 'Hello {{actor.firstName}}, Welcome to {{organizationName}}' as string, + }, + ], + }, + ], + }); + + await sendTrigger(session, template, newSubscriberId, {}, {}, '', actorSubscriber.subscriberId); + + await session.awaitRunningJobs(template._id); + + const createdSubscriber = await subscriberRepository.findBySubscriberId(session.environment._id, newSubscriberId); + + const message = await messageRepository.findOne({ + _environmentId: session.environment._id, + _subscriberId: createdSubscriber?._id, + channel: channelType, + }); + + const block = message!.content[0] as IEmailBlock; + + expect(block.content).to.equal('Hello Actor, Welcome to Umbrella Corp'); + }); + it('should not trigger notification with subscriber data if integration is inactive', async function () { const newSubscriberIdInAppNotification = SubscriberRepository.createObjectId(); const channelType = ChannelTypeEnum.SMS; @@ -1663,13 +1704,13 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { field: 'run', value: 'true', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, on: FilterPartTypeEnum.PAYLOAD, }, ], @@ -1696,13 +1737,13 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { field: 'subscriberId', value: subscriber.subscriberId, - operator: 'NOT_EQUAL', + operator: FieldOperatorEnum.NOT_EQUAL, on: FilterPartTypeEnum.SUBSCRIBER, }, ], @@ -1761,12 +1802,12 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { field: 'isOnline', value: 'true', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, on: FilterPartTypeEnum.WEBHOOK, webhookUrl: 'www.user.com/webhook', }, @@ -1863,12 +1904,12 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { field: 'isOnline', value: 'true', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, on: FilterPartTypeEnum.WEBHOOK, webhookUrl: 'www.user.com/webhook', }, @@ -1928,12 +1969,12 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { field: 'isOnline', value: 'true', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, on: FilterPartTypeEnum.WEBHOOK, webhookUrl: 'www.user.com/webhook', }, @@ -2022,6 +2063,148 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { // axiosPostStub.restore(); }); + describe('in-app avatar', () => { + it('should send the message with choosed system avatar', async () => { + const firstStepUuid = uuid(); + template = await session.createTemplate({ + steps: [ + { + type: StepTypeEnum.IN_APP, + content: 'Hello world!', + uuid: firstStepUuid, + actor: { + type: ActorTypeEnum.SYSTEM_ICON, + data: SystemAvatarIconEnum.WARNING, + }, + }, + ], + }); + + await axiosInstance.post( + `${session.serverUrl}${eventTriggerPath}`, + { + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: {}, + }, + { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, + } + ); + + await session.awaitRunningJobs(template?._id, true, 1); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _subscriberId: subscriber._id, + channel: StepTypeEnum.IN_APP, + }); + + expect(messages.length).to.equal(1); + expect(messages[0].actor).to.be.ok; + expect(messages[0].actor?.type).to.eq(ActorTypeEnum.SYSTEM_ICON); + expect(messages[0].actor?.data).to.eq(SystemAvatarIconEnum.WARNING); + }); + + it('should send the message with custom system avatar url', async () => { + const firstStepUuid = uuid(); + const avatarUrl = 'https://gravatar.com/avatar/5246ec47a6a90ef2bcd29f0ef7d2faa6?s=400&d=robohash&r=x'; + + template = await session.createTemplate({ + steps: [ + { + type: StepTypeEnum.IN_APP, + content: 'Hello world!', + uuid: firstStepUuid, + actor: { + type: ActorTypeEnum.SYSTEM_CUSTOM, + data: avatarUrl, + }, + }, + ], + }); + + await axiosInstance.post( + `${session.serverUrl}${eventTriggerPath}`, + { + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: {}, + }, + { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, + } + ); + + await session.awaitRunningJobs(template?._id, true, 1); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _subscriberId: subscriber._id, + channel: StepTypeEnum.IN_APP, + }); + + expect(messages.length).to.equal(1); + expect(messages[0].actor).to.be.ok; + expect(messages[0].actor?.type).to.eq(ActorTypeEnum.SYSTEM_CUSTOM); + expect(messages[0].actor?.data).to.eq(avatarUrl); + }); + + it('should send the message with the actor avatar', async () => { + const firstStepUuid = uuid(); + const avatarUrl = 'https://gravatar.com/avatar/5246ec47a6a90ef2bcd29f0ef7d2faa6?s=400&d=robohash&r=x'; + + const actor = await subscriberService.createSubscriber({ avatar: avatarUrl }); + + template = await session.createTemplate({ + steps: [ + { + type: StepTypeEnum.IN_APP, + content: 'Hello world!', + uuid: firstStepUuid, + actor: { + type: ActorTypeEnum.USER, + data: null, + }, + }, + ], + }); + + await axiosInstance.post( + `${session.serverUrl}${eventTriggerPath}`, + { + name: template.triggers[0].identifier, + to: [subscriber.subscriberId], + payload: {}, + actor: actor.subscriberId, + }, + { + headers: { + authorization: `ApiKey ${session.apiKey}`, + }, + } + ); + + await session.awaitRunningJobs(template?._id, true, 1); + + const messages = await messageRepository.find({ + _environmentId: session.environment._id, + _subscriberId: subscriber._id, + channel: StepTypeEnum.IN_APP, + }); + + expect(messages.length).to.equal(1); + expect(messages[0].actor).to.be.ok; + expect(messages[0].actor?.type).to.eq(ActorTypeEnum.USER); + expect(messages[0].actor?.data).to.eq(null); + expect(messages[0]._actorId).to.eq(actor._id); + }); + }); + describe('seen/read filter', () => { it('should filter in app seen/read step', async function () { const firstStepUuid = uuid(); @@ -2037,7 +2220,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, }, @@ -2048,7 +2231,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PREVIOUS_STEP, @@ -2127,7 +2310,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { content: '', metadata: { unit: DigestUnitEnum.SECONDS, - amount: 1, + amount: 2, type: DelayTypeEnum.REGULAR, }, }, @@ -2140,7 +2323,7 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PREVIOUS_STEP, @@ -2207,18 +2390,6 @@ describe(`Trigger event - ${eventTriggerPath} (POST)`, function () { expect(messagesAfter.length).to.equal(1); }); }); - }); - describe.skip('Trigger Event - [IS_MULTI_PROVIDER_CONFIGURATION_ENABLED=true]', function () { - beforeEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - process.env.LAUNCH_DARKLY_SDK_KEY = ''; - session = new UserSession(); - await session.initialize(); - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - }); it('should trigger message with override integration identifier', async function () { const newSubscriberId = SubscriberRepository.createObjectId(); @@ -2298,7 +2469,8 @@ export async function sendTrigger( newSubscriberIdInAppNotification: string, payload: Record = {}, overrides: Record = {}, - tenant?: string + tenant?: string, + actor?: string ): Promise { return await axiosInstance.post( `${session.serverUrl}${eventTriggerPath}`, @@ -2312,6 +2484,7 @@ export async function sendTrigger( }, overrides, tenant, + actor, }, { headers: { diff --git a/apps/api/src/app/events/events.controller.ts b/apps/api/src/app/events/events.controller.ts index 47ddb117449..4389964b7dd 100644 --- a/apps/api/src/app/events/events.controller.ts +++ b/apps/api/src/app/events/events.controller.ts @@ -60,6 +60,7 @@ export class EventsController { @Body() body: TriggerEventRequestDto ): Promise { const mappedTenant = body.tenant ? this.mapTenant(body.tenant) : null; + const mappedActor = body.actor ? this.mapActor(body.actor) : null; const result = await this.parseEventRequest.execute( ParseEventRequestCommand.create({ @@ -70,7 +71,7 @@ export class EventsController { payload: body.payload || {}, overrides: body.overrides || {}, to: body.to, - actor: body.actor, + actor: mappedActor, tenant: mappedTenant, transactionId: body.transactionId, }) diff --git a/apps/api/src/app/events/events.module.ts b/apps/api/src/app/events/events.module.ts index c2661192243..2df63fe6d4f 100644 --- a/apps/api/src/app/events/events.module.ts +++ b/apps/api/src/app/events/events.module.ts @@ -4,7 +4,7 @@ import { TerminusModule } from '@nestjs/terminus'; import { AddJob, AddDelayJob, - AddDigestJob, + MergeOrCreateDigest, CreateExecutionDetails, CreateNotificationJobs, DigestFilterSteps, @@ -41,7 +41,7 @@ import { TenantModule } from '../tenant/tenant.module'; const PROVIDERS = [ AddJob, AddDelayJob, - AddDigestJob, + MergeOrCreateDigest, CreateExecutionDetails, CreateNotificationJobs, DigestFilterSteps, diff --git a/apps/api/src/app/events/usecases/cancel-delayed/cancel-delayed.usecase.ts b/apps/api/src/app/events/usecases/cancel-delayed/cancel-delayed.usecase.ts index 74da1a23231..954d652394a 100644 --- a/apps/api/src/app/events/usecases/cancel-delayed/cancel-delayed.usecase.ts +++ b/apps/api/src/app/events/usecases/cancel-delayed/cancel-delayed.usecase.ts @@ -7,20 +7,25 @@ export class CancelDelayed { constructor(private jobRepository: JobRepository) {} public async execute(command: CancelDelayedCommand): Promise { - const job = await this.jobRepository.findOne({ - _environmentId: command.environmentId, - transactionId: command.transactionId, - status: JobStatusEnum.DELAYED, - }); + const jobs = await this.jobRepository.find( + { + _environmentId: command.environmentId, + transactionId: command.transactionId, + status: JobStatusEnum.DELAYED, + }, + '_id' + ); - if (!job) { + if (!jobs?.length) { return false; } await this.jobRepository.update( { _environmentId: command.environmentId, - _id: job._id, + _id: { + $in: jobs.map((job) => job._id), + }, }, { $set: { diff --git a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts index 1ce91ea52e1..d3cbb4d2937 100644 --- a/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts +++ b/apps/api/src/app/events/usecases/parse-event-request/parse-event-request.command.ts @@ -22,6 +22,7 @@ export class ParseEventRequestCommand extends EnvironmentWithUserCommand { transactionId?: string; @IsOptional() + @ValidateNested() actor?: TriggerRecipientSubscriber | null; @IsOptional() diff --git a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts index 56751f3acd9..71746dcd424 100644 --- a/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts +++ b/apps/api/src/app/events/usecases/process-bulk-trigger/process-bulk-trigger.usecase.ts @@ -18,6 +18,7 @@ export class ProcessBulkTrigger { for (const event of command.events) { let result: TriggerEventResponseDto; const mappedTenant = event.tenant ? this.parseEventRequest.mapTenant(event.tenant) : null; + const mappedActor = event.actor ? this.mapTriggerRecipients.mapSubscriber(event.actor) : null; try { result = (await this.parseEventRequest.execute( @@ -29,7 +30,7 @@ export class ProcessBulkTrigger { payload: event.payload, overrides: event.overrides || {}, to: event.to, - actor: event.actor, + actor: mappedActor, tenant: mappedTenant, transactionId: event.transactionId, }) diff --git a/apps/api/src/app/integrations/dtos/credentials.dto.ts b/apps/api/src/app/integrations/dtos/credentials.dto.ts index 5701d25fbc2..eaed4cdc65f 100644 --- a/apps/api/src/app/integrations/dtos/credentials.dto.ts +++ b/apps/api/src/app/integrations/dtos/credentials.dto.ts @@ -136,4 +136,34 @@ export class CredentialsDto implements ICredentials { @IsString() @IsOptional() ipPoolName?: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + apiKeyRequestHeader?: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + secretKeyRequestHeader?: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + idPath?: string; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + datePath?: string; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + authenticateByToken?: boolean; + + @ApiPropertyOptional() + @IsString() + @IsOptional() + authenticationTokenKey?: string; } diff --git a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts index 2700017dc1e..6fb6813fffa 100644 --- a/apps/api/src/app/integrations/e2e/create-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/create-integration.e2e.ts @@ -4,14 +4,13 @@ import { ChannelTypeEnum, ChatProviderIdEnum, EmailProviderIdEnum, + FieldOperatorEnum, InAppProviderIdEnum, PushProviderIdEnum, SmsProviderIdEnum, } from '@novu/shared'; import { expect } from 'chai'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Create Integration - /integration (POST)', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); @@ -20,11 +19,6 @@ describe('Create Integration - /integration (POST)', function () { beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('should get the email integration successfully', async function () { @@ -113,7 +107,7 @@ describe('Create Integration - /integration (POST)', function () { check: false, conditions: [ { - children: [{ field: 'identifier', value: 'test', operator: 'EQUAL', on: 'tenant' }], + children: [{ field: 'identifier', value: 'test', operator: FieldOperatorEnum.EQUAL, on: 'tenant' }], }, ], }; diff --git a/apps/api/src/app/integrations/e2e/deactivate-integration.e2e.ts b/apps/api/src/app/integrations/e2e/deactivate-integration.e2e.ts index 26219fc9725..605ade881a8 100644 --- a/apps/api/src/app/integrations/e2e/deactivate-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/deactivate-integration.e2e.ts @@ -1,29 +1,17 @@ import { IntegrationRepository } from '@novu/dal'; import { UserSession } from '@novu/testing'; import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { GetIsMultiProviderConfigurationEnabled } from '@novu/application-generic'; describe('Deactivate Integration', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); - let stub: sinon.SinonStub; beforeEach(async () => { session = new UserSession(); - const service = session.testServer?.getService(GetIsMultiProviderConfigurationEnabled); - stub = sinon.stub(service, 'execute'); - stub.callsFake(() => { - return true; - }); await session.initialize(); }); - afterEach(() => { - stub.restore(); - }); - - it('should not deactivated old providers when feature flag is active', async function () { + it('should not deactivate old providers when a new provider is created', async function () { const payload = { providerId: 'mailgun', channel: 'email', diff --git a/apps/api/src/app/integrations/e2e/get-active-integration.e2e.ts b/apps/api/src/app/integrations/e2e/get-active-integration.e2e.ts index fd57cb55dd2..26c5ffafb56 100644 --- a/apps/api/src/app/integrations/e2e/get-active-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/get-active-integration.e2e.ts @@ -4,22 +4,16 @@ import { ChannelTypeEnum, EmailProviderIdEnum, SmsProviderIdEnum } from '@novu/s import { IntegrationService } from '@novu/testing'; import { IntegrationEntity } from '@novu/dal'; -describe('Get Active Integrations [IS_MULTI_PROVIDER_CONFIGURATION_ENABLED=true] - /integrations/active (GET)', function () { +describe('Get Active Integrations - Multi-Provider Configuration - /integrations/active (GET)', function () { let session: UserSession; const integrationService = new IntegrationService(); - const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; process.env.LAUNCH_DARKLY_SDK_KEY = ''; }); - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - }); - it('should get active integrations', async function () { await integrationService.createIntegration({ environmentId: session.environment._id, diff --git a/apps/api/src/app/integrations/e2e/get-decrypted-integrations.e2e.ts b/apps/api/src/app/integrations/e2e/get-decrypted-integrations.e2e.ts index 9810c383cd9..32aa7c4e916 100644 --- a/apps/api/src/app/integrations/e2e/get-decrypted-integrations.e2e.ts +++ b/apps/api/src/app/integrations/e2e/get-decrypted-integrations.e2e.ts @@ -3,8 +3,6 @@ import { expect } from 'chai'; import { ChannelTypeEnum, EmailProviderIdEnum } from '@novu/shared'; import { IntegrationRepository } from '@novu/dal'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Get Decrypted Integrations - /integrations (GET)', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); @@ -12,11 +10,6 @@ describe('Get Decrypted Integrations - /integrations (GET)', function () { beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('should get active decrypted integration', async function () { diff --git a/apps/api/src/app/integrations/e2e/get-integration.e2e.ts b/apps/api/src/app/integrations/e2e/get-integration.e2e.ts index 46786a848b1..a5594e9e24a 100644 --- a/apps/api/src/app/integrations/e2e/get-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/get-integration.e2e.ts @@ -3,19 +3,12 @@ import { expect } from 'chai'; import { ChannelTypeEnum, EmailProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; import { IntegrationEntity } from '@novu/dal'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Get Integrations - /integrations (GET)', function () { let session: UserSession; beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('should retrieve all the integrations of all environments from an organization from the prefilled test data', async () => { diff --git a/apps/api/src/app/integrations/e2e/remove-integration.e2e.ts b/apps/api/src/app/integrations/e2e/remove-integration.e2e.ts index 383df2a5fd5..797c5ee4e26 100644 --- a/apps/api/src/app/integrations/e2e/remove-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/remove-integration.e2e.ts @@ -10,8 +10,6 @@ import { } from '@novu/shared'; import { HttpStatus } from '@nestjs/common'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Delete Integration - /integration/:integrationId (DELETE)', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); @@ -19,11 +17,6 @@ describe('Delete Integration - /integration/:integrationId (DELETE)', function ( beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('should throw not found exception when integration is not found', async function () { diff --git a/apps/api/src/app/integrations/e2e/set-itegration-as-primary.e2e.ts b/apps/api/src/app/integrations/e2e/set-itegration-as-primary.e2e.ts index 1e32cde3ee0..4c2826347a6 100644 --- a/apps/api/src/app/integrations/e2e/set-itegration-as-primary.e2e.ts +++ b/apps/api/src/app/integrations/e2e/set-itegration-as-primary.e2e.ts @@ -9,8 +9,6 @@ import { PushProviderIdEnum, } from '@novu/shared'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Set Integration As Primary - /integrations/:integrationId/set-primary (POST)', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); @@ -18,11 +16,6 @@ describe('Set Integration As Primary - /integrations/:integrationId/set-primary beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('when integration id is not valid should throw bad request exception', async () => { @@ -228,7 +221,10 @@ describe('Set Integration As Primary - /integrations/:integrationId/set-primary expect(data.active).to.equal(true); expect(data.priority).to.equal(2); - const updatedOldPrimary = (await integrationRepository.findById(oldPrimaryIntegration._id)) as IntegrationEntity; + const updatedOldPrimary = (await integrationRepository.findOne({ + _id: oldPrimaryIntegration._id, + _environmentId: oldPrimaryIntegration._environmentId, + })) as IntegrationEntity; expect(updatedOldPrimary.primary).to.equal(false); expect(updatedOldPrimary.active).to.equal(true); diff --git a/apps/api/src/app/integrations/e2e/update-integration.e2e.ts b/apps/api/src/app/integrations/e2e/update-integration.e2e.ts index 96a176d296d..398193e68f1 100644 --- a/apps/api/src/app/integrations/e2e/update-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/update-integration.e2e.ts @@ -10,8 +10,6 @@ import { PushProviderIdEnum, } from '@novu/shared'; -const ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - describe('Update Integration - /integrations/:integrationId (PUT)', function () { let session: UserSession; const integrationRepository = new IntegrationRepository(); @@ -20,11 +18,6 @@ describe('Update Integration - /integrations/:integrationId (PUT)', function () beforeEach(async () => { session = new UserSession(); await session.initialize(); - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = 'true'; - }); - - afterEach(async () => { - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = ORIGINAL_IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; }); it('should throw not found exception when integration is not found', async function () { 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 342a550998a..c7ae5a61749 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 @@ -15,13 +15,10 @@ import { encryptCredentials, buildIntegrationKey, InvalidateCacheService, - GetIsMultiProviderConfigurationEnabled, - FeatureFlagCommand, } from '@novu/application-generic'; import { CreateIntegrationCommand } from './create-integration.command'; import { ApiException } from '../../../shared/exceptions/api.exception'; -import { DeactivateSimilarChannelIntegrations } from '../deactivate-integration/deactivate-integration.usecase'; import { CheckIntegrationCommand } from '../check-integration/check-integration.command'; import { CheckIntegration } from '../check-integration/check-integration.usecase'; @@ -32,9 +29,7 @@ export class CreateIntegration { constructor( private invalidateCache: InvalidateCacheService, private integrationRepository: IntegrationRepository, - private deactivateSimilarChannelIntegrations: DeactivateSimilarChannelIntegrations, - private analyticsService: AnalyticsService, - private getIsMultiProviderConfigurationEnabled: GetIsMultiProviderConfigurationEnabled + private analyticsService: AnalyticsService ) {} private async calculatePriorityAndPrimary(command: CreateIntegrationCommand) { @@ -71,26 +66,12 @@ export class CreateIntegration { } async execute(command: CreateIntegrationCommand): Promise { - const isMultiProviderConfigurationEnabled = await this.getIsMultiProviderConfigurationEnabled.execute( - FeatureFlagCommand.create({ - userId: command.userId, - organizationId: command.organizationId, - environmentId: command.environmentId, - }) - ); - const existingIntegration = await this.integrationRepository.findOne({ _environmentId: command.environmentId, providerId: command.providerId, channel: command.channel, }); - if (!isMultiProviderConfigurationEnabled && existingIntegration) { - throw new BadRequestException( - 'Duplicate key - One environment may not have two providers of the same channel type' - ); - } - if ( existingIntegration && command.providerId === InAppProviderIdEnum.Novu && @@ -169,7 +150,7 @@ export class CreateIntegration { const isActiveAndChannelSupportsPrimary = command.active && CHANNELS_WITH_PRIMARY.includes(command.channel); - if (isMultiProviderConfigurationEnabled && isActiveAndChannelSupportsPrimary) { + if (isActiveAndChannelSupportsPrimary) { const { primary, priority } = await this.calculatePriorityAndPrimary(command); query.primary = primary; @@ -178,20 +159,6 @@ export class CreateIntegration { const integrationEntity = await this.integrationRepository.create(query); - if ( - !isMultiProviderConfigurationEnabled && - command.active && - ![ChannelTypeEnum.CHAT, ChannelTypeEnum.PUSH].includes(command.channel) - ) { - await this.deactivateSimilarChannelIntegrations.execute({ - environmentId: command.environmentId, - organizationId: command.organizationId, - integrationId: integrationEntity._id, - channel: command.channel, - userId: command.userId, - }); - } - return integrationEntity; } catch (e) { if (e instanceof DalException) { diff --git a/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.command.ts b/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.command.ts deleted file mode 100644 index fb0439c05dc..00000000000 --- a/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.command.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IsDefined } from 'class-validator'; -import { ChannelTypeEnum } from '@novu/shared'; -import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; - -export class DeactivateSimilarChannelIntegrationsCommand extends EnvironmentWithUserCommand { - @IsDefined() - integrationId: string; - - @IsDefined() - channel: ChannelTypeEnum; -} diff --git a/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.usecase.ts b/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.usecase.ts deleted file mode 100644 index 826b56a0c1a..00000000000 --- a/apps/api/src/app/integrations/usecases/deactivate-integration/deactivate-integration.usecase.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IntegrationRepository } from '@novu/dal'; -import { FeatureFlagCommand, GetIsMultiProviderConfigurationEnabled } from '@novu/application-generic'; - -import { DeactivateSimilarChannelIntegrationsCommand } from './deactivate-integration.command'; - -@Injectable() -export class DeactivateSimilarChannelIntegrations { - constructor( - private integrationRepository: IntegrationRepository, - private getIsMultiProviderConfigurationEnabled: GetIsMultiProviderConfigurationEnabled - ) {} - async execute(command: DeactivateSimilarChannelIntegrationsCommand): Promise { - const shouldKeepIntegrationsActive = await this.getIsMultiProviderConfigurationEnabled.execute( - FeatureFlagCommand.create({ - environmentId: command.environmentId, - organizationId: command.organizationId, - userId: command.userId, - }) - ); - - if (shouldKeepIntegrationsActive) { - return; - } - - const otherExistedIntegration = await this.integrationRepository.find({ - _id: { $ne: command.integrationId }, - _environmentId: command.environmentId, - channel: command.channel, - active: true, - }); - - if (otherExistedIntegration.length) { - await this.integrationRepository.update( - { _environmentId: command.environmentId, _id: { $in: otherExistedIntegration.map((i) => i._id) } }, - { $set: { active: false } } - ); - } - } -} diff --git a/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.command.ts b/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.command.ts deleted file mode 100644 index 910278ffa31..00000000000 --- a/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.command.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChannelTypeEnum } from '@novu/shared'; -import { IsDefined, IsEnum, IsString } from 'class-validator'; -import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; - -export class DisableNovuIntegrationCommand extends EnvironmentWithUserCommand { - @IsDefined() - @IsEnum(ChannelTypeEnum) - channel: ChannelTypeEnum; - - @IsDefined() - @IsString() - providerId: string; -} diff --git a/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.usecase.ts b/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.usecase.ts deleted file mode 100644 index 7aaf7a346c4..00000000000 --- a/apps/api/src/app/integrations/usecases/disable-novu-integration/disable-novu-integration.usecase.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IntegrationRepository } from '@novu/dal'; - -import { DisableNovuIntegrationCommand } from './disable-novu-integration.command'; -import { ChannelTypeEnum, EmailProviderIdEnum, ProvidersIdEnum, SmsProviderIdEnum } from '@novu/shared'; - -@Injectable() -export class DisableNovuIntegration { - private channelProviderIdMap = new Map(); - - constructor(private integrationRepository: IntegrationRepository) { - this.channelProviderIdMap.set(ChannelTypeEnum.EMAIL, EmailProviderIdEnum.Novu); - this.channelProviderIdMap.set(ChannelTypeEnum.SMS, SmsProviderIdEnum.Novu); - } - - async execute(command: DisableNovuIntegrationCommand): Promise { - const novuProviderId = this.channelProviderIdMap.get(command.channel); - - if (!novuProviderId) { - return; - } - - if (command.providerId === novuProviderId) { - return; - } - - await this.integrationRepository.update( - { _environmentId: command.environmentId, providerId: novuProviderId, channel: command.channel }, - { $set: { active: false, primary: false, priority: 0 } } - ); - } -} diff --git a/apps/api/src/app/integrations/usecases/index.ts b/apps/api/src/app/integrations/usecases/index.ts index 3a236504c10..e93e2fecdf3 100644 --- a/apps/api/src/app/integrations/usecases/index.ts +++ b/apps/api/src/app/integrations/usecases/index.ts @@ -10,14 +10,12 @@ import { CreateIntegration } from './create-integration/create-integration.useca import { GetIntegrations } from './get-integrations/get-integrations.usecase'; import { UpdateIntegration } from './update-integration/update-integration.usecase'; import { RemoveIntegration } from './remove-integration/remove-integration.usecase'; -import { DeactivateSimilarChannelIntegrations } from './deactivate-integration/deactivate-integration.usecase'; import { GetActiveIntegrations } from './get-active-integration/get-active-integration.usecase'; import { CheckIntegration } from './check-integration/check-integration.usecase'; import { CheckIntegrationEMail } from './check-integration/check-integration-email.usecase'; import { GetInAppActivated } from './get-in-app-activated/get-in-app-activated.usecase'; import { SetIntegrationAsPrimary } from './set-integration-as-primary/set-integration-as-primary.usecase'; import { CreateNovuIntegrations } from './create-novu-integrations/create-novu-integrations.usecase'; -import { DisableNovuIntegration } from './disable-novu-integration/disable-novu-integration.usecase'; export const USE_CASES = [ GetInAppActivated, @@ -30,11 +28,9 @@ export const USE_CASES = [ GetDecryptedIntegrations, UpdateIntegration, RemoveIntegration, - DeactivateSimilarChannelIntegrations, CheckIntegration, CheckIntegrationEMail, CalculateLimitNovuIntegration, SetIntegrationAsPrimary, CreateNovuIntegrations, - DisableNovuIntegration, ]; diff --git a/apps/api/src/app/integrations/usecases/remove-integration/remove-integration.usecase.ts b/apps/api/src/app/integrations/usecases/remove-integration/remove-integration.usecase.ts index 296a8055e21..6084be4cbb2 100644 --- a/apps/api/src/app/integrations/usecases/remove-integration/remove-integration.usecase.ts +++ b/apps/api/src/app/integrations/usecases/remove-integration/remove-integration.usecase.ts @@ -1,12 +1,7 @@ import { Injectable, NotFoundException, Scope } from '@nestjs/common'; import { IntegrationRepository, DalException } from '@novu/dal'; import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; -import { - buildIntegrationKey, - FeatureFlagCommand, - GetIsMultiProviderConfigurationEnabled, - InvalidateCacheService, -} from '@novu/application-generic'; +import { buildIntegrationKey, InvalidateCacheService } from '@novu/application-generic'; import { RemoveIntegrationCommand } from './remove-integration.command'; import { ApiException } from '../../../shared/exceptions/api.exception'; @@ -15,15 +10,14 @@ import { ApiException } from '../../../shared/exceptions/api.exception'; scope: Scope.REQUEST, }) export class RemoveIntegration { - constructor( - private invalidateCache: InvalidateCacheService, - private integrationRepository: IntegrationRepository, - private getIsMultiProviderConfigurationEnabled: GetIsMultiProviderConfigurationEnabled - ) {} + constructor(private invalidateCache: InvalidateCacheService, private integrationRepository: IntegrationRepository) {} async execute(command: RemoveIntegrationCommand) { try { - const existingIntegration = await this.integrationRepository.findById(command.integrationId); + const existingIntegration = await this.integrationRepository.findOne({ + _id: command.integrationId, + _organizationId: command.organizationId, + }); if (!existingIntegration) { throw new NotFoundException(`Entity with id ${command.integrationId} not found`); } @@ -39,16 +33,8 @@ export class RemoveIntegration { _organizationId: existingIntegration._organizationId, }); - const isMultiProviderConfigurationEnabled = await this.getIsMultiProviderConfigurationEnabled.execute( - FeatureFlagCommand.create({ - userId: command.userId, - organizationId: command.organizationId, - environmentId: command.environmentId, - }) - ); - const isChannelSupportsPrimary = CHANNELS_WITH_PRIMARY.includes(existingIntegration.channel); - if (isMultiProviderConfigurationEnabled && isChannelSupportsPrimary) { + if (isChannelSupportsPrimary) { await this.integrationRepository.recalculatePriorityForAllActive({ _organizationId: existingIntegration._organizationId, _environmentId: existingIntegration._environmentId, 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 b6cf7c877a0..316435be89c 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 @@ -1,13 +1,7 @@ import { Injectable, NotFoundException, Logger, BadRequestException } from '@nestjs/common'; import { IntegrationEntity, IntegrationRepository } from '@novu/dal'; import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; -import { - AnalyticsService, - buildIntegrationKey, - FeatureFlagCommand, - GetIsMultiProviderConfigurationEnabled, - InvalidateCacheService, -} from '@novu/application-generic'; +import { AnalyticsService, buildIntegrationKey, InvalidateCacheService } from '@novu/application-generic'; import { SetIntegrationAsPrimaryCommand } from './set-integration-as-primary.command'; @@ -16,8 +10,7 @@ export class SetIntegrationAsPrimary { constructor( private invalidateCache: InvalidateCacheService, private integrationRepository: IntegrationRepository, - private analyticsService: AnalyticsService, - private getIsMultiProviderConfigurationEnabled: GetIsMultiProviderConfigurationEnabled + private analyticsService: AnalyticsService ) {} private async updatePrimaryFlag({ existingIntegration }: { existingIntegration: IntegrationEntity }) { @@ -55,7 +48,10 @@ export class SetIntegrationAsPrimary { async execute(command: SetIntegrationAsPrimaryCommand): Promise { Logger.verbose('Executing Set Integration As Primary Usecase'); - const existingIntegration = await this.integrationRepository.findById(command.integrationId); + const existingIntegration = await this.integrationRepository.findOne({ + _id: command.integrationId, + _organizationId: command.organizationId, + }); if (!existingIntegration) { throw new NotFoundException(`Integration with id ${command.integrationId} not found`); } @@ -65,14 +61,7 @@ export class SetIntegrationAsPrimary { } const { _organizationId, _environmentId, channel, providerId } = existingIntegration; - const isMultiProviderConfigurationEnabled = await this.getIsMultiProviderConfigurationEnabled.execute( - FeatureFlagCommand.create({ - userId: command.userId, - organizationId: _organizationId, - environmentId: _environmentId, - }) - ); - if (!isMultiProviderConfigurationEnabled || existingIntegration.primary) { + if (existingIntegration.primary) { return existingIntegration; } 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 d1a4d1cd2d9..c36738cba9e 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 @@ -5,13 +5,10 @@ import { encryptCredentials, buildIntegrationKey, InvalidateCacheService, - GetIsMultiProviderConfigurationEnabled, - FeatureFlagCommand, } from '@novu/application-generic'; -import { ChannelTypeEnum, CHANNELS_WITH_PRIMARY } from '@novu/shared'; +import { CHANNELS_WITH_PRIMARY } from '@novu/shared'; import { UpdateIntegrationCommand } from './update-integration.command'; -import { DeactivateSimilarChannelIntegrations } from '../deactivate-integration/deactivate-integration.usecase'; import { CheckIntegration } from '../check-integration/check-integration.usecase'; import { CheckIntegrationCommand } from '../check-integration/check-integration.command'; @@ -22,9 +19,7 @@ export class UpdateIntegration { constructor( private invalidateCache: InvalidateCacheService, private integrationRepository: IntegrationRepository, - private deactivateSimilarChannelIntegrations: DeactivateSimilarChannelIntegrations, - private analyticsService: AnalyticsService, - private getIsMultiProviderConfigurationEnabled: GetIsMultiProviderConfigurationEnabled + private analyticsService: AnalyticsService ) {} private async calculatePriorityAndPrimaryForActive({ @@ -101,7 +96,10 @@ export class UpdateIntegration { async execute(command: UpdateIntegrationCommand): Promise { Logger.verbose('Executing Update Integration Command'); - const existingIntegration = await this.integrationRepository.findById(command.integrationId); + const existingIntegration = await this.integrationRepository.findOne({ + _id: command.integrationId, + _organizationId: command.organizationId, + }); if (!existingIntegration) { throw new NotFoundException(`Entity with id ${command.integrationId} not found`); } @@ -177,18 +175,10 @@ export class UpdateIntegration { throw new BadRequestException('No properties found for update'); } - const isMultiProviderConfigurationEnabled = await this.getIsMultiProviderConfigurationEnabled.execute( - FeatureFlagCommand.create({ - userId: command.userId, - organizationId: command.organizationId, - environmentId: command.userEnvironmentId, - }) - ); - const haveConditions = updatePayload.conditions && updatePayload.conditions?.length > 0; const isChannelSupportsPrimary = CHANNELS_WITH_PRIMARY.includes(existingIntegration.channel); - if (isMultiProviderConfigurationEnabled && isActiveChanged && isChannelSupportsPrimary) { + if (isActiveChanged && isChannelSupportsPrimary) { const { primary, priority } = await this.calculatePriorityAndPrimary({ existingIntegration, active: !!command.active, @@ -222,20 +212,6 @@ export class UpdateIntegration { }); } - if ( - !isMultiProviderConfigurationEnabled && - command.active && - ![ChannelTypeEnum.CHAT, ChannelTypeEnum.PUSH].includes(existingIntegration.channel) - ) { - await this.deactivateSimilarChannelIntegrations.execute({ - environmentId, - organizationId: command.organizationId, - integrationId: command.integrationId, - channel: existingIntegration.channel, - userId: command.userId, - }); - } - const updatedIntegration = await this.integrationRepository.findOne({ _id: command.integrationId, _environmentId: environmentId, diff --git a/apps/api/src/app/message-template/usecases/create-message-template/create-message-template.usecase.ts b/apps/api/src/app/message-template/usecases/create-message-template/create-message-template.usecase.ts index a433793f877..6839e089a18 100644 --- a/apps/api/src/app/message-template/usecases/create-message-template/create-message-template.usecase.ts +++ b/apps/api/src/app/message-template/usecases/create-message-template/create-message-template.usecase.ts @@ -38,7 +38,10 @@ export class CreateMessageTemplate { }); if (item?._id) { - item = (await this.messageTemplateRepository.findById(item._id)) as MessageTemplateEntity; + item = (await this.messageTemplateRepository.findOne({ + _id: item._id, + _organizationId: command.organizationId, + })) as MessageTemplateEntity; } await this.createChange.execute( diff --git a/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts b/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts index 56e402eb386..413ce36dc61 100644 --- a/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts +++ b/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts @@ -103,7 +103,10 @@ export class UpdateMessageTemplate { } ); - const item = await this.messageTemplateRepository.findById(command.templateId); + const item = await this.messageTemplateRepository.findOne({ + _id: command.templateId, + _organizationId: command.organizationId, + }); if (!item) throw new NotFoundException(`Message template with id ${command.templateId} is not found`); if (command.feedId || (!command.feedId && existingTemplate._feedId)) { diff --git a/apps/api/src/app/messages/e2e/remove-message.e2e.ts b/apps/api/src/app/messages/e2e/remove-message.e2e.ts index 6ef2812699a..4498919e834 100644 --- a/apps/api/src/app/messages/e2e/remove-message.e2e.ts +++ b/apps/api/src/app/messages/e2e/remove-message.e2e.ts @@ -63,7 +63,7 @@ describe('Delete Message - /messages/:messageId (DELETE)', function () { }, }); - const result = await messageRepository.findById(message._id); + const result = await messageRepository.findOne({ _id: message._id, _environmentId: message._environmentId }); expect(result).to.not.be.ok; }); }); diff --git a/apps/api/src/app/organization/dtos/member-response.dto.ts b/apps/api/src/app/organization/dtos/member-response.dto.ts new file mode 100644 index 00000000000..1aebd1ec48e --- /dev/null +++ b/apps/api/src/app/organization/dtos/member-response.dto.ts @@ -0,0 +1,78 @@ +import { MemberRoleEnum, MemberStatusEnum } from '@novu/shared'; +import { IsArray, IsDate, IsObject, IsString, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class MemberUserDto { + @ApiProperty() + @IsString() + _id: string; + + @ApiProperty() + @IsString() + firstName: string; + + @ApiProperty() + @IsString() + lastName: string; + + @ApiProperty() + @IsString() + email: string; +} + +export class MemberInviteDTO { + @ApiProperty() + @IsString() + email: string; + + @ApiProperty() + @IsString() + token: string; + + @ApiProperty() + @IsDate() + invitationDate: Date; + + @ApiPropertyOptional() + @IsDate() + answerDate?: Date; + + @ApiProperty() + @IsString() + _inviterId: string; +} + +export class MemberResponseDto { + @ApiProperty() + @IsString() + _id: string; + + @ApiProperty() + @IsString() + _userId: string; + + @ApiPropertyOptional() + @IsObject() + user?: MemberUserDto; + + @ApiPropertyOptional({ + enum: MemberRoleEnum, + isArray: true, + }) + @IsEnum(MemberRoleEnum) + roles?: MemberRoleEnum; + + @ApiPropertyOptional() + @IsObject() + invite?: MemberInviteDTO; + + @ApiPropertyOptional({ + enum: { ...MemberStatusEnum }, + }) + @IsEnum(MemberStatusEnum) + memberStatus?: MemberStatusEnum; + + @ApiProperty() + @IsString() + _organizationId: string; +} diff --git a/apps/api/src/app/organization/dtos/organization-response.dto.ts b/apps/api/src/app/organization/dtos/organization-response.dto.ts new file mode 100644 index 00000000000..46f9c81becc --- /dev/null +++ b/apps/api/src/app/organization/dtos/organization-response.dto.ts @@ -0,0 +1,56 @@ +import { PartnerTypeEnum, DirectionEnum } from '@novu/dal'; +import { IsObject, IsArray, IsString, IsEnum } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { UpdateBrandingDetailsDto } from './update-branding-details.dto'; + +export class IPartnerConfigurationResponseDto { + @ApiPropertyOptional() + @IsArray() + @IsString({ each: true }) + projectIds?: string[]; + + @ApiProperty() + @IsString() + accessToken: string; + + @ApiProperty() + @IsString() + configurationId: string; + + @ApiPropertyOptional() + @IsString() + teamId: string; + + @ApiProperty({ + enum: { ...PartnerTypeEnum }, + description: 'Partner Type Enum', + }) + @IsEnum(PartnerTypeEnum) + partnerType: PartnerTypeEnum; +} + +export class OrganizationBrandingResponseDto extends UpdateBrandingDetailsDto { + @ApiPropertyOptional({ + enum: { ...DirectionEnum }, + }) + @IsString() + direction?: DirectionEnum; +} + +export class OrganizationResponseDto { + @ApiProperty() + @IsString() + name: string; + + @ApiPropertyOptional() + @IsString() + logo?: string; + + @ApiProperty() + @IsObject() + branding: OrganizationBrandingResponseDto; + + @ApiPropertyOptional() + @IsObject() + partnerConfigurations: IPartnerConfigurationResponseDto[]; +} diff --git a/apps/api/src/app/organization/e2e/members/remove-member.e2e.ts b/apps/api/src/app/organization/e2e/members/remove-member.e2e.ts index 5ab97ba9d49..41163a189fd 100644 --- a/apps/api/src/app/organization/e2e/members/remove-member.e2e.ts +++ b/apps/api/src/app/organization/e2e/members/remove-member.e2e.ts @@ -57,7 +57,7 @@ describe('Remove organization member - /organizations/members/:memberId (DELETE) const originalCreatorAfterRemoval = membersAfterRemoval.find((i) => i._userId === originalCreator.user._id); expect(originalCreatorAfterRemoval).to.not.be.ok; - const environment = await environmentRepository.findById(session.environment._id); + const environment = await environmentRepository.findOne({ _id: session.environment._id }); expect(environment.apiKeys[0]._userId).to.not.equal(session.user._id); }); @@ -77,7 +77,7 @@ describe('Remove organization member - /organizations/members/:memberId (DELETE) /** * The API Key owner should not be updated if non creator was removed */ - const environment = await environmentRepository.findById(session.environment._id); + const environment = await environmentRepository.findOne({ _id: session.environment._id }); expect(environment.apiKeys[0]._userId).to.equal(session.user._id); }); diff --git a/apps/api/src/app/organization/organization.controller.ts b/apps/api/src/app/organization/organization.controller.ts index aa739fe9468..9a92742186f 100644 --- a/apps/api/src/app/organization/organization.controller.ts +++ b/apps/api/src/app/organization/organization.controller.ts @@ -13,7 +13,7 @@ import { } from '@nestjs/common'; import { OrganizationEntity } from '@novu/dal'; import { IJwtPayload, MemberRoleEnum } from '@novu/shared'; -import { ApiExcludeController, ApiTags } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiParam } from '@nestjs/swagger'; import { Roles } from '../auth/framework/roles.decorator'; import { UserSession } from '../shared/framework/user.decorator'; import { CreateOrganizationDto } from './dtos/create-organization.dto'; @@ -39,12 +39,14 @@ import { RenameOrganization } from './usecases/rename-organization/rename-organi import { RenameOrganizationDto } from './dtos/rename-organization.dto'; import { UpdateBrandingDetailsDto } from './dtos/update-branding-details.dto'; import { UpdateMemberRolesDto } from './dtos/update-member-roles.dto'; - +import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { ApiResponse } from '../shared/framework/response.decorator'; +import { OrganizationBrandingResponseDto, OrganizationResponseDto } from './dtos/organization-response.dto'; +import { MemberResponseDto } from './dtos/member-response.dto'; @Controller('/organizations') @UseInterceptors(ClassSerializerInterceptor) @UseGuards(JwtAuthGuard) @ApiTags('Organizations') -@ApiExcludeController() export class OrganizationController { constructor( private createOrganizationUsecase: CreateOrganization, @@ -58,6 +60,11 @@ export class OrganizationController { ) {} @Post('/') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto, 201) + @ApiOperation({ + summary: 'Create an organization', + }) async createOrganization( @UserSession() user: IJwtPayload, @Body() body: CreateOrganizationDto @@ -72,6 +79,11 @@ export class OrganizationController { } @Get('/') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto, 200, true) + @ApiOperation({ + summary: 'Fetch all organizations', + }) async getOrganizations(@UserSession() user: IJwtPayload): Promise { const command = GetOrganizationsCommand.create({ userId: user._id, @@ -81,6 +93,11 @@ export class OrganizationController { } @Get('/me') + @ExternalApiAccessible() + @ApiResponse(OrganizationResponseDto) + @ApiOperation({ + summary: 'Fetch current organization details', + }) async getMyOrganization(@UserSession() user: IJwtPayload): Promise { const command = GetMyOrganizationCommand.create({ userId: user._id, @@ -91,7 +108,13 @@ export class OrganizationController { } @Delete('/members/:memberId') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(MemberResponseDto) + @ApiOperation({ + summary: 'Remove a member from organization using memberId', + }) + @ApiParam({ name: 'memberId', type: String, required: true }) async removeMember(@UserSession() user: IJwtPayload, @Param('memberId') memberId: string) { return await this.removeMemberUsecase.execute( RemoveMemberCommand.create({ @@ -103,7 +126,13 @@ export class OrganizationController { } @Put('/members/:memberId/roles') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(MemberResponseDto) + @ApiOperation({ + summary: 'Update a member role to admin', + }) + @ApiParam({ name: 'memberId', type: String, required: true }) async updateMemberRoles( @UserSession() user: IJwtPayload, @Param('memberId') memberId: string, @@ -120,6 +149,11 @@ export class OrganizationController { } @Get('/members') + @ExternalApiAccessible() + @ApiResponse(MemberResponseDto, 200, true) + @ApiOperation({ + summary: 'Fetch all members of current organizations', + }) async getMember(@UserSession() user: IJwtPayload) { return await this.getMembers.execute( GetMembersCommand.create({ @@ -130,19 +164,12 @@ export class OrganizationController { ); } - @Post('/members/invite') - @Roles(MemberRoleEnum.ADMIN) - async inviteMember(@UserSession() user: IJwtPayload) { - return await this.getMembers.execute( - GetMembersCommand.create({ - user, - userId: user._id, - organizationId: user.organizationId, - }) - ); - } - @Put('/branding') + @ExternalApiAccessible() + @ApiResponse(OrganizationBrandingResponseDto) + @ApiOperation({ + summary: 'Update organization branding details', + }) async updateBrandingDetails(@UserSession() user: IJwtPayload, @Body() body: UpdateBrandingDetailsDto) { return await this.updateBrandingDetailsUsecase.execute( UpdateBrandingDetailsCommand.create({ @@ -158,7 +185,12 @@ export class OrganizationController { } @Patch('/') + @ExternalApiAccessible() @Roles(MemberRoleEnum.ADMIN) + @ApiResponse(RenameOrganizationDto) + @ApiOperation({ + summary: 'Rename organization name', + }) async renameOrganization(@UserSession() user: IJwtPayload, @Body() body: RenameOrganizationDto) { return await this.renameOrganizationUsecase.execute( RenameOrganizationCommand.create({ diff --git a/apps/api/src/app/organization/organization.module.ts b/apps/api/src/app/organization/organization.module.ts index 5cc77ed1573..9e058638c81 100644 --- a/apps/api/src/app/organization/organization.module.ts +++ b/apps/api/src/app/organization/organization.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule, RequestMethod, forwardRef } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { EnvironmentsModule } from '../environments/environments.module'; import { IntegrationModule } from '../integrations/integrations.module'; @@ -6,9 +6,10 @@ import { SharedModule } from '../shared/shared.module'; import { UserModule } from '../user/user.module'; import { OrganizationController } from './organization.controller'; import { USE_CASES } from './usecases'; +import { AuthModule } from '../auth/auth.module'; @Module({ - imports: [SharedModule, UserModule, EnvironmentsModule, IntegrationModule], + imports: [SharedModule, UserModule, EnvironmentsModule, IntegrationModule, forwardRef(() => AuthModule)], controllers: [OrganizationController], providers: [...USE_CASES], exports: [...USE_CASES], diff --git a/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts b/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts index f60a885af8e..b51ffc67604 100644 --- a/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts +++ b/apps/api/src/app/organization/usecases/membership/get-members/get-members.usecase.ts @@ -1,5 +1,5 @@ import { Injectable, Scope } from '@nestjs/common'; -import { OrganizationRepository, MemberRepository } from '@novu/dal'; +import { MemberRepository } from '@novu/dal'; import { MemberRoleEnum, MemberStatusEnum } from '@novu/shared'; import { GetMembersCommand } from './get-members.command'; @@ -7,7 +7,7 @@ import { GetMembersCommand } from './get-members.command'; scope: Scope.REQUEST, }) export class GetMembers { - constructor(private organizationRepository: OrganizationRepository, private membersRepository: MemberRepository) {} + constructor(private membersRepository: MemberRepository) {} async execute(command: GetMembersCommand) { return (await this.membersRepository.getOrganizationMembers(command.organizationId)) diff --git a/apps/api/src/app/shared/framework/idempotency.interceptor.ts b/apps/api/src/app/shared/framework/idempotency.interceptor.ts new file mode 100644 index 00000000000..566824c0cd0 --- /dev/null +++ b/apps/api/src/app/shared/framework/idempotency.interceptor.ts @@ -0,0 +1,247 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, + Logger, + HttpException, + InternalServerErrorException, + ServiceUnavailableException, + UnprocessableEntityException, + BadRequestException, + ConflictException, +} from '@nestjs/common'; +import { CacheService } from '@novu/application-generic'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { createHash } from 'crypto'; +import * as jwt from 'jsonwebtoken'; +import { IJwtPayload } from '@novu/shared'; + +const LOG_CONTEXT = 'IdempotencyInterceptor'; +const IDEMPOTENCY_CACHE_TTL = 60 * 60 * 24; //24h +const IDEMPOTENCY_PROGRESS_TTL = 60 * 5; //5min + +const HEADER_KEYS = { + IDEMPOTENCY_KEY: 'Idempotency-Key', + RETRY_AFTER: 'Retry-After', + IDEMPOTENCY_REPLAY: 'Idempotency-Replay', + LINK: 'Link', +}; + +const DOCS_LINK = 'docs.novu.co/idempotency'; + +enum ReqStatusEnum { + PROGRESS = 'in-progress', + SUCCESS = 'success', + ERROR = 'error', +} + +@Injectable() +export class IdempotencyInterceptor implements NestInterceptor { + constructor(private readonly cacheService: CacheService) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request = context.switchToHttp().getRequest(); + const idempotencyKey = this.getIdempotencyKey(context); + const isEnabled = process.env.IS_API_IDEMPOTENCY_ENABLED == 'true'; + if (!isEnabled || !idempotencyKey || !['post', 'patch'].includes(request.method.toLowerCase())) { + return next.handle(); + } + if (idempotencyKey?.length > 255) { + return throwError( + () => + new BadRequestException( + `idempotencyKey "${idempotencyKey}" has exceeded the maximum allowed length of 255 characters` + ) + ); + } + const cacheKey = this.getCacheKey(context); + + try { + const bodyHash = this.hashRequestBody(request.body); + //if 1st time we are seeing the request, marks the request as in-progress if not, does nothing + const isNewReq = await this.setCache( + cacheKey, + { status: ReqStatusEnum.PROGRESS, bodyHash }, + IDEMPOTENCY_PROGRESS_TTL, + true + ); + // Check if the idempotency key is in the cache + if (isNewReq) { + return await this.handleNewRequest(context, next, bodyHash); + } else { + return await this.handlerDuplicateRequest(context, bodyHash); + } + } catch (err) { + Logger.warn( + `An error occurred while making idempotency check, key:${idempotencyKey}. error: ${err.message}`, + LOG_CONTEXT + ); + if (err instanceof HttpException) { + return throwError(() => err); + } + } + + //something unexpected happened, both cached response and handler did not execute as expected + return throwError(() => new ServiceUnavailableException()); + } + + private getIdempotencyKey(context: ExecutionContext): string | undefined { + const request = context.switchToHttp().getRequest(); + + return request.headers[HEADER_KEYS.IDEMPOTENCY_KEY.toLocaleLowerCase()]; + } + + private getReqUser(context: ExecutionContext): IJwtPayload | null { + const req = context.switchToHttp().getRequest(); + if (req?.user?.organizationId) { + return req.user; + } + if (req.headers?.authorization?.length) { + const token = req.headers.authorization.split(' ')[1]; + if (token) { + return jwt.decode(token); + } + } + + return null; + } + + private getCacheKey(context: ExecutionContext): string { + const { organizationId } = this.getReqUser(context) || {}; + const env = process.env.NODE_ENV; + + return `${env}-${organizationId}-${this.getIdempotencyKey(context)}`; + } + + async setCache( + key: string, + val: { status: ReqStatusEnum; bodyHash: string; data?: any; statusCode?: number }, + ttl: number, + ifNotExists?: boolean + ): Promise { + try { + if (ifNotExists) { + return await this.cacheService.setIfNotExist(key, JSON.stringify(val), { ttl }); + } + await this.cacheService.set(key, JSON.stringify(val), { ttl }); + } catch (err) { + Logger.warn(`An error occurred while setting idempotency cache, key:${key} error: ${err.message}`, LOG_CONTEXT); + } + + return null; + } + + private buildError(error: any): HttpException { + const statusCode = error.status || error.response?.statusCode || 500; + if (statusCode == 500 && !error.response) { + //some unhandled exception occurred + return new InternalServerErrorException(); + } + + return new HttpException(error.response || error.message, statusCode, error.response?.options); + } + + private setHeaders(response: any, headers: Record) { + Object.keys(headers).map((key) => { + if (headers[key]) { + response.set(key, headers[key]); + } + }); + } + + private hashRequestBody(body: object): string { + const hash = createHash('blake2s256'); + hash.update(Buffer.from(JSON.stringify(body))); + + return hash.digest('hex'); + } + + private async handlerDuplicateRequest(context: ExecutionContext, bodyHash: string): Promise> { + const cacheKey = this.getCacheKey(context); + const idempotencyKey = this.getIdempotencyKey(context)!; + const data = await this.cacheService.get(cacheKey); + this.setHeaders(context.switchToHttp().getResponse(), { [HEADER_KEYS.IDEMPOTENCY_KEY]: idempotencyKey }); + const parsed = JSON.parse(data); + if (parsed.status === ReqStatusEnum.PROGRESS) { + // api call is in progress, so client need to handle this case + Logger.error(`previous api call in progress rejecting the request. key:${idempotencyKey}`, LOG_CONTEXT); + this.setHeaders(context.switchToHttp().getResponse(), { + [HEADER_KEYS.RETRY_AFTER]: `1`, + [HEADER_KEYS.LINK]: DOCS_LINK, + }); + + throw new ConflictException( + `Request with key "${idempotencyKey}" is currently being processed. Please retry after 1 second` + ); + } + if (bodyHash !== parsed.bodyHash) { + //different body sent than before + Logger.error(`idempotency key is being reused for different bodies. key:${idempotencyKey}`, LOG_CONTEXT); + this.setHeaders(context.switchToHttp().getResponse(), { + [HEADER_KEYS.LINK]: DOCS_LINK, + }); + + throw new UnprocessableEntityException( + `Request with key "${idempotencyKey}" is being reused for a different body` + ); + } + this.setHeaders(context.switchToHttp().getResponse(), { [HEADER_KEYS.IDEMPOTENCY_REPLAY]: 'true' }); + + //already seen the request return cached response + if (parsed.status === ReqStatusEnum.ERROR) { + Logger.error(`returning cached error response. key:${idempotencyKey}`, LOG_CONTEXT); + + throw this.buildError(parsed.data); + } + + return of(parsed.data); + } + + private async handleNewRequest( + context: ExecutionContext, + next: CallHandler, + bodyHash: string + ): Promise> { + const cacheKey = this.getCacheKey(context); + const idempotencyKey = this.getIdempotencyKey(context)!; + + return next.handle().pipe( + map(async (response) => { + const httpResponse = context.switchToHttp().getResponse(); + const statusCode = httpResponse.statusCode; + + // Cache the success response and return it + await this.setCache( + cacheKey, + { status: ReqStatusEnum.SUCCESS, bodyHash, statusCode: statusCode, data: response }, + IDEMPOTENCY_CACHE_TTL + ); + Logger.verbose(`cached the success response for idempotency key:${idempotencyKey}`, LOG_CONTEXT); + this.setHeaders(httpResponse, { [HEADER_KEYS.IDEMPOTENCY_KEY]: idempotencyKey }); + + return response; + }), + catchError((err) => { + const httpException = this.buildError(err); + // Cache the error response and return it + const error = err instanceof HttpException ? err : httpException; + this.setCache( + cacheKey, + { + status: ReqStatusEnum.ERROR, + statusCode: httpException.getStatus(), + bodyHash, + data: error, + }, + IDEMPOTENCY_CACHE_TTL + ).catch(() => {}); + Logger.verbose(`cached the error response for idempotency key:${idempotencyKey}`, LOG_CONTEXT); + this.setHeaders(context.switchToHttp().getResponse(), { [HEADER_KEYS.IDEMPOTENCY_KEY]: idempotencyKey }); + + throw err; + }) + ); + } +} diff --git a/apps/api/src/app/shared/helpers/content.service.spec.ts b/apps/api/src/app/shared/helpers/content.service.spec.ts index 70f80455447..32984c527b1 100644 --- a/apps/api/src/app/shared/helpers/content.service.spec.ts +++ b/apps/api/src/app/shared/helpers/content.service.spec.ts @@ -3,6 +3,8 @@ import { DelayTypeEnum, DigestTypeEnum, DigestUnitEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, FilterPartTypeEnum, StepTypeEnum, TriggerContextTypeEnum, @@ -292,13 +294,13 @@ describe('ContentService', function () { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.PAYLOAD, field: 'counter', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, diff --git a/apps/api/src/app/shared/shared.module.ts b/apps/api/src/app/shared/shared.module.ts index eadfc5b2318..ebd6c710093 100644 --- a/apps/api/src/app/shared/shared.module.ts +++ b/apps/api/src/app/shared/shared.module.ts @@ -32,7 +32,6 @@ import { DalServiceHealthIndicator, distributedLockService, featureFlagsService, - getIsMultiProviderConfigurationEnabled, getIsTopicNotificationEnabled, InvalidateCacheService, LoggerModule, @@ -85,7 +84,6 @@ const PROVIDERS = [ DalServiceHealthIndicator, distributedLockService, featureFlagsService, - getIsMultiProviderConfigurationEnabled, getIsTopicNotificationEnabled, InvalidateCacheService, storageService, diff --git a/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts b/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts index 317b3c222ba..5eb27f99e63 100644 --- a/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts +++ b/apps/api/src/app/subscribers/usecases/chat-oauth-callback/chat-oauth-callback.usecase.ts @@ -84,7 +84,7 @@ export class ChatOauthCallback { } private async getEnvironment(environmentId: string): Promise { - const environment = await this.environmentRepository.findById(environmentId); + const environment = await this.environmentRepository.findOne({ _id: environmentId }); if (environment == null) { throw new NotFoundException(`Environment ID: ${environmentId} not found`); diff --git a/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts b/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts index 9691e738634..3e58fe3b16a 100644 --- a/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts +++ b/apps/api/src/app/subscribers/usecases/delete-subscriber-credentials/delete-subscriber-credentials.spec.ts @@ -55,7 +55,10 @@ describe('Delete subscriber provider credentials', function () { }) ); - let updatedSubscriber = await subscriberRepository.findById(subscriber._id); + let updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const newDiscordProvider = updatedSubscriber?.channels?.find( (channel) => channel.providerId === ChatProviderIdEnum.Discord @@ -72,7 +75,10 @@ describe('Delete subscriber provider credentials', function () { }) ); - updatedSubscriber = await subscriberRepository.findById(subscriber._id); + updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const isDiscordProviderDeleted = updatedSubscriber?.channels?.find( (channel) => channel.providerId === ChatProviderIdEnum.Discord diff --git a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts b/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts index 9dedc6f9e7f..0ed4904f0b0 100644 --- a/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts +++ b/apps/api/src/app/subscribers/usecases/update-subscriber-channel/update-subscriber-channel.spec.ts @@ -46,7 +46,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - const updatedSubscriber = await subscriberRepository.findById(subscriber._id); + const updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const newChannel = updatedSubscriber?.channels?.find( (channel) => channel.providerId === subscriberChannel.providerId @@ -85,7 +88,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - const updatedSubscriber = await subscriberRepository.findById(subscriber._id); + const updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const updatedChannel = updatedSubscriber?.channels?.find( (channel) => channel.providerId === newSlackSubscribersChannel.providerId @@ -119,7 +125,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - const updatedSubscriber = await subscriberRepository.findById(subscriber._id); + const updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const newChannel = updatedSubscriber?.channels?.find( (channel) => channel.providerId === newSlackCredentials.providerId @@ -157,7 +166,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - const updatedSubscriber = await subscriberRepository.findById(subscriber._id); + const updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const updatedChannel = updatedSubscriber?.channels?.find( (channel) => channel.providerId === ChatProviderIdEnum.Slack && channel._integrationId === integration._id @@ -186,7 +198,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - let updatedSubscriber = await subscriberRepository.findById(subscriber._id); + let updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const addedFcmToken = updatedSubscriber?.channels?.find( (channel) => channel.providerId === fcmCredentials.providerId @@ -217,7 +232,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - let updatedSubscriber = await subscriberRepository.findById(subscriber._id); + let updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const addedFcmToken = updatedSubscriber?.channels?.find( (channel) => channel.providerId === fcmCredentials.providerId @@ -237,7 +255,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - updatedSubscriber = await subscriberRepository.findById(subscriber._id); + updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); const updatedProviderWithEmptyDeviceToken = updatedSubscriber?.channels?.find( (channel) => channel.providerId === fcmCredentials.providerId @@ -262,7 +283,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - let updatedSubscriber = await subscriberRepository.findById(subscriber._id); + let updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); let updateToken = updatedSubscriber?.channels?.find((channel) => channel.providerId === PushProviderIdEnum.FCM); @@ -280,7 +304,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - updatedSubscriber = await subscriberRepository.findById(subscriber._id); + updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); updateToken = updatedSubscriber?.channels?.find((channel) => channel.providerId === PushProviderIdEnum.FCM); @@ -298,7 +325,10 @@ describe('Update Subscriber channel credentials', function () { }) ); - updatedSubscriber = await subscriberRepository.findById(subscriber._id); + updatedSubscriber = await subscriberRepository.findOne({ + _id: subscriber._id, + _environmentId: subscriber._environmentId, + }); updateToken = updatedSubscriber?.channels?.find((channel) => channel.providerId === PushProviderIdEnum.FCM); diff --git a/apps/api/src/app/testing/dtos/idempotency.dto.ts b/apps/api/src/app/testing/dtos/idempotency.dto.ts new file mode 100644 index 00000000000..6f885116da1 --- /dev/null +++ b/apps/api/src/app/testing/dtos/idempotency.dto.ts @@ -0,0 +1,3 @@ +export class IdempotencyBodyDto { + data: number; +} diff --git a/apps/api/src/app/testing/testing.controller.ts b/apps/api/src/app/testing/testing.controller.ts index f23c67f0e76..1939ca49148 100644 --- a/apps/api/src/app/testing/testing.controller.ts +++ b/apps/api/src/app/testing/testing.controller.ts @@ -1,12 +1,16 @@ -import { Body, Controller, NotFoundException, Post } from '@nestjs/common'; +import { Body, Controller, Get, HttpException, NotFoundException, Post, UseGuards } from '@nestjs/common'; import { DalService } from '@novu/dal'; import { IUserEntity } from '@novu/shared'; import { ISeedDataResponseDto, SeedDataBodyDto } from './dtos/seed-data.dto'; +import { IdempotencyBodyDto } from './dtos/idempotency.dto'; + import { SeedData } from './usecases/seed-data/seed-data.usecase'; import { SeedDataCommand } from './usecases/seed-data/seed-data.command'; import { CreateSession } from './usecases/create-session/create-session.usecase'; import { CreateSessionCommand } from './usecases/create-session/create-session.command'; import { ApiExcludeController } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/framework/auth.guard'; +import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; @Controller('/testing') @ApiExcludeController() @@ -47,4 +51,28 @@ export class TestingController { return await this.seedDataUsecase.execute(command); } + + @ExternalApiAccessible() + @UseGuards(JwtAuthGuard) + @Post('/idempotency') + async idempotency(@Body() body: IdempotencyBodyDto): Promise<{ number: number }> { + if (process.env.NODE_ENV !== 'test') throw new NotFoundException(); + + if (body.data > 300) { + throw new HttpException(`` + Math.random(), body.data); + } + if (body.data === 250) { + //for testing conflict + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + return { number: Math.random() }; + } + + @Get('/idempotency') + async idempotencyGet(): Promise<{ number: number }> { + if (process.env.NODE_ENV !== 'test') throw new NotFoundException(); + + return { number: Math.random() }; + } } diff --git a/apps/api/src/app/widgets/dtos/update-subscriber-preference-response.dto.ts b/apps/api/src/app/widgets/dtos/update-subscriber-preference-response.dto.ts index f8ceb822b6a..b737785eaac 100644 --- a/apps/api/src/app/widgets/dtos/update-subscriber-preference-response.dto.ts +++ b/apps/api/src/app/widgets/dtos/update-subscriber-preference-response.dto.ts @@ -1,7 +1,99 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { PreferenceChannels } from '../../shared/dtos/preference-channels'; +import { + INotificationTrigger, + INotificationTriggerVariable, + ITemplateConfiguration, + ITriggerReservedVariable, + TemplateVariableTypeEnum, + TriggerContextTypeEnum, + TriggerTypeEnum, +} from '@novu/shared'; -class TemplateResponse { +class Preference { + @ApiProperty({ + description: 'Sets if the workflow is fully enabled for all channels or not for the subscriber.', + type: Boolean, + }) + enabled: boolean; + + @ApiProperty({ + type: PreferenceChannels, + description: 'Subscriber preferences for the different channels regarding this workflow', + }) + channels: PreferenceChannels; +} + +export class NotificationTriggerVariableResponse implements INotificationTriggerVariable { + @ApiProperty({ + type: String, + description: 'The name of the variable', + }) + name: string; + + @ApiPropertyOptional() + @ApiProperty({ + description: 'The value of the variable', + }) + value?: any; + + @ApiPropertyOptional() + @ApiProperty({ + type: TemplateVariableTypeEnum, + description: 'The type of the variable', + }) + type?: TemplateVariableTypeEnum; +} + +export class TriggerReservedVariableResponse implements ITriggerReservedVariable { + @ApiProperty({ + type: TriggerContextTypeEnum, + description: 'The type of the reserved variable', + }) + type: TriggerContextTypeEnum; + + @ApiProperty({ + type: Array, + description: 'The reserved variables of the trigger', + }) + variables: NotificationTriggerVariableResponse[]; +} + +export class NotificationTriggerResponse implements INotificationTrigger { + @ApiProperty({ + type: TriggerTypeEnum, + description: 'The type of the trigger', + }) + type: TriggerTypeEnum; + + @ApiProperty({ + type: String, + description: 'The identifier of the trigger', + }) + identifier: string; + + @ApiProperty({ + type: Array, + description: 'The variables of the trigger', + }) + variables: NotificationTriggerVariableResponse[]; + + @ApiPropertyOptional() + @ApiProperty({ + type: Array, + description: 'The subscriber variables of the trigger', + }) + subscriberVariables?: NotificationTriggerVariableResponse[]; + + @ApiPropertyOptional() + @ApiProperty({ + type: Array, + description: 'The reserved variables of the trigger', + }) + reservedVariables?: TriggerReservedVariableResponse[]; +} + +class TemplateResponse implements ITemplateConfiguration { @ApiProperty({ description: 'Unique identifier of the workflow', type: String, @@ -20,20 +112,12 @@ class TemplateResponse { type: Boolean, }) critical: boolean; -} -class Preference { @ApiProperty({ - description: 'Sets if the workflow is fully enabled for all channels or not for the subscriber.', - type: Boolean, + description: 'Triggers are the events that will trigger the workflow.', + type: Array, }) - enabled: boolean; - - @ApiProperty({ - type: PreferenceChannels, - description: 'Subscriber preferences for the different channels regarding this workflow', - }) - channels: PreferenceChannels; + triggers: NotificationTriggerResponse[]; } export class UpdateSubscriberPreferenceResponseDto { diff --git a/apps/api/src/app/widgets/e2e/get-subscriber-preference.e2e.ts b/apps/api/src/app/widgets/e2e/get-subscriber-preference.e2e.ts index 6751742fae6..9d04e5344c9 100644 --- a/apps/api/src/app/widgets/e2e/get-subscriber-preference.e2e.ts +++ b/apps/api/src/app/widgets/e2e/get-subscriber-preference.e2e.ts @@ -23,6 +23,11 @@ describe('GET /widget/preferences', function () { const data = response.data.data[0]; + expect(data.template.name).to.exist; + expect(data.template.tags[0]).to.equal('test-tag'); + expect(data.template.critical).to.equal(false); + expect(data.template.triggers[0].identifier).to.contains('test-event-'); + expect(data.preference.channels.email).to.equal(true); expect(data.preference.channels.in_app).to.equal(true); diff --git a/apps/api/src/app/widgets/e2e/mark-as-seen.e2e.ts b/apps/api/src/app/widgets/e2e/mark-as-seen.e2e.ts index acf61255e02..bd00c8cd97e 100644 --- a/apps/api/src/app/widgets/e2e/mark-as-seen.e2e.ts +++ b/apps/api/src/app/widgets/e2e/mark-as-seen.e2e.ts @@ -52,7 +52,10 @@ describe('Mark as Seen - /widgets/messages/:messageId/seen (POST)', async () => } ); - const modifiedMessage = (await messageRepository.findById(messageId)) as MessageEntity; + const modifiedMessage = (await messageRepository.findOne({ + _id: messageId, + _environmentId: session.environment._id, + })) as MessageEntity; expect(modifiedMessage.seen).to.equal(true); expect(modifiedMessage.lastSeenDate).to.be.ok; diff --git a/apps/api/src/app/widgets/usecases/mark-action-as-done/update-message-actions.usecase.ts b/apps/api/src/app/widgets/usecases/mark-action-as-done/update-message-actions.usecase.ts index 6bda8b1c26f..e5ad3412f15 100644 --- a/apps/api/src/app/widgets/usecases/mark-action-as-done/update-message-actions.usecase.ts +++ b/apps/api/src/app/widgets/usecases/mark-action-as-done/update-message-actions.usecase.ts @@ -84,6 +84,9 @@ export class UpdateMessageActions { }); } - return (await this.messageRepository.findById(command.messageId)) as MessageEntity; + return (await this.messageRepository.findOne({ + _environmentId: command.environmentId, + _id: command.messageId, + })) as MessageEntity; } } diff --git a/apps/api/src/app/widgets/usecases/mark-all-messages-as/mark-all-messages-as.usecase.ts b/apps/api/src/app/widgets/usecases/mark-all-messages-as/mark-all-messages-as.usecase.ts index 4525f4203e2..767ea7b7d57 100644 --- a/apps/api/src/app/widgets/usecases/mark-all-messages-as/mark-all-messages-as.usecase.ts +++ b/apps/api/src/app/widgets/usecases/mark-all-messages-as/mark-all-messages-as.usecase.ts @@ -72,7 +72,6 @@ export class MarkAllMessagesAs { userId: subscriber._id, _environmentId: command.environmentId, }, - undefined, subscriber._organizationId ); diff --git a/apps/api/src/app/widgets/usecases/mark-message-as/mark-message-as.usecase.ts b/apps/api/src/app/widgets/usecases/mark-message-as/mark-message-as.usecase.ts index 6552ed675a7..35bb46f4f83 100644 --- a/apps/api/src/app/widgets/usecases/mark-message-as/mark-message-as.usecase.ts +++ b/apps/api/src/app/widgets/usecases/mark-message-as/mark-message-as.usecase.ts @@ -92,7 +92,6 @@ export class MarkMessageAs { userId: subscriber._id, _environmentId: subscriber._environmentId, }, - undefined, subscriber._organizationId ); } diff --git a/apps/api/src/app/widgets/usecases/remove-message/remove-message.usecase.ts b/apps/api/src/app/widgets/usecases/remove-message/remove-message.usecase.ts index 3e75f8ee32e..eff7b3eac25 100644 --- a/apps/api/src/app/widgets/usecases/remove-message/remove-message.usecase.ts +++ b/apps/api/src/app/widgets/usecases/remove-message/remove-message.usecase.ts @@ -105,7 +105,6 @@ export class RemoveMessage { userId: subscriber._id, _environmentId: subscriber._environmentId, }, - undefined, subscriber._organizationId ); } diff --git a/apps/api/src/app/widgets/usecases/remove-messages/remove-all-messages.usecase.ts b/apps/api/src/app/widgets/usecases/remove-messages/remove-all-messages.usecase.ts index 09d897c7f85..8025a66b4e9 100644 --- a/apps/api/src/app/widgets/usecases/remove-messages/remove-all-messages.usecase.ts +++ b/apps/api/src/app/widgets/usecases/remove-messages/remove-all-messages.usecase.ts @@ -40,7 +40,7 @@ export class RemoveAllMessages { try { let feed; if (command.feedId) { - feed = await this.feedRepository.findById(command.feedId); + feed = await this.feedRepository.findOne({ _id: command.feedId, _organizationId: command.organizationId }); if (!feed) { throw new NotFoundException(`Feed with ${command.feedId} not found`); } @@ -107,7 +107,6 @@ export class RemoveAllMessages { userId: subscriber._id, _environmentId: subscriber._environmentId, }, - undefined, subscriber._organizationId ); } diff --git a/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts b/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts index 0f404d73e90..68cb6190a25 100644 --- a/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts +++ b/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts @@ -4,6 +4,8 @@ import { ChannelCTATypeEnum, ChannelTypeEnum, EmailBlockTypeEnum, + FieldLogicalOperatorEnum, + FieldOperatorEnum, StepTypeEnum, INotificationTemplate, TriggerTypeEnum, @@ -80,13 +82,13 @@ describe('Create Workflow - /workflows (POST)', async () => { { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, @@ -504,13 +506,13 @@ export async function createTemplateFromBlueprint({ { isNegated: false, type: 'GROUP', - value: 'AND', + value: FieldLogicalOperatorEnum.AND, children: [ { on: FilterPartTypeEnum.SUBSCRIBER, field: 'firstName', value: 'test value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, }, ], }, diff --git a/apps/api/src/bootstrap.ts b/apps/api/src/bootstrap.ts index aa3be49acf6..b4f9bc22d68 100644 --- a/apps/api/src/bootstrap.ts +++ b/apps/api/src/bootstrap.ts @@ -97,7 +97,7 @@ export async function bootstrap(expressApp?): Promise { const options = new DocumentBuilder() .setTitle('Novu API') - .setDescription('The Novu API description') + .setDescription('Open API Specification for Novu API') .setVersion('1.0') .addTag('Events') .addTag('Subscribers') @@ -114,6 +114,7 @@ export async function bootstrap(expressApp?): Promise { .addTag('Feeds') .addTag('Tenants') .addTag('Messages') + .addTag('Organizations') .addTag('Execution Details') .build(); const document = SwaggerModule.createDocument(app, options); @@ -140,6 +141,7 @@ const corsOptionsDelegate = function (req, callback) { const corsOptions = { origin: false as boolean | string | string[], preflightContinue: false, + maxAge: 86400, allowedHeaders: ['Content-Type', 'Authorization', 'sentry-trace'], methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], }; diff --git a/apps/api/src/config/env-validator.ts b/apps/api/src/config/env-validator.ts index f7103cb0b52..1bcf1ef6fb1 100644 --- a/apps/api/src/config/env-validator.ts +++ b/apps/api/src/config/env-validator.ts @@ -31,6 +31,9 @@ const validators: { [K in keyof any]: ValidatorSpec } = { default: '', }), MONGO_URL: str(), + MONGO_MIN_POOL_SIZE: num({ + default: 10, + }), MONGO_MAX_POOL_SIZE: num({ default: 500, }), diff --git a/apps/api/src/types/env.d.ts b/apps/api/src/types/env.d.ts index ea3de977652..7f30bd31b6a 100644 --- a/apps/api/src/types/env.d.ts +++ b/apps/api/src/types/env.d.ts @@ -2,6 +2,7 @@ declare namespace NodeJS { // eslint-disable-next-line @typescript-eslint/naming-convention export interface ProcessEnv { MONGO_URL: string; + MONGO_MIN_POOL_SIZE: number; MONGO_MAX_POOL_SIZE: number; REDIS_URL: string; SYNC_PATH: string; @@ -10,6 +11,7 @@ declare namespace NodeJS { NODE_ENV: 'test' | 'production' | 'dev' | 'ci' | 'local'; PORT: string; DISABLE_USER_REGISTRATION: 'true' | 'false'; + IS_API_IDEMPOTENCY_ENABLED: 'true' | 'false'; FRONT_BASE_URL: string; SENTRY_DSN: string; } diff --git a/apps/inbound-mail/Dockerfile b/apps/inbound-mail/Dockerfile index 6592c77d7aa..fbe4ec0ebf6 100644 --- a/apps/inbound-mail/Dockerfile +++ b/apps/inbound-mail/Dockerfile @@ -5,7 +5,7 @@ ENV BULL_MQ_PRO_NPM_TOKEN=$BULL_MQ_PRO_TOKEN ENV NX_DAEMON=false RUN npm i pm2 -g -RUN npm --no-update-notifier --no-fund --global install pnpm@7.33.4 +RUN npm --no-update-notifier --no-fund --global install pnpm@8.9.0 RUN pnpm --version diff --git a/apps/inbound-mail/package.json b/apps/inbound-mail/package.json index d3631b027b5..6bed05dc73b 100644 --- a/apps/inbound-mail/package.json +++ b/apps/inbound-mail/package.json @@ -1,6 +1,6 @@ { "name": "@novu/inbound-mail", - "version": "0.20.0-alpha.0", + "version": "0.20.0", "description": "", "author": "", "private": true, @@ -19,8 +19,8 @@ "test": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --trace-warnings --timeout 10000 --require ts-node/register --exit --file e2e/setup.ts src/**/**/*.spec.ts" }, "dependencies": { - "@novu/application-generic": "^0.20.0-alpha.0", - "@novu/shared": "^0.20.0-alpha.0", + "@novu/application-generic": "^0.20.0", + "@novu/shared": "^0.20.0", "@sentry/node": "^7.12.1", "bluebird": "^2.9.30", "dotenv": "^8.6.0", @@ -39,17 +39,17 @@ "winston": "^3.9.0" }, "devDependencies": { - "@novu/testing": "^0.20.0-alpha.0", + "@novu/testing": "^0.20.0", "@types/chai": "^4.2.11", "@types/express": "^4.17.8", "@types/html-to-text": "^9.0.1", - "@types/mocha": "^8.2.3", + "@types/mocha": "^10.0.2", "@types/node": "^14.14.6", "@types/sinon": "^9.0.0", "@types/smtp-server": "^3.5.7", "cross-env": "^7.0.3", - "mocha": "^8.4.0", - "nodemon": "^2.0.7", + "mocha": "^10.2.0", + "nodemon": "^3.0.1", "prettier": "~2.8.0", "sinon": "^9.2.4", "ts-jest": "^27.0.7", diff --git a/apps/web/.storybook/preview.jsx b/apps/web/.storybook/preview.jsx index 410d4fe09cd..3c844f59af3 100644 --- a/apps/web/.storybook/preview.jsx +++ b/apps/web/.storybook/preview.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { useDarkMode } from 'storybook-dark-mode'; -import { ThemeProvider } from '../src/design-system/ThemeProvider'; +import { ThemeProvider } from '@novu/design-system'; import { DocsContainer } from './Doc.container'; export const parameters = { diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index ab2f64095a8..bc8805a7a98 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -5,7 +5,7 @@ ENV NX_DAEMON=false WORKDIR /usr/src/app RUN apk add --no-cache bash -RUN npm install -g pnpm@7.33.4 --loglevel notice +RUN npm install -g pnpm@8.9.0 --loglevel notice COPY .npmrc . COPY package.json . @@ -42,7 +42,7 @@ FROM node:16-alpine WORKDIR /app RUN apk add --no-cache bash -RUN npm install -g pnpm@7.33.4 http-server --loglevel notice +RUN npm install -g pnpm@8.9.0 http-server --loglevel notice COPY --from=builder /usr/src/app/apps/web/env.sh /app/env.sh COPY --from=builder /usr/src/app/apps/web/.env /app/.env diff --git a/apps/web/cypress/tests/integration-store.spec.ts b/apps/web/cypress/tests/integration-store.spec.ts deleted file mode 100644 index b09c8c906b9..00000000000 --- a/apps/web/cypress/tests/integration-store.spec.ts +++ /dev/null @@ -1,225 +0,0 @@ -// @ts-nocheck -import { ChannelTypeEnum, InAppProviderIdEnum } from '@novu/shared'; - -Cypress.on('window:before:load', (win) => { - win._cypress = { - ...win._cypress, - IS_MULTI_PROVIDER_CONFIGURATION_ENABLED: 'false', - }; -}); - -describe.skip('Integration store page', function () { - beforeEach(function () { - cy.initializeSession().as('session'); - }); - - describe('In App', () => { - it('should display in app card for connection', function () { - cy.intercept('*/integrations', { - data: [], - }); - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - cy.getByTestId('integration-group-in-app') - .getByTestId('integration-provider-card-novu') - .eq(0) - .get('[data-test-id="integration-provider-card-novu"] button') - .contains('Connect'); - }); - - it('should create integration on clicking connect', function () { - cy.intercept('*/integrations', { - data: [], - }); - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - cy.intercept( - { url: '*/integrations', method: 'post' }, - { - data: { - integrationId: 'test', - createdAt: new Date().toISOString(), - active: true, - channel: ChannelTypeEnum.IN_APP, - providerId: InAppProviderIdEnum.Novu, - }, - } - ).as('create-integration'); - - cy.getByTestId('integration-group-in-app') - .getByTestId('integration-provider-card-novu') - .eq(0) - .get('[data-test-id="integration-provider-card-novu"] button') - .click(); - - cy.wait('@create-integration'); - cy.contains('Select a framework'); - }); - - it('should show guide on clicking connect', function () { - cy.intercept('*/integrations', { - data: [], - }); - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - cy.intercept( - { url: '*/integrations', method: 'post' }, - { - data: { - integrationId: 'test', - createdAt: new Date().toISOString(), - active: true, - channel: ChannelTypeEnum.IN_APP, - providerId: InAppProviderIdEnum.Novu, - }, - } - ).as('create-integration'); - - cy.getByTestId('integration-group-in-app') - .getByTestId('integration-provider-card-novu') - .eq(0) - .get('[data-test-id="integration-provider-card-novu"] button') - .click(); - - cy.wait('@create-integration'); - cy.contains('Select a framework'); - cy.getByTestId('in-app-select-framework-react').click(); - cy.contains('React integration guide'); - cy.contains('Configure Later').click(); - cy.contains('In-App notification center'); - }); - - it('should display in app modal', function () { - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - cy.getByTestId('integration-group-in-app').getByTestId('integration-provider-card-novu').eq(0).click(); - - cy.getByTestId('connect-integration-form-active-text').contains('Active'); - cy.getByTestId('connect-integration-in-app-hmac-text').contains('Disabled'); - cy.getByTestId('connect-integration-in-app-hmac').click({ force: true }); - cy.getByTestId('connect-integration-in-app-hmac-text').contains('Active'); - - cy.getByTestId('connect-integration-form-submit').click(); - cy.visit('/integrations'); - cy.getByTestId('integration-group-in-app').getByTestId('integration-provider-card-novu').eq(0).click(); - cy.getByTestId('connect-integration-form-active-text').contains('Active'); - cy.getByTestId('connect-integration-in-app-hmac-text').contains('Active'); - }); - }); - - describe('Sendgrid', () => { - it('should display email available for connection', function () { - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - getFirstIntegrationCard().get('button').contains('Connect'); - }); - - it('should display integrated sendgrid provider', function () { - interceptSendgridIntegration(true); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('card-status-bar-active').contains('Active'); - }); - - it('should display not integrated sendgrid provider', function () { - interceptSendgridIntegration(false); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('card-status-bar-active').contains('Disabled'); - }); - - it('should display use credentials on settings modal', function () { - interceptSendgridIntegration(true); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('provider-card-settings-svg').click(); - - cy.getByTestId('apiKey').should('have.value', '123'); - cy.getByTestId('from').should('have.value', 'cypress'); - }); - }); - - describe('Nodemailer', () => { - it('should display email available for connection', function () { - cy.visit('/integrations'); - cy.location('pathname').should('equal', '/integrations'); - - getFirstIntegrationCard().get('button').contains('Connect'); - }); - - it('should display integrated nodemailer provider', function () { - interceptNodemailerIntegration(true); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('card-status-bar-active').contains('Active'); - }); - - it('should display not integrated sendgrid provider', function () { - interceptNodemailerIntegration(false); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('card-status-bar-active').contains('Disabled'); - }); - - it('should display use credentials on settings modal', function () { - interceptNodemailerIntegration(true); - - cy.visit('/integrations'); - - getFirstIntegrationCard().getByTestId('provider-card-settings-svg').click(); - - cy.getByTestId('from').should('have.value', 'cypress-nodemailer'); - cy.getByTestId('senderName').should('have.value', 'cypress-novu'); - cy.getByTestId('host').should('have.value', 'localhost.novu.co'); - }); - }); -}); - -function getFirstIntegrationCard() { - return cy.getByTestId('integration-group-email').getByTestId('integration-provider-card-sendgrid').eq(0); -} - -function interceptSendgridIntegration(isActive: boolean) { - cy.intercept('*/integrations', { - data: [ - { - channel: ChannelTypeEnum.EMAIL, - providerId: 'sendgrid', - active: isActive, - credentials: { apiKey: '123', from: 'cypress' }, - }, - ], - }); -} - -function interceptNodemailerIntegration(isActive: boolean) { - cy.intercept('*/integrations', { - data: [ - { - channel: ChannelTypeEnum.EMAIL, - providerId: 'nodemailer', - active: isActive, - credentials: { - from: 'cypress-nodemailer', - senderName: 'cypress-novu', - host: 'localhost.novu.co', - port: 587, - secure: true, - requireTls: true, - ignoreTls: false, - tlsOptions: { requireUnauthorized: false }, - }, - }, - ], - }); -} diff --git a/apps/web/cypress/tests/integrations-list-modal.spec.ts b/apps/web/cypress/tests/integrations-list-modal.spec.ts index e3a302e0f9c..5310510f31c 100644 --- a/apps/web/cypress/tests/integrations-list-modal.spec.ts +++ b/apps/web/cypress/tests/integrations-list-modal.spec.ts @@ -14,7 +14,6 @@ import { Cypress.on('window:before:load', (win) => { win._cypress = { ...win._cypress, - IS_MULTI_PROVIDER_CONFIGURATION_ENABLED: 'true', }; win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts index 03341a1623e..23684be8dc5 100644 --- a/apps/web/cypress/tests/integrations-list-page.spec.ts +++ b/apps/web/cypress/tests/integrations-list-page.spec.ts @@ -14,7 +14,6 @@ import { Cypress.on('window:before:load', (win) => { win._cypress = { ...win._cypress, - IS_MULTI_PROVIDER_CONFIGURATION_ENABLED: 'true', }; win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); diff --git a/apps/web/env.sh b/apps/web/env.sh index 38a22999589..8b43d9ed94a 100755 --- a/apps/web/env.sh +++ b/apps/web/env.sh @@ -2,6 +2,7 @@ # Recreate config file rm -rf ./env-config.js + touch ./env-config.js # Add assignment diff --git a/apps/web/package.json b/apps/web/package.json index 507a3701ea2..4bbdb3e9208 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@novu/web", - "version": "0.20.0-alpha.0", + "version": "0.20.0", "private": true, "scripts": { "start": "cross-env PORT=4200 react-app-rewired start", @@ -27,9 +27,9 @@ "@ant-design/icons": "^4.6.2", "@babel/plugin-proposal-optional-chaining": "^7.20.7", "@babel/plugin-transform-react-display-name": "^7.18.6", - "@babel/plugin-transform-runtime": "^7.19.6", - "@cypress/react": "^7.0.2", - "@cypress/webpack-dev-server": "^3.1.2", + "@babel/plugin-transform-runtime": "^7.23.2", + "@cypress/react": "^7.0.3", + "@cypress/webpack-dev-server": "^3.6.1", "@editorjs/editorjs": "^2.19.3", "@editorjs/paragraph": "^2.8.0", "@emotion/babel-plugin": "^11.7.2", @@ -49,8 +49,9 @@ "@mantine/notifications": "^5.7.1", "@mantine/prism": "^5.7.1", "@mantine/spotlight": "^5.7.1", - "@novu/notification-center": "^0.20.0-alpha.0", - "@novu/shared": "^0.20.0-alpha.0", + "@novu/design-system": "^0.20.0", + "@novu/notification-center": "^0.20.0", + "@novu/shared": "^0.20.0", "@segment/analytics-next": "^1.48.0", "@sentry/react": "^7.40.0", "@sentry/tracing": "^7.40.0", @@ -91,7 +92,6 @@ "polished": "^4.1.3", "react": "^17.0.1", "react-ace": "^9.4.3", - "react-app-rewired": "^2.2.1", "react-chartjs-2": "^4.0.1", "react-color": "^2.19.3", "react-css-theme-switcher": "^0.3.0", @@ -105,8 +105,7 @@ "react-hook-form": "7.43.9", "react-is": "^18.2.0", "react-password-strength-bar": "^0.4.1", - "react-router-dom": "^6.2.2", - "react-scripts": "^5.0.1", + "react-router-dom": "6.2.2", "react-syntax-highlighter": "^15.4.3", "react-table": "^7.8.0", "react-use-intercom": "^2.0.0", @@ -122,12 +121,12 @@ }, "devDependencies": { "@babel/polyfill": "^7.12.1", - "@babel/preset-env": "^7.13.15", + "@babel/preset-env": "^7.23.2", "@babel/preset-react": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "@babel/runtime": "^7.20.13", - "@novu/dal": "^0.20.0-alpha.0", - "@novu/testing": "^0.20.0-alpha.0", + "@novu/dal": "^0.20.0", + "@novu/testing": "^0.20.0", "@storybook/addon-actions": "^7.4.2", "@storybook/addon-essentials": "^7.4.2", "@storybook/addon-links": "^7.4.2", @@ -139,15 +138,17 @@ "@testing-library/jest-dom": "^4.2.4", "@types/react": "^17.0.0", "@types/testing-library__jest-dom": "^5.14.5", - "cypress": "^12.17.2", - "cypress-localstorage-commands": "^1.7.0", - "cypress-network-idle": "^1.11.2", - "cypress-wait-until": "^1.7.2", - "eslint-plugin-cypress": "^2.12.1", + "cypress": "^13.3.1", + "cypress-localstorage-commands": "^2.2.4", + "cypress-network-idle": "^1.14.2", + "cypress-wait-until": "^2.0.1", + "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-storybook": "^0.6.13", "http-server": "^0.13.0", "less-loader": "4.1.0", - "nodemon": "^2.0.15", + "nodemon": "^3.0.1", + "react-scripts": "^5.0.1", + "react-app-rewired": "^2.2.1", "react-error-overlay": "6.0.11", "react-joyride": "^2.5.3", "storybook": "^7.4.2", @@ -172,9 +173,6 @@ "eslint" ] }, - "resolutions": { - "react-error-overlay": "6.0.11" - }, "eslintConfig": { "overrides": [ { @@ -187,4 +185,4 @@ } ] } -} +} \ No newline at end of file diff --git a/apps/web/public/static/images/providers/dark/bandwidth.png b/apps/web/public/static/images/providers/dark/bandwidth.png new file mode 100644 index 00000000000..9c8cbb1edd2 Binary files /dev/null and b/apps/web/public/static/images/providers/dark/bandwidth.png differ diff --git a/apps/web/public/static/images/providers/dark/clicksend.png b/apps/web/public/static/images/providers/dark/clicksend.png new file mode 100644 index 00000000000..ab0a193382b Binary files /dev/null and b/apps/web/public/static/images/providers/dark/clicksend.png differ diff --git a/apps/web/public/static/images/providers/dark/generic-sms.svg b/apps/web/public/static/images/providers/dark/generic-sms.svg new file mode 100644 index 00000000000..87e11a06f5d --- /dev/null +++ b/apps/web/public/static/images/providers/dark/generic-sms.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/dark/pushpad.svg b/apps/web/public/static/images/providers/dark/pushpad.svg new file mode 100644 index 00000000000..7abc52842a3 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/pushpad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/static/images/providers/dark/ryver.png b/apps/web/public/static/images/providers/dark/ryver.png new file mode 100644 index 00000000000..ed418f8d186 Binary files /dev/null and b/apps/web/public/static/images/providers/dark/ryver.png differ diff --git a/apps/web/public/static/images/providers/dark/square/clicksend.svg b/apps/web/public/static/images/providers/dark/square/clicksend.svg new file mode 100644 index 00000000000..62b47604355 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/square/clicksend.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/static/images/providers/dark/square/generic-sms.svg b/apps/web/public/static/images/providers/dark/square/generic-sms.svg new file mode 100644 index 00000000000..87e11a06f5d --- /dev/null +++ b/apps/web/public/static/images/providers/dark/square/generic-sms.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/dark/square/pushpad.svg b/apps/web/public/static/images/providers/dark/square/pushpad.svg new file mode 100644 index 00000000000..03ae25bac83 --- /dev/null +++ b/apps/web/public/static/images/providers/dark/square/pushpad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/static/images/providers/light/bandwidth.png b/apps/web/public/static/images/providers/light/bandwidth.png new file mode 100644 index 00000000000..9c8cbb1edd2 Binary files /dev/null and b/apps/web/public/static/images/providers/light/bandwidth.png differ diff --git a/apps/web/public/static/images/providers/light/clicksend.png b/apps/web/public/static/images/providers/light/clicksend.png new file mode 100644 index 00000000000..62b4e06c3ae Binary files /dev/null and b/apps/web/public/static/images/providers/light/clicksend.png differ diff --git a/apps/web/public/static/images/providers/light/generic-sms.svg b/apps/web/public/static/images/providers/light/generic-sms.svg new file mode 100644 index 00000000000..f22ac13d527 --- /dev/null +++ b/apps/web/public/static/images/providers/light/generic-sms.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/light/pushpad.svg b/apps/web/public/static/images/providers/light/pushpad.svg new file mode 100644 index 00000000000..7abc52842a3 --- /dev/null +++ b/apps/web/public/static/images/providers/light/pushpad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/public/static/images/providers/light/ryver.png b/apps/web/public/static/images/providers/light/ryver.png new file mode 100644 index 00000000000..ed418f8d186 Binary files /dev/null and b/apps/web/public/static/images/providers/light/ryver.png differ diff --git a/apps/web/public/static/images/providers/light/square/clicksend.svg b/apps/web/public/static/images/providers/light/square/clicksend.svg new file mode 100644 index 00000000000..62b47604355 --- /dev/null +++ b/apps/web/public/static/images/providers/light/square/clicksend.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/static/images/providers/light/square/generic-sms.svg b/apps/web/public/static/images/providers/light/square/generic-sms.svg new file mode 100644 index 00000000000..f22ac13d527 --- /dev/null +++ b/apps/web/public/static/images/providers/light/square/generic-sms.svg @@ -0,0 +1 @@ + diff --git a/apps/web/public/static/images/providers/light/square/pushpad.svg b/apps/web/public/static/images/providers/light/square/pushpad.svg new file mode 100644 index 00000000000..03ae25bac83 --- /dev/null +++ b/apps/web/public/static/images/providers/light/square/pushpad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 17ad94e5914..811793820cd 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -58,8 +58,7 @@ import { UpdateTenantPage } from './pages/tenants/UpdateTenantPage'; import { ApiKeysCard } from './pages/settings/tabs'; import { EmailSettings } from './pages/settings/tabs/EmailSettings'; import { ProductLead } from './components/utils/ProductLead'; -import { SSO, UserAccess } from './design-system/icons'; -import { Cloud } from './design-system/icons/general/Cloud'; +import { SSO, UserAccess, Cloud } from '@novu/design-system'; import { BrandingForm, LayoutsListPage } from './pages/brand/tabs'; library.add(far, fas); diff --git a/apps/web/src/components/conditions/Conditions.tsx b/apps/web/src/components/conditions/Conditions.tsx index 79d621617f1..cc1e3167cb8 100644 --- a/apps/web/src/components/conditions/Conditions.tsx +++ b/apps/web/src/components/conditions/Conditions.tsx @@ -3,12 +3,28 @@ import styled from '@emotion/styled'; import { useMemo } from 'react'; import { Control, Controller, useFieldArray, useForm, useWatch } from 'react-hook-form'; -import { FILTER_TO_LABEL, FilterPartTypeEnum } from '@novu/shared'; +import { FILTER_TO_LABEL, FilterPartTypeEnum, FieldLogicalOperatorEnum, FieldOperatorEnum } from '@novu/shared'; -import { Button, colors, Dropdown, Input, Select, Sidebar, Text, Title, Tooltip } from '../../design-system'; -import { ConditionPlus, DotsHorizontal, Duplicate, Trash, Condition, ErrorIcon } from '../../design-system/icons'; -import { When } from '../utils/When'; +import { + Button, + colors, + Dropdown, + Input, + Select, + Sidebar, + Text, + Title, + Tooltip, + ConditionPlus, + DotsHorizontal, + Duplicate, + Trash, + Condition, + ErrorIcon, + When, +} from '@novu/design-system'; import { ConditionsContextEnum, ConditionsContextFields, IConditions } from './types'; +import { HEADER_HEIGHT } from '../layout/constants'; interface IConditionsForm { conditions: IConditions[]; @@ -78,6 +94,7 @@ export function Conditions({ return ( { return ( ; i - {operator !== 'IS_DEFINED' && ( + {operator !== FieldOperatorEnum.IS_DEFINED && ( { - const { payload, overrides, tenant } = step || {}; + const { payload, overrides, tenant, actorId } = step || {}; - const curlSnippet = getCurlTriggerSnippet(identifier, subscriberVariables, payload, overrides, { tenant }); + const curlSnippet = getCurlTriggerSnippet(identifier, subscriberVariables, payload, overrides, { + ...(tenant && { tenant }), + ...(actorId && { actor: { subscriberId: actorId } }), + }); return ( <> diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsAccordion.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsAccordion.tsx index 19f5765f0f2..1672ecd97b1 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsAccordion.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsAccordion.tsx @@ -2,7 +2,7 @@ import { Accordion, createStyles } from '@mantine/core'; import { ExecutionDetailsStepContent } from './ExecutionDetailsStepContent'; import { ExecutionDetailsStepHeader } from './ExecutionDetailsStepHeader'; -import { colors } from '../../design-system'; +import { colors } from '@novu/design-system'; const useStyles = createStyles((theme) => ({ control: { diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx index 5a6e0009e9f..fe6a07788e6 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.cy.tsx @@ -1,4 +1,4 @@ -import { ICondition } from '@novu/shared'; +import { FieldOperatorEnum, ICondition } from '@novu/shared'; import { TestWrapper } from '../../testing'; import { ExecutionDetailsConditionItem } from './ExecutionDetailsConditionItem'; @@ -7,7 +7,7 @@ const condition: ICondition = { field: 'test', expected: '1000', actual: '100000000', - operator: 'LARGER', + operator: FieldOperatorEnum.LARGER, passed: true, }; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.tsx index 69820ae4be4..c66fa6593d4 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsConditionItem.tsx @@ -2,8 +2,7 @@ import React from 'react'; import styled from '@emotion/styled'; import { Group, Stack } from '@mantine/core'; import { ICondition } from '@novu/shared'; -import { colors, Text, Tooltip } from '../../design-system'; -import { CheckCircle, ErrorIcon } from '../../design-system/icons'; +import { colors, Text, Tooltip, CheckCircle, ErrorIcon } from '@novu/design-system'; export function ExecutionDetailsConditionItem({ condition }: { condition: ICondition }) { const isPassed = condition.passed; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx index dab60f4c9d5..01499e7c616 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsConditions.cy.tsx @@ -1,4 +1,4 @@ -import { ICondition, TimeOperatorEnum } from '@novu/shared'; +import { FieldOperatorEnum, ICondition, TimeOperatorEnum } from '@novu/shared'; import { TestWrapper } from '../../testing'; import { ExecutionDetailsConditions } from './ExecutionDetailsConditions'; @@ -9,7 +9,7 @@ const conditions: ICondition[] = [ field: 'test', expected: '1000', actual: '100000000', - operator: 'LARGER', + operator: FieldOperatorEnum.LARGER, passed: true, }, { @@ -17,7 +17,7 @@ const conditions: ICondition[] = [ field: 'isOnline', expected: 'true', actual: 'true', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, passed: true, }, { @@ -33,7 +33,7 @@ const conditions: ICondition[] = [ field: 'test-key', expected: 'test-value', actual: 'wrong-value', - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, passed: false, }, ]; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsFooter.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsFooter.tsx index 08f2d98771b..6f2bd9c3f36 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsFooter.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsFooter.tsx @@ -2,7 +2,7 @@ import { Group } from '@mantine/core'; import styled from '@emotion/styled'; import { GotAQuestionButton } from '../utils/GotAQuestionButton'; -import { Container } from '../../design-system'; +import { Container } from '@novu/design-system'; const ActionsWrapper = styled(Container)` margin: 0; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsModal.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsModal.tsx index c77df552df7..017ce8cb0dc 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsModal.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsModal.tsx @@ -6,7 +6,7 @@ import { JobStatusEnum } from '@novu/shared'; import { ExecutionDetailsAccordion } from './ExecutionDetailsAccordion'; import { ExecutionDetailsFooter } from './ExecutionDetailsFooter'; import { getNotification } from '../../api/activity'; -import { colors, shadows, Text, Title } from '../../design-system'; +import { colors, shadows, Text, Title } from '@novu/design-system'; import { When } from '../utils/When'; import { useNotificationStatus } from '../../pages/activities/hooks/useNotificationStatus'; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsStepContent.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsStepContent.tsx index 2641ed456ef..daa5ce94714 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsStepContent.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsStepContent.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { ExecutionDetail } from './ExecutionDetail'; import { ExecutionDetailRawSnippet } from './ExecutionDetailShowRaw'; import { ExecutionDetailTrigger } from './ExecutionDetailTrigger'; -import { colors, Text } from '../../design-system'; +import { colors, Text } from '@novu/design-system'; import { When } from '../utils/When'; const ExecutionDetailsStepContentWrapper = styled.div` diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsStepHeader.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsStepHeader.tsx index 9c825e9a903..7767962e585 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsStepHeader.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsStepHeader.tsx @@ -5,8 +5,7 @@ import { StepTypeEnum, DelayTypeEnum, JobStatusEnum } from '@novu/shared'; import { ExecutionDetailsWebhookFeedback } from './ExecutionDetailsWebhookFeedback'; import { getLogoByType } from './helpers'; -import { colors, Text } from '../../design-system'; -import { CheckCircle, ErrorIcon } from '../../design-system/icons'; +import { colors, Text, CheckCircle, ErrorIcon } from '@novu/design-system'; const StepName = styled(Text)` color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.white : colors.B40)}; diff --git a/apps/web/src/components/execution-detail/ExecutionDetailsWebhookFeedback.tsx b/apps/web/src/components/execution-detail/ExecutionDetailsWebhookFeedback.tsx index 62621687a97..7588571ffe2 100644 --- a/apps/web/src/components/execution-detail/ExecutionDetailsWebhookFeedback.tsx +++ b/apps/web/src/components/execution-detail/ExecutionDetailsWebhookFeedback.tsx @@ -2,7 +2,7 @@ import { Group } from '@mantine/core'; import styled from '@emotion/styled'; import { format, parseISO } from 'date-fns'; -import { colors, Container, Text, Tooltip } from '../../design-system'; +import { colors, Container, Text, Tooltip } from '@novu/design-system'; import { mappedWebhookStatuses } from './helpers'; const WebhookFeedbackWrapper = styled(Container)` diff --git a/apps/web/src/components/execution-detail/helpers.ts b/apps/web/src/components/execution-detail/helpers.ts index 07e4175407b..f04e58edc76 100644 --- a/apps/web/src/components/execution-detail/helpers.ts +++ b/apps/web/src/components/execution-detail/helpers.ts @@ -1,7 +1,7 @@ import { ExecutionDetailsStatusEnum, StepTypeEnum } from '@novu/shared'; import { MantineTheme } from '@mantine/core'; -import { colors } from '../../design-system'; import { + colors, Chat, Check, CheckCircle, @@ -17,8 +17,8 @@ import { Sent, Sms, Timer, -} from '../../design-system/icons'; -import { WarningIcon } from '../../design-system/icons/general/WarningIcon'; + WarningIcon, +} from '@novu/design-system'; export const getColorByStatus = (theme: MantineTheme, status: ExecutionDetailsStatusEnum): string => { if (status === ExecutionDetailsStatusEnum.FAILED) { diff --git a/apps/web/src/components/layout/AppLayout.tsx b/apps/web/src/components/layout/AppLayout.tsx index ec1956a1cfe..1218d80b116 100644 --- a/apps/web/src/components/layout/AppLayout.tsx +++ b/apps/web/src/components/layout/AppLayout.tsx @@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react'; import { Outlet } from 'react-router-dom'; import styled from '@emotion/styled'; -import { ThemeProvider } from '../../design-system/ThemeProvider'; +import { ThemeProvider } from '@novu/design-system'; import { HeaderNav } from './components/HeaderNav'; import { SideNav } from './components/SideNav'; import { IntercomProvider } from 'react-use-intercom'; diff --git a/apps/web/src/components/layout/components/AuthContainer.tsx b/apps/web/src/components/layout/components/AuthContainer.tsx index 2319d913603..a939acc2dc2 100644 --- a/apps/web/src/components/layout/components/AuthContainer.tsx +++ b/apps/web/src/components/layout/components/AuthContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { colors, Text, Title, Container } from '../../../design-system'; +import { colors, Text, Title, Container } from '@novu/design-system'; import PageMeta from './PageMeta'; export default function AuthContainer({ diff --git a/apps/web/src/components/layout/components/AuthLayout.tsx b/apps/web/src/components/layout/components/AuthLayout.tsx index 6184a80538f..df223bc7e0f 100644 --- a/apps/web/src/components/layout/components/AuthLayout.tsx +++ b/apps/web/src/components/layout/components/AuthLayout.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { createStyles, Box } from '@mantine/core'; -import { ThemeProvider } from '../../../design-system/ThemeProvider'; +import { ThemeProvider } from '@novu/design-system'; import { CONTEXT_PATH } from '../../../config'; export default function AuthLayout({ children }: { children?: React.ReactNode }) { diff --git a/apps/web/src/components/layout/components/Card.tsx b/apps/web/src/components/layout/components/Card.tsx index 5592a71f575..be157ab6889 100644 --- a/apps/web/src/components/layout/components/Card.tsx +++ b/apps/web/src/components/layout/components/Card.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Container, Space, Sx } from '@mantine/core'; -import { Title } from '../../../design-system'; +import { Title } from '@novu/design-system'; const Card = ({ title, diff --git a/apps/web/src/components/layout/components/ChangesCountBadge.tsx b/apps/web/src/components/layout/components/ChangesCountBadge.tsx index 0bcca4822ec..6e222574f9a 100644 --- a/apps/web/src/components/layout/components/ChangesCountBadge.tsx +++ b/apps/web/src/components/layout/components/ChangesCountBadge.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { getChangesCount } from '../../../api/changes'; import { QueryKeys } from '../../../api/query.keys'; -import { NotificationBadge } from '../../../design-system'; +import { NotificationBadge } from '@novu/design-system'; export const ChangesCountBadge = () => { const { data: changesCount = 0 } = useQuery([QueryKeys.changesCount], getChangesCount); diff --git a/apps/web/src/components/layout/components/HeaderNav.tsx b/apps/web/src/components/layout/components/HeaderNav.tsx index fb12825af38..2706d08d591 100644 --- a/apps/web/src/components/layout/components/HeaderNav.tsx +++ b/apps/web/src/components/layout/components/HeaderNav.tsx @@ -7,8 +7,20 @@ import { useIntercom } from 'react-use-intercom'; import LogRocket from 'logrocket'; import { CONTEXT_PATH, INTERCOM_APP_ID, IS_DOCKER_HOSTED, LOGROCKET_ID, REACT_APP_VERSION } from '../../../config'; import { ROUTES } from '../../../constants/routes.enum'; -import { colors, Dropdown, shadows, Text, Tooltip } from '../../../design-system'; -import { Ellipse, Mail, Moon, Question, Sun, Trash } from '../../../design-system/icons'; +import { + colors, + Dropdown, + shadows, + Text, + Tooltip, + Ellipse, + Mail, + Moon, + Question, + Sun, + Logout, +} from '@novu/design-system'; + import { useLocalThemePreference } from '../../../hooks'; import { discordInviteUrl } from '../../../pages/quick-start/consts'; import { useAuthContext } from '../../providers/AuthProvider'; @@ -111,7 +123,7 @@ export function HeaderNav({ isIntercomOpened }: Props) { { id: 'sign-out', title: 'Sign out', - icon: , + icon: , onTrigger: () => { logout(); }, @@ -153,8 +165,8 @@ export function HeaderNav({ isIntercomOpened }: Props) { )), - } onClick={logout} data-test-id="logout-button"> - Sign Out + } onClick={logout} data-test-id="logout-button"> + Log Out , ]; diff --git a/apps/web/src/components/layout/components/OrganizationSelect.tsx b/apps/web/src/components/layout/components/OrganizationSelect.tsx index a5baffedcfd..125c77a28c9 100644 --- a/apps/web/src/components/layout/components/OrganizationSelect.tsx +++ b/apps/web/src/components/layout/components/OrganizationSelect.tsx @@ -4,7 +4,7 @@ import * as capitalize from 'lodash.capitalize'; import styled from '@emotion/styled'; import { IOrganizationEntity } from '@novu/shared'; -import { Select } from '../../../design-system'; +import { Select } from '@novu/design-system'; import { addOrganization, switchOrganization } from '../../../api/organization'; import { useAuthContext } from '../../providers/AuthProvider'; import { useSpotlightContext } from '../../providers/SpotlightProvider'; diff --git a/apps/web/src/components/layout/components/PageContainer.tsx b/apps/web/src/components/layout/components/PageContainer.tsx index e6b6578b977..ddc17a77987 100644 --- a/apps/web/src/components/layout/components/PageContainer.tsx +++ b/apps/web/src/components/layout/components/PageContainer.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import React, { CSSProperties } from 'react'; -import { Container } from '../../../design-system'; +import { Container } from '@novu/design-system'; import PageMeta from './PageMeta'; function PageContainer({ diff --git a/apps/web/src/components/layout/components/PageHeader.tsx b/apps/web/src/components/layout/components/PageHeader.tsx index 58c443d2fb5..a6bb8a11e41 100644 --- a/apps/web/src/components/layout/components/PageHeader.tsx +++ b/apps/web/src/components/layout/components/PageHeader.tsx @@ -1,5 +1,5 @@ import { Group } from '@mantine/core'; -import { Title, Container } from '../../../design-system'; +import { Title, Container } from '@novu/design-system'; function PageHeader({ actions, title }: { actions?: JSX.Element; title: string }) { return ( diff --git a/apps/web/src/components/layout/components/PolishingBanner.tsx b/apps/web/src/components/layout/components/PolishingBanner.tsx index 2c65a702a7a..f844ac709c5 100644 --- a/apps/web/src/components/layout/components/PolishingBanner.tsx +++ b/apps/web/src/components/layout/components/PolishingBanner.tsx @@ -1,6 +1,6 @@ import { useMantineTheme } from '@mantine/core'; import { useSegment } from '../../providers/SegmentProvider'; -import { Close } from '../../../design-system/icons/actions/Close'; +import { Close } from '@novu/design-system'; import styled from '@emotion/styled'; import { useLocalStorage } from '@mantine/hooks'; diff --git a/apps/web/src/components/layout/components/SideNav.tsx b/apps/web/src/components/layout/components/SideNav.tsx index 23b5ded3eaa..4b72b0a7e7e 100644 --- a/apps/web/src/components/layout/components/SideNav.tsx +++ b/apps/web/src/components/layout/components/SideNav.tsx @@ -12,7 +12,7 @@ import { useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { ROUTES } from '../../../constants/routes.enum'; -import { colors, NavMenu, SegmentedControl, shadows } from '../../../design-system'; +import { colors, NavMenu, SegmentedControl, shadows } from '@novu/design-system'; import { Activity, Bolt, @@ -24,7 +24,7 @@ import { Repeat, Settings, Team, -} from '../../../design-system/icons'; +} from '@novu/design-system'; import { useEnvController, useIsMultiTenancyEnabled } from '../../../hooks'; import { currentOnboardingStep } from '../../../pages/quick-start/components/route/store'; import { useSpotlightContext } from '../../providers/SpotlightProvider'; diff --git a/apps/web/src/components/quick-start/common/Workflow.styles.tsx b/apps/web/src/components/quick-start/common/Workflow.styles.tsx index 2a6bdba47ce..a75eef68611 100644 --- a/apps/web/src/components/quick-start/common/Workflow.styles.tsx +++ b/apps/web/src/components/quick-start/common/Workflow.styles.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; export const WorkflowWrapper = styled.div<{ height: string }>` height: ${({ height }) => height}; diff --git a/apps/web/src/components/quick-start/digest-demo-flow/DigestNode.tsx b/apps/web/src/components/quick-start/digest-demo-flow/DigestNode.tsx index c8034dd50af..5524b4bfabe 100644 --- a/apps/web/src/components/quick-start/digest-demo-flow/DigestNode.tsx +++ b/apps/web/src/components/quick-start/digest-demo-flow/DigestNode.tsx @@ -3,10 +3,9 @@ import { createStyles, NumberInput, Loader } from '@mantine/core'; import styled from '@emotion/styled'; import { NodeStepWithPopover } from './NodeStepWithPopover'; -import { CountdownTimer, DigestAction } from '../../../design-system/icons'; +import { CountdownTimer, DigestAction, colors } from '@novu/design-system'; import { useDigestDemoFlowContext } from './DigestDemoFlowProvider'; import { Indicator } from './Indicator'; -import { colors } from '../../../design-system'; import { useEffect, useState } from 'react'; import { useInterval } from '@mantine/hooks'; import { useDataRef } from '../../../hooks'; diff --git a/apps/web/src/components/quick-start/digest-demo-flow/EmailNode.tsx b/apps/web/src/components/quick-start/digest-demo-flow/EmailNode.tsx index 87ae2ff20ee..27a67385bb0 100644 --- a/apps/web/src/components/quick-start/digest-demo-flow/EmailNode.tsx +++ b/apps/web/src/components/quick-start/digest-demo-flow/EmailNode.tsx @@ -1,7 +1,7 @@ import { Handle, Position } from 'react-flow-renderer'; import { NodeStepWithPopover } from './NodeStepWithPopover'; -import { EmailFilled } from '../../../design-system/icons'; +import { EmailFilled } from '@novu/design-system'; import { useDigestDemoFlowContext } from './DigestDemoFlowProvider'; import { Indicator } from './Indicator'; import { useAuthContext } from '../../providers/AuthProvider'; diff --git a/apps/web/src/components/quick-start/digest-demo-flow/Indicator.tsx b/apps/web/src/components/quick-start/digest-demo-flow/Indicator.tsx index 87ebf79239f..2bf34ef7877 100644 --- a/apps/web/src/components/quick-start/digest-demo-flow/Indicator.tsx +++ b/apps/web/src/components/quick-start/digest-demo-flow/Indicator.tsx @@ -1,7 +1,7 @@ import { useRef } from 'react'; import styled from '@emotion/styled'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; const IndicatorHolder = styled.span<{ isShown }>` display: flex; diff --git a/apps/web/src/components/quick-start/digest-demo-flow/NodeStepWithPopover.tsx b/apps/web/src/components/quick-start/digest-demo-flow/NodeStepWithPopover.tsx index 9e5f54c65be..ac18e851f35 100644 --- a/apps/web/src/components/quick-start/digest-demo-flow/NodeStepWithPopover.tsx +++ b/apps/web/src/components/quick-start/digest-demo-flow/NodeStepWithPopover.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { Popover } from '../../../design-system'; +import { Popover } from '@novu/design-system'; import { guidePreview, guidePlayground, @@ -90,7 +90,7 @@ export function NodeStepWithPopover({ transitionDuration={600} opacity={getOpacity(id, hoveredHintId, sequence)} target={ - + } @@ -130,7 +130,7 @@ function useCounter() { const StyledDiv = styled.div` svg { - stop:first-child { + stop:first-of-type { stop-color: #dd2476 !important; } stop:last-child { @@ -139,7 +139,7 @@ const StyledDiv = styled.div` } [data-blue-gradient-svg] { - stop:first-child { + stop:first-of-type { stop-color: #4c6dd4 !important; } stop:last-child { diff --git a/apps/web/src/components/quick-start/digest-demo-flow/TriggerNode.tsx b/apps/web/src/components/quick-start/digest-demo-flow/TriggerNode.tsx index c2818b00b4c..86cd936a5cf 100644 --- a/apps/web/src/components/quick-start/digest-demo-flow/TriggerNode.tsx +++ b/apps/web/src/components/quick-start/digest-demo-flow/TriggerNode.tsx @@ -1,7 +1,6 @@ import { Handle, Position } from 'react-flow-renderer'; -import { Button } from '../../../design-system'; -import { BoltOutlinedGradient } from '../../../design-system/icons'; +import { Button, BoltOutlinedGradient } from '@novu/design-system'; import { useDigestDemoFlowContext } from './DigestDemoFlowProvider'; import { NodeStepWithPopover } from './NodeStepWithPopover'; diff --git a/apps/web/src/components/quick-start/in-app-onboarding/InAppNode.tsx b/apps/web/src/components/quick-start/in-app-onboarding/InAppNode.tsx index 47dba5055ae..fcf04c549ff 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/InAppNode.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/InAppNode.tsx @@ -1,5 +1,5 @@ import { Handle, Position } from 'react-flow-renderer'; -import { InAppFilled } from '../../../design-system/icons'; +import { InAppFilled } from '@novu/design-system'; import { NodeStep } from '../../workflow'; export function InAppNode({ data }: { data: { label: string; email?: string } }) { diff --git a/apps/web/src/components/quick-start/in-app-onboarding/InAppSandbox.tsx b/apps/web/src/components/quick-start/in-app-onboarding/InAppSandbox.tsx index 3da02832a07..8d2156baaf9 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/InAppSandbox.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/InAppSandbox.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { Group, Overlay } from '@mantine/core'; -import { colors, shadows, Text } from '../../../design-system'; +import { colors, shadows, Text } from '@novu/design-system'; import InAppSandboxWorkflow from './InAppSandboxWorkflow'; import { YourAppHeaderSection } from './YourAppHeaderSection'; diff --git a/apps/web/src/components/quick-start/in-app-onboarding/InAppSandboxWorkflow.tsx b/apps/web/src/components/quick-start/in-app-onboarding/InAppSandboxWorkflow.tsx index def48ce9a4b..55562e41d9b 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/InAppSandboxWorkflow.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/InAppSandboxWorkflow.tsx @@ -2,8 +2,7 @@ import ReactFlow, { Background, BackgroundVariant, Edge, Node } from 'react-flow import { useMantineColorScheme } from '@mantine/core'; -import styled from '@emotion/styled'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { InAppNode } from './InAppNode'; import { TriggerNode } from './TriggerNode'; import { WorkflowWrapper } from '../common'; diff --git a/apps/web/src/components/quick-start/in-app-onboarding/SandboxFooter.tsx b/apps/web/src/components/quick-start/in-app-onboarding/SandboxFooter.tsx index 42d1da4192c..bc1d434aa73 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/SandboxFooter.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/SandboxFooter.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../../../constants/routes.enum'; -import { Button, colors, shadows } from '../../../design-system'; +import { Button, colors, shadows } from '@novu/design-system'; import { OnBoardingAnalyticsEnum } from '../../../pages/quick-start/consts'; import { useSegment } from '../../providers/SegmentProvider'; diff --git a/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx b/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx index e57bd774f1b..35c10e0d5c2 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/TriggerNode.tsx @@ -1,6 +1,6 @@ import { Handle, Position } from 'react-flow-renderer'; -import { Button, colors, shadows, Text, Title } from '../../../design-system'; +import { Button, colors, shadows, Text, Title, BoltOutlinedGradient, Playground } from '@novu/design-system'; import styled from '@emotion/styled'; import { createStyles, Group, Popover, Stack, useMantineColorScheme } from '@mantine/core'; @@ -18,7 +18,6 @@ import { import { NodeStep } from '../../workflow'; import { useSegment } from '../../providers/SegmentProvider'; import { errorMessage } from '../../../utils/notifications'; -import { BoltOutlinedGradient, Playground } from '../../../design-system/icons'; import { TemplateCreationSourceEnum } from '../../../pages/templates/shared'; const useStyles = createStyles((theme) => ({ diff --git a/apps/web/src/components/quick-start/in-app-onboarding/YourAppHeaderSection.tsx b/apps/web/src/components/quick-start/in-app-onboarding/YourAppHeaderSection.tsx index e05b2fbeaca..9e4ae127c4e 100644 --- a/apps/web/src/components/quick-start/in-app-onboarding/YourAppHeaderSection.tsx +++ b/apps/web/src/components/quick-start/in-app-onboarding/YourAppHeaderSection.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; import { createStyles, Popover } from '@mantine/core'; -import { colors, shadows } from '../../../design-system'; -import { Bell, User } from '../../../design-system/icons'; +import { colors, shadows, Bell, User } from '@novu/design-system'; import { SandboxNotificationCenter } from './SandboxNotificationCenter'; const useStyles = createStyles((theme) => ({ diff --git a/apps/web/src/components/utils/GotAQuestionButton.tsx b/apps/web/src/components/utils/GotAQuestionButton.tsx index 10d9c2b7e2b..ba248d43886 100644 --- a/apps/web/src/components/utils/GotAQuestionButton.tsx +++ b/apps/web/src/components/utils/GotAQuestionButton.tsx @@ -1,6 +1,6 @@ import { useIntercom } from 'react-use-intercom'; import { INTERCOM_APP_ID } from '../../config'; -import { Button, Size } from '../../design-system'; +import { Button, Size } from '@novu/design-system'; interface GotAQuestionButtonProps { mt: number; diff --git a/apps/web/src/components/utils/ProductLead.tsx b/apps/web/src/components/utils/ProductLead.tsx index bdd6c735b8e..5598551b970 100644 --- a/apps/web/src/components/utils/ProductLead.tsx +++ b/apps/web/src/components/utils/ProductLead.tsx @@ -4,8 +4,7 @@ import { CSSProperties, ReactNode, useEffect } from 'react'; import styled from '@emotion/styled'; import { IS_DOCKER_HOSTED } from '../../config'; -import { Button, colors, Text } from '../../design-system'; -import { Calendar, Close } from '../../design-system/icons'; +import { Button, colors, Text, Calendar, Close } from '@novu/design-system'; import { useAuthContext } from '../providers/AuthProvider'; import { useSegment } from '../providers/SegmentProvider'; import { When } from './When'; diff --git a/apps/web/src/components/utils/Spotlight.tsx b/apps/web/src/components/utils/Spotlight.tsx index 44409dc630d..a51b6934978 100644 --- a/apps/web/src/components/utils/Spotlight.tsx +++ b/apps/web/src/components/utils/Spotlight.tsx @@ -1,7 +1,7 @@ import { SpotlightProvider } from '@mantine/spotlight'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Activity, Bolt, Box, Settings, Repeat, Team, Brand, Chat } from '../../design-system/icons'; +import { Activity, Bolt, Box, Settings, Repeat, Team, Brand, Chat } from '@novu/design-system'; import { useSpotlightContext } from '../providers/SpotlightProvider'; import { ROUTES } from '../../constants/routes.enum'; diff --git a/apps/web/src/components/workflow/FlowEditor.tsx b/apps/web/src/components/workflow/FlowEditor.tsx index e24dc4fe32c..7509ece23d2 100644 --- a/apps/web/src/components/workflow/FlowEditor.tsx +++ b/apps/web/src/components/workflow/FlowEditor.tsx @@ -20,7 +20,7 @@ import { v4 as uuid4 } from 'uuid'; import cloneDeep from 'lodash.clonedeep'; import { StepTypeEnum } from '@novu/shared'; -import { colors } from '../../design-system'; +import { colors } from '@novu/design-system'; import { getChannel } from '../../utils/channels'; import { useEnvController } from '../../hooks'; import type { IEdge, IFlowStep } from './types'; @@ -88,7 +88,6 @@ export function FlowEditor({ const [edges, setEdges, onEdgesChange] = useEdgesState([]); const reactFlowInstance = useReactFlow(); const { readonly } = useEnvController(); - const [displayEdgeTimeout, setDisplayEdgeTimeout] = useState>(new Map()); useEffect(() => { const clientWidth = reactFlowWrapper.current?.clientWidth; @@ -245,39 +244,16 @@ export function FlowEditor({ }; } - const handleDisplayAddNodeOnEdge = (edgeId: string) => { + const handleMouseEnterEdgeOrNode = (edgeId: string) => { const edgeElement = document.getElementById(edgeId); - if (!edgeElement) return; - const ADD_NODE_DISPLAY_TIMEOUT = 10000; - - if (isEdgeAddNodeButtonVisible(edgeElement)) { - const nodeTimeout = displayEdgeTimeout.get(edgeId); - - if (nodeTimeout) { - clearTimeout(nodeTimeout); - setDisplayEdgeTimeout(displayEdgeTimeout.set(edgeId, null)); - } - } else { - toggleAddNodeButtonOpacity(edgeElement); - } - - setDisplayEdgeTimeout( - displayEdgeTimeout.set( - edgeId, - setTimeout(() => { - toggleAddNodeButtonOpacity(edgeElement); - }, ADD_NODE_DISPLAY_TIMEOUT) - ) - ); - - function toggleAddNodeButtonOpacity(target) { - target.classList.toggle('fade'); - } + edgeElement.classList.add('fade'); + }; - function isEdgeAddNodeButtonVisible(element: HTMLElement) { - return element?.classList.contains('fade'); - } + const handleMouseLeaveEdgeOrNode = (edgeId: string) => { + const edgeElement = document.getElementById(edgeId); + if (!edgeElement) return; + edgeElement.classList.remove('fade'); }; return ( @@ -293,14 +269,24 @@ export function FlowEditor({ onEdgesChange={onEdgesChange} onDrop={onDrop} onDragOver={onDragOver} - onNodeMouseMove={(event, node) => { + onNodeMouseEnter={(event, node) => { + if (!readonly) { + handleMouseEnterEdgeOrNode(`edge-button-${node.id}`); + } + }} + onNodeMouseLeave={(event, node) => { + if (!readonly) { + handleMouseLeaveEdgeOrNode(`edge-button-${node.id}`); + } + }} + onEdgeMouseEnter={(event: ReactMouseEvent, edge: Edge) => { if (!readonly) { - handleDisplayAddNodeOnEdge(`edge-button-${node.id}`); + handleMouseEnterEdgeOrNode(`edge-button-${edge.source}`); } }} - onEdgeMouseMove={(event: ReactMouseEvent, edge: Edge) => { + onEdgeMouseLeave={(event: ReactMouseEvent, edge: Edge) => { if (!readonly) { - handleDisplayAddNodeOnEdge(`edge-button-${edge.source}`); + handleMouseLeaveEdgeOrNode(`edge-button-${edge.source}`); } }} /* @@ -346,7 +332,7 @@ const Wrapper = styled.div<{ dark: boolean }>` cursor: pointer; [data-blue-gradient-svg] { - stop:first-child { + stop:first-of-type { stop-color: #4c6dd4 !important; } stop:last-child { @@ -407,7 +393,7 @@ const Wrapper = styled.div<{ dark: boolean }>` } [data-template-store-editor] [data-workflow-node-icon] { - stop:first-child { + stop:first-of-type { stop-color: #dd2476 !important; } stop:last-child { diff --git a/apps/web/src/components/workflow/NodeStep.tsx b/apps/web/src/components/workflow/NodeStep.tsx index b505fbcafe5..c168161b039 100644 --- a/apps/web/src/components/workflow/NodeStep.tsx +++ b/apps/web/src/components/workflow/NodeStep.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import React from 'react'; -import { colors, shadows, Text } from '../../design-system'; +import { colors, shadows, Text } from '@novu/design-system'; type NodeStepProps = { className?: string; @@ -44,7 +44,7 @@ const LeftContent = styled.div` align-items: center; gap: 15px; svg { - stop:first-child { + stop:first-of-type { stop-color: #dd2476 !important; } stop:last-child { diff --git a/apps/web/src/config/index.ts b/apps/web/src/config/index.ts index 7fc49a1cc54..b7273864741 100644 --- a/apps/web/src/config/index.ts +++ b/apps/web/src/config/index.ts @@ -62,15 +62,6 @@ export const IS_TEMPLATE_STORE_ENABLED = isCypress ? window._env_.IS_TEMPLATE_STORE_ENABLED || process.env.IS_TEMPLATE_STORE_ENABLED || 'true' : window._env_.IS_TEMPLATE_STORE_ENABLED || process.env.IS_TEMPLATE_STORE_ENABLED || 'false'; -export const IS_MULTI_PROVIDER_CONFIGURATION_ENABLED = isCypress - ? window._cypress?.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED || - window._env_.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED || - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED || - 'true' - : window._env_.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED || - process.env.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED || - 'false'; - export const IS_MULTI_TENANCY_ENABLED = isCypress ? window._env_.IS_MULTI_TENANCY_ENABLED || process.env.IS_MULTI_TENANCY_ENABLED || 'true' : window._env_.IS_MULTI_TENANCY_ENABLED || process.env.IS_MULTI_TENANCY_ENABLED || 'false'; diff --git a/apps/web/src/design-system/template-button/Button.tsx b/apps/web/src/design-system/template-button/Button.tsx deleted file mode 100644 index 90f3aa8d30c..00000000000 --- a/apps/web/src/design-system/template-button/Button.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import styled from '@emotion/styled'; -import { UnstyledButton, UnstyledButtonProps, createPolymorphicComponent } from '@mantine/core'; - -export const Button = createPolymorphicComponent<'button', UnstyledButtonProps>( - React.forwardRef((props, ref) => { - return ; - }) -); - -export const WrapperButton: any = styled(UnstyledButton)` - position: relative; - - @media screen and (max-width: 1400px) { - padding: 0 5px; - } -`; diff --git a/apps/web/src/design-system/emotion.d.ts b/apps/web/src/emotion.d.ts similarity index 100% rename from apps/web/src/design-system/emotion.d.ts rename to apps/web/src/emotion.d.ts diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts index e2661f2991d..c49c6ca05ca 100644 --- a/apps/web/src/hooks/index.ts +++ b/apps/web/src/hooks/index.ts @@ -8,7 +8,6 @@ export * from './useFeatureFlags'; export * from './useIsMounted'; export * from './useLayouts'; export * from './useLayoutsEditor'; -export * from './useLocalThemePreference'; export * from './useNotificationGroup'; export * from './useNovu'; export * from './useProcessVariables'; @@ -19,7 +18,6 @@ export * from './useVariablesManager'; export * from './useVercelIntegration'; export * from './useVercelParams'; export * from './useEffectOnce'; -export * from './useDataRef'; export * from './useInlineComponent'; export * from './useHoverOverItem'; -export * from './useKeyDown'; +export { useDataRef, useKeyDown, useLocalThemePreference } from '@novu/design-system'; diff --git a/apps/web/src/hooks/integrations/useGetPrimaryIntegration.ts b/apps/web/src/hooks/integrations/useGetPrimaryIntegration.ts index d0d1f320055..5de44162cb6 100644 --- a/apps/web/src/hooks/integrations/useGetPrimaryIntegration.ts +++ b/apps/web/src/hooks/integrations/useGetPrimaryIntegration.ts @@ -1,6 +1,5 @@ import { ChannelTypeEnum } from '@novu/shared'; import { useMemo } from 'react'; -import { useIsMultiProviderConfigurationEnabled } from '../useFeatureFlags'; import { useHasActiveIntegrations } from './useHasActiveIntegrations'; type UseHasPrimaryIntegrationProps = { @@ -13,7 +12,6 @@ export function useGetPrimaryIntegration({ filterByEnv = true, channelType }: Us () => channelType === ChannelTypeEnum.EMAIL || channelType === ChannelTypeEnum.SMS, [channelType] ); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const { activeIntegrationsByEnv, hasActiveIntegration } = useHasActiveIntegrations({ filterByEnv, @@ -25,14 +23,9 @@ export function useGetPrimaryIntegration({ filterByEnv = true, channelType }: Us return undefined; } - if (!isMultiProviderConfigurationEnabled) { - return activeIntegrationsByEnv?.find((integration) => integration.channel === channelType && integration.active) - ?.providerId; - } - return activeIntegrationsByEnv?.find((integration) => integration.primary && integration.channel === channelType) ?.providerId; - }, [isMultiProviderConfigurationEnabled, hasActiveIntegration, activeIntegrationsByEnv, channelType, isPrimaryStep]); + }, [hasActiveIntegration, activeIntegrationsByEnv, channelType, isPrimaryStep]); return { primaryIntegration: getPrimaryIntegration, diff --git a/apps/web/src/hooks/useDebounce.ts b/apps/web/src/hooks/useDebounce.ts index b3fa283e253..d37519bf12d 100644 --- a/apps/web/src/hooks/useDebounce.ts +++ b/apps/web/src/hooks/useDebounce.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; import debounce from 'lodash.debounce'; -import { useDataRef } from './useDataRef'; +import { useDataRef } from '@novu/design-system'; export const useDebounce = (callback: (args: Arguments) => void, ms = 0) => { const callbackRef = useDataRef(callback); diff --git a/apps/web/src/hooks/useFeatureFlags.ts b/apps/web/src/hooks/useFeatureFlags.ts index adce50bda95..71c6e909cc1 100644 --- a/apps/web/src/hooks/useFeatureFlags.ts +++ b/apps/web/src/hooks/useFeatureFlags.ts @@ -1,11 +1,7 @@ import { FeatureFlagsKeysEnum } from '@novu/shared'; import { useFlags } from 'launchdarkly-react-client-sdk'; -import { - IS_TEMPLATE_STORE_ENABLED, - IS_MULTI_PROVIDER_CONFIGURATION_ENABLED, - IS_MULTI_TENANCY_ENABLED, -} from '../config'; +import { IS_TEMPLATE_STORE_ENABLED, IS_MULTI_TENANCY_ENABLED } from '../config'; const prepareBooleanStringFeatureFlag = (value: string | undefined, defaultValue: boolean): boolean => { const preparedValue = value === 'true'; @@ -29,18 +25,6 @@ export const useIsTemplateStoreEnabled = (): boolean => { return isTemplateStoreEnabled ?? defaultValue; }; -export const useIsMultiProviderConfigurationEnabled = (): boolean => { - const value = IS_MULTI_PROVIDER_CONFIGURATION_ENABLED; - const fallbackValue = false; - const defaultValue = prepareBooleanStringFeatureFlag(value, fallbackValue); - - const isMultiProviderConfigurationEnabled = useGetFlagByKey( - FeatureFlagsKeysEnum.IS_MULTI_PROVIDER_CONFIGURATION_ENABLED - ); - - return isMultiProviderConfigurationEnabled ?? defaultValue; -}; - export const useIsMultiTenancyEnabled = (): boolean => { const value = IS_MULTI_TENANCY_ENABLED; const fallbackValue = false; diff --git a/apps/web/src/hooks/useInlineComponent.tsx b/apps/web/src/hooks/useInlineComponent.tsx index cec00f743d9..82a63fb5a35 100644 --- a/apps/web/src/hooks/useInlineComponent.tsx +++ b/apps/web/src/hooks/useInlineComponent.tsx @@ -1,7 +1,6 @@ import { useMemo } from 'react'; import type { ComponentType, ReactNode } from 'react'; - -import { useDataRef } from './useDataRef'; +import { useDataRef } from '@novu/design-system'; export const useInlineComponent: ( Component: ComponentType, diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 4387f5d6281..d71181aa865 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -15,5 +15,4 @@ ReactDOM.render( * to log results (for example: reportWebVitals(console.log)) * or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals . */ - reportWebVitals(); diff --git a/apps/web/src/pages/activities/ActivitiesPage.tsx b/apps/web/src/pages/activities/ActivitiesPage.tsx index a49f86a57c1..66c7bb1f0f4 100644 --- a/apps/web/src/pages/activities/ActivitiesPage.tsx +++ b/apps/web/src/pages/activities/ActivitiesPage.tsx @@ -8,7 +8,7 @@ import { useTemplates, useDebounce } from '../../hooks'; import { getActivityList } from '../../api/activity'; import PageContainer from '../../components/layout/components/PageContainer'; import PageHeader from '../../components/layout/components/PageHeader'; -import { Select, Input, Button } from '../../design-system'; +import { Select, Input, Button } from '@novu/design-system'; import { ActivityStatistics } from './components/ActivityStatistics'; import { ActivityGraph } from './components/ActivityGraph'; import { ActivityList } from './components/ActivityList'; diff --git a/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx b/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx index b08a5dd1a96..e8268c8ec07 100644 --- a/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx +++ b/apps/web/src/pages/activities/components/ActivityGraphGlobalStyles.tsx @@ -1,5 +1,5 @@ import { css, Global } from '@emotion/react'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; export function ActivityGraphGlobalStyles({ isTriggerSent, isDark }: { isTriggerSent: boolean; isDark: boolean }) { return ; diff --git a/apps/web/src/pages/activities/components/ActivityItem.tsx b/apps/web/src/pages/activities/components/ActivityItem.tsx index 909a06043ad..582e5663be5 100644 --- a/apps/web/src/pages/activities/components/ActivityItem.tsx +++ b/apps/web/src/pages/activities/components/ActivityItem.tsx @@ -8,8 +8,7 @@ import { ActivityStep } from './ActivityStep'; import { DigestedStep } from './DigestedStep'; import { When } from '../../../components/utils/When'; -import { colors } from '../../../design-system'; -import { CheckCircle, ErrorIcon, Timer } from '../../../design-system/icons'; +import { colors, CheckCircle, ErrorIcon, Timer } from '@novu/design-system'; import { useNotificationStatus } from '../hooks/useNotificationStatus'; const JOB_LENGTH_UPPER_THRESHOLD = 3; diff --git a/apps/web/src/pages/activities/components/ActivityList.tsx b/apps/web/src/pages/activities/components/ActivityList.tsx index 7a4ed20e9c0..4bc6a3b0044 100644 --- a/apps/web/src/pages/activities/components/ActivityList.tsx +++ b/apps/web/src/pages/activities/components/ActivityList.tsx @@ -2,8 +2,7 @@ import React from 'react'; import { Button, LoadingOverlay, Pagination, useMantineColorScheme } from '@mantine/core'; import { ActivityItem } from './ActivityItem'; -import { colors } from '../../../design-system'; -import { ChevronLeft, ChevronRight } from '../../../design-system/icons'; +import { colors, ChevronLeft, ChevronRight } from '@novu/design-system'; export type Data = Record; diff --git a/apps/web/src/pages/activities/components/ActivityStatistics.tsx b/apps/web/src/pages/activities/components/ActivityStatistics.tsx index 7eacabeaa5a..415327a8ccb 100644 --- a/apps/web/src/pages/activities/components/ActivityStatistics.tsx +++ b/apps/web/src/pages/activities/components/ActivityStatistics.tsx @@ -4,7 +4,7 @@ import { Skeleton, useMantineTheme } from '@mantine/core'; import { getActivityStats } from '../../../api/activity'; import { formatNumber } from '../../../utils'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; export function ActivityStatistics() { const { data: activityStats } = useQuery<{ diff --git a/apps/web/src/pages/activities/components/ActivityStep.tsx b/apps/web/src/pages/activities/components/ActivityStep.tsx index 3a65b1b9e99..2579567f13b 100644 --- a/apps/web/src/pages/activities/components/ActivityStep.tsx +++ b/apps/web/src/pages/activities/components/ActivityStep.tsx @@ -2,10 +2,20 @@ import { Grid, Text, useMantineTheme } from '@mantine/core'; import { JobStatusEnum, StepTypeEnum } from '@novu/shared'; import * as capitalize from 'lodash.capitalize'; import styled from '@emotion/styled'; -import { colors, shadows } from '../../../design-system'; -import { CheckCircle, ErrorIcon } from '../../../design-system/icons'; +import { + colors, + shadows, + CheckCircle, + ErrorIcon, + Digest, + Mail, + Mobile, + Chat, + Sms, + InApp, + Timer, +} from '@novu/design-system'; import { When } from '../../../components/utils/When'; -import { Digest, Mail, Mobile, Chat, Sms, InApp, Timer } from '../../../design-system/icons'; const TypeIcon = ({ type }: { type: StepTypeEnum }) => { const theme = useMantineTheme(); diff --git a/apps/web/src/pages/activities/components/DigestedStep.tsx b/apps/web/src/pages/activities/components/DigestedStep.tsx index 17fb0293af7..ce305bfe14d 100644 --- a/apps/web/src/pages/activities/components/DigestedStep.tsx +++ b/apps/web/src/pages/activities/components/DigestedStep.tsx @@ -1,5 +1,5 @@ import { Center, Grid, UnstyledButton } from '@mantine/core'; -import { colors, Text } from '../../../design-system'; +import { colors, Text } from '@novu/design-system'; export const DigestedStep = ({ digestedId, span = 4, onClick }) => { return ( diff --git a/apps/web/src/pages/activities/components/MessageContainer.tsx b/apps/web/src/pages/activities/components/MessageContainer.tsx index 91829b94b04..7e69fedfbea 100644 --- a/apps/web/src/pages/activities/components/MessageContainer.tsx +++ b/apps/web/src/pages/activities/components/MessageContainer.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { colors, Text } from '../../../design-system'; +import { colors, Text } from '@novu/design-system'; export function MessageContainer({ isDark }: { isDark: boolean }) { return ( diff --git a/apps/web/src/pages/activities/services/chart-bar/data.service.ts b/apps/web/src/pages/activities/services/chart-bar/data.service.ts index 3148882061e..7776dfc0582 100644 --- a/apps/web/src/pages/activities/services/chart-bar/data.service.ts +++ b/apps/web/src/pages/activities/services/chart-bar/data.service.ts @@ -3,7 +3,7 @@ import { ScriptableContext } from 'chart.js'; import { format, subDays } from 'date-fns'; import { IActivityGraphStats, IChartData } from '../../interfaces'; import { activityGraphStatsMock } from '../../consts'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; export function getChartData(data: IActivityGraphStats[] | undefined, isDark: boolean): IChartData { if (!data || data?.length === 0) { diff --git a/apps/web/src/pages/auth/InvitationPage.tsx b/apps/web/src/pages/auth/InvitationPage.tsx index 504657a6d62..8288dadff8d 100644 --- a/apps/web/src/pages/auth/InvitationPage.tsx +++ b/apps/web/src/pages/auth/InvitationPage.tsx @@ -8,7 +8,7 @@ import { getInviteTokenData } from '../../api/invitation'; import AuthLayout from '../../components/layout/components/AuthLayout'; import AuthContainer from '../../components/layout/components/AuthContainer'; import { SignUpForm } from './components/SignUpForm'; -import { colors, Text, Button } from '../../design-system'; +import { colors, Text, Button } from '@novu/design-system'; import { useAuthContext } from '../../components/providers/AuthProvider'; import { useAcceptInvite } from './components/useAcceptInvite'; import { LoginForm } from './components/LoginForm'; diff --git a/apps/web/src/pages/auth/PasswordResetPage.tsx b/apps/web/src/pages/auth/PasswordResetPage.tsx index 06258319c0b..d64cced9ca4 100644 --- a/apps/web/src/pages/auth/PasswordResetPage.tsx +++ b/apps/web/src/pages/auth/PasswordResetPage.tsx @@ -5,7 +5,7 @@ import AuthLayout from '../../components/layout/components/AuthLayout'; import AuthContainer from '../../components/layout/components/AuthContainer'; import { PasswordResetRequestForm } from './components/PasswordResetRequestForm'; import { PasswordResetForm } from './components/PasswordResetForm'; -import { Button, Text } from '../../design-system'; +import { Button } from '@novu/design-system'; import { ROUTES } from '../../constants/routes.enum'; import { useVercelParams } from '../../hooks'; diff --git a/apps/web/src/pages/auth/components/CreateOrganizationForm.tsx b/apps/web/src/pages/auth/components/CreateOrganizationForm.tsx index 0e07e84ac37..7d2be323c7b 100644 --- a/apps/web/src/pages/auth/components/CreateOrganizationForm.tsx +++ b/apps/web/src/pages/auth/components/CreateOrganizationForm.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import decode from 'jwt-decode'; import { IJwtPayload } from '@novu/shared'; -import { Button, Input } from '../../../design-system'; +import { Button, Input } from '@novu/design-system'; import { api } from '../../../api/api.client'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { useVercelIntegration, useVercelParams } from '../../../hooks'; diff --git a/apps/web/src/pages/auth/components/LoginForm.tsx b/apps/web/src/pages/auth/components/LoginForm.tsx index 0b4d9efb91e..e5b0518cfa9 100644 --- a/apps/web/src/pages/auth/components/LoginForm.tsx +++ b/apps/web/src/pages/auth/components/LoginForm.tsx @@ -8,8 +8,7 @@ import { Divider, Button as MantineButton, Center } from '@mantine/core'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { api } from '../../../api/api.client'; -import { PasswordInput, Button, colors, Input, Text } from '../../../design-system'; -import { GitHub, Google } from '../../../design-system/icons'; +import { PasswordInput, Button, colors, Input, Text, GitHub } from '@novu/design-system'; import { IS_DOCKER_HOSTED } from '../../../config'; import { useVercelParams } from '../../../hooks'; import { useAcceptInvite } from './useAcceptInvite'; diff --git a/apps/web/src/pages/auth/components/PasswordRequirementPopover.tsx b/apps/web/src/pages/auth/components/PasswordRequirementPopover.tsx index 3b38c787570..2785f1536e5 100644 --- a/apps/web/src/pages/auth/components/PasswordRequirementPopover.tsx +++ b/apps/web/src/pages/auth/components/PasswordRequirementPopover.tsx @@ -1,9 +1,7 @@ import { useState, useMemo } from 'react'; import { Text, Box, createStyles, Popover } from '@mantine/core'; import { useWatch } from 'react-hook-form'; -import { Check } from '../../../design-system/icons'; -import { Close } from '../../../design-system/icons/actions/Close'; -import { colors } from '../../../design-system'; +import { Check, Close, colors } from '@novu/design-system'; import { PasswordStrengthBar } from './PasswordStrengthBar'; const usePopoverStyles = createStyles(({ colorScheme }) => ({ diff --git a/apps/web/src/pages/auth/components/PasswordResetForm.tsx b/apps/web/src/pages/auth/components/PasswordResetForm.tsx index 4ee3e443f49..3fe0930873b 100644 --- a/apps/web/src/pages/auth/components/PasswordResetForm.tsx +++ b/apps/web/src/pages/auth/components/PasswordResetForm.tsx @@ -7,7 +7,7 @@ import { passwordConstraints } from '@novu/shared'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { api } from '../../../api/api.client'; -import { PasswordInput, Button, colors, Text } from '../../../design-system'; +import { PasswordInput, Button, colors, Text } from '@novu/design-system'; import { PasswordRequirementPopover } from './PasswordRequirementPopover'; import { ROUTES } from '../../../constants/routes.enum'; diff --git a/apps/web/src/pages/auth/components/PasswordResetRequestForm.tsx b/apps/web/src/pages/auth/components/PasswordResetRequestForm.tsx index 50fd323d8b6..4eba2bee996 100644 --- a/apps/web/src/pages/auth/components/PasswordResetRequestForm.tsx +++ b/apps/web/src/pages/auth/components/PasswordResetRequestForm.tsx @@ -5,7 +5,7 @@ import { useForm } from 'react-hook-form'; import { Center } from '@mantine/core'; import { api } from '../../../api/api.client'; -import { Button, colors, Input, Text } from '../../../design-system'; +import { Button, colors, Input, Text } from '@novu/design-system'; import { useVercelParams } from '../../../hooks'; import { ROUTES } from '../../../constants/routes.enum'; diff --git a/apps/web/src/pages/auth/components/SetupLoader.tsx b/apps/web/src/pages/auth/components/SetupLoader.tsx index 6af55367367..12aa21ff044 100644 --- a/apps/web/src/pages/auth/components/SetupLoader.tsx +++ b/apps/web/src/pages/auth/components/SetupLoader.tsx @@ -1,5 +1,5 @@ import { Loader, Paper } from '@mantine/core'; -import { colors, Text } from '../../../design-system'; +import { colors, Text } from '@novu/design-system'; type Props = { title: string; diff --git a/apps/web/src/pages/auth/components/SignUpForm.tsx b/apps/web/src/pages/auth/components/SignUpForm.tsx index 4a1eb8d87b1..49a6ca4bd8a 100644 --- a/apps/web/src/pages/auth/components/SignUpForm.tsx +++ b/apps/web/src/pages/auth/components/SignUpForm.tsx @@ -9,8 +9,7 @@ import { passwordConstraints } from '@novu/shared'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { api } from '../../../api/api.client'; -import { PasswordInput, Button, colors, Input, Text, Checkbox } from '../../../design-system'; -import { GitHub } from '../../../design-system/icons'; +import { PasswordInput, Button, colors, Input, Text, Checkbox, GitHub } from '@novu/design-system'; import { IS_DOCKER_HOSTED } from '../../../config'; import { applyToken, useVercelParams } from '../../../hooks'; import { useAcceptInvite } from './useAcceptInvite'; diff --git a/apps/web/src/pages/brand/BrandPage.tsx b/apps/web/src/pages/brand/BrandPage.tsx index 3a2869954e3..8a2f2bd3ceb 100644 --- a/apps/web/src/pages/brand/BrandPage.tsx +++ b/apps/web/src/pages/brand/BrandPage.tsx @@ -7,8 +7,7 @@ import PageHeader from '../../components/layout/components/PageHeader'; import { useAuthContext } from '../../components/providers/AuthProvider'; import { useSegment } from '../../components/providers/SegmentProvider'; import { ROUTES } from '../../constants/routes.enum'; -import { colors } from '../../design-system'; -import useStyles from '../../design-system/tabs/Tabs.styles'; +import { colors, useTabsStyles } from '@novu/design-system'; import { useEnvController } from '../../hooks'; const BRANDING = 'Assets'; @@ -18,7 +17,7 @@ export function BrandPage() { const { currentOrganization, currentUser } = useAuthContext(); const { environment } = useEnvController(); const segment = useSegment(); - const { classes } = useStyles(false); + const { classes } = useTabsStyles(false); const navigate = useNavigate(); const { pathname } = useLocation(); const tabValue = useMemo(() => { diff --git a/apps/web/src/pages/brand/tabs/BrandingForm.tsx b/apps/web/src/pages/brand/tabs/BrandingForm.tsx index 6abc74428c7..b09ec508bae 100644 --- a/apps/web/src/pages/brand/tabs/BrandingForm.tsx +++ b/apps/web/src/pages/brand/tabs/BrandingForm.tsx @@ -1,18 +1,18 @@ -import { Flex, Grid, Group, Input, LoadingOverlay, Stack, useMantineTheme } from '@mantine/core'; +import { Flex, Grid, Group, Input, LoadingOverlay, Stack, UnstyledButton, useMantineTheme } from '@mantine/core'; import { Dropzone } from '@mantine/dropzone'; import { useMutation } from '@tanstack/react-query'; import axios from 'axios'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import styled from '@emotion/styled'; import { useOutletContext } from 'react-router-dom'; import { IOrganizationEntity } from '@novu/shared'; import { updateBrandingSettings } from '../../../api/organization'; import { getSignedUrl } from '../../../api/storage'; import Card from '../../../components/layout/components/Card'; -import { Button, ColorInput, colors, Select } from '../../../design-system'; -import { inputStyles } from '../../../design-system/config/inputs.styles'; -import { Upload } from '../../../design-system/icons'; +import { Button, ColorInput, colors, Select, inputStyles, Upload, Trash } from '@novu/design-system'; + import { successMessage } from '../../../utils/notifications'; const mimeTypes = { @@ -55,6 +55,11 @@ export function BrandingForm() { } }, [organization, setValue]); + function removeFile() { + setValue('file', ''); + setValue('image', ''); + } + async function handleUpload(files: File[]) { const file = files[0]; if (!file) return; @@ -82,10 +87,12 @@ export function BrandingForm() { setValue('image', path); } + const dropzoneRef = useRef<() => void>(null); + async function saveBrandsForm({ color, fontFamily, image }) { const brandData = { color, - logo: image, + logo: image || null, fontFamily, }; @@ -110,38 +117,62 @@ export function BrandingForm() { label="Your Logo" description="Will be used on email templates and inbox" > - - + {field.value && ( + + dropzoneRef.current?.()}> + + Update + + + + + Remove + + + )} + - {!field.value ? ( - - ) : ( - avatar - )} - - + + {!field.value ? ( +
+ + Upload +
+ ) : ( + avatar + )} +
+ + )} control={control} @@ -211,3 +242,45 @@ export function BrandingForm() { ); } + +const DropzoneButton: any = styled(UnstyledButton)` + color: ${colors.B70}; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + + &:hover { + color: ${colors.white}; + } +`; + +const DropzoneOverlay = styled('div')` + display: none; + justify-content: center; + align-items: center; + gap: 1.5rem; + z-index: 20; + border-radius: 7px; + position: absolute; + top: 0; + left: 0; + background-color: ${colors.BGDark + 'D6'}; + backdrop-filter: blur(5px); + width: 100%; + height: 100%; +`; + +const DropzoneWrapper = styled('div')` + position: relative; + border-radius: 7px; + border: 1px solid ${({ theme }) => (theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[5])}; + + &:hover { + cursor: pointer; + + ${DropzoneOverlay} { + display: flex; + } + } +`; diff --git a/apps/web/src/pages/brand/tabs/LayoutEditor.tsx b/apps/web/src/pages/brand/tabs/LayoutEditor.tsx index 51646db6d7d..a91bdde15e4 100644 --- a/apps/web/src/pages/brand/tabs/LayoutEditor.tsx +++ b/apps/web/src/pages/brand/tabs/LayoutEditor.tsx @@ -8,8 +8,19 @@ import { useClipboard } from '@mantine/hooks'; import { getTemplateVariables, ITemplateVariable, isReservedVariableName, LayoutId } from '@novu/shared'; -import { ArrowLeft, Check, Copy } from '../../../design-system/icons'; -import { Button, Checkbox, colors, Input, Text, LoadingOverlay, shadows, Tooltip } from '../../../design-system'; +import { + ArrowLeft, + Check, + Copy, + Button, + Checkbox, + colors, + Input, + Text, + LoadingOverlay, + shadows, + Tooltip, +} from '@novu/design-system'; import { useEnvController, useLayoutsEditor, usePrompt } from '../../../hooks'; import { errorMessage, successMessage } from '../../../utils/notifications'; import { QueryKeys } from '../../../api/query.keys'; diff --git a/apps/web/src/pages/brand/tabs/LayoutsListPage.tsx b/apps/web/src/pages/brand/tabs/LayoutsListPage.tsx index afac7e07366..71c18d85e9d 100644 --- a/apps/web/src/pages/brand/tabs/LayoutsListPage.tsx +++ b/apps/web/src/pages/brand/tabs/LayoutsListPage.tsx @@ -9,9 +9,17 @@ import { useOutletContext } from 'react-router-dom'; import { deleteLayoutById } from '../../../api/layouts'; import { QueryKeys } from '../../../api/query.keys'; import { When } from '../../../components/utils/When'; -import { colors, Text, Tooltip, PlusButton, withCellLoading } from '../../../design-system'; -import { Edit, Trash } from '../../../design-system/icons'; -import { IExtendedColumn, Table } from '../../../design-system/table/Table'; +import { + colors, + Text, + Tooltip, + PlusButton, + withCellLoading, + Edit, + Trash, + IExtendedColumn, + Table, +} from '@novu/design-system'; import { useEnvController, useLayouts } from '../../../hooks'; import { errorMessage, successMessage } from '../../../utils/notifications'; import { DeleteConfirmModal } from '../../templates/components/DeleteConfirmModal'; diff --git a/apps/web/src/pages/changes/PromoteChangesPage.tsx b/apps/web/src/pages/changes/PromoteChangesPage.tsx index 448d918ee95..c43040d0d6f 100644 --- a/apps/web/src/pages/changes/PromoteChangesPage.tsx +++ b/apps/web/src/pages/changes/PromoteChangesPage.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import PageHeader from '../../components/layout/components/PageHeader'; import PageContainer from '../../components/layout/components/PageContainer'; -import { Button, Tabs } from '../../design-system'; +import { Button, Tabs } from '@novu/design-system'; import { useEnvController, usePromotedChanges, useUnPromotedChanges } from '../../hooks'; import { ChangesTable } from './components/ChangesTableLayout'; import { bulkPromoteChanges } from '../../api/changes'; diff --git a/apps/web/src/pages/changes/components/ChangesTableLayout.tsx b/apps/web/src/pages/changes/components/ChangesTableLayout.tsx index a1f1e83c91a..4b20365b990 100644 --- a/apps/web/src/pages/changes/components/ChangesTableLayout.tsx +++ b/apps/web/src/pages/changes/components/ChangesTableLayout.tsx @@ -6,8 +6,7 @@ import { ChangeEntityTypeEnum } from '@novu/shared'; import { useEffect } from 'react'; import { showNotification } from '@mantine/notifications'; -import { IExtendedColumn, Table } from '../../../design-system/table/Table'; -import { Button, colors, Text, withCellLoading } from '../../../design-system'; +import { IExtendedColumn, Table, Button, colors, Text, withCellLoading } from '@novu/design-system'; import { promoteChange } from '../../../api/changes'; import { QueryKeys } from '../../../api/query.keys'; diff --git a/apps/web/src/pages/integrations/IntegrationsList.tsx b/apps/web/src/pages/integrations/IntegrationsList.tsx index daff3f5f1ac..75a4d729ecb 100644 --- a/apps/web/src/pages/integrations/IntegrationsList.tsx +++ b/apps/web/src/pages/integrations/IntegrationsList.tsx @@ -6,7 +6,7 @@ import { ChannelTypeEnum } from '@novu/shared'; import PageContainer from '../../components/layout/components/PageContainer'; import PageHeader from '../../components/layout/components/PageHeader'; -import { Table, Text, withCellLoading, IExtendedColumn } from '../../design-system'; +import { Table, Text, withCellLoading, IExtendedColumn } from '@novu/design-system'; import { useIntegrations } from '../../hooks'; import { IntegrationsListToolbar } from './components/IntegrationsListToolbar'; import { useFetchEnvironments } from '../../hooks/useFetchEnvironments'; diff --git a/apps/web/src/pages/integrations/IntegrationsListModal.tsx b/apps/web/src/pages/integrations/IntegrationsListModal.tsx index 5da7a0fc82a..449005a18db 100644 --- a/apps/web/src/pages/integrations/IntegrationsListModal.tsx +++ b/apps/web/src/pages/integrations/IntegrationsListModal.tsx @@ -3,8 +3,7 @@ import { Group, Modal, ActionIcon, createStyles, MantineTheme } from '@mantine/c import { ChannelTypeEnum } from '@novu/shared'; import { useKeyDown } from '../../hooks'; -import { colors } from '../../design-system'; -import { Close } from '../../design-system/icons'; +import { colors, Close } from '@novu/design-system'; import { useSegment } from '../../components/providers/SegmentProvider'; import { IntegrationsStoreModalAnalytics } from './constants'; import type { IIntegratedProvider, ITableIntegration } from './types'; diff --git a/apps/web/src/pages/integrations/IntegrationsListPage.tsx b/apps/web/src/pages/integrations/IntegrationsListPage.tsx index bbcfe678294..a7ce20ee1b7 100644 --- a/apps/web/src/pages/integrations/IntegrationsListPage.tsx +++ b/apps/web/src/pages/integrations/IntegrationsListPage.tsx @@ -4,14 +4,11 @@ import { useNavigate } from 'react-router-dom'; import { Row } from 'react-table'; import { ROUTES } from '../../constants/routes.enum'; -import { useIsMultiProviderConfigurationEnabled } from '../../hooks'; import { IntegrationsList } from './IntegrationsList'; -import { IntegrationsStore } from './IntegrationsStorePage'; import { ITableIntegration } from './types'; export const IntegrationsListPage = () => { const navigate = useNavigate(); - const isIntegrationsListPageEnabled = useIsMultiProviderConfigurationEnabled(); const onRowClickCallback = useCallback( (item: Row) => { @@ -31,13 +28,11 @@ export const IntegrationsListPage = () => { [navigate] ); - return isIntegrationsListPageEnabled ? ( + return ( - ) : ( - ); }; diff --git a/apps/web/src/pages/integrations/IntegrationsStoreModal.tsx b/apps/web/src/pages/integrations/IntegrationsStoreModal.tsx deleted file mode 100644 index 7ac2a7707b6..00000000000 --- a/apps/web/src/pages/integrations/IntegrationsStoreModal.tsx +++ /dev/null @@ -1,307 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import styled from '@emotion/styled'; -import { Grid, Group, Modal, ActionIcon, createStyles, MantineTheme, Drawer } from '@mantine/core'; -import { ChannelTypeEnum, EmailProviderIdEnum, InAppProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; - -import { useAuthController, useEnvController } from '../../hooks'; -import { When } from '../../components/utils/When'; -import { NovuEmailProviderModal } from './components/NovuEmailProviderModal'; -import { NovuInAppProviderModal } from './components/NovuInAppProviderModal'; -import { ChannelGroup } from './components/Modal/ChannelGroup'; -import { colors, shadows, Title } from '../../design-system'; -import { ConnectIntegrationForm } from './components/Modal/ConnectIntegrationForm'; -import { Close } from '../../design-system/icons'; -import { useProviders } from './useProviders'; -import { useSegment } from '../../components/providers/SegmentProvider'; -import { IntegrationsStoreModalAnalytics } from './constants'; -import { NovuSmsProviderModal } from './components/NovuSmsProviderModal'; -import { useCreateInAppIntegration } from '../../hooks/useCreateInAppIntegration'; -import type { IIntegratedProvider } from './types'; - -export function IntegrationsStoreModal({ - scrollTo, - openIntegration, - closeIntegration, - selectedProvider = null, -}: { - scrollTo?: ChannelTypeEnum; - openIntegration: boolean; - closeIntegration: () => void; - selectedProvider?: IIntegratedProvider | null; -}) { - const segment = useSegment(); - const { environment } = useEnvController(); - const { organization } = useAuthController(); - const { emailProviders, smsProvider, chatProvider, pushProvider, inAppProvider, isLoading } = useProviders(); - const [isFormOpened, setFormIsOpened] = useState(false); - const [isCreateIntegrationModal, setIsCreateIntegrationModal] = useState(false); - const [provider, setProvider] = useState(null); - const { create } = useCreateInAppIntegration((data: any) => { - setProvider({ - ...(provider as IIntegratedProvider), - integrationId: data._id, - active: data.active, - }); - }); - - const { classes } = useModalStyles(); - const { classes: drawerClasses } = useDrawerStyles(); - - useEffect(() => { - setFormIsOpened(selectedProvider !== null); - setProvider(selectedProvider); - }, [selectedProvider]); - - async function handleOnProviderClick( - visible: boolean, - createIntegrationModal: boolean, - providerConfig: IIntegratedProvider - ) { - setFormIsOpened(visible); - if (providerConfig.providerId === InAppProviderIdEnum.Novu && providerConfig.channel === ChannelTypeEnum.IN_APP) { - create(); - } - setProvider(providerConfig); - setIsCreateIntegrationModal(createIntegrationModal); - segment.track(IntegrationsStoreModalAnalytics.SELECT_PROVIDER_CLICK, { - providerId: provider?.providerId, - channel: provider?.channel, - name: provider?.displayName, - active: provider?.active, - }); - } - - const handleModalClose = useCallback(() => { - closeIntegration(); - setFormIsOpened(false); - setProvider(null); - segment.track(IntegrationsStoreModalAnalytics.CLOSE_MODAL); - }, [segment, closeIntegration]); - - const handleCloseForm = useCallback(() => { - if (isFormOpened) { - setProvider(null); - setFormIsOpened(false); - - return; - } - - closeIntegration(); - segment.track(IntegrationsStoreModalAnalytics.CLOSE_MODAL); - }, [segment, isFormOpened, setProvider, setFormIsOpened, closeIntegration]); - - useEffect(() => { - const handleKeyDown = (e) => { - if (openIntegration && e.key === 'Escape') { - handleCloseForm(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - - return () => document.removeEventListener('keydown', handleKeyDown); - }, [openIntegration, handleCloseForm]); - - useEffect(() => { - if (!scrollTo || !openIntegration) return; - - setTimeout(() => { - const channelSection = document.getElementById(scrollTo); - const modalContainer = document.querySelector('.mantine-Modal-modal'); - if (channelSection && modalContainer) { - modalContainer.scrollBy({ - top: channelSection.getBoundingClientRect().top - HEADER_HEIGHT - HEADER_MARGIN, - behavior: 'smooth', - }); - } - }, 0); - }, [openIntegration, scrollTo]); - - return ( - - Integration Store - - - - - } - classNames={classes} - fullScreen - opened={openIntegration} - onClose={handleModalClose} - > - - - {!isLoading ? ( - <> - - - - - - - ) : null} - - - - - - - -
- -
-
- -
- -
-
- -
- -
-
-
-
-
-
- ); -} -const DRAWER_PADDING = 40; -const DRAWER_PADDING_SMALL = 20; -const HEADER_HEIGHT_SMALL = 70; -const HEADER_HEIGHT = 90; -const HEADER_MARGIN = 10; -const DISTANCE_FROM_HEADER = 64; -const INTEGRATION_SETTING_TOP_SMALL = HEADER_HEIGHT_SMALL + HEADER_MARGIN; -const INTEGRATION_SETTING_TOP = HEADER_HEIGHT + HEADER_MARGIN + DISTANCE_FROM_HEADER; - -const IntegrationCardWrapper = styled.div` - position: sticky; - box-sizing: border-box; - padding: 0; - display: flex; - justify-content: space-between; - flex-direction: column; - height: calc(100vh - ${INTEGRATION_SETTING_TOP_SMALL + DRAWER_PADDING_SMALL}px); - box-shadow: ${({ theme }) => (theme.colorScheme === 'dark' ? shadows.dark : shadows.medium)}; - border-radius: 7px; - background-color: ${({ theme }) => (theme.colorScheme === 'dark' ? colors.B20 : colors.B98)}; - overflow: hidden; - - @media screen and (min-width: 1367px) { - top: ${INTEGRATION_SETTING_TOP}px; - height: calc(100vh - ${INTEGRATION_SETTING_TOP + DRAWER_PADDING}px); - } -`; - -const useModalStyles = createStyles((theme: MantineTheme) => { - const dark = theme.colorScheme === 'dark'; - - return { - header: { - position: 'sticky', - top: 0, - padding: '30px', - marginLeft: '-30px', - marginRight: '-30px', - height: HEADER_HEIGHT_SMALL, - zIndex: 9, - boxShadow: dark ? shadows.dark : shadows.medium, - backgroundColor: dark ? colors.BGDark : colors.white, - marginBottom: 10, - '@media screen and (min-width: 1367px)': { - height: HEADER_HEIGHT, - }, - }, - title: { - width: '100%', - marginRight: 0, - h1: { - fontSize: 22, - }, - '@media screen and (min-width: 1367px)': { - h1: { - fontSize: 26, - }, - }, - }, - modal: { - backdropFilter: 'blur(15px)', - padding: '0px 30px !important', - backgroundColor: dark ? theme.fn.rgba(colors.BGDark, 0.8) : theme.fn.rgba(colors.white, 0.7), - }, - }; -}); - -const useDrawerStyles = createStyles((theme: MantineTheme) => { - return { - drawer: { - top: `${INTEGRATION_SETTING_TOP_SMALL - DRAWER_PADDING_SMALL}px`, - display: 'flex', - flexDirection: 'column', - justifyContent: 'end', - background: 'transparent', - width: 660, - padding: `${DRAWER_PADDING_SMALL}px !important`, - boxShadow: 'none', - - '@media screen and (min-width: 1367px)': { - top: `${INTEGRATION_SETTING_TOP - DRAWER_PADDING}px`, - padding: `${DRAWER_PADDING}px !important`, - }, - }, - }; -}); diff --git a/apps/web/src/pages/integrations/IntegrationsStorePage.tsx b/apps/web/src/pages/integrations/IntegrationsStorePage.tsx deleted file mode 100644 index d35512aeb78..00000000000 --- a/apps/web/src/pages/integrations/IntegrationsStorePage.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useState } from 'react'; -import styled from '@emotion/styled'; -import { Modal } from '@mantine/core'; -import { ChannelTypeEnum, EmailProviderIdEnum, InAppProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; - -import PageHeader from '../../components/layout/components/PageHeader'; -import PageContainer from '../../components/layout/components/PageContainer'; -import { ChannelGroup } from './components/ChannelGroup'; -import { ConnectIntegrationForm } from './components/ConnectIntegrationForm'; -import { When } from '../../components/utils/When'; -import { NovuEmailProviderModal } from './components/NovuEmailProviderModal'; -import { NovuInAppProviderModal } from './components/NovuInAppProviderModal'; -import { useProviders } from './useProviders'; -import { NovuSmsProviderModal } from './components/NovuSmsProviderModal'; -import { useCreateInAppIntegration } from '../../hooks/useCreateInAppIntegration'; -import { LoadingOverlay } from '../../design-system'; -import type { IIntegratedProvider } from './types'; - -export function IntegrationsStore() { - const { emailProviders, smsProvider, chatProvider, pushProvider, inAppProvider, isLoading, refetch } = useProviders(); - const [isModalOpened, setModalIsOpened] = useState(false); - const [isCreateIntegrationModal, setIsCreateIntegrationModal] = useState(false); - const [provider, setProvider] = useState(null); - const { create } = useCreateInAppIntegration((data: any) => { - setProvider({ - ...(provider as IIntegratedProvider), - integrationId: data._id, - active: data.active, - }); - }); - - async function handlerVisible( - visible: boolean, - createIntegrationModal: boolean, - providerConfig: IIntegratedProvider - ) { - setModalIsOpened(visible); - if (providerConfig.providerId === InAppProviderIdEnum.Novu && providerConfig.channel === ChannelTypeEnum.IN_APP) { - create(); - } - setProvider(providerConfig); - setIsCreateIntegrationModal(createIntegrationModal); - } - - async function handlerShowModal(showModal: boolean) { - await setModalIsOpened(showModal); - if (!showModal) { - await refetch(); - } - } - - return ( - - - - - setModalIsOpened(false)} - > - - setModalIsOpened(false)} - provider={provider as IIntegratedProvider} - showModal={handlerShowModal} - createModel={isCreateIntegrationModal} - /> - - - setModalIsOpened(false)} /> - - - setModalIsOpened(false)} - /> - - - setModalIsOpened(false)} /> - - - - - - - - - - - - - - ); -} - -const ContentWrapper = styled.div` - padding: 0 30px; -`; diff --git a/apps/web/src/pages/integrations/components/CardStatusBar.tsx b/apps/web/src/pages/integrations/components/CardStatusBar.tsx index e9ec1a12a5c..6c898e75909 100644 --- a/apps/web/src/pages/integrations/components/CardStatusBar.tsx +++ b/apps/web/src/pages/integrations/components/CardStatusBar.tsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled'; -import { BoltGradient } from '../../../design-system/icons'; -import { colors, Text } from '../../../design-system'; +import { BoltGradient, colors, Text } from '@novu/design-system'; export function CardStatusBar({ active }: { active: boolean }) { const iconProps = active ? { fill: colors.success } : {}; diff --git a/apps/web/src/pages/integrations/components/ChannelGroup.tsx b/apps/web/src/pages/integrations/components/ChannelGroup.tsx index ac1472bb112..83ad82874a0 100644 --- a/apps/web/src/pages/integrations/components/ChannelGroup.tsx +++ b/apps/web/src/pages/integrations/components/ChannelGroup.tsx @@ -1,6 +1,6 @@ import { Grid } from '@mantine/core'; import { ProviderCard } from './ProviderCard'; -import { Title } from '../../../design-system'; +import { Title } from '@novu/design-system'; import { ChannelTypeEnum, EmailProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; import type { IIntegratedProvider } from '../types'; diff --git a/apps/web/src/pages/integrations/components/ConditionCell.tsx b/apps/web/src/pages/integrations/components/ConditionCell.tsx index eabd3fc969f..40b6ad0e90c 100644 --- a/apps/web/src/pages/integrations/components/ConditionCell.tsx +++ b/apps/web/src/pages/integrations/components/ConditionCell.tsx @@ -1,6 +1,5 @@ import { Group, useMantineColorScheme } from '@mantine/core'; -import { colors, IExtendedCellProps, withCellLoading } from '../../../design-system'; -import { Condition } from '../../../design-system/icons'; +import { colors, IExtendedCellProps, withCellLoading, Condition } from '@novu/design-system'; import type { ITableIntegration } from '../types'; const ConditionCellBase = ({ row: { original } }: IExtendedCellProps) => { diff --git a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx index 2444756a267..e5df9bf443f 100644 --- a/apps/web/src/pages/integrations/components/ConditionIconButton.tsx +++ b/apps/web/src/pages/integrations/components/ConditionIconButton.tsx @@ -2,8 +2,7 @@ import { useState } from 'react'; import styled from '@emotion/styled'; import { Group, ActionIcon, Center } from '@mantine/core'; import { When } from '../../../components/utils/When'; -import { colors, Tooltip, Text, Modal, Button, Title } from '../../../design-system'; -import { Condition, ConditionPlus, Warning } from '../../../design-system/icons'; +import { colors, Tooltip, Text, Modal, Button, Title, Condition, ConditionPlus, Warning } from '@novu/design-system'; const IconButton = styled(Group)` text-align: center; diff --git a/apps/web/src/pages/integrations/components/ConnectIntegrationForm.tsx b/apps/web/src/pages/integrations/components/ConnectIntegrationForm.tsx index 20aeb36f0d3..dd0bec585ed 100644 --- a/apps/web/src/pages/integrations/components/ConnectIntegrationForm.tsx +++ b/apps/web/src/pages/integrations/components/ConnectIntegrationForm.tsx @@ -14,14 +14,12 @@ import { CredentialsKeyEnum, } from '@novu/shared'; -import { Button, colors, Input, Switch, Text } from '../../../design-system'; +import { Button, colors, Input, Switch, Text, Close, Check, Copy } from '@novu/design-system'; import type { IIntegratedProvider } from '../types'; import { createIntegration, getWebhookSupportStatus, updateIntegration } from '../../../api/integration'; -import { Close } from '../../../design-system/icons'; import { IntegrationInput } from './IntegrationInput'; import { IS_DOCKER_HOSTED, WEBHOOK_URL } from '../../../config'; import { useEnvController, useAuthController } from '../../../hooks'; -import { Check, Copy } from '../../../design-system/icons'; import { CONTEXT_PATH } from '../../../config'; import { ShareableUrl } from './Modal/ConnectIntegrationForm'; diff --git a/apps/web/src/pages/integrations/components/FrameworkDisplay.tsx b/apps/web/src/pages/integrations/components/FrameworkDisplay.tsx index 372968a9285..b369e148273 100644 --- a/apps/web/src/pages/integrations/components/FrameworkDisplay.tsx +++ b/apps/web/src/pages/integrations/components/FrameworkDisplay.tsx @@ -1,14 +1,7 @@ import { Text, UnstyledButton, Group, useMantineTheme } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; import { colors } from '@novu/notification-center'; -import { - AngularGradient, - Copy, - ReactGradient, - VueGradient, - JsGradient, - CodeGradient, -} from '../../../design-system/icons'; +import { AngularGradient, Copy, ReactGradient, VueGradient, JsGradient, CodeGradient } from '@novu/design-system'; import { useEnvController } from '../../../hooks'; import { FrameworkEnum } from '../../quick-start/consts'; diff --git a/apps/web/src/pages/integrations/components/IntegrationChannel.tsx b/apps/web/src/pages/integrations/components/IntegrationChannel.tsx index 5e231da0d5a..a9c4d5e2111 100644 --- a/apps/web/src/pages/integrations/components/IntegrationChannel.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationChannel.tsx @@ -3,7 +3,7 @@ import { Skeleton } from '@mantine/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { ChannelTypeEnum } from '@novu/shared'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { CHANNEL_TYPE_TO_ICON_NAME } from '../constants'; const IconSkeleton = styled(Skeleton)` diff --git a/apps/web/src/pages/integrations/components/IntegrationChannelCell.tsx b/apps/web/src/pages/integrations/components/IntegrationChannelCell.tsx index a5f3d1a5512..34c7325f7cf 100644 --- a/apps/web/src/pages/integrations/components/IntegrationChannelCell.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationChannelCell.tsx @@ -1,4 +1,4 @@ -import { IExtendedCellProps, withCellLoading } from '../../../design-system'; +import { IExtendedCellProps, withCellLoading } from '@novu/design-system'; import type { ITableIntegration } from '../types'; import { IntegrationChannel } from './IntegrationChannel'; diff --git a/apps/web/src/pages/integrations/components/IntegrationEnvironmentCell.tsx b/apps/web/src/pages/integrations/components/IntegrationEnvironmentCell.tsx index 2d93389f201..a7ecfb11d7c 100644 --- a/apps/web/src/pages/integrations/components/IntegrationEnvironmentCell.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationEnvironmentCell.tsx @@ -1,4 +1,4 @@ -import { IExtendedCellProps, withCellLoading } from '../../../design-system'; +import { IExtendedCellProps, withCellLoading } from '@novu/design-system'; import type { ITableIntegration } from '../types'; import { IntegrationEnvironmentPill } from './IntegrationEnvironmentPill'; diff --git a/apps/web/src/pages/integrations/components/IntegrationEnvironmentPill.tsx b/apps/web/src/pages/integrations/components/IntegrationEnvironmentPill.tsx index 0d6759cd95d..88dd11237d4 100644 --- a/apps/web/src/pages/integrations/components/IntegrationEnvironmentPill.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationEnvironmentPill.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { Skeleton } from '@mantine/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; const SkeletonPill = styled(Skeleton)` width: 120px; diff --git a/apps/web/src/pages/integrations/components/IntegrationInput.tsx b/apps/web/src/pages/integrations/components/IntegrationInput.tsx index 707261b4e23..681e260d485 100644 --- a/apps/web/src/pages/integrations/components/IntegrationInput.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationInput.tsx @@ -2,7 +2,7 @@ import { useMantineTheme } from '@mantine/core'; import styled from '@emotion/styled'; import { CredentialsKeyEnum, IConfigCredentials, secureCredentials } from '@novu/shared'; -import { Input, PasswordInput, Switch, Textarea, Text, Tooltip } from '../../../design-system'; +import { Input, PasswordInput, Switch, Textarea, Text, Tooltip } from '@novu/design-system'; import { IntegrationSecretTextarea } from './IntegrationSecretTextarea'; const SwitchWrapper = styled.div` diff --git a/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx b/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx index 14dafb26ede..35c252709e6 100644 --- a/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationNameCell.tsx @@ -2,8 +2,7 @@ import styled from '@emotion/styled'; import { Skeleton, useMantineColorScheme } from '@mantine/core'; import { useState } from 'react'; -import { colors, IExtendedCellProps, Popover, Text } from '../../../design-system'; -import { Star } from '../../../design-system/icons'; +import { colors, IExtendedCellProps, Popover, Text, Star } from '@novu/design-system'; import type { ITableIntegration } from '../types'; import { ChannelTypeEnum } from '@novu/shared'; diff --git a/apps/web/src/pages/integrations/components/IntegrationSecretTextarea.tsx b/apps/web/src/pages/integrations/components/IntegrationSecretTextarea.tsx index 401ad148350..40e879eab74 100644 --- a/apps/web/src/pages/integrations/components/IntegrationSecretTextarea.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationSecretTextarea.tsx @@ -3,7 +3,7 @@ import { Grid, UnstyledButton } from '@mantine/core'; import { IConfigCredentials } from '@novu/shared'; import { useState } from 'react'; import { When } from '../../../components/utils/When'; -import { Textarea } from '../../../design-system'; +import { Textarea } from '@novu/design-system'; export const IntegrationSecretTextarea = ({ credential, diff --git a/apps/web/src/pages/integrations/components/IntegrationStatus.tsx b/apps/web/src/pages/integrations/components/IntegrationStatus.tsx index 852cc1149ba..712717f34b6 100644 --- a/apps/web/src/pages/integrations/components/IntegrationStatus.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationStatus.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; const StatusHolder = styled.div` width: fit-content; diff --git a/apps/web/src/pages/integrations/components/IntegrationStatusCell.tsx b/apps/web/src/pages/integrations/components/IntegrationStatusCell.tsx index ec3d4cf7893..84da28c1b33 100644 --- a/apps/web/src/pages/integrations/components/IntegrationStatusCell.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationStatusCell.tsx @@ -1,4 +1,4 @@ -import { IExtendedCellProps, withCellLoading } from '../../../design-system'; +import { IExtendedCellProps, withCellLoading } from '@novu/design-system'; import type { ITableIntegration } from '../types'; import { IntegrationStatus } from './IntegrationStatus'; diff --git a/apps/web/src/pages/integrations/components/IntegrationsListNoData.tsx b/apps/web/src/pages/integrations/components/IntegrationsListNoData.tsx index 8bbf6ac69f6..44131b361b5 100644 --- a/apps/web/src/pages/integrations/components/IntegrationsListNoData.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationsListNoData.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { ChannelTypeEnum } from '@novu/shared'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CardTile, colors } from '../../../design-system'; +import { CardTile, colors } from '@novu/design-system'; import { CHANNEL_TYPE_TO_ICON_NAME } from '../constants'; import { CHANNEL_TYPE_TO_STRING } from '../../../utils/channels'; diff --git a/apps/web/src/pages/integrations/components/IntegrationsListToolbar.tsx b/apps/web/src/pages/integrations/components/IntegrationsListToolbar.tsx index f410f30ab99..6dcbfbb2368 100644 --- a/apps/web/src/pages/integrations/components/IntegrationsListToolbar.tsx +++ b/apps/web/src/pages/integrations/components/IntegrationsListToolbar.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import React from 'react'; -import { PlusButton } from '../../../design-system'; +import { PlusButton } from '@novu/design-system'; const IntegrationsListToolbarHolder = styled.div` display: flex; diff --git a/apps/web/src/pages/integrations/components/LimitBar.tsx b/apps/web/src/pages/integrations/components/LimitBar.tsx index d199f501803..5f6c3d3410c 100644 --- a/apps/web/src/pages/integrations/components/LimitBar.tsx +++ b/apps/web/src/pages/integrations/components/LimitBar.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled/macro'; import { Stack, useMantineColorScheme } from '@mantine/core'; import { ChannelTypeEnum } from '@novu/shared'; -import { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { When } from '../../../components/utils/When'; -import { colors, Text } from '../../../design-system'; +import { colors, Text } from '@novu/design-system'; const WARNING_LIMIT = { [ChannelTypeEnum.EMAIL]: 50, diff --git a/apps/web/src/pages/integrations/components/Modal/ChannelGroup.tsx b/apps/web/src/pages/integrations/components/Modal/ChannelGroup.tsx index 7453113704d..2bcc03d8bf9 100644 --- a/apps/web/src/pages/integrations/components/Modal/ChannelGroup.tsx +++ b/apps/web/src/pages/integrations/components/Modal/ChannelGroup.tsx @@ -1,5 +1,5 @@ import { Container, Grid } from '@mantine/core'; -import { Title } from '../../../../design-system'; +import { Title } from '@novu/design-system'; import { ChannelTypeEnum, EmailProviderIdEnum, SmsProviderIdEnum } from '@novu/shared'; import type { IIntegratedProvider } from '../../types'; diff --git a/apps/web/src/pages/integrations/components/Modal/ConnectIntegrationForm.tsx b/apps/web/src/pages/integrations/components/Modal/ConnectIntegrationForm.tsx index 372b499f5f3..d5160ee3fe1 100644 --- a/apps/web/src/pages/integrations/components/Modal/ConnectIntegrationForm.tsx +++ b/apps/web/src/pages/integrations/components/Modal/ConnectIntegrationForm.tsx @@ -18,13 +18,11 @@ import { import styled from '@emotion/styled'; import { keyframes } from '@emotion/react'; -import { Button, colors, Input, shadows, Switch, Text } from '../../../../design-system'; +import { Button, colors, Input, shadows, Switch, Text, Close, Check, Copy } from '@novu/design-system'; import type { IIntegratedProvider } from '../../types'; import { createIntegration, getWebhookSupportStatus, updateIntegration } from '../../../../api/integration'; -import { Close } from '../../../../design-system/icons'; import { IntegrationInput } from '../IntegrationInput'; import { API_ROOT, CONTEXT_PATH } from '../../../../config'; -import { Check, Copy } from '../../../../design-system/icons'; import { successMessage } from '../../../../utils/notifications'; import { QueryKeys } from '../../../../api/query.keys'; import { useSegment } from '../../../../components/providers/SegmentProvider'; diff --git a/apps/web/src/pages/integrations/components/Modal/NovuIntegrationCard.tsx b/apps/web/src/pages/integrations/components/Modal/NovuIntegrationCard.tsx index 54c1b81d9b3..010bb209290 100644 --- a/apps/web/src/pages/integrations/components/Modal/NovuIntegrationCard.tsx +++ b/apps/web/src/pages/integrations/components/Modal/NovuIntegrationCard.tsx @@ -2,11 +2,10 @@ import { IConfigCredentials } from '@novu/shared'; import styled from '@emotion/styled'; import { Group, useMantineColorScheme } from '@mantine/core'; -import { colors, shadows } from '../../../../design-system'; +import { colors, shadows, getGradient } from '@novu/design-system'; import { CardStatusBar } from '../CardStatusBar'; import type { IIntegratedProvider } from '../../types'; import { LimitBar } from '../LimitBar'; -import { getGradient } from '../../../../design-system/config/helper'; import { useIntegrationLimit } from '../../../../hooks'; export function NovuIntegrationCard({ diff --git a/apps/web/src/pages/integrations/components/Modal/ProviderCard.tsx b/apps/web/src/pages/integrations/components/Modal/ProviderCard.tsx index fd5d66fe895..e6003eff2e0 100644 --- a/apps/web/src/pages/integrations/components/Modal/ProviderCard.tsx +++ b/apps/web/src/pages/integrations/components/Modal/ProviderCard.tsx @@ -2,13 +2,11 @@ import { IConfigCredentials } from '@novu/shared'; import styled from '@emotion/styled'; import { Group, useMantineColorScheme } from '@mantine/core'; -import { Button, colors, shadows } from '../../../../design-system'; +import { Button, colors, shadows, Settings, getGradient } from '@novu/design-system'; import { CardStatusBar } from '../CardStatusBar'; -import { Settings } from '../../../../design-system/icons'; import type { IIntegratedProvider } from '../../types'; import { When } from '../../../../components/utils/When'; import { CONTEXT_PATH } from '../../../../config'; -import { getGradient } from '../../../../design-system/config/helper'; export function ProviderCard({ provider, diff --git a/apps/web/src/pages/integrations/components/NovuEmailProviderModal.tsx b/apps/web/src/pages/integrations/components/NovuEmailProviderModal.tsx index 5530d04deb1..9d98823bf9c 100644 --- a/apps/web/src/pages/integrations/components/NovuEmailProviderModal.tsx +++ b/apps/web/src/pages/integrations/components/NovuEmailProviderModal.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled/macro'; import { List, Text } from '@mantine/core'; import { ChannelTypeEnum } from '@novu/shared'; import { useAuthContext } from '../../../components/providers/AuthProvider'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { NovuProviderBase } from './NovuProviderBase'; export function NovuEmailProviderModal({ onClose }: { onClose: () => void }) { diff --git a/apps/web/src/pages/integrations/components/NovuInAppForm.tsx b/apps/web/src/pages/integrations/components/NovuInAppForm.tsx index 9cf4916fe00..538e8b8e485 100644 --- a/apps/web/src/pages/integrations/components/NovuInAppForm.tsx +++ b/apps/web/src/pages/integrations/components/NovuInAppForm.tsx @@ -7,8 +7,7 @@ import { ICredentialsDto } from '@novu/shared'; import { IIntegratedProvider } from '../types'; import { updateIntegration } from '../../../api/integration'; -import { Switch, Button, colors } from '../../../design-system'; -import { CircleArrowRight } from '../../../design-system/icons'; +import { Switch, Button, colors, CircleArrowRight } from '@novu/design-system'; import { When } from '../../../components/utils/When'; import { errorMessage, successMessage } from '../../../utils/notifications'; diff --git a/apps/web/src/pages/integrations/components/NovuInAppFrameworkHeader.tsx b/apps/web/src/pages/integrations/components/NovuInAppFrameworkHeader.tsx index c67c4be694e..ec67dbc1cc7 100644 --- a/apps/web/src/pages/integrations/components/NovuInAppFrameworkHeader.tsx +++ b/apps/web/src/pages/integrations/components/NovuInAppFrameworkHeader.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; -import { Text } from '../../../design-system'; -import { AngularLogo, IframeLogo, ReactLogo, VueLogo } from '../../../design-system/icons'; +import { Text, AngularLogo, IframeLogo, ReactLogo, VueLogo } from '@novu/design-system'; import { FrameworkEnum } from '../../quick-start/consts'; const NovuInAppFrameworkHeaderHolder = styled.div` diff --git a/apps/web/src/pages/integrations/components/NovuInAppFrameworks.tsx b/apps/web/src/pages/integrations/components/NovuInAppFrameworks.tsx index a31e17bd158..a5060b4b775 100644 --- a/apps/web/src/pages/integrations/components/NovuInAppFrameworks.tsx +++ b/apps/web/src/pages/integrations/components/NovuInAppFrameworks.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; -import { colors, Text } from '../../../design-system'; -import { ReactLogo, AngularLogo, JavaScriptLogo, VueLogo, IframeLogo } from '../../../design-system/icons'; +import { colors, Text, ReactLogo, AngularLogo, JavaScriptLogo, VueLogo, IframeLogo } from '@novu/design-system'; import { FrameworkEnum } from '../../quick-start/consts'; const NovuInAppFrameworksHolder = styled.div` diff --git a/apps/web/src/pages/integrations/components/NovuInAppProviderModal.tsx b/apps/web/src/pages/integrations/components/NovuInAppProviderModal.tsx index 0138077cd34..fec361e2d0c 100644 --- a/apps/web/src/pages/integrations/components/NovuInAppProviderModal.tsx +++ b/apps/web/src/pages/integrations/components/NovuInAppProviderModal.tsx @@ -2,8 +2,7 @@ import { useState } from 'react'; import styled from '@emotion/styled/macro'; import { Accordion, Box, Center, Loader, useMantineTheme } from '@mantine/core'; -import { colors } from '../../../design-system'; -import { Close } from '../../../design-system/icons/actions/Close'; +import { colors, Close } from '@novu/design-system'; import { IIntegratedProvider } from '../types'; import { SetupTimeline } from '../../quick-start/components/SetupTimeline'; import { NovuInAppForm } from './NovuInAppForm'; diff --git a/apps/web/src/pages/integrations/components/NovuIntegrationCard.tsx b/apps/web/src/pages/integrations/components/NovuIntegrationCard.tsx index 3d83992faab..219ff2db2e7 100644 --- a/apps/web/src/pages/integrations/components/NovuIntegrationCard.tsx +++ b/apps/web/src/pages/integrations/components/NovuIntegrationCard.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { createStyles, Group, useMantineColorScheme } from '@mantine/core'; import { ChannelTypeEnum, IConfigCredentials } from '@novu/shared'; -import { colors, shadows, Text, Tooltip } from '../../../design-system'; +import { colors, shadows, Text, Tooltip } from '@novu/design-system'; import { useIntegrationLimit } from '../../../hooks'; import type { IIntegratedProvider } from '../types'; import { CardStatusBar } from './CardStatusBar'; diff --git a/apps/web/src/pages/integrations/components/NovuProviderBase.tsx b/apps/web/src/pages/integrations/components/NovuProviderBase.tsx index 0ce6a1fe5fe..ba03c18b9a7 100644 --- a/apps/web/src/pages/integrations/components/NovuProviderBase.tsx +++ b/apps/web/src/pages/integrations/components/NovuProviderBase.tsx @@ -4,8 +4,7 @@ import { Stack, Text, useMantineColorScheme } from '@mantine/core'; import { ChannelTypeEnum } from '@novu/shared'; import { When } from '../../../components/utils/When'; import { CONTEXT_PATH } from '../../../config'; -import { colors } from '../../../design-system'; -import { Close } from '../../../design-system/icons/actions/Close'; +import { colors, Close } from '@novu/design-system'; import { useIntegrationLimit } from '../../../hooks'; import { LimitBar } from './LimitBar'; diff --git a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx index 250807840a7..00ec153169a 100644 --- a/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx +++ b/apps/web/src/pages/integrations/components/PrimaryIconButton.tsx @@ -2,8 +2,7 @@ import styled from '@emotion/styled'; import { Group, ActionIcon, Text } from '@mantine/core'; import { useState } from 'react'; import { When } from '../../../components/utils/When'; -import { Tooltip, Button, colors, Modal, Title } from '../../../design-system'; -import { RemoveCondition, StarEmpty, Warning } from '../../../design-system/icons'; +import { Tooltip, Button, colors, Modal, Title, RemoveCondition, StarEmpty, Warning } from '@novu/design-system'; const IconButton = styled(Group)` text-align: center; diff --git a/apps/web/src/pages/integrations/components/ProviderCard.tsx b/apps/web/src/pages/integrations/components/ProviderCard.tsx index f29171204c6..6da46a25b92 100644 --- a/apps/web/src/pages/integrations/components/ProviderCard.tsx +++ b/apps/web/src/pages/integrations/components/ProviderCard.tsx @@ -2,9 +2,8 @@ import { IConfigCredentials } from '@novu/shared'; import styled from '@emotion/styled'; import { Group, useMantineColorScheme } from '@mantine/core'; -import { Button, colors, shadows } from '../../../design-system'; +import { Button, colors, shadows, Settings } from '@novu/design-system'; import { CardStatusBar } from './CardStatusBar'; -import { Settings } from '../../../design-system/icons'; import type { IIntegratedProvider } from '../types'; import { When } from '../../../components/utils/When'; import { CONTEXT_PATH } from '../../../config'; diff --git a/apps/web/src/pages/integrations/components/SetupFrameworkHeader.tsx b/apps/web/src/pages/integrations/components/SetupFrameworkHeader.tsx index 71e6cd2bc2b..d09dc39b79c 100644 --- a/apps/web/src/pages/integrations/components/SetupFrameworkHeader.tsx +++ b/apps/web/src/pages/integrations/components/SetupFrameworkHeader.tsx @@ -2,8 +2,8 @@ import styled from '@emotion/styled'; import { Group, Stack, Title, UnstyledButton } from '@mantine/core'; import { colors } from '@novu/notification-center'; import * as capitalize from 'lodash.capitalize'; -import { shadows } from '../../../design-system'; import { + shadows, AngularGradient, ArrowLeft, Close, @@ -11,7 +11,7 @@ import { JsGradient, ReactGradient, VueGradient, -} from '../../../design-system/icons'; +} from '@novu/design-system'; import { FrameworkEnum } from '../../quick-start/consts'; const Icon = ({ framework }: { framework: string }) => { diff --git a/apps/web/src/pages/integrations/components/SetupWarning.tsx b/apps/web/src/pages/integrations/components/SetupWarning.tsx index 7f67371b881..b3d16af83ca 100644 --- a/apps/web/src/pages/integrations/components/SetupWarning.tsx +++ b/apps/web/src/pages/integrations/components/SetupWarning.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { Group } from '@mantine/core'; import { When } from '../../../components/utils/When'; -import { DisconnectGradient } from '../../../design-system/icons'; +import { DisconnectGradient } from '@novu/design-system'; const WarningMessage = styled(Group)` display: flex; diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationCommonFields.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationCommonFields.tsx index 8ba815cc173..528020681d7 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationCommonFields.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationCommonFields.tsx @@ -2,10 +2,8 @@ import { Controller, useFormContext } from 'react-hook-form'; import styled from '@emotion/styled'; import { useClipboard } from '@mantine/hooks'; -import { Input, Switch } from '../../../design-system'; -import { Check, Copy } from '../../../design-system/icons'; +import { Input, Switch, Check, Copy } from '@novu/design-system'; import type { IIntegratedProvider } from '../types'; -import { When } from '../../../components/utils/When'; const CopyWrapper = styled.div` cursor: pointer; diff --git a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx index bf50ae76243..f1deb2a9441 100644 --- a/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx +++ b/apps/web/src/pages/integrations/components/UpdateIntegrationSidebarHeader.tsx @@ -3,14 +3,24 @@ import { Group, useMantineTheme } from '@mantine/core'; 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'; +import { + Button, + colors, + Dropdown, + Modal, + NameInput, + Text, + Title, + DotsHorizontal, + StarEmpty, + Trash, +} from '@novu/design-system'; import { useFetchEnvironments } from '../../../hooks/useFetchEnvironments'; import { ProviderImage } from './multi-provider/SelectProviderSidebar'; import type { IIntegratedProvider, IntegrationEntity } from '../types'; import { useProviders } from '../useProviders'; import { useDeleteIntegration } from '../../../api/hooks'; import { errorMessage, successMessage } from '../../../utils/notifications'; -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'; 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 2670564df62..0f03102346d 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/CreateProviderInstanceSidebar.tsx @@ -9,9 +9,16 @@ import { Controller, useForm } from 'react-hook-form'; import { createIntegration } from '../../../../api/integration'; import { QueryKeys } from '../../../../api/query.keys'; import { useSegment } from '../../../../components/providers/SegmentProvider'; -import { Button, colors, NameInput, Sidebar } from '../../../../design-system'; -import { ConditionPlus, ArrowLeft, Condition } from '../../../../design-system/icons'; -import { inputStyles } from '../../../../design-system/config/inputs.styles'; +import { + Button, + colors, + NameInput, + Sidebar, + ConditionPlus, + ArrowLeft, + Condition, + inputStyles, +} from '@novu/design-system'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; import { CHANNEL_TYPE_TO_STRING } from '../../../../utils/channels'; import { errorMessage, successMessage } from '../../../../utils/notifications'; @@ -22,6 +29,7 @@ import { When } from '../../../../components/utils/When'; import { Conditions, IConditions } from '../../../../components/conditions'; import { ConditionIconButton } from '../ConditionIconButton'; import { ProviderImage } from './SelectProviderSidebar'; +import { HEADER_HEIGHT } from '../../../../components/layout/constants'; interface ICreateProviderInstanceForm { name: string; @@ -170,6 +178,7 @@ export function CreateProviderInstanceSidebar({ { handleSubmit(onCreateIntegrationInstance)(e); e.stopPropagation(); diff --git a/apps/web/src/pages/integrations/components/multi-provider/NovuProviderSidebarContent.tsx b/apps/web/src/pages/integrations/components/multi-provider/NovuProviderSidebarContent.tsx index 0f646e49b90..ec9c8f03717 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/NovuProviderSidebarContent.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/NovuProviderSidebarContent.tsx @@ -1,8 +1,7 @@ import { Group, Stack, useMantineColorScheme } from '@mantine/core'; import { ChannelTypeEnum } from '@novu/shared'; -import { ErrorIcon } from '../../../../design-system/icons'; -import { colors, Text } from '../../../../design-system'; +import { ErrorIcon, colors, Text } from '@novu/design-system'; import { LimitBar } from '../LimitBar'; import { useIntegrationLimit } from '../../../../hooks'; import type { IIntegratedProvider } from '../../types'; 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 53ebc5dd8d4..4854127c01c 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectPrimaryIntegrationModal.tsx @@ -14,7 +14,7 @@ import { Text, Title, withCellLoading, -} from '../../../../design-system'; +} from '@novu/design-system'; import { IntegrationChannel } from '../IntegrationChannel'; import { IntegrationEnvironmentPill } from '../IntegrationEnvironmentPill'; import { useFetchEnvironments } from '../../../../hooks/useFetchEnvironments'; diff --git a/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx b/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx index a6a0561af0a..dc478bea06c 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/SelectProviderSidebar.tsx @@ -11,11 +11,18 @@ import { InAppProviderIdEnum, } from '@novu/shared'; -import { colors, Sidebar } from '../../../../design-system'; -import { Button, Input, Title, Tooltip, Text } from '../../../../design-system'; -import { getGradient } from '../../../../design-system/config/helper'; -import { Search } from '../../../../design-system/icons'; -import useStyles from '../../../../design-system/tabs/Tabs.styles'; +import { + colors, + Sidebar, + Button, + Input, + Title, + Tooltip, + Text, + getGradient, + Search, + useTabsStyles, +} from '@novu/design-system'; import { useDebounce } from '../../../../hooks'; import { ChannelTitle } from '../../../templates/components/ChannelTitle'; import type { IIntegratedProvider } from '../../types'; @@ -26,6 +33,7 @@ import { sortProviders } from './sort-providers'; import { When } from '../../../../components/utils/When'; import { CONTEXT_PATH } from '../../../../config'; import { useProviders } from '../../useProviders'; +import { HEADER_HEIGHT } from '../../../../components/layout/constants'; const filterSearch = (list, search: string) => list.filter((prov) => prov.displayName.toLowerCase().includes(search.toLowerCase())); @@ -75,7 +83,7 @@ export function SelectProviderSidebar({ }, [integrations]); const [selectedProvider, setSelectedProvider] = useState(null); - const { classes: tabsClasses } = useStyles(false); + const { classes: tabsClasses } = useTabsStyles(false); const debouncedSearchChange = useDebounce((search: string) => { setProvidersList({ @@ -123,6 +131,7 @@ export function SelectProviderSidebar({ 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 ef7ccea745c..ada9526c6d4 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/UpdateProviderSidebar.tsx @@ -16,7 +16,7 @@ import { SmsProviderIdEnum, } from '@novu/shared'; -import { Button, colors, Sidebar, Text } from '../../../../design-system'; +import { Button, colors, Sidebar, Text } from '@novu/design-system'; import { useProviders } from '../../useProviders'; import type { IIntegratedProvider } from '../../types'; import { IntegrationInput } from '../IntegrationInput'; @@ -38,6 +38,7 @@ import { useSelectPrimaryIntegrationModal } from './useSelectPrimaryIntegrationM import { ShareableUrl } from '../Modal/ConnectIntegrationForm'; import { Conditions, IConditions } from '../../../../components/conditions'; import { useDisclosure } from '@mantine/hooks'; +import { HEADER_HEIGHT } from '../../../../components/layout/constants'; interface IProviderForm { name: string; @@ -253,6 +254,7 @@ export function UpdateProviderSidebar({ return ( = { PushProviderIdEnum.APNS, PushProviderIdEnum.PushWebhook, PushProviderIdEnum.OneSignal, + PushProviderIdEnum.Pushpad, ], [ChannelTypeEnum.SMS]: [ SmsProviderIdEnum.Twilio, @@ -67,6 +68,7 @@ const providers: Record = { SmsProviderIdEnum.Termii, SmsProviderIdEnum.AfricasTalking, SmsProviderIdEnum.Novu, + SmsProviderIdEnum.Bandwidth, ].sort(), ], }; 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..797d42ec2d1 100644 --- a/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx +++ b/apps/web/src/pages/integrations/components/multi-provider/useSelectPrimaryIntegrationModal.tsx @@ -1,17 +1,14 @@ import { useState, useCallback } from 'react'; import { useDisclosure } from '@mantine/hooks'; -import { useInlineComponent, useIsMultiProviderConfigurationEnabled } from '../../../../hooks'; +import { useInlineComponent } from '../../../../hooks'; import { ISelectPrimaryIntegrationModalProps, SelectPrimaryIntegrationModal } from './SelectPrimaryIntegrationModal'; export const useSelectPrimaryIntegrationModal = () => { - const [opened, { open, close }] = useDisclosure(false); + const [isOpened, { open, close }] = useDisclosure(false); const [{ environmentId, channelType, exclude, onClose }, setModalProps] = useState< Partial> >({}); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); - const isOpened = opened && isMultiProviderConfigurationEnabled; - const onCloseCallback = useCallback(() => { close(); onClose?.(); diff --git a/apps/web/src/pages/integrations/useProviders.ts b/apps/web/src/pages/integrations/useProviders.ts index ec5c477566f..fb9c136d5ca 100644 --- a/apps/web/src/pages/integrations/useProviders.ts +++ b/apps/web/src/pages/integrations/useProviders.ts @@ -9,7 +9,7 @@ import { PushProviderIdEnum, } from '@novu/shared'; -import { useIntegrations, useIsMultiProviderConfigurationEnabled } from '../../hooks'; +import { useIntegrations } from '../../hooks'; import type { IIntegratedProvider, IntegrationEntity } from './types'; import { IS_DOCKER_HOSTED } from '../../config'; @@ -30,45 +30,6 @@ function fcmFallback(integration: IntegrationEntity | undefined, clonedCredentia } } -function initializeProviders(integrations: IntegrationEntity[]): IIntegratedProvider[] { - return providers - .filter((provider) => !NOVU_SMS_EMAIL_PROVIDERS.includes(provider.id)) - .map((providerItem) => { - const integration = integrations.find((integrationItem) => integrationItem.providerId === providerItem.id); - - const clonedCredentials = cloneDeep(providerItem.credentials); - - if (integration?.credentials && Object.keys(clonedCredentials).length !== 0) { - clonedCredentials.forEach((credential) => { - // eslint-disable-next-line no-param-reassign - credential.value = - credential.type === 'boolean' || credential.type === 'switch' - ? integration.credentials[credential.key] - : integration.credentials[credential.key]?.toString(); - }); - } - - // Remove this like after the run of the fcm-credentials-migration script - fcmFallback(integration, clonedCredentials); - - return { - providerId: providerItem.id, - integrationId: integration?._id ? integration._id : '', - displayName: providerItem.displayName, - channel: providerItem.channel, - credentials: integration?.credentials ? clonedCredentials : providerItem.credentials, - docReference: providerItem.docReference, - comingSoon: !!providerItem.comingSoon, - betaVersion: !!providerItem.betaVersion, - active: integration?.active ?? false, - connected: !!integration, - logoFileName: providerItem.logoFileName, - environmentId: integration?._environmentId, - primary: integration?.primary ?? false, - }; - }); -} - function initializeProvidersByIntegration(integrations: IntegrationEntity[]): IIntegratedProvider[] { return integrations .filter((integrationItem) => { @@ -140,13 +101,10 @@ const sortProviders = (unsortedProviders: IIntegratedProvider[]) => { export const useProviders = () => { const { integrations, loading: isLoading, refetch } = useIntegrations(); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const sortedProviders = useMemo(() => { if (integrations) { - const initializedProviders = isMultiProviderConfigurationEnabled - ? initializeProvidersByIntegration(integrations) - : initializeProviders(integrations); + const initializedProviders = initializeProvidersByIntegration(integrations); return { emailProviders: sortProviders( @@ -176,7 +134,7 @@ export const useProviders = () => { inAppProvider: [], providers: [], }; - }, [isMultiProviderConfigurationEnabled, integrations]); + }, [integrations]); return { ...sortedProviders, diff --git a/apps/web/src/pages/invites/MembersInvitePage.tsx b/apps/web/src/pages/invites/MembersInvitePage.tsx index b34d5047848..8ad57c66205 100644 --- a/apps/web/src/pages/invites/MembersInvitePage.tsx +++ b/apps/web/src/pages/invites/MembersInvitePage.tsx @@ -16,8 +16,7 @@ import { resendInviteMember, } from '../../api/organization'; import { MembersTable } from './components/MembersTable'; -import { Button, Input } from '../../design-system'; -import { Invite, UserAccess } from '../../design-system/icons'; +import { Button, Input, Invite, UserAccess } from '@novu/design-system'; import { useAuthContext } from '../../components/providers/AuthProvider'; import { parseUrl } from '../../utils/routeUtils'; import { ROUTES } from '../../constants/routes.enum'; diff --git a/apps/web/src/pages/invites/components/MemberRole.tsx b/apps/web/src/pages/invites/components/MemberRole.tsx index 92c952d9eb3..b27b3f2d738 100644 --- a/apps/web/src/pages/invites/components/MemberRole.tsx +++ b/apps/web/src/pages/invites/components/MemberRole.tsx @@ -1,7 +1,7 @@ import { MemberRoleEnum } from '@novu/shared'; import styled from '@emotion/styled'; -import { Dropdown, Tag } from '../../../design-system'; +import { Dropdown, Tag } from '@novu/design-system'; export function MemberRole({ member, onChangeMemberRole, isEnableMemberActions, allowChangeRole = false }) { const TagElement = () => { diff --git a/apps/web/src/pages/invites/components/MembersTable.tsx b/apps/web/src/pages/invites/components/MembersTable.tsx index d6e41a4fa58..e33de663559 100644 --- a/apps/web/src/pages/invites/components/MembersTable.tsx +++ b/apps/web/src/pages/invites/components/MembersTable.tsx @@ -4,9 +4,7 @@ import * as capitalize from 'lodash.capitalize'; import { useClipboard } from '@mantine/hooks'; import { MemberRoleEnum, MemberStatusEnum } from '@novu/shared'; -import { DotsHorizontal, Mail, Trash } from '../../../design-system/icons'; -import { colors, Dropdown, Tag } from '../../../design-system'; -import useStyles from '../../../design-system/config/text.styles'; +import { DotsHorizontal, Mail, Trash, useTextStyles, colors, Dropdown, Tag } from '@novu/design-system'; import { MemberRole } from './MemberRole'; import { When } from '../../../components/utils/When'; import { parseUrl } from '../../../utils/routeUtils'; @@ -21,7 +19,7 @@ export function MembersTable({ onChangeMemberRole, allowChangeRole = false, }) { - const { classes, theme } = useStyles(); + const { classes, theme } = useTextStyles(); const clipboardInviteLink = useClipboard({ timeout: 1000 }); const selfHosted = process.env.REACT_APP_DOCKER_HOSTED_ENV === 'true'; diff --git a/apps/web/src/pages/partner-integrations/LinkVercelProjectPage.tsx b/apps/web/src/pages/partner-integrations/LinkVercelProjectPage.tsx index f8d82f34499..b33184a37bb 100644 --- a/apps/web/src/pages/partner-integrations/LinkVercelProjectPage.tsx +++ b/apps/web/src/pages/partner-integrations/LinkVercelProjectPage.tsx @@ -3,8 +3,7 @@ import { Stack } from '@mantine/core'; import { LinkProjectContainer } from './components/LinkProjectContainer'; import { PartnerIntegrationLayout } from '../../components/layout/components/PartnerIntegrationLayout'; -import { Container, Text, Title } from '../../design-system'; -import { ThemeProvider } from '../../design-system/ThemeProvider'; +import { Container, Text, Title, ThemeProvider } from '@novu/design-system'; export function LinkVercelProjectPage({ type }: { type: 'edit' | 'create' }) { return ( diff --git a/apps/web/src/pages/partner-integrations/components/LinkMoreProjectRow.tsx b/apps/web/src/pages/partner-integrations/components/LinkMoreProjectRow.tsx index 5e0238437e8..3ed97c2bd0c 100644 --- a/apps/web/src/pages/partner-integrations/components/LinkMoreProjectRow.tsx +++ b/apps/web/src/pages/partner-integrations/components/LinkMoreProjectRow.tsx @@ -1,6 +1,5 @@ import { Group } from '@mantine/core'; -import { Button } from '../../../design-system'; -import { PlusCircle } from '../../../design-system/icons'; +import { Button, PlusCircle } from '@novu/design-system'; type LinkMoreProjectRowProps = { addMoreProjectRow: VoidFunction; diff --git a/apps/web/src/pages/partner-integrations/components/LinkProjectContainer.tsx b/apps/web/src/pages/partner-integrations/components/LinkProjectContainer.tsx index 737ae685e31..11c1b8f577f 100644 --- a/apps/web/src/pages/partner-integrations/components/LinkProjectContainer.tsx +++ b/apps/web/src/pages/partner-integrations/components/LinkProjectContainer.tsx @@ -12,7 +12,7 @@ import { import { useVercelParams, useAuthController } from '../../../hooks'; import { LinkMoreProjectRow } from './LinkMoreProjectRow'; import { ProjectRow } from './ProjectRow'; -import { Text, colors, Button } from '../../../design-system'; +import { Text, colors, Button } from '@novu/design-system'; import { errorMessage, successMessage } from '../../../utils/notifications'; import SetupLoader from '../../auth/components/SetupLoader'; diff --git a/apps/web/src/pages/partner-integrations/components/ProjectRow.tsx b/apps/web/src/pages/partner-integrations/components/ProjectRow.tsx index 1f2cb42a5aa..d86b31b414a 100644 --- a/apps/web/src/pages/partner-integrations/components/ProjectRow.tsx +++ b/apps/web/src/pages/partner-integrations/components/ProjectRow.tsx @@ -6,7 +6,7 @@ import { useIntersection } from '@mantine/hooks'; import type { FetchNextPageOptions, InfiniteQueryObserverResult } from '@tanstack/react-query'; import { IOrganizationEntity } from '@novu/shared'; -import { Text, Select } from '../../../design-system'; +import { Text, Select } from '@novu/design-system'; import { ProjectLinkFormValues } from './LinkProjectContainer'; type ProjectDataType = { diff --git a/apps/web/src/pages/quick-start/components/ChannelsConfiguration.tsx b/apps/web/src/pages/quick-start/components/ChannelsConfiguration.tsx index 815d7eb8a9e..575bda9cdf0 100644 --- a/apps/web/src/pages/quick-start/components/ChannelsConfiguration.tsx +++ b/apps/web/src/pages/quick-start/components/ChannelsConfiguration.tsx @@ -6,10 +6,9 @@ import { ChannelTypeEnum, InAppProviderIdEnum } from '@novu/shared'; import { IQuickStartChannelConfiguration, OnBoardingAnalyticsEnum, quickStartChannels } from '../consts'; import { When } from '../../../components/utils/When'; -import { ActiveLabel } from '../../../design-system/icons/general/ActiveLabel'; +import { ActiveLabel, Button, colors } from '@novu/design-system'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { useActiveIntegrations, useIntegrationLimit } from '../../../hooks'; -import { Button, colors } from '../../../design-system'; import type { IntegrationEntity } from '../../integrations/types'; import { useCreateInAppIntegration } from '../../../hooks/useCreateInAppIntegration'; diff --git a/apps/web/src/pages/quick-start/components/NavButton.tsx b/apps/web/src/pages/quick-start/components/NavButton.tsx index 74f828fb507..f4ae368d1cc 100644 --- a/apps/web/src/pages/quick-start/components/NavButton.tsx +++ b/apps/web/src/pages/quick-start/components/NavButton.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Center } from '@mantine/core'; import styled from '@emotion/styled'; -import { Button } from '../../../design-system'; +import { Button } from '@novu/design-system'; import { currentOnboardingStep } from './route/store'; export function NavButton({ diff --git a/apps/web/src/pages/quick-start/components/OnboardingSteps.styles.ts b/apps/web/src/pages/quick-start/components/OnboardingSteps.styles.ts index b4d6b45628a..b78a91d1940 100644 --- a/apps/web/src/pages/quick-start/components/OnboardingSteps.styles.ts +++ b/apps/web/src/pages/quick-start/components/OnboardingSteps.styles.ts @@ -1,5 +1,5 @@ import { createStyles } from '@mantine/core'; -import { colors, shadows, Text } from '../../../design-system'; +import { colors, shadows, Text } from '@novu/design-system'; import styled from '@emotion/styled'; export default createStyles((theme, _params, getRef) => { diff --git a/apps/web/src/pages/quick-start/components/QuickStartWrapper.tsx b/apps/web/src/pages/quick-start/components/QuickStartWrapper.tsx index bda345b1ade..88833a8d837 100644 --- a/apps/web/src/pages/quick-start/components/QuickStartWrapper.tsx +++ b/apps/web/src/pages/quick-start/components/QuickStartWrapper.tsx @@ -9,8 +9,7 @@ import { useSegment } from '../../../components/providers/SegmentProvider'; import { When } from '../../../components/utils/When'; import { INTERCOM_APP_ID } from '../../../config'; import { ROUTES } from '../../../constants/routes.enum'; -import { ArrowButton, colors, Text } from '../../../design-system'; -import { Discord } from '../../../design-system/icons'; +import { ArrowButton, colors, Text, Discord } from '@novu/design-system'; import { useEffectOnce } from '../../../hooks'; import { discordInviteUrl, notificationCenterDocsUrl, OnBoardingAnalyticsEnum } from '../consts'; import { currentOnboardingStep } from './route/store'; diff --git a/apps/web/src/pages/quick-start/components/SetupStatus.tsx b/apps/web/src/pages/quick-start/components/SetupStatus.tsx index 1b25efc8a4e..e27df5ed5df 100644 --- a/apps/web/src/pages/quick-start/components/SetupStatus.tsx +++ b/apps/web/src/pages/quick-start/components/SetupStatus.tsx @@ -2,8 +2,7 @@ import { Group, Stack } from '@mantine/core'; import { useEffect } from 'react'; import styled from '@emotion/styled'; -import { Button, colors, Text } from '../../../design-system'; -import { Label } from '../../../design-system/typography/label'; +import { Button, colors, Text, Label } from '@novu/design-system'; import { When } from '../../../components/utils/When'; import { useDataRef } from '../../../hooks'; diff --git a/apps/web/src/pages/quick-start/components/SetupTimeline.tsx b/apps/web/src/pages/quick-start/components/SetupTimeline.tsx index 8516295f9f9..cfe4e5f14f4 100644 --- a/apps/web/src/pages/quick-start/components/SetupTimeline.tsx +++ b/apps/web/src/pages/quick-start/components/SetupTimeline.tsx @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import { getApiKeys } from '../../../api/environment'; import { When } from '../../../components/utils/When'; import { API_ROOT, ENV, IS_DOCKER_HOSTED, WS_URL } from '../../../config'; -import { colors, shadows, Text } from '../../../design-system'; +import { colors, shadows, Text } from '@novu/design-system'; import { useEnvController } from '../../../hooks'; import { PrismOnCopy } from '../../settings/tabs/components/Prism'; import { SetupStatus } from './SetupStatus'; diff --git a/apps/web/src/pages/quick-start/components/TriggerCard.tsx b/apps/web/src/pages/quick-start/components/TriggerCard.tsx index 062a9b5b4fd..8b7a00a42b5 100644 --- a/apps/web/src/pages/quick-start/components/TriggerCard.tsx +++ b/apps/web/src/pages/quick-start/components/TriggerCard.tsx @@ -3,7 +3,7 @@ import { Card, Grid, Group } from '@mantine/core'; import styled from '@emotion/styled'; import { CONTEXT_PATH } from '../../../config'; -import { colors, Text } from '../../../design-system'; +import { colors, Text } from '@novu/design-system'; export const TriggerCard = ({ name, diff --git a/apps/web/src/pages/quick-start/components/layout/BodyLayout.tsx b/apps/web/src/pages/quick-start/components/layout/BodyLayout.tsx index 5bede0d61a5..ee939e90398 100644 --- a/apps/web/src/pages/quick-start/components/layout/BodyLayout.tsx +++ b/apps/web/src/pages/quick-start/components/layout/BodyLayout.tsx @@ -3,7 +3,7 @@ import { Grid, Timeline } from '@mantine/core'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { getStartedSteps, OnBoardingAnalyticsEnum } from '../../consts'; import { useSegment } from '../../../../components/providers/SegmentProvider'; import { ROUTES } from '../../../../constants/routes.enum'; diff --git a/apps/web/src/pages/quick-start/components/layout/FooterLayout.tsx b/apps/web/src/pages/quick-start/components/layout/FooterLayout.tsx index 5d2261cc597..24d021e7966 100644 --- a/apps/web/src/pages/quick-start/components/layout/FooterLayout.tsx +++ b/apps/web/src/pages/quick-start/components/layout/FooterLayout.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import styled from '@emotion/styled'; -import { colors, DotsNavigation } from '../../../../design-system'; +import { colors, DotsNavigation } from '@novu/design-system'; import { useLocation, useNavigate } from 'react-router-dom'; import { ROUTES } from '../../../../constants/routes.enum'; import { Grid } from '@mantine/core'; diff --git a/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx b/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx index af543eed4dd..d2830082bec 100644 --- a/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx +++ b/apps/web/src/pages/quick-start/components/layout/GetStartedLayout.tsx @@ -8,7 +8,7 @@ import { currentOnboardingStep } from '../route/store'; import { BodyLayout } from './BodyLayout'; import { FooterLayout } from './FooterLayout'; import { HeaderLayout } from './HeaderLayout'; -import { Title } from '../../../../design-system'; +import { Title } from '@novu/design-system'; interface IGetStartedLayoutProps { children?: React.ReactNode; diff --git a/apps/web/src/pages/quick-start/components/layout/HeaderLayout.tsx b/apps/web/src/pages/quick-start/components/layout/HeaderLayout.tsx index 7e6d6029792..dc8651b314b 100644 --- a/apps/web/src/pages/quick-start/components/layout/HeaderLayout.tsx +++ b/apps/web/src/pages/quick-start/components/layout/HeaderLayout.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from '@emotion/styled'; -import { colors, Text, Title } from '../../../../design-system'; +import { colors, Text, Title } from '@novu/design-system'; export function HeaderLayout({ children }: { children: React.ReactNode }) { return {children}; diff --git a/apps/web/src/pages/quick-start/consts.tsx b/apps/web/src/pages/quick-start/consts.tsx index 1b0cdd3a2eb..943006b29f3 100644 --- a/apps/web/src/pages/quick-start/consts.tsx +++ b/apps/web/src/pages/quick-start/consts.tsx @@ -3,7 +3,7 @@ import { Stack } from '@mantine/core'; import { NavigateFunction } from 'react-router-dom'; import { ChannelTypeEnum } from '@novu/shared'; -import { Bell, Chat, Mail, Mobile, Sms } from '../../design-system/icons'; +import { Bell, Chat, Mail, Mobile, Sms } from '@novu/design-system'; import { ROUTES } from '../../constants/routes.enum'; import { WIDGET_EMBED_PATH } from '../../config'; diff --git a/apps/web/src/pages/quick-start/steps/DigestPreview.tsx b/apps/web/src/pages/quick-start/steps/DigestPreview.tsx index bf7bc1f011f..d1fcac1a254 100644 --- a/apps/web/src/pages/quick-start/steps/DigestPreview.tsx +++ b/apps/web/src/pages/quick-start/steps/DigestPreview.tsx @@ -8,10 +8,7 @@ import { useCreateDigestDemoWorkflow } from '../../../api/hooks/notification-tem import { DigestDemoFlow } from '../../../components'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { ROUTES } from '../../../constants/routes.enum'; -import { Button } from '../../../design-system'; -import { ArrowLeft } from '../../../design-system/icons'; -import { ArrowLeftGradient } from '../../../design-system/icons/gradient/ArrowLeftGradient'; -import { Label } from '../../../design-system/typography/label'; +import { Button, ArrowLeft, ArrowLeftGradient, Label } from '@novu/design-system'; import { NavButton } from '../components/NavButton'; import useStyles from '../components/OnboardingSteps.styles'; import { getStartedSteps, OnBoardingAnalyticsEnum } from '../consts'; diff --git a/apps/web/src/pages/quick-start/steps/FrameworkSetup.tsx b/apps/web/src/pages/quick-start/steps/FrameworkSetup.tsx index 505c72c3eb4..9805c9104ce 100644 --- a/apps/web/src/pages/quick-start/steps/FrameworkSetup.tsx +++ b/apps/web/src/pages/quick-start/steps/FrameworkSetup.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { ROUTES } from '../../../constants/routes.enum'; -import { Cards } from '../../../design-system'; +import { Cards } from '@novu/design-system'; import { QuickStartWrapper } from '../components/QuickStartWrapper'; import { frameworkSetupTitle, OnBoardingAnalyticsEnum } from '../consts'; diff --git a/apps/web/src/pages/quick-start/steps/GetStarted.tsx b/apps/web/src/pages/quick-start/steps/GetStarted.tsx index 933be3cec5e..9a67138a54a 100644 --- a/apps/web/src/pages/quick-start/steps/GetStarted.tsx +++ b/apps/web/src/pages/quick-start/steps/GetStarted.tsx @@ -3,10 +3,8 @@ import { ChannelTypeEnum } from '@novu/shared'; import { useEffect, useState } from 'react'; import { useSegment } from '../../../components/providers/SegmentProvider'; -import { ArrowRight } from '../../../design-system/icons/arrows/ArrowRight'; -import { useIsMultiProviderConfigurationEnabled } from '../../../hooks'; +import { ArrowRight } from '@novu/design-system'; import { IntegrationsListModal } from '../../integrations/IntegrationsListModal'; -import { IntegrationsStoreModal } from '../../integrations/IntegrationsStoreModal'; import { ChannelsConfiguration } from '../components/ChannelsConfiguration'; import { GetStartedLayout } from '../components/layout/GetStartedLayout'; import { NavButton } from '../components/NavButton'; @@ -27,7 +25,6 @@ const ChannelsConfigurationHolder = styled.div` export function GetStarted() { const segment = useSegment(); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const [clickedChannel, setClickedChannel] = useState<{ open: boolean; channelType?: ChannelTypeEnum; @@ -56,20 +53,11 @@ export function GetStarted() { }} > - {isMultiProviderConfigurationEnabled ? ( - - ) : ( - - )} - + diff --git a/apps/web/src/pages/quick-start/steps/InAppSuccess.tsx b/apps/web/src/pages/quick-start/steps/InAppSuccess.tsx index 9016fa4ce6b..f0a2240461f 100644 --- a/apps/web/src/pages/quick-start/steps/InAppSuccess.tsx +++ b/apps/web/src/pages/quick-start/steps/InAppSuccess.tsx @@ -6,7 +6,7 @@ import PageContainer from '../../../components/layout/components/PageContainer'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { SandBoxSetupSuccess } from '../../../components/quick-start/in-app-onboarding/SandboxSetupSuccess'; import { ROUTES } from '../../../constants/routes.enum'; -import { Button, colors } from '../../../design-system'; +import { Button, colors } from '@novu/design-system'; import { currentOnboardingStep } from '../components/route/store'; import { FlowTypeEnum, OnBoardingAnalyticsEnum } from '../consts'; diff --git a/apps/web/src/pages/settings/SettingsPage.tsx b/apps/web/src/pages/settings/SettingsPage.tsx index 6ffe6757e0c..b77bd26113a 100644 --- a/apps/web/src/pages/settings/SettingsPage.tsx +++ b/apps/web/src/pages/settings/SettingsPage.tsx @@ -5,10 +5,9 @@ import { ApiKeysCard } from './tabs'; import { Outlet } from 'react-router-dom'; import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import useStyles from '../../design-system/tabs/Tabs.styles'; import { ROUTES } from '../../constants/routes.enum'; import { useAuthContext } from '../../components/providers/AuthProvider'; -import { colors } from '../../design-system'; +import { colors, useTabsStyles } from '@novu/design-system'; const SettingsPageWrapper = ({ children }: { children: React.ReactNode }) => { return ( @@ -24,7 +23,7 @@ const SettingsPageWrapper = ({ children }: { children: React.ReactNode }) => { export function SettingsPage() { const { currentOrganization } = useAuthContext(); const selfHosted = process.env.REACT_APP_DOCKER_HOSTED_ENV === 'true'; - const { classes } = useStyles(false); + const { classes } = useTabsStyles(false); const navigate = useNavigate(); const { pathname } = useLocation(); const value = useMemo(() => { diff --git a/apps/web/src/pages/settings/tabs/ApiKeysCard.tsx b/apps/web/src/pages/settings/tabs/ApiKeysCard.tsx index dab88ede4fb..651c2250c17 100644 --- a/apps/web/src/pages/settings/tabs/ApiKeysCard.tsx +++ b/apps/web/src/pages/settings/tabs/ApiKeysCard.tsx @@ -3,10 +3,8 @@ import { useClipboard } from '@mantine/hooks'; import { useQuery } from '@tanstack/react-query'; import styled from '@emotion/styled'; -import { Input, Tooltip, colors } from '../../../design-system'; -import { Check, Copy } from '../../../design-system/icons'; +import { Input, Tooltip, colors, Check, Copy, inputStyles } from '@novu/design-system'; import { getApiKeys } from '../../../api/environment'; -import { inputStyles } from '../../../design-system/config/inputs.styles'; import { useEnvController } from '../../../hooks'; import { Regenerate } from './components/Regenerate'; import { When } from '../../../components/utils/When'; diff --git a/apps/web/src/pages/settings/tabs/EmailSettings.tsx b/apps/web/src/pages/settings/tabs/EmailSettings.tsx index ba18d2e9373..712a1c835f7 100644 --- a/apps/web/src/pages/settings/tabs/EmailSettings.tsx +++ b/apps/web/src/pages/settings/tabs/EmailSettings.tsx @@ -1,17 +1,25 @@ import { useClipboard } from '@mantine/hooks'; import styled from '@emotion/styled'; -import { inputStyles } from '../../../design-system/config/inputs.styles'; +import { + colors, + Text, + Input, + Tooltip, + Button, + Check, + CheckCircle, + Copy, + WarningIcon, + inputStyles, +} from '@novu/design-system'; import Card from '../../../components/layout/components/Card'; import { ActionIcon, Center, Input as MantineInput } from '@mantine/core'; -import { colors, Text, Input, Tooltip, Button } from '../../../design-system'; -import { Check, CheckCircle, Copy } from '../../../design-system/icons'; import React, { useEffect } from 'react'; import { Control, Controller, useForm } from 'react-hook-form'; import { useEffectOnce, useEnvController } from '../../../hooks'; import { useMutation } from '@tanstack/react-query'; import { updateDnsSettings } from '../../../api/environment'; import { showNotification } from '@mantine/notifications'; -import { WarningIcon } from '../../../design-system/icons/general/WarningIcon'; import { validateMxRecord } from '../../../api/inbound-parse'; import { MAIL_SERVER_DOMAIN } from '../../../config'; diff --git a/apps/web/src/pages/settings/tabs/components/ConfirmRegenerationModal.tsx b/apps/web/src/pages/settings/tabs/components/ConfirmRegenerationModal.tsx index 68e72a68603..8e9b12ab353 100644 --- a/apps/web/src/pages/settings/tabs/components/ConfirmRegenerationModal.tsx +++ b/apps/web/src/pages/settings/tabs/components/ConfirmRegenerationModal.tsx @@ -1,5 +1,5 @@ import { Group, Modal, useMantineTheme } from '@mantine/core'; -import { Button, colors, shadows, Title, Text } from '../../../../design-system'; +import { Button, colors, shadows, Title, Text } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; export function ConfirmRegenerationModal({ diff --git a/apps/web/src/pages/settings/tabs/components/Prism.tsx b/apps/web/src/pages/settings/tabs/components/Prism.tsx index 82b06d42e37..06dc7ccf797 100644 --- a/apps/web/src/pages/settings/tabs/components/Prism.tsx +++ b/apps/web/src/pages/settings/tabs/components/Prism.tsx @@ -1,6 +1,5 @@ -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { Prism as MantinePrism } from '@mantine/prism'; -import React from 'react'; import { useClipboard } from '@mantine/hooks'; export function Prism({ code }: { code: string }) { diff --git a/apps/web/src/pages/settings/tabs/components/Regenerate.tsx b/apps/web/src/pages/settings/tabs/components/Regenerate.tsx index 3399b9bb219..e10a7fd4851 100644 --- a/apps/web/src/pages/settings/tabs/components/Regenerate.tsx +++ b/apps/web/src/pages/settings/tabs/components/Regenerate.tsx @@ -5,7 +5,7 @@ import { showNotification } from '@mantine/notifications'; import { Group } from '@mantine/core'; import { ConfirmRegenerationModal } from './ConfirmRegenerationModal'; -import { Button, Text } from '../../../../design-system'; +import { Button, Text } from '@novu/design-system'; import { regenerateApiKeys } from '../../../../api/environment'; export const Regenerate = ({ fetchApiKeys }: { fetchApiKeys: () => void }) => { diff --git a/apps/web/src/pages/subscribers/SubscribersListPage.tsx b/apps/web/src/pages/subscribers/SubscribersListPage.tsx index c248fe10f53..e60ab61d164 100644 --- a/apps/web/src/pages/subscribers/SubscribersListPage.tsx +++ b/apps/web/src/pages/subscribers/SubscribersListPage.tsx @@ -6,9 +6,7 @@ import type { ISubscriber } from '@novu/shared'; import { useSubscribers } from '../../hooks'; import PageHeader from '../../components/layout/components/PageHeader'; import PageContainer from '../../components/layout/components/PageContainer'; -import { Table, withCellLoading, IExtendedColumn } from '../../design-system'; -import { ViewportWide } from '../../design-system/icons/general/ViewportWide'; -import { HoverCard } from '../../design-system/hover-card/HoverCard'; +import { Table, withCellLoading, IExtendedColumn, ViewportWide, HoverCard } from '@novu/design-system'; const columns: IExtendedColumn[] = [ { diff --git a/apps/web/src/pages/templates/TemplatesDigestPlaygroundPage.tsx b/apps/web/src/pages/templates/TemplatesDigestPlaygroundPage.tsx index 0fbfdfd3b66..6dcfc6bdd8c 100644 --- a/apps/web/src/pages/templates/TemplatesDigestPlaygroundPage.tsx +++ b/apps/web/src/pages/templates/TemplatesDigestPlaygroundPage.tsx @@ -4,7 +4,7 @@ import { ReactFlowProvider } from 'react-flow-renderer'; import { useNavigate, useParams } from 'react-router-dom'; import PageContainer from '../../components/layout/components/PageContainer'; -import { ArrowButton, colors, Title, Text, Button } from '../../design-system'; +import { ArrowButton, colors, Title, Text, Button } from '@novu/design-system'; import { parseUrl } from '../../utils/routeUtils'; import { ROUTES } from '../../constants/routes.enum'; import { DigestDemoFlow } from '../../components'; diff --git a/apps/web/src/pages/templates/TemplatesListNoData.tsx b/apps/web/src/pages/templates/TemplatesListNoData.tsx index a648595e057..130bf786096 100644 --- a/apps/web/src/pages/templates/TemplatesListNoData.tsx +++ b/apps/web/src/pages/templates/TemplatesListNoData.tsx @@ -5,7 +5,7 @@ import { faFile } from '@fortawesome/free-regular-svg-icons'; import { Skeleton } from '@mantine/core'; import { useSegment } from '../../components/providers/SegmentProvider'; -import { CardTile, colors, Popover, shadows } from '../../design-system'; +import { CardTile, colors, Popover } from '@novu/design-system'; import { IBlueprintTemplate } from '../../api/types'; import { TemplateCreationSourceEnum } from './shared'; import { useHoverOverItem } from '../../hooks'; diff --git a/apps/web/src/pages/templates/TemplatesListNoDataOld.tsx b/apps/web/src/pages/templates/TemplatesListNoDataOld.tsx index 98533e39f20..87975b144b1 100644 --- a/apps/web/src/pages/templates/TemplatesListNoDataOld.tsx +++ b/apps/web/src/pages/templates/TemplatesListNoDataOld.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; -import { Cards, colors, Text } from '../../design-system'; -import { PageGradient, DigestGradient } from '../../design-system/icons'; +import { Cards, colors, Text, PageGradient, DigestGradient } from '@novu/design-system'; const NoDataHolder = styled.div` display: flex; diff --git a/apps/web/src/pages/templates/WorkflowListPage.tsx b/apps/web/src/pages/templates/WorkflowListPage.tsx index e6e461321a9..df5fe8ec1c6 100644 --- a/apps/web/src/pages/templates/WorkflowListPage.tsx +++ b/apps/web/src/pages/templates/WorkflowListPage.tsx @@ -13,9 +13,22 @@ import { } from '../../hooks'; import PageHeader from '../../components/layout/components/PageHeader'; import PageContainer from '../../components/layout/components/PageContainer'; -import { Tag, Table, colors, Text, IExtendedColumn, withCellLoading, PlusButton, Container } from '../../design-system'; -import { Bolt, BoltFilled, BoltOffFilled, Edit, ProviderMissing } from '../../design-system/icons'; -import { Tooltip } from '../../design-system'; +import { + Tag, + Table, + colors, + Text, + IExtendedColumn, + withCellLoading, + PlusButton, + Container, + Bolt, + BoltFilled, + BoltOffFilled, + Edit, + ProviderMissing, + Tooltip, +} from '@novu/design-system'; import { ROUTES } from '../../constants/routes.enum'; import { parseUrl } from '../../utils/routeUtils'; import { TemplatesListNoData } from './TemplatesListNoData'; diff --git a/apps/web/src/pages/templates/components/BlueprintModal.tsx b/apps/web/src/pages/templates/components/BlueprintModal.tsx index ff3e47a0a76..108bd3df79f 100644 --- a/apps/web/src/pages/templates/components/BlueprintModal.tsx +++ b/apps/web/src/pages/templates/components/BlueprintModal.tsx @@ -5,7 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { useEffect, useState } from 'react'; import { IUserEntity } from '@novu/shared'; -import { colors, shadows, Title, Text, Button } from '../../../design-system'; +import { colors, shadows, Title, Text, Button } from '@novu/design-system'; import { updateUserOnBoarding } from '../../../api/user'; import { getBlueprintTemplateById } from '../../../api/notification-templates'; import { errorMessage } from '../../../utils/notifications'; diff --git a/apps/web/src/pages/templates/components/ChannelStepEditor.tsx b/apps/web/src/pages/templates/components/ChannelStepEditor.tsx index 07c66ad186b..754eb801cef 100644 --- a/apps/web/src/pages/templates/components/ChannelStepEditor.tsx +++ b/apps/web/src/pages/templates/components/ChannelStepEditor.tsx @@ -11,7 +11,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { SubPageWrapper } from './SubPageWrapper'; import { DigestMetadata } from '../workflow/DigestMetadata'; import { DelayMetadata } from '../workflow/DelayMetadata'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { useEffect, useMemo } from 'react'; import { useBasePath } from '../hooks/useBasePath'; import { StepName } from './StepName'; diff --git a/apps/web/src/pages/templates/components/ChannelTitle.tsx b/apps/web/src/pages/templates/components/ChannelTitle.tsx index 4d5319f84e7..e6989d50400 100644 --- a/apps/web/src/pages/templates/components/ChannelTitle.tsx +++ b/apps/web/src/pages/templates/components/ChannelTitle.tsx @@ -1,6 +1,6 @@ import { Group } from '@mantine/core'; import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; -import { Chat, DigestGradient, InApp, Mail, Mobile, Sms, TimerGradient } from '../../../design-system/icons'; +import { Chat, DigestGradient, InApp, Mail, Mobile, Sms, TimerGradient } from '@novu/design-system'; export const ChannelTitle = ({ channel, diff --git a/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx b/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx index b39ae7e166d..696af7da395 100644 --- a/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx +++ b/apps/web/src/pages/templates/components/CreateWorkflowDropdown.tsx @@ -4,8 +4,7 @@ import { faFile } from '@fortawesome/free-regular-svg-icons'; import { faDiagramNext } from '@fortawesome/free-solid-svg-icons'; import styled from '@emotion/styled'; -import { Button, Dropdown, PlusButton, Popover } from '../../../design-system'; -import { PlusCircle } from '../../../design-system/icons'; +import { Dropdown, PlusButton, Popover } from '@novu/design-system'; import { IBlueprintTemplate } from '../../../api/types'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { TemplateCreationSourceEnum } from '../shared'; diff --git a/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx b/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx index 0b276c06c0c..2922cf904a4 100644 --- a/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx +++ b/apps/web/src/pages/templates/components/DeleteConfirmModal.tsx @@ -1,6 +1,6 @@ import { Alert, Group, Modal, useMantineTheme } from '@mantine/core'; import { WarningOutlined } from '@ant-design/icons'; -import { Button, colors, shadows, Title, Text } from '../../../design-system'; +import { Button, colors, shadows, Title, Text } from '@novu/design-system'; export function DeleteConfirmModal({ target, diff --git a/apps/web/src/pages/templates/components/DeleteStepRow.tsx b/apps/web/src/pages/templates/components/DeleteStepRow.tsx index d27609e05c1..40af34e6f0c 100644 --- a/apps/web/src/pages/templates/components/DeleteStepRow.tsx +++ b/apps/web/src/pages/templates/components/DeleteStepRow.tsx @@ -1,9 +1,8 @@ import { StepTypeEnum } from '@novu/shared'; import { useEnvController } from '../../../hooks'; import { useOutletContext, useParams } from 'react-router-dom'; -import { Button, colors } from '../../../design-system'; +import { Button, colors, Trash } from '@novu/design-system'; import styled from '@emotion/styled'; -import { Trash } from '../../../design-system/icons'; import { Group } from '@mantine/core'; import { When } from '../../../components/utils/When'; diff --git a/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx b/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx index d595b159b1a..51c34eb6364 100644 --- a/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx +++ b/apps/web/src/pages/templates/components/EditorPreviewSwitch.tsx @@ -1,5 +1,5 @@ import { SegmentedControl, useMantineTheme } from '@mantine/core'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { ViewEnum } from './email-editor/EmailMessagesCards'; export const EditorPreviewSwitch = ({ view, setView }) => { diff --git a/apps/web/src/pages/templates/components/ExecutionDetailsModalWrapper.tsx b/apps/web/src/pages/templates/components/ExecutionDetailsModalWrapper.tsx index 8b6f10b185e..7e7abc136b4 100644 --- a/apps/web/src/pages/templates/components/ExecutionDetailsModalWrapper.tsx +++ b/apps/web/src/pages/templates/components/ExecutionDetailsModalWrapper.tsx @@ -3,7 +3,7 @@ import { LoadingOverlay, useMantineTheme } from '@mantine/core'; import { getActivityList } from '../../../api/activity'; import { ExecutionDetailsModal } from '../../../components/execution-detail/ExecutionDetailsModal'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; interface Props { transactionId: string; diff --git a/apps/web/src/pages/templates/components/LackIntegrationAlert.tsx b/apps/web/src/pages/templates/components/LackIntegrationAlert.tsx index 0f3d3925c79..13ace2c6e27 100644 --- a/apps/web/src/pages/templates/components/LackIntegrationAlert.tsx +++ b/apps/web/src/pages/templates/components/LackIntegrationAlert.tsx @@ -4,12 +4,10 @@ import styled from '@emotion/styled'; import { ChannelTypeEnum } from '@novu/shared'; -import { colors, Text } from '../../../design-system'; -import { ProviderMissing } from '../../../design-system/icons'; -import { IntegrationsStoreModal } from '../../integrations/IntegrationsStoreModal'; +import { colors, Text, ErrorIcon, WarningIcon, CircleArrowRight } from '@novu/design-system'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { stepNames, TemplateEditorAnalyticsEnum } from '../constants'; -import { useEnvController, useIsMultiProviderConfigurationEnabled } from '../../../hooks'; +import { useEnvController } from '../../../hooks'; import { IntegrationsListModal } from '../../integrations/IntegrationsListModal'; import { Group } from '@mantine/core'; import { useSelectPrimaryIntegrationModal } from '../../integrations/components/multi-provider/useSelectPrimaryIntegrationModal'; @@ -30,64 +28,58 @@ export function LackIntegrationAlert({ const segment = useSegment(); const { environment } = useEnvController(); const [isIntegrationsModalOpened, openIntegrationsModal] = useState(false); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const { openModal: openSelectPrimaryIntegrationModal, SelectPrimaryIntegrationModal } = useSelectPrimaryIntegrationModal(); const onIntegrationModalClose = () => openIntegrationsModal(false); + const handleErrorRectangleClick = () => { + if (isPrimaryMissing) { + openSelectPrimaryIntegrationModal({ + environmentId: environment?._id, + channelType: channelType, + onClose: () => { + segment.track(TemplateEditorAnalyticsEnum.CONFIGURE_PRIMARY_PROVIDER_BANNER_CLICK); + }, + }); + } else { + openIntegrationsModal(true); + segment.track(TemplateEditorAnalyticsEnum.CONFIGURE_PROVIDER_BANNER_CLICK); + } + }; return ( <> - - -
- { - if (isPrimaryMissing) { - openSelectPrimaryIntegrationModal({ - environmentId: environment?._id, - channelType: channelType, - onClose: () => { - segment.track(TemplateEditorAnalyticsEnum.CONFIGURE_PRIMARY_PROVIDER_BANNER_CLICK); - }, - }); - } else { - openIntegrationsModal(true); - segment.track(TemplateEditorAnalyticsEnum.CONFIGURE_PROVIDER_BANNER_CLICK); - } - }} - /> + + + +
+ + {text + ? text + : `Please configure or activate a provider instance for the ${stepNames[channelType]} channel to send notifications over this node`} +
- - {text - ? text - : `Please configure or activate a provider instance for the ${stepNames[channelType]} channel to send notifications over this node`} - +
- {isMultiProviderConfigurationEnabled ? ( - - ) : ( - - )} + ); } -const MissingIcon = styled(ProviderMissing)<{ color?: string | undefined }>` - cursor: pointer; - color: ${({ color }) => color}; -`; +const AlertIcon = ({ color, alertType }: { color?: string | undefined; alertType: alertType }) => { + switch (alertType) { + case 'warning': + return ; + default: + return ; + } +}; const WarningMessage = styled.div<{ backgroundColor: string }>` display: flex; @@ -97,15 +89,17 @@ const WarningMessage = styled.div<{ backgroundColor: string }>` padding: 15px; margin-bottom: 40px; color: #e54545; + cursor: pointer; background: ${({ backgroundColor }) => backgroundColor}; border-radius: 7px; + cursor: pointer; `; function alertTypeToDoubleArrowColor(type: alertType) { switch (type) { case 'warning': - return 'rgba(234, 169, 0, 1)'; + return 'rgb(234, 169, 0)'; default: return 'undefined'; } @@ -126,7 +120,8 @@ function alertTypeToMessageTextColor(type: alertType) { switch (type) { case 'error': return colors.error; - + case 'warning': + return 'rgb(234, 169, 0)'; default: return undefined; } diff --git a/apps/web/src/pages/templates/components/ListProviders.tsx b/apps/web/src/pages/templates/components/ListProviders.tsx index be5c63de652..c48932525f3 100644 --- a/apps/web/src/pages/templates/components/ListProviders.tsx +++ b/apps/web/src/pages/templates/components/ListProviders.tsx @@ -2,8 +2,8 @@ import { Group, Stack, Text, UnstyledButton, useMantineColorScheme } from '@mant import { ChannelTypeEnum, NOVU_SMS_EMAIL_PROVIDERS } from '@novu/shared'; import { When } from '../../../components/utils/When'; -import { Button, colors, Tooltip } from '../../../design-system'; -import { useEnvController, useIsMultiProviderConfigurationEnabled } from '../../../hooks'; +import { Button, colors, Tooltip } from '@novu/design-system'; +import { useEnvController } from '../../../hooks'; import { IntegrationEnvironmentPill } from '../../integrations/components/IntegrationEnvironmentPill'; import { IntegrationStatus } from '../../integrations/components/IntegrationStatus'; import type { IIntegratedProvider } from '../../integrations/types'; @@ -23,7 +23,6 @@ export const ListProviders = ({ setProvider: (provider: IIntegratedProvider) => void; }) => { const { colorScheme } = useMantineColorScheme(); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const { environment: currentEnvironment } = useEnvController(); return ( @@ -57,7 +56,6 @@ export const ListProviders = ({
- {providers .filter((provider) => provider.connected && provider.environmentId === currentEnvironment?._id) .map((provider) => { @@ -66,7 +64,7 @@ export const ListProviders = ({ key={provider.identifier ?? provider.providerId} style={{ width: '100%', - padding: isMultiProviderConfigurationEnabled ? '8px 12px' : 15, + padding: '8px 12px', background: colorScheme === 'dark' ? colors.B20 : colors.B98, borderRadius: 8, marginBottom: 12, @@ -92,19 +90,16 @@ export const ListProviders = ({ - + {provider.name || provider.displayName} - +
- - - +
); })} +
); }; @@ -143,6 +137,7 @@ const LackIntegrationByType = ({ providers: IIntegratedProvider[]; channel: ChannelTypeEnum; }) => { + const { environment: currentEnvironment } = useEnvController(); const containsNovuProvider = NOVU_SMS_EMAIL_PROVIDERS.some( (providerId) => providerId === providers.find((provider) => provider.connected)?.providerId ); @@ -161,7 +156,12 @@ const LackIntegrationByType = ({ /> - provider.connected).length === 1 && containsNovuProvider}> + provider.connected && provider.environmentId === currentEnvironment?._id) + .length === 1 && containsNovuProvider + } + >
(undefined); const [provider, setProvider] = useState(null); - const isMultiProviderConfigurationEnabled = useIsMultiProviderConfigurationEnabled(); const onIntegrationModalClose = () => { setProvider(null); @@ -74,21 +71,12 @@ export function ProvidersPage() { /> - {isMultiProviderConfigurationEnabled ? ( - - ) : ( - - )} + ); } diff --git a/apps/web/src/pages/templates/components/SaveChangesModal.tsx b/apps/web/src/pages/templates/components/SaveChangesModal.tsx index f2ba51ccd44..f5c58ef27a3 100644 --- a/apps/web/src/pages/templates/components/SaveChangesModal.tsx +++ b/apps/web/src/pages/templates/components/SaveChangesModal.tsx @@ -1,7 +1,7 @@ import { Group } from '@mantine/core'; import { FieldErrors, useFormContext } from 'react-hook-form'; -import { Button, Title, Text, Modal } from '../../../design-system'; +import { Button, Title, Text, Modal } from '@novu/design-system'; import type { IForm } from './formTypes'; export function SaveChangesModal({ diff --git a/apps/web/src/pages/templates/components/SnippetPage.tsx b/apps/web/src/pages/templates/components/SnippetPage.tsx index 9b5b8163934..e07e8d304f5 100644 --- a/apps/web/src/pages/templates/components/SnippetPage.tsx +++ b/apps/web/src/pages/templates/components/SnippetPage.tsx @@ -1,5 +1,5 @@ import { Text } from '@mantine/core'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { TriggerSnippetTabs } from './TriggerSnippetTabs'; import { useTemplateEditorForm } from './TemplateEditorFormProvider'; import { SubPageWrapper } from './SubPageWrapper'; diff --git a/apps/web/src/pages/templates/components/SubPageWrapper.tsx b/apps/web/src/pages/templates/components/SubPageWrapper.tsx index 2009e2b7903..3cbc4879f91 100644 --- a/apps/web/src/pages/templates/components/SubPageWrapper.tsx +++ b/apps/web/src/pages/templates/components/SubPageWrapper.tsx @@ -2,8 +2,7 @@ import { Stack, Title, UnstyledButton, useMantineColorScheme } from '@mantine/co import { CSSProperties } from 'react'; import { useNavigate } from 'react-router-dom'; -import { colors } from '../../../design-system'; -import { Close } from '../../../design-system/icons/actions/Close'; +import { colors, Close } from '@novu/design-system'; import { useBasePath } from '../hooks/useBasePath'; export const SubPageWrapper = ({ diff --git a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx index c39a597aa94..451085de4b4 100644 --- a/apps/web/src/pages/templates/components/TemplatePushEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplatePushEditor.tsx @@ -1,7 +1,7 @@ import { ChannelTypeEnum } from '@novu/shared'; import { Control, Controller, useFormContext } from 'react-hook-form'; -import { Textarea } from '../../../design-system'; +import { Textarea } from '@novu/design-system'; import { useEnvController, useHasActiveIntegrations, useVariablesManager } from '../../../hooks'; import { StepSettings } from '../workflow/SideBar/StepSettings'; import type { IForm } from './formTypes'; diff --git a/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx b/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx index cee7c2e120d..6b64802e42e 100644 --- a/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx +++ b/apps/web/src/pages/templates/components/TemplateSMSEditor.tsx @@ -3,7 +3,7 @@ import { ChannelTypeEnum } from '@novu/shared'; import { LackIntegrationAlert } from './LackIntegrationAlert'; import type { IForm } from './formTypes'; -import { Textarea } from '../../../design-system'; +import { Textarea } from '@novu/design-system'; import { useEnvController, useHasActiveIntegrations, diff --git a/apps/web/src/pages/templates/components/TemplateSettings.tsx b/apps/web/src/pages/templates/components/TemplateSettings.tsx index b361d9c75de..c8e9b8714cd 100644 --- a/apps/web/src/pages/templates/components/TemplateSettings.tsx +++ b/apps/web/src/pages/templates/components/TemplateSettings.tsx @@ -3,9 +3,8 @@ import { useNavigate, useParams } from 'react-router-dom'; import { Group } from '@mantine/core'; import styled from '@emotion/styled'; -import { Button, colors } from '../../../design-system'; +import { Button, colors, Trash } from '@novu/design-system'; import { NotificationSettingsForm } from './notification-setting-form/NotificationSettingsForm'; -import { Trash } from '../../../design-system/icons'; import { DeleteConfirmModal } from './DeleteConfirmModal'; import { useEnvController } from '../../../hooks'; import { useTemplateEditorForm } from './TemplateEditorFormProvider'; diff --git a/apps/web/src/pages/templates/components/TestWorkflow.tsx b/apps/web/src/pages/templates/components/TestWorkflow.tsx index 0c25565d335..55498eb3527 100644 --- a/apps/web/src/pages/templates/components/TestWorkflow.tsx +++ b/apps/web/src/pages/templates/components/TestWorkflow.tsx @@ -6,8 +6,7 @@ import * as Sentry from '@sentry/react'; import * as capitalize from 'lodash.capitalize'; import { IUserEntity, INotificationTriggerVariable } from '@novu/shared'; -import { Button, colors } from '../../../design-system'; -import { inputStyles } from '../../../design-system/config/inputs.styles'; +import { Button, colors, inputStyles } from '@novu/design-system'; import { errorMessage, successMessage } from '../../../utils/notifications'; import { useAuthContext } from '../../../components/providers/AuthProvider'; import { getSubscriberValue, getPayloadValue } from './TriggerSnippetTabs'; diff --git a/apps/web/src/pages/templates/components/TranslateProductLead.tsx b/apps/web/src/pages/templates/components/TranslateProductLead.tsx index fd408a25d9c..3e5445081ba 100644 --- a/apps/web/src/pages/templates/components/TranslateProductLead.tsx +++ b/apps/web/src/pages/templates/components/TranslateProductLead.tsx @@ -1,6 +1,6 @@ import { CSSProperties } from 'react'; import { ProductLead, ProductLeadVariants } from '../../../components/utils/ProductLead'; -import { Translate } from '../../../design-system/icons'; +import { Translate } from '@novu/design-system'; export const TranslateProductLead = ({ id, style = {} }: { id: string; style?: CSSProperties }) => { return ( diff --git a/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx b/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx index 667588d9199..6c5b455dac9 100644 --- a/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx +++ b/apps/web/src/pages/templates/components/TriggerSegmentControl.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { SegmentedControl } from '../../../design-system'; +import { SegmentedControl } from '@novu/design-system'; import { useBasePath } from '../hooks/useBasePath'; export const TriggerSegmentControl = () => { diff --git a/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx b/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx index e2ee27ea1f0..19c7e846d5a 100644 --- a/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx +++ b/apps/web/src/pages/templates/components/TriggerSnippetTabs.tsx @@ -6,7 +6,7 @@ import * as get from 'lodash.get'; import { INotificationTrigger, INotificationTriggerVariable, TemplateVariableTypeEnum } from '@novu/shared'; import { API_ROOT } from '../../../config'; -import { colors, Tabs } from '../../../design-system'; +import { colors, Tabs } from '@novu/design-system'; const NODE_JS = 'Node.js'; const CURL = 'Curl'; diff --git a/apps/web/src/pages/templates/components/UnsavedChangesModal.tsx b/apps/web/src/pages/templates/components/UnsavedChangesModal.tsx index 0c802dd3e87..bbb8c38e8b1 100644 --- a/apps/web/src/pages/templates/components/UnsavedChangesModal.tsx +++ b/apps/web/src/pages/templates/components/UnsavedChangesModal.tsx @@ -1,5 +1,5 @@ import { Group, Modal, useMantineTheme } from '@mantine/core'; -import { Button, colors, shadows, Title, Text } from '../../../design-system'; +import { Button, colors, shadows, Title, Text } from '@novu/design-system'; export function UnsavedChangesModal({ isOpen, diff --git a/apps/web/src/pages/templates/components/UpdateButton.tsx b/apps/web/src/pages/templates/components/UpdateButton.tsx index a0fea7abce9..ff0010b54f6 100644 --- a/apps/web/src/pages/templates/components/UpdateButton.tsx +++ b/apps/web/src/pages/templates/components/UpdateButton.tsx @@ -1,4 +1,4 @@ -import { Button } from '../../../design-system'; +import { Button } from '@novu/design-system'; import { IForm } from './formTypes'; import { useFormContext } from 'react-hook-form'; import { useEnvController } from '../../../hooks'; diff --git a/apps/web/src/pages/templates/components/VariableManager.tsx b/apps/web/src/pages/templates/components/VariableManager.tsx index e7765524bfb..99a1be69dd6 100644 --- a/apps/web/src/pages/templates/components/VariableManager.tsx +++ b/apps/web/src/pages/templates/components/VariableManager.tsx @@ -3,7 +3,7 @@ import { Controller, useWatch } from 'react-hook-form'; import { Code, Space, Table } from '@mantine/core'; import styled from '@emotion/styled'; -import { colors, Input, Switch, Text } from '../../../design-system'; +import { colors, Input, Switch, Text } from '@novu/design-system'; import { When } from '../../../components/utils/When'; import { useEnvController } from '../../../hooks'; diff --git a/apps/web/src/pages/templates/components/VariableManagerModal.tsx b/apps/web/src/pages/templates/components/VariableManagerModal.tsx index d1bdd495dc0..ea6b2cac50d 100644 --- a/apps/web/src/pages/templates/components/VariableManagerModal.tsx +++ b/apps/web/src/pages/templates/components/VariableManagerModal.tsx @@ -1,4 +1,4 @@ -import { colors, shadows, Button } from '../../../design-system'; +import { colors, shadows, Button } from '@novu/design-system'; import { VariableManager } from './VariableManager'; import { Group, Modal, Title, useMantineTheme } from '@mantine/core'; diff --git a/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx b/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx index ba541fea83b..a7f5518d663 100644 --- a/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx +++ b/apps/web/src/pages/templates/components/WorkflowSettingsTabs.tsx @@ -1,11 +1,11 @@ import { Tabs } from '@mantine/core'; import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import useStyles from '../../../design-system/tabs/Tabs.styles'; +import { useTabsStyles } from '@novu/design-system'; import { useBasePath } from '../hooks/useBasePath'; export const WorkflowSettingsTabs = () => { - const { classes } = useStyles(false); + const { classes } = useTabsStyles(false); const basePath = useBasePath(); const navigate = useNavigate(); const { pathname } = useLocation(); diff --git a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx index c1aafd003cb..8ccba6fea19 100644 --- a/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx +++ b/apps/web/src/pages/templates/components/chat-editor/TemplateChatEditor.tsx @@ -1,7 +1,7 @@ import { ChannelTypeEnum } from '@novu/shared'; import { Control, Controller, useFormContext } from 'react-hook-form'; -import { Textarea } from '../../../../design-system'; +import { Textarea } from '@novu/design-system'; import { useEnvController, useHasActiveIntegrations, useVariablesManager } from '../../../../hooks'; import { StepSettings } from '../../workflow/SideBar/StepSettings'; import type { IForm } from '../formTypes'; diff --git a/apps/web/src/pages/templates/components/email-editor/ButtonRowContent.tsx b/apps/web/src/pages/templates/components/email-editor/ButtonRowContent.tsx index 763df9c2dc9..a316c3d1013 100644 --- a/apps/web/src/pages/templates/components/email-editor/ButtonRowContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/ButtonRowContent.tsx @@ -4,8 +4,7 @@ import { showNotification } from '@mantine/notifications'; import { TextInput as MantineInput, Popover, Button as MantineButton, createStyles } from '@mantine/core'; import { TextAlignEnum } from '@novu/shared'; -import { colors, shadows } from '../../../../design-system'; -import { TextAlignment, Wifi } from '../../../../design-system/icons'; +import { colors, shadows, TextAlignment, Wifi } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; import type { IForm } from '../formTypes'; diff --git a/apps/web/src/pages/templates/components/email-editor/ContentRow.tsx b/apps/web/src/pages/templates/components/email-editor/ContentRow.tsx index 762e2d1aa95..a0534dcc763 100644 --- a/apps/web/src/pages/templates/components/email-editor/ContentRow.tsx +++ b/apps/web/src/pages/templates/components/email-editor/ContentRow.tsx @@ -5,8 +5,7 @@ import styled from '@emotion/styled'; import { AlignCenterOutlined, AlignLeftOutlined, AlignRightOutlined } from '@ant-design/icons'; import { TextAlignEnum } from '@novu/shared'; -import { DotsHorizontalOutlined, Trash } from '../../../../design-system/icons'; -import { Button, colors, Dropdown } from '../../../../design-system'; +import { DotsHorizontalOutlined, Trash, Button, colors, Dropdown } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; export function ContentRow({ diff --git a/apps/web/src/pages/templates/components/email-editor/ControlBar.tsx b/apps/web/src/pages/templates/components/email-editor/ControlBar.tsx index 91e6f742ab6..b3ef93be72c 100644 --- a/apps/web/src/pages/templates/components/email-editor/ControlBar.tsx +++ b/apps/web/src/pages/templates/components/email-editor/ControlBar.tsx @@ -1,6 +1,5 @@ import { ActionIcon, Divider } from '@mantine/core'; -import { DoubleArrowRight, PlusCircleOutlined, TextAlignment } from '../../../../design-system/icons'; -import { colors, Dropdown } from '../../../../design-system'; +import { DoubleArrowRight, PlusCircleOutlined, TextAlignment, colors, Dropdown } from '@novu/design-system'; import { EmailBlockTypeEnum } from '@novu/shared'; export function ControlBar({ top, onBlockAdd }: { top: number; onBlockAdd: (type: EmailBlockTypeEnum) => void }) { diff --git a/apps/web/src/pages/templates/components/email-editor/EmailContentCard.tsx b/apps/web/src/pages/templates/components/email-editor/EmailContentCard.tsx index 3d0feecf97c..0f96f92c557 100644 --- a/apps/web/src/pages/templates/components/email-editor/EmailContentCard.tsx +++ b/apps/web/src/pages/templates/components/email-editor/EmailContentCard.tsx @@ -2,8 +2,8 @@ import { IOrganizationEntity } from '@novu/shared'; import { useEffect, useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { Tabs } from '../../../../design-system'; -import { useActiveIntegrations, useEnvController, useIsMultiProviderConfigurationEnabled } from '../../../../hooks'; +import { Tabs } from '@novu/design-system'; +import { useActiveIntegrations, useEnvController } from '../../../../hooks'; import { EmailCustomCodeEditor } from './EmailCustomCodeEditor'; import { EmailInboxContent } from './EmailInboxContent'; import { EmailMessageEditor } from './EmailMessageEditor'; @@ -22,7 +22,6 @@ export function EmailContentCard({ const { control, setValue, watch } = useFormContext(); // retrieve all hook methods const contentType = watch(`steps.${index}.template.contentType`); const activeTab = contentType === 'customHtml' ? CUSTOM_CODE : EDITOR; - const isMultiProviderConfigEnabled = useIsMultiProviderConfigurationEnabled(); const { integrations = [] } = useActiveIntegrations(); const [integration, setIntegration]: any = useState(null); @@ -30,12 +29,8 @@ export function EmailContentCard({ if (integrations.length === 0) { return; } - setIntegration( - integrations.find((item) => - isMultiProviderConfigEnabled ? item.channel === 'email' && item.primary : item.channel === 'email' - ) || null - ); - }, [isMultiProviderConfigEnabled, integrations, setIntegration]); + setIntegration(integrations.find((item) => item.channel === 'email' && item.primary) || null); + }, [integrations, setIntegration]); const onTabChange = (value: string | null) => { setValue(`steps.${index}.template.contentType`, value === EDITOR ? 'editor' : 'customHtml'); diff --git a/apps/web/src/pages/templates/components/email-editor/EmailCustomCodeEditor.tsx b/apps/web/src/pages/templates/components/email-editor/EmailCustomCodeEditor.tsx index 16382766b78..d6e5afebb76 100644 --- a/apps/web/src/pages/templates/components/email-editor/EmailCustomCodeEditor.tsx +++ b/apps/web/src/pages/templates/components/email-editor/EmailCustomCodeEditor.tsx @@ -4,7 +4,7 @@ import 'ace-builds/src-noconflict/theme-monokai'; import { addCompleter } from 'ace-builds/src-noconflict/ext-language_tools'; import { Card } from '@mantine/core'; import { SystemVariablesWithTypes, HandlebarHelpers } from '@novu/shared'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; export function EmailCustomCodeEditor({ diff --git a/apps/web/src/pages/templates/components/email-editor/EmailInboxContent.tsx b/apps/web/src/pages/templates/components/email-editor/EmailInboxContent.tsx index fb07c602329..100e0e74614 100644 --- a/apps/web/src/pages/templates/components/email-editor/EmailInboxContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/EmailInboxContent.tsx @@ -1,6 +1,6 @@ import { Grid, useMantineTheme } from '@mantine/core'; import { Controller, useFormContext } from 'react-hook-form'; -import { colors, Input, Select, Tooltip } from '../../../../design-system'; +import { colors, Input, Select, Tooltip } from '@novu/design-system'; import { useLayouts } from '../../../../hooks'; import { useEffect } from 'react'; diff --git a/apps/web/src/pages/templates/components/email-editor/EmailMessageEditor.tsx b/apps/web/src/pages/templates/components/email-editor/EmailMessageEditor.tsx index c2261842f75..cd42e985f30 100644 --- a/apps/web/src/pages/templates/components/email-editor/EmailMessageEditor.tsx +++ b/apps/web/src/pages/templates/components/email-editor/EmailMessageEditor.tsx @@ -5,8 +5,7 @@ import { Dropzone } from '@mantine/dropzone'; import { useFormContext, useFieldArray } from 'react-hook-form'; import { EmailBlockTypeEnum, IEmailBlock } from '@novu/shared'; -import { Upload } from '../../../../design-system/icons'; -import { colors, Text } from '../../../../design-system'; +import { Upload, colors, Text } from '@novu/design-system'; import { ContentRow } from './ContentRow'; import { ControlBar } from './ControlBar'; import { ButtonRowContent } from './ButtonRowContent'; diff --git a/apps/web/src/pages/templates/components/email-editor/EmailMessagesCards.tsx b/apps/web/src/pages/templates/components/email-editor/EmailMessagesCards.tsx index da673b015ce..8fc5f5ee18e 100644 --- a/apps/web/src/pages/templates/components/email-editor/EmailMessagesCards.tsx +++ b/apps/web/src/pages/templates/components/email-editor/EmailMessagesCards.tsx @@ -6,7 +6,7 @@ import { Preview } from '../../editor/Preview'; import { EditorPreviewSwitch } from '../EditorPreviewSwitch'; import { Grid, SegmentedControl, useMantineTheme } from '@mantine/core'; import { TestSendEmail } from './TestSendEmail'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { MobileIcon } from '../../editor/PreviewSegment/MobileIcon'; import { WebIcon } from '../../editor/PreviewSegment/WebIcon'; import { useHotkeys } from '@mantine/hooks'; diff --git a/apps/web/src/pages/templates/components/email-editor/TestSendEmail.tsx b/apps/web/src/pages/templates/components/email-editor/TestSendEmail.tsx index f64ca94ab69..f624a6e267c 100644 --- a/apps/web/src/pages/templates/components/email-editor/TestSendEmail.tsx +++ b/apps/web/src/pages/templates/components/email-editor/TestSendEmail.tsx @@ -6,12 +6,20 @@ import { useFormContext, useWatch } from 'react-hook-form'; import styled from '@emotion/styled'; import { ChannelTypeEnum, MemberStatusEnum } from '@novu/shared'; -import { Button, Text, colors, Tooltip } from '../../../../design-system'; import { errorMessage, successMessage } from '../../../../utils/notifications'; import { useAuthContext } from '../../../../components/providers/AuthProvider'; -import { ArrowDown, Check, Copy, Invite } from '../../../../design-system/icons'; -import { inputStyles } from '../../../../design-system/config/inputs.styles'; -import useStyles from '../../../../design-system/select/Select.styles'; +import { + Button, + Text, + colors, + Tooltip, + ArrowDown, + Check, + Copy, + Invite, + inputStyles, + useSelectStyles, +} from '@novu/design-system'; import { getOrganizationMembers } from '../../../../api/organization'; import { useProcessVariables, useIntegrationLimit } from '../../../../hooks'; import { testSendEmailMessage } from '../../../../api/notification-templates'; @@ -21,7 +29,7 @@ export function TestSendEmail({ index, isIntegrationActive }: { index: number; i const { control } = useFormContext(); const clipboardJson = useClipboard({ timeout: 1000 }); - const { classes } = useStyles(); + const { classes } = useSelectStyles(); const { mutateAsync: testSendEmailEvent, isLoading } = useMutation(testSendEmailMessage); const template = useWatch({ diff --git a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx index 56fe2d50e14..f6475eaae7f 100644 --- a/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx +++ b/apps/web/src/pages/templates/components/email-editor/TextRowContent.tsx @@ -3,7 +3,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import styled from '@emotion/styled'; import { TextAlignEnum } from '@novu/shared'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; import type { IForm } from '../formTypes'; diff --git a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItem.tsx b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItem.tsx index d24c27b3ea5..a6cffe57d59 100644 --- a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItem.tsx +++ b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItem.tsx @@ -1,5 +1,5 @@ import { useMantineTheme } from '@mantine/core'; -import { colors } from '../../../../../design-system'; +import { colors } from '@novu/design-system'; export const VarItem = ({ name, diff --git a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemTooltip.tsx b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemTooltip.tsx index efeea1286f2..5e4b09dc7bb 100644 --- a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemTooltip.tsx +++ b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemTooltip.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { ActionIcon, Tooltip } from '@mantine/core'; import { useClipboard } from '@mantine/hooks'; -import { Check, Copy } from '../../../../../design-system/icons'; +import { Check, Copy } from '@novu/design-system'; import { VarItem } from './VarItem'; export const VarItemTooltip = ({ pathToCopy, name, type }: { pathToCopy: string; name: string; type: string }) => { diff --git a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemsDropdown.tsx b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemsDropdown.tsx index af406f4bc7d..8a972cdbb41 100644 --- a/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemsDropdown.tsx +++ b/apps/web/src/pages/templates/components/email-editor/variables-management/VarItemsDropdown.tsx @@ -1,9 +1,7 @@ import { useState } from 'react'; import { Collapse, UnstyledButton, useMantineTheme } from '@mantine/core'; -import { ChevronUp } from '../../../../../design-system/icons'; -import { ChevronDown } from '../../../../../design-system/icons'; +import { ChevronUp, ChevronDown, colors } from '@novu/design-system'; import { VarItem } from './VarItem'; -import { colors } from '../../../../../design-system'; import { VarItemTooltip } from './VarItemTooltip'; export const VarItemsDropdown = ({ name, type }) => { diff --git a/apps/web/src/pages/templates/components/email-editor/variables-management/VarLabel.tsx b/apps/web/src/pages/templates/components/email-editor/variables-management/VarLabel.tsx index 78c6e7fc90d..f5c2aeebbb7 100644 --- a/apps/web/src/pages/templates/components/email-editor/variables-management/VarLabel.tsx +++ b/apps/web/src/pages/templates/components/email-editor/variables-management/VarLabel.tsx @@ -1,8 +1,6 @@ import { useState } from 'react'; -import { colors } from '../../../../../design-system'; +import { colors, ChevronUp, ChevronDown } from '@novu/design-system'; import { Collapse, UnstyledButton, useMantineTheme } from '@mantine/core'; -import { ChevronUp } from '../../../../../design-system/icons'; -import { ChevronDown } from '../../../../../design-system/icons'; export const VarLabel = ({ label, children }) => { const [open, setOpen] = useState(true); diff --git a/apps/web/src/pages/templates/components/email-editor/variables-management/VariablesManagement.tsx b/apps/web/src/pages/templates/components/email-editor/variables-management/VariablesManagement.tsx index bd1f933606a..f6e5a5a8d00 100644 --- a/apps/web/src/pages/templates/components/email-editor/variables-management/VariablesManagement.tsx +++ b/apps/web/src/pages/templates/components/email-editor/variables-management/VariablesManagement.tsx @@ -2,10 +2,9 @@ import { useWatch } from 'react-hook-form'; import { UnstyledButton } from '@mantine/core'; import { SystemVariablesWithTypes } from '@novu/shared'; -import { Text, Tooltip } from '../../../../../design-system'; +import { Text, Tooltip, EditGradient } from '@novu/design-system'; import { VarItemsDropdown } from './VarItemsDropdown'; import { VarLabel } from './VarLabel'; -import { EditGradient } from '../../../../../design-system/icons/gradient/EditGradient'; import { useProcessVariables } from '../../../../../hooks'; import { VarItemTooltip } from './VarItemTooltip'; import { When } from '../../../../../components/utils/When'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/AvatarFeedFields.tsx b/apps/web/src/pages/templates/components/in-app-editor/AvatarFeedFields.tsx index 4d927ba9a26..7631d2bcf0d 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/AvatarFeedFields.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/AvatarFeedFields.tsx @@ -6,11 +6,10 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { showNotification } from '@mantine/notifications'; import { IFeedEntity } from '@novu/shared'; -import { Checkbox, colors, Input } from '../../../../design-system'; +import { Checkbox, colors, Input, PlusGradient } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; import { createFeed, getFeeds } from '../../../../api/feeds'; import { QueryKeys } from '../../../../api/query.keys'; -import { PlusGradient } from '../../../../design-system/icons'; import { FeedItems } from './FeedItems'; import { EnableAvatarSwitch } from './EnableAvatarSwitch'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplates.tsx b/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplates.tsx index 8529050ef33..267e6ba4d72 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplates.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplates.tsx @@ -1,7 +1,7 @@ import { ButtonTypeEnum, IMessageButton, darkButtonStyle, lightButtonStyle } from '@novu/shared'; import styled from '@emotion/styled'; import { Divider, useMantineTheme } from '@mantine/core'; -import { Button } from '../../../../design-system'; +import { Button } from '@novu/design-system'; interface IButtonsTemplatesProps { setTemplateSelected: (boolean) => void; diff --git a/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplatesPopover.tsx b/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplatesPopover.tsx index 9dd524cfe53..7bc6e56f8f9 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplatesPopover.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/ButtonsTemplatesPopover.tsx @@ -1,7 +1,7 @@ import { Popover, createStyles } from '@mantine/core'; -import { IMessageAction, IMessageButton, MessageActionStatusEnum } from '@novu/shared'; +import { IMessageButton, MessageActionStatusEnum } from '@novu/shared'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { ButtonsTemplates } from './ButtonsTemplates'; const usePopoverStyles = createStyles(({ colorScheme }) => ({ diff --git a/apps/web/src/pages/templates/components/in-app-editor/EnableAvatarSwitch.tsx b/apps/web/src/pages/templates/components/in-app-editor/EnableAvatarSwitch.tsx index 601b840155a..e3698d4c472 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/EnableAvatarSwitch.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/EnableAvatarSwitch.tsx @@ -1,7 +1,7 @@ import { Group } from '@mantine/core'; import { Control, useController } from 'react-hook-form'; -import { Text, Switch } from '../../../../design-system'; +import { Text, Switch } from '@novu/design-system'; import type { IForm } from '../formTypes'; export function EnableAvatarSwitch({ diff --git a/apps/web/src/pages/templates/components/in-app-editor/FeedChip.tsx b/apps/web/src/pages/templates/components/in-app-editor/FeedChip.tsx index 917e7d4a959..56bac7992c9 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/FeedChip.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/FeedChip.tsx @@ -3,8 +3,7 @@ import styled from '@emotion/styled'; import { ColorScheme, useMantineTheme } from '@mantine/core'; import { IFeedEntity } from '@novu/shared'; -import { colors, shadows } from '../../../../design-system'; -import { DotsHorizontal } from '../../../../design-system/icons'; +import { colors, shadows, DotsHorizontal } from '@novu/design-system'; import { useEnvController } from '../../../../hooks'; interface IFeedItemProps { diff --git a/apps/web/src/pages/templates/components/in-app-editor/FeedItems.tsx b/apps/web/src/pages/templates/components/in-app-editor/FeedItems.tsx index 54bf2fca097..edaac0464fa 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/FeedItems.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/FeedItems.tsx @@ -8,8 +8,7 @@ import { showNotification } from '@mantine/notifications'; import { IFeedEntity } from '@novu/shared'; import { FeedChip } from './FeedChip'; -import { colors, shadows, Text, Tooltip, Button } from '../../../../design-system'; -import { Copy, Trash } from '../../../../design-system/icons'; +import { colors, shadows, Text, Tooltip, Button, Copy, Trash } from '@novu/design-system'; import { deleteFeed, getFeeds } from '../../../../api/feeds'; import { QueryKeys } from '../../../../api/query.keys'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/InAppContentCard.tsx b/apps/web/src/pages/templates/components/in-app-editor/InAppContentCard.tsx index 93d9bf1d75e..987e3982b71 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/InAppContentCard.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/InAppContentCard.tsx @@ -1,10 +1,9 @@ import { useFormContext, useWatch } from 'react-hook-form'; -import { colors } from '../../../../design-system'; +import { colors, inputStyles } from '@novu/design-system'; import { useEnvController, useProcessVariables } from '../../../../hooks'; import { InAppEditorBlock } from './InAppEditorBlock'; import { Grid, useMantineTheme, JsonInput, SegmentedControl } from '@mantine/core'; import { VariablesManagement } from '../email-editor/variables-management/VariablesManagement'; -import { inputStyles } from '../../../../design-system/config/inputs.styles'; import { useState, useEffect } from 'react'; import { AvatarFeedFields } from './AvatarFeedFields'; import { When } from '../../../../components/utils/When'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx index 3c3d0b8b62d..cd99d03d606 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/TemplateInAppEditor.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { Control, Controller, useFormContext } from 'react-hook-form'; import { ChannelTypeEnum } from '@novu/shared'; -import { Input } from '../../../../design-system'; +import { Input } from '@novu/design-system'; import { useEnvController, useHasActiveIntegrations, useVariablesManager } from '../../../../hooks'; import { StepSettings } from '../../workflow/SideBar/StepSettings'; import type { IForm, ITemplates } from '../formTypes'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/preview/ActionBlockContainer.tsx b/apps/web/src/pages/templates/components/in-app-editor/preview/ActionBlockContainer.tsx index 2f3012a1c4e..63ad1351fef 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/preview/ActionBlockContainer.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/preview/ActionBlockContainer.tsx @@ -8,9 +8,8 @@ import { MessageActionStatusEnum, } from '@novu/shared'; import { ColorScheme, TextInput, useMantineColorScheme, useMantineTheme } from '@mantine/core'; -import { RemoveCircle } from '../../../../../design-system/icons/general/RemoveCircle'; +import { RemoveCircle, Button, colors } from '@novu/design-system'; import styled from '@emotion/styled'; -import { Button, colors } from '../../../../../design-system'; import { When } from '../../../../../components/utils/When'; export function ActionBlockContainer({ diff --git a/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.styles.ts b/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.styles.ts index 8e415834319..b43d42de806 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.styles.ts +++ b/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.styles.ts @@ -1,6 +1,6 @@ import { createStyles } from '@mantine/core'; import styled from '@emotion/styled'; -import { colors } from '../../../../../design-system'; +import { colors } from '@novu/design-system'; export const useStyles = createStyles((theme) => { const dark = theme.colorScheme === 'dark'; diff --git a/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.tsx b/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.tsx index adf6b50d5cd..5e4df7c3915 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/preview/AvatarContainer.tsx @@ -11,8 +11,7 @@ import { import { useController } from 'react-hook-form'; import { SystemAvatarIconEnum, IActor, ActorTypeEnum } from '@novu/shared'; -import { colors, Input, Switch, Text, Tooltip } from '../../../../../design-system'; -import { Avatar, Camera } from '../../../../../design-system/icons'; +import { colors, Input, Switch, Text, Tooltip, Avatar, Camera } from '@novu/design-system'; import { AvatarWrapper, IconWrapper, useStyles } from './AvatarContainer.styles'; const MENU_CLICK_OUTSIDE_EVENTS = ['click', 'mousedown', 'touchstart']; diff --git a/apps/web/src/pages/templates/components/in-app-editor/preview/InAppWidgetPreview.tsx b/apps/web/src/pages/templates/components/in-app-editor/preview/InAppWidgetPreview.tsx index 295f890bdcc..1d0692c71b6 100644 --- a/apps/web/src/pages/templates/components/in-app-editor/preview/InAppWidgetPreview.tsx +++ b/apps/web/src/pages/templates/components/in-app-editor/preview/InAppWidgetPreview.tsx @@ -6,7 +6,7 @@ import { IMessageAction } from '@novu/shared'; import { ActionBlockContainer } from './ActionBlockContainer'; import AvatarContainer from './AvatarContainer'; -import { colors, shadows, Text, Title } from '../../../../../design-system'; +import { colors, shadows, Text, Title } from '@novu/design-system'; import { ButtonsTemplatesPopover } from '../ButtonsTemplatesPopover'; function minutesAgo(num: number): string { diff --git a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx index ce1b4e7eba5..40d0683ae4e 100644 --- a/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx +++ b/apps/web/src/pages/templates/components/notification-setting-form/NotificationSettingsForm.tsx @@ -5,8 +5,7 @@ import { Controller, useFormContext } from 'react-hook-form'; import { INotificationTrigger } from '@novu/shared'; import { api } from '../../../../api/api.client'; -import { Input, Select, Switch, Tooltip } from '../../../../design-system'; -import { Check, Copy } from '../../../../design-system/icons'; +import { Input, Select, Switch, Tooltip, Check, Copy } from '@novu/design-system'; import { useEnvController, useNotificationGroup } from '../../../../hooks'; import type { IForm } from '../formTypes'; import { useTemplateEditorForm } from '../TemplateEditorFormProvider'; diff --git a/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx b/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx index fe0c03d13ed..dcd8b5b6905 100644 --- a/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx +++ b/apps/web/src/pages/templates/components/notification-setting-form/TemplatePreference.tsx @@ -3,7 +3,7 @@ import { Group, Input, InputWrapperProps, Text } from '@mantine/core'; import { useFormContext, Controller } from 'react-hook-form'; import { useEnvController } from '../../../../hooks'; -import { Checkbox, colors, Switch } from '../../../../design-system'; +import { Checkbox, colors, Switch } from '@novu/design-system'; import type { IForm } from '../formTypes'; import { LabelWithTooltip } from '../../workflow/LabelWithTooltip'; import { ChannelTitle } from '../ChannelTitle'; diff --git a/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx b/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx index e60afcf5a7c..d7e5caeaf70 100644 --- a/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx +++ b/apps/web/src/pages/templates/components/templates-store/TemplatesStoreModal.tsx @@ -4,8 +4,7 @@ import { ActionIcon, Modal, useMantineTheme } from '@mantine/core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useNavigate } from 'react-router-dom'; -import { Button, colors, shadows } from '../../../../design-system'; -import { Close } from '../../../../design-system/icons'; +import { Button, colors, shadows, Close } from '@novu/design-system'; import { CanvasHolder, GroupName, diff --git a/apps/web/src/pages/templates/components/templates-store/TriggerNode.tsx b/apps/web/src/pages/templates/components/templates-store/TriggerNode.tsx index 9f9317a009b..053a3f52c3e 100644 --- a/apps/web/src/pages/templates/components/templates-store/TriggerNode.tsx +++ b/apps/web/src/pages/templates/components/templates-store/TriggerNode.tsx @@ -1,7 +1,7 @@ import { Handle, NodeProps, Position } from 'react-flow-renderer'; import { NodeStep } from '../../../../components/workflow'; -import { BoltOutlinedGradient } from '../../../../design-system/icons'; +import { BoltOutlinedGradient } from '@novu/design-system'; export const TriggerNode = ({ data }: NodeProps) => { return ( diff --git a/apps/web/src/pages/templates/components/templates-store/templateStoreStyles.ts b/apps/web/src/pages/templates/components/templates-store/templateStoreStyles.ts index 60d73628ae6..937107302de 100644 --- a/apps/web/src/pages/templates/components/templates-store/templateStoreStyles.ts +++ b/apps/web/src/pages/templates/components/templates-store/templateStoreStyles.ts @@ -1,8 +1,7 @@ import { createStyles } from '@mantine/core'; import styled from '@emotion/styled'; -import { colors } from '../../../../design-system'; -import { MadeByNovu } from '../../../../design-system/icons'; +import { colors, MadeByNovu } from '@novu/design-system'; export const ModalBodyHolder = styled.div` display: flex; diff --git a/apps/web/src/pages/templates/constants.tsx b/apps/web/src/pages/templates/constants.tsx index ecf47e8309b..3750095c1dc 100644 --- a/apps/web/src/pages/templates/constants.tsx +++ b/apps/web/src/pages/templates/constants.tsx @@ -1,5 +1,5 @@ import { ChannelTypeEnum, StepTypeEnum } from '@novu/shared'; -import { Bell, Chat, DigestGradient, Mail, Mobile, Sms, TimerGradient } from '../../design-system/icons'; +import { Bell, Chat, DigestGradient, Mail, Mobile, Sms, TimerGradient } from '@novu/design-system'; export enum TemplateAnalyticsEnum { CREATE_TEMPLATE_CLICK = 'Create Template Click - [Templates]', diff --git a/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx b/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx index d2453baab5d..9754dae8645 100644 --- a/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx +++ b/apps/web/src/pages/templates/editor/DigestWorkflowTourTooltip.tsx @@ -6,8 +6,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { StepTypeEnum } from '@novu/shared'; import { useTour } from './TourProvider'; -import { Button, colors, DotsNavigation } from '../../../design-system'; -import { Clock, LetterOpened, BellWithNotification } from '../../../design-system/icons'; +import { Button, colors, DotsNavigation, Clock, LetterOpened, BellWithNotification } from '@novu/design-system'; import { IForm } from '../components/formTypes'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { DigestWorkflowTourAnalyticsEnum, HINT_INDEX_TO_CLICK_ANALYTICS, ordinalNumbers } from '../constants'; diff --git a/apps/web/src/pages/templates/editor/Mobile.tsx b/apps/web/src/pages/templates/editor/Mobile.tsx index 1e35ffc4118..b71049a5b36 100644 --- a/apps/web/src/pages/templates/editor/Mobile.tsx +++ b/apps/web/src/pages/templates/editor/Mobile.tsx @@ -1,5 +1,5 @@ import { createStyles } from '@mantine/core'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; const useStyles = createStyles((theme) => ({ phone: { diff --git a/apps/web/src/pages/templates/editor/Preview.tsx b/apps/web/src/pages/templates/editor/Preview.tsx index d077a7a9db2..7d57be29914 100644 --- a/apps/web/src/pages/templates/editor/Preview.tsx +++ b/apps/web/src/pages/templates/editor/Preview.tsx @@ -6,13 +6,12 @@ import type { IEmailBlock, MessageTemplateContentType } from '@novu/shared'; import { previewEmail } from '../../../api/content-templates'; import { When } from '../../../components/utils/When'; -import { Button, colors } from '../../../design-system'; -import { inputStyles } from '../../../design-system/config/inputs.styles'; +import { Button, colors, inputStyles } from '@novu/design-system'; import { useProcessVariables } from '../../../hooks'; import { PreviewMobile } from './PreviewMobile'; import { PreviewWeb } from './PreviewWeb'; import { errorMessage } from '../../../utils/notifications'; -import { useActiveIntegrations, useIsMultiProviderConfigurationEnabled } from '../../../hooks'; +import { useActiveIntegrations } from '../../../hooks'; export const Preview = ({ activeStep, view }: { activeStep: number; view: string }) => { const { control } = useFormContext(); @@ -44,7 +43,6 @@ export const Preview = ({ activeStep, view }: { activeStep: number; view: string }); const { integrations = [] } = useActiveIntegrations(); - const isMultiProviderConfigEnabled = useIsMultiProviderConfigurationEnabled(); const [integration, setIntegration]: any = useState(null); const [parsedSubject, setParsedSubject] = useState(subject); const [content, setContent] = useState('
'); @@ -93,12 +91,8 @@ export const Preview = ({ activeStep, view }: { activeStep: number; view: string if (integrations.length === 0) { return; } - setIntegration( - integrations.find((item) => - isMultiProviderConfigEnabled ? item.channel === 'email' && item.primary : item.channel === 'email' - ) || null - ); - }, [isMultiProviderConfigEnabled, integrations, setIntegration]); + setIntegration(integrations.find((item) => item.channel === 'email' && item.primary) || null); + }, [integrations, setIntegration]); return ( <> diff --git a/apps/web/src/pages/templates/editor/PreviewMobile.tsx b/apps/web/src/pages/templates/editor/PreviewMobile.tsx index ef1c2b9bc1d..f3f3863fd2a 100644 --- a/apps/web/src/pages/templates/editor/PreviewMobile.tsx +++ b/apps/web/src/pages/templates/editor/PreviewMobile.tsx @@ -1,7 +1,7 @@ import { Center, createStyles, Group, Loader } from '@mantine/core'; import { format } from 'date-fns'; import Frame from 'react-frame-component'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { PreviewDateIcon } from './PreviewDateIcon'; import { PreviewUserIcon } from './PreviewUserIcon'; import { ErrorBoundary } from 'react-error-boundary'; diff --git a/apps/web/src/pages/templates/editor/PreviewMobileInbox/DateArrow.tsx b/apps/web/src/pages/templates/editor/PreviewMobileInbox/DateArrow.tsx index 0d10e3b43b1..b6a584fee95 100644 --- a/apps/web/src/pages/templates/editor/PreviewMobileInbox/DateArrow.tsx +++ b/apps/web/src/pages/templates/editor/PreviewMobileInbox/DateArrow.tsx @@ -1,5 +1,5 @@ /* eslint-disable */ -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; export const DateArrow = () => { return ( diff --git a/apps/web/src/pages/templates/editor/PreviewMobileInbox/InboxItem.tsx b/apps/web/src/pages/templates/editor/PreviewMobileInbox/InboxItem.tsx index 935732677fd..c98ff3c2645 100644 --- a/apps/web/src/pages/templates/editor/PreviewMobileInbox/InboxItem.tsx +++ b/apps/web/src/pages/templates/editor/PreviewMobileInbox/InboxItem.tsx @@ -1,5 +1,5 @@ import { Skeleton, useMantineTheme } from '@mantine/core'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; export const ItemSkeleton = () => { const theme = useMantineTheme(); diff --git a/apps/web/src/pages/templates/editor/PreviewMobileInbox/index.tsx b/apps/web/src/pages/templates/editor/PreviewMobileInbox/index.tsx index 3efe7649b58..afb637c2541 100644 --- a/apps/web/src/pages/templates/editor/PreviewMobileInbox/index.tsx +++ b/apps/web/src/pages/templates/editor/PreviewMobileInbox/index.tsx @@ -1,6 +1,6 @@ import { createStyles, Group } from '@mantine/core'; import { format } from 'date-fns'; -import { colors } from '../../../../design-system'; +import { colors } from '@novu/design-system'; import { EmailIntegrationInfo } from '../EmailIntegrationInfo'; import { Mobile } from '../Mobile'; import { DateArrow } from './DateArrow'; diff --git a/apps/web/src/pages/templates/editor/PreviewUserIcon.tsx b/apps/web/src/pages/templates/editor/PreviewUserIcon.tsx index adc77df1beb..a9ec055729e 100644 --- a/apps/web/src/pages/templates/editor/PreviewUserIcon.tsx +++ b/apps/web/src/pages/templates/editor/PreviewUserIcon.tsx @@ -1,5 +1,5 @@ import { useMantineTheme } from '@mantine/core'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; /* eslint-disable */ export const PreviewUserIcon = () => { diff --git a/apps/web/src/pages/templates/editor/PreviewWeb.tsx b/apps/web/src/pages/templates/editor/PreviewWeb.tsx index 209e689ea7f..8c94eadf12e 100644 --- a/apps/web/src/pages/templates/editor/PreviewWeb.tsx +++ b/apps/web/src/pages/templates/editor/PreviewWeb.tsx @@ -1,6 +1,6 @@ import { Center, createStyles, Group, Loader } from '@mantine/core'; import { format } from 'date-fns'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { PreviewDateIcon } from './PreviewDateIcon'; import { PreviewUserIcon } from './PreviewUserIcon'; import Frame from 'react-frame-component'; diff --git a/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.styles.tsx b/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.styles.tsx index 6adf57536ea..2bb1ef9d3eb 100644 --- a/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.styles.tsx +++ b/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.styles.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { colors, DotsNavigation } from '../../../design-system'; +import { colors, DotsNavigation } from '@novu/design-system'; export const TooltipContainer = styled.div<{ width: React.CSSProperties['width']; diff --git a/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.tsx b/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.tsx index 38631ee94c2..d7b85f21c8a 100644 --- a/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.tsx +++ b/apps/web/src/pages/templates/editor/StartFromScratchTourTooltip.tsx @@ -9,15 +9,17 @@ import { useAuthContext } from '../../../components/providers/AuthProvider'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { When } from '../../../components/utils/When'; -import { Button, colors, Text } from '../../../design-system'; import { + Button, + colors, + Text, BuildWorkflow, Pencil, QuickGuide, RightArrow, RunTestBell, WorkflowSettings, -} from '../../../design-system/icons'; +} from '@novu/design-system'; import { errorMessage } from '../../../utils/notifications'; import { ordinalNumbers, SCRATCH_HINT_INDEX_TO_CLICK_ANALYTICS, StartFromScratchTourAnalyticsEnum } from '../constants'; import { useBasePath } from '../hooks/useBasePath'; diff --git a/apps/web/src/pages/templates/editor/TourProvider.tsx b/apps/web/src/pages/templates/editor/TourProvider.tsx index 7b5070ff168..7f3bd0cf80d 100644 --- a/apps/web/src/pages/templates/editor/TourProvider.tsx +++ b/apps/web/src/pages/templates/editor/TourProvider.tsx @@ -1,7 +1,7 @@ import { useMantineColorScheme } from '@mantine/core'; import React, { useMemo, useCallback } from 'react'; import Joyride from 'react-joyride'; -import { colors } from '../../../design-system'; +import { colors } from '@novu/design-system'; import { useDigestWorkflowTour } from './useDigestWorkflowTour'; import { useStartFromScratchTour } from './useStartFromScratchTour'; diff --git a/apps/web/src/pages/templates/filter/FilterModal.styles.ts b/apps/web/src/pages/templates/filter/FilterModal.styles.ts index 30302d66fe6..cab574b5c58 100644 --- a/apps/web/src/pages/templates/filter/FilterModal.styles.ts +++ b/apps/web/src/pages/templates/filter/FilterModal.styles.ts @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { Button, colors } from '../../../design-system'; +import { Button, colors } from '@novu/design-system'; export const FilterButton = styled(Button)` margin-top: 0px; diff --git a/apps/web/src/pages/templates/filter/FilterModal.tsx b/apps/web/src/pages/templates/filter/FilterModal.tsx index 44431e82f01..ea27f2d5a09 100644 --- a/apps/web/src/pages/templates/filter/FilterModal.tsx +++ b/apps/web/src/pages/templates/filter/FilterModal.tsx @@ -1,10 +1,9 @@ import { Divider, Grid, Group, Modal, useMantineTheme } from '@mantine/core'; import { Controller, useFieldArray, useWatch } from 'react-hook-form'; -import { FILTER_TO_LABEL, FilterPartTypeEnum, ChannelTypeEnum } from '@novu/shared'; +import { FILTER_TO_LABEL, FilterPartTypeEnum, ChannelTypeEnum, FieldOperatorEnum } from '@novu/shared'; import { When } from '../../../components/utils/When'; -import { Button, colors, Input, Select, shadows, Title } from '../../../design-system'; -import { Trash } from '../../../design-system/icons'; +import { Button, colors, Input, Select, shadows, Title, Trash } from '@novu/design-system'; import { DeleteStepButton, FilterButton } from './FilterModal.styles'; import { OnlineFiltersForms } from './OnlineFiltersForms'; import { PreviousStepFiltersForm } from './PreviousStepFiltersForm'; @@ -17,6 +16,7 @@ export function FilterModal({ control, stepIndex, setValue, + readonly, }: { isOpen: boolean; cancel: () => void; @@ -24,6 +24,7 @@ export function FilterModal({ control: any; stepIndex: number; setValue: any; + readonly: boolean; }) { const theme = useMantineTheme(); @@ -121,6 +122,7 @@ export function FilterModal({ ]} {...field} data-test-id="group-rules-dropdown" + disabled={readonly} /> ); }} @@ -134,11 +136,12 @@ export function FilterModal({ mt={30} onClick={() => { append({ - operator: 'EQUAL', + operator: FieldOperatorEnum.EQUAL, on: 'payload', }); }} data-test-id="create-rule-btn" + disabled={readonly} > Create rule @@ -169,6 +172,7 @@ export function FilterModal({ {...field} onChange={handleOnChildOnChange(index)} data-test-id="filter-on-dropdown" + disabled={readonly} /> ); }} @@ -176,7 +180,7 @@ export function FilterModal({ - + @@ -199,6 +204,7 @@ export function FilterModal({ stepIndex={stepIndex} index={index} remove={remove} + readonly={readonly} /> - + index + 1}> @@ -228,7 +241,7 @@ export function FilterModal({ - @@ -237,7 +250,17 @@ export function FilterModal({ ); } -function WebHookUrlForm({ control, stepIndex, index }: { control; stepIndex: number; index: number }) { +function WebHookUrlForm({ + control, + stepIndex, + index, + readonly, +}: { + control: any; + stepIndex: number; + index: number; + readonly: boolean; +}) { return ( <> @@ -252,6 +275,7 @@ function WebHookUrlForm({ control, stepIndex, index }: { control; stepIndex: num error={fieldState.error?.message} placeholder="Url" data-test-id="webhook-filter-url-input" + disabled={readonly} /> ); }} @@ -268,13 +292,15 @@ function EqualityForm({ index, remove, setValue, + readonly, }: { fieldOn: string; - control; + control: any; stepIndex: number; index: number; remove: (index?: number | number[]) => void; setValue; + readonly: boolean; }) { const spaSize = fieldOn === 'webhook' ? 3 : 2; const operator = useWatch({ @@ -291,7 +317,13 @@ function EqualityForm({ defaultValue="" render={({ field, fieldState }) => { return ( - + ); }} /> @@ -300,27 +332,28 @@ function EqualityForm({ { return ( +