Compare commits

..

63 Commits

Author SHA1 Message Date
Piyush Jain 4dc2c5e3df chore(networking): add vpc CIDR blocks on database and cluster (#5569) 2025-04-29 08:51:11 +00:00
Piyush Gupta 1797c2ae20 fix: matrix question logic condition text (#5570) 2025-04-29 08:38:26 +00:00
Piyush Jain 3b5da01c0a chore(staging): add release for staging env in formbricks-stage ns (#5486)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-04-29 07:30:28 +00:00
Piyush Gupta 0f1bdce002 feat: advanced matrix question logic (#5408)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-29 05:47:05 +00:00
Matti Nannt 7c8f3e826f chore: relocate locales to apps/web (#5564) 2025-04-28 23:21:48 +02:00
Matti Nannt f21d63bb55 chore: remove unused changeset action (#5563) 2025-04-28 22:46:05 +02:00
Matti Nannt f223bb3d3f chore: remove unused langfuse packages (#5561) 2025-04-28 22:00:34 +02:00
Matti Nannt 51001d07b6 chore: remove old AI classification feature (#5529)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-28 19:18:07 +00:00
Jakob Schott a9eedd3c7a fix: Editing active surveys (#5015)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-04-28 14:50:25 +00:00
dependabot[bot] b0aa08fe4e chore(deps): bump docker/login-action from 3.3.0 to 3.4.0 (#5269)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-28 13:12:38 +02:00
Matti Nannt 8d45d24d55 fix: error should not be thrown if SMTP is unconfigured (#5524) 2025-04-28 09:48:14 +02:00
victorvhs017 8c1b9f81b9 chore: Added the tests to file upload summary (#5504)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-28 07:00:46 +00:00
Harsh Bhat 71fad1c22b docs: add swift sdk docs (#5423) 2025-04-27 23:35:21 -07:00
Harsh Bhat 292266c597 docs: add more inbound links (#5424) 2025-04-27 23:35:07 -07:00
Anshuman Pandey 54e589a6a0 fix: surveys package X button hover and modal bg fix (#5518) 2025-04-27 21:44:10 -07:00
Gulshan Kumar fb3f425c27 fix: Enhances ux in input box in login-page (#5509) 2025-04-27 20:53:55 -07:00
Jakob Schott 1aaa30c6e9 fix: empty headlines; useage of Error as variable and excluded… (#5491)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-25 14:12:31 +00:00
Dhruwang Jariwala 8611410b21 chore: refactored templates file (#5492)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-25 13:34:31 +00:00
victorvhs017 40fa7a69c0 fix: refactor clickable divs into buttons (#5489)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-25 08:13:15 +00:00
Anshuman Pandey 5eca30e513 fix: android sonarqube issues (#5503) 2025-04-25 06:53:39 +00:00
Piyush Gupta 4b78493782 fix: SAML flow jose package import (#5497) 2025-04-25 06:24:59 +00:00
Anshuman Pandey 2ce44b734f fix: stop timers on logout (#5498) 2025-04-24 12:56:28 +00:00
Anshuman Pandey 85d8f8c3ae fix: iOS sonarqube issues (#5494) 2025-04-24 11:19:52 +00:00
victorvhs017 3f16291137 fix: updated encryption algorithm and added sort function (#5485)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-04-24 07:47:22 +00:00
victorvhs017 a5958d5653 chore: update sonar properties file (#5472) 2025-04-23 22:09:16 +02:00
victorvhs017 fdbdf8207a chore: add js-core to SonarQube check (#5466) 2025-04-23 22:08:31 +02:00
Piyush Gupta 630e5489ec feat: Implement v2 management api endpoint for contact attribute keys (#5316)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-23 15:48:18 +00:00
Anshuman Pandey 36943bb786 fix: android sdk callbacks, tweaks and fixes (#5487) 2025-04-23 13:37:22 +00:00
Dhruwang Jariwala e1bbb0a10f fix: billing (#5483) 2025-04-23 09:54:08 +00:00
Piyush Jain 27da540846 chore: fix buckets and iam for staging env (#5475) 2025-04-23 08:24:45 +00:00
Piyush Jain 7d7f6ed04a chore(terraform): add valkey and rds for staging env (#5471) 2025-04-22 16:11:16 +00:00
Vijay ff01bc342d fix: Some DoS (usage of regex) Sonar Security Hotspots (#5334) 2025-04-22 17:16:22 +02:00
Anshuman Pandey cd8b40b569 fix: cleanup issue in iOS package (#5473) 2025-04-22 14:50:28 +00:00
Anshuman Pandey 31c742f7a8 fix: setLanguage and icon issue for the iOS SDK (#5470) 2025-04-22 13:18:28 +00:00
Dhruwang Jariwala d6a7a2c21f fix: x button not visible when close on click outside is not allowed (#5464)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-22 07:13:04 +00:00
Dhruwang Jariwala 499ecab691 chore: update alpine version (#5465) 2025-04-22 06:48:52 +00:00
victorvhs017 df06540f1b chore: move package lib to web/lib (#5425) 2025-04-21 15:57:54 +02:00
victorvhs017 a32b213ca5 chore: Enable Sentry integration (#5337)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-21 12:41:54 +00:00
Piyush Gupta 6120f992a4 chore: updates prisma to latest version (#5395)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-21 13:51:39 +02:00
Matti Nannt 389a551a69 fix: docker build error because of deprecated Github cache (#5428) 2025-04-21 11:56:27 +02:00
Peter Pesti-Varga 8ddbdc0e1e fix: Address SonarQube code smells (#5416)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-21 11:30:45 +02:00
Piyush Gupta 302c6a90c0 chore: removes formbricks_encryption_key-environment-variable (#5426) 2025-04-21 05:55:48 +00:00
Anshuman Pandey 18e597d8a3 fix: smaller fixes and tweaks (#5417) 2025-04-18 12:26:21 +00:00
victorvhs017 81d717ccff fix: iOS SDK memory leaks (#5388)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-17 18:03:05 +00:00
Dhruwang Jariwala 2e979c7323 fix: managers should not be allowed to create api keys (#5409) 2025-04-17 14:12:55 +00:00
Anshuman Pandey 4dfd15d6dd fix: adds no-cache header when debug mode is ON (#5405) 2025-04-17 09:03:44 +00:00
Matti Nannt 5b9bf3ff43 chore(infra): increase cloudwatch elb alarm limit to 10 (#5407) 2025-04-17 06:41:13 +00:00
Anshuman Pandey d2f7485098 feat: advanced follow ups (#5340)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2025-04-17 06:39:22 +00:00
Dhruwang Jariwala f8fee1fba7 fix: refactor end screen card description ux (#5386)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-04-17 06:28:18 +00:00
Matti Nannt 19249ca00f chore: enable performance insights for rds (#5404) 2025-04-17 06:27:34 +00:00
Anshuman Pandey 01e5700340 fix: adds eslint rules for using test and refactors the current tests (#5397) 2025-04-17 03:32:03 +00:00
Johannes ff2f7660a6 chore: add segment id to modal view (#5391)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-04-17 02:56:32 +00:00
Johannes 2bc05e2b4a docs: tweak quick start guides (#5403) 2025-04-16 19:38:54 -07:00
Johannes 137c6447b7 docs: tweak Data Prefilling docs for clarity (#5402) 2025-04-16 18:43:00 -07:00
Piyush Gupta ebc8f0c917 fix: docker build test (#5396) 2025-04-16 12:32:38 +00:00
Anshuman Pandey 5a8d10b5b4 fix: removes the onFinished callbacks from the iOS package (#5384) 2025-04-16 11:11:30 +00:00
Anshuman Pandey 875815fb62 fix: fixes cb urls (#5392) 2025-04-16 04:47:14 +00:00
Dhruwang Jariwala cdf526e130 fix: type issue in notion integration (#5385) 2025-04-16 04:15:27 +00:00
Dhruwang Jariwala b685032b34 chore: make env permissions optional in api key (#5309)
Co-authored-by: Victor Santos <victor@formbricks.com>
2025-04-15 11:42:48 +00:00
Vijay a171f9cb00 fix: security hotspots in Dockerfile (#5314) 2025-04-15 04:15:56 -07:00
Dhruwang Jariwala c452f05ec2 feat: questionid to summary (#5381) 2025-04-15 05:58:10 +00:00
Dhruwang Jariwala 93d91f80f2 fix: progress bar calculation (#5339) 2025-04-15 00:51:47 +00:00
Piyush Gupta 7b764c8427 fix: adds api_key label to the view permission modal (#5326) 2025-04-14 08:53:14 +00:00
1147 changed files with 42089 additions and 16709 deletions
+5 -6
View File
@@ -206,12 +206,6 @@ UNKEY_ROOT_KEY=
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
# CUSTOM_CACHE_DISABLED=1
# Azure AI settings
# AI_AZURE_RESSOURCE_NAME=
# AI_AZURE_API_KEY=
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
# AI_AZURE_LLM_DEPLOYMENT_ID=
# INTERCOM_APP_ID=
# INTERCOM_SECRET_KEY=
@@ -219,3 +213,8 @@ UNKEY_ROOT_KEY=
# PROMETHEUS_ENABLED=
# PROMETHEUS_EXPORTER_PORT=
# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry.
# SENTRY_DSN=
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
# It's used automatically by Sentry during the build for authentication when uploading source maps.
# SENTRY_AUTH_TOKEN=
+26
View File
@@ -0,0 +1,26 @@
# Testing Instructions
When generating test files inside the "/app/web" path, follow these rules:
- Use vitest
- Ensure 100% code coverage
- Add as few comments as possible
- The test file should be located in the same folder as the original file
- Use the `test` function instead of `it`
- Follow the same test pattern used for other files in the package where the file is located
- All imports should be at the top of the file, not inside individual tests
- For mocking inside "test" blocks use "vi.mocked"
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
If it's a test for a ".tsx" file, follow these extra instructions:
- Add this code inside the "describe" block and before any test:
afterEach(() => {
cleanup();
});
- the "afterEach" function should only have "cleanup()" inside it and should be adde to the "vitest" imports
- For click events, import userEvent from "@testing-library/user-event"
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
+30 -1
View File
@@ -12,6 +12,13 @@ on:
required: false
type: string
default: 'ghcr.io/formbricks/formbricks'
ENVIRONMENT:
description: 'The environment to deploy to'
required: true
type: choice
options:
- stage
- prod
workflow_call:
inputs:
VERSION:
@@ -23,6 +30,10 @@ on:
required: false
type: string
default: 'ghcr.io/formbricks/formbricks'
ENVIRONMENT:
description: 'The environment to deploy to'
required: true
type: string
permissions:
id-token: write
@@ -48,6 +59,8 @@ jobs:
AWS_REGION: eu-central-1
- uses: helmfile/helmfile-action@v2
name: Deploy Formbricks Cloud Prod
if: (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && github.event.inputs.ENVIRONMENT == 'prod'
env:
VERSION: ${{ inputs.VERSION }}
REPOSITORY: ${{ inputs.REPOSITORY }}
@@ -58,7 +71,23 @@ jobs:
helm-plugins: >
https://github.com/databus23/helm-diff,
https://github.com/jkroepke/helm-secrets
helmfile-args: apply
helmfile-args: apply -l environment=prod
helmfile-auto-init: "false"
helmfile-workdirectory: infra/formbricks-cloud-helm
- uses: helmfile/helmfile-action@v2
name: Deploy Formbricks Cloud Stage
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
env:
VERSION: ${{ inputs.VERSION }}
REPOSITORY: ${{ inputs.REPOSITORY }}
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
with:
helm-plugins: >
https://github.com/databus23/helm-diff,
https://github.com/jkroepke/helm-secrets
helmfile-args: apply -l environment=stage
helmfile-auto-init: "false"
helmfile-workdirectory: infra/formbricks-cloud-helm
+1
View File
@@ -31,3 +31,4 @@ jobs:
- helm-chart-release
with:
VERSION: ${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: "prod"
-56
View File
@@ -1,56 +0,0 @@
name: Release Changesets
on:
workflow_dispatch:
#push:
# branches:
# - main
permissions:
contents: write
pull-requests: write
packages: write
concurrency: ${{ github.workflow }}-${{ github.ref }}
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 15
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout Repo
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
- name: Setup Node.js 18.x
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
with:
node-version: 18.x
- name: Install pnpm
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
- name: Install Dependencies
run: pnpm install --config.platform=linux --config.architecture=x64
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
with:
# This expects you to have a script called release which does a build for your packages and calls changeset publish
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -51,7 +51,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -82,8 +82,6 @@ jobs:
secrets: |
database_url=${{ secrets.DUMMY_DATABASE_URL }}
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
+1 -3
View File
@@ -71,7 +71,7 @@ jobs:
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -102,8 +102,6 @@ jobs:
secrets: |
database_url=${{ secrets.DUMMY_DATABASE_URL }}
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
+1
View File
@@ -72,3 +72,4 @@ infra/terraform/.terraform/
# IntelliJ IDEA
/.idea/
/*.iml
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
+1 -1
View File
@@ -16,6 +16,6 @@ if [ -f branch.json ]; then
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
else
pnpm run tolgee-pull
git add packages/lib/messages
git add apps/web/locales
fi
fi
+7 -7
View File
@@ -4,33 +4,33 @@
"patterns": ["./apps/web/**/*.ts?(x)"],
"projectId": 10304,
"pull": {
"path": "./packages/lib/messages"
"path": "./apps/web/locales"
},
"push": {
"files": [
{
"language": "en-US",
"path": "./packages/lib/messages/en-US.json"
"path": "./apps/web/locales/en-US.json"
},
{
"language": "de-DE",
"path": "./packages/lib/messages/de-DE.json"
"path": "./apps/web/locales/de-DE.json"
},
{
"language": "fr-FR",
"path": "./packages/lib/messages/fr-FR.json"
"path": "./apps/web/locales/fr-FR.json"
},
{
"language": "pt-BR",
"path": "./packages/lib/messages/pt-BR.json"
"path": "./apps/web/locales/pt-BR.json"
},
{
"language": "zh-Hant-TW",
"path": "./packages/lib/messages/zh-Hant-TW.json"
"path": "./apps/web/locales/zh-Hant-TW.json"
},
{
"language": "pt-PT",
"path": "./packages/lib/messages/pt-PT.json"
"path": "./apps/web/locales/pt-PT.json"
}
],
"forceMode": "OVERRIDE"
+3 -1
View File
@@ -1,8 +1,10 @@
{
"javascript.updateImportsOnFileMove.enabled": "always",
"sonarlint.connectedMode.project": {
"connectionId": "formbricks",
"projectKey": "formbricks_formbricks"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.updateImportsOnFileMove.enabled": "always"
}
+17
View File
@@ -1,3 +1,20 @@
module.exports = {
extends: ["@formbricks/eslint-config/legacy-next.js"],
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
overrides: [
{
files: ["lib/messages/**/*.json"],
plugins: ["i18n-json"],
rules: {
"i18n-json/identical-keys": [
"error",
{
filePath: require("path").join(__dirname, "messages", "en-US.json"),
checkExtraKeys: false,
checkMissingKeys: true,
},
],
},
},
],
};
+27 -26
View File
@@ -1,4 +1,4 @@
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
FROM node:22-alpine3.21 AS base
#
## step 1: Prune monorepo
@@ -81,13 +81,14 @@ RUN corepack enable
RUN apk add --no-cache curl \
&& apk add --no-cache supercronic \
# && addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
&& addgroup -S nextjs \
&& adduser -S -u 1001 -G nextjs nextjs
WORKDIR /home/nextjs
# Ensure no write permissions are assigned to the copied resources
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
RUN chmod -R 755 ./
COPY --from=installer /app/apps/web/.next/standalone ./
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
COPY --from=installer /app/apps/web/next.config.mjs .
RUN chmod 644 ./next.config.mjs
@@ -95,38 +96,38 @@ RUN chmod 644 ./next.config.mjs
COPY --from=installer /app/apps/web/package.json .
RUN chmod 644 ./package.json
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
RUN chmod -R 755 ./apps/web/.next/static
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
RUN chmod -R 755 ./apps/web/public
COPY --from=installer /app/apps/web/public ./apps/web/public
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
RUN chmod 644 ./packages/database/schema.prisma
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
RUN chmod 644 ./packages/database/package.json
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
RUN chmod -R 755 ./packages/database/migration
COPY --from=installer /app/packages/database/migration ./packages/database/migration
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
RUN chmod -R 755 ./packages/database/src
COPY --from=installer /app/packages/database/src ./packages/database/src
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
RUN chmod -R 755 ./packages/database/node_modules
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
RUN chmod -R 755 ./node_modules/@prisma/client
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
RUN chmod -R 755 ./node_modules/.prisma
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
RUN chmod 644 ./prisma_version.txt
COPY --from=installer /prisma_version.txt .
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
COPY /docker/cronjobs /app/docker/cronjobs
RUN chmod -R 755 /app/docker/cronjobs
@@ -1,11 +1,11 @@
"use client";
import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { ArrowRight } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProjectConfigChannel } from "@formbricks/types/project";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
@@ -101,17 +101,17 @@ export const OnboardingSetupInstructions = ({
<div>
{activeTab === "npm" ? (
<div className="prose prose-slate w-full">
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
npm install @formbricks/js
</CodeBlock>
<p>{t("common.or")}</p>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
yarn add @formbricks/js
</CodeBlock>
<p className="text-sm text-slate-700">
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
</p>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
</CodeBlock>
<Button id="onboarding-inapp-connect-read-npm-docs" className="mt-3" variant="secondary" asChild>
@@ -125,11 +125,11 @@ export const OnboardingSetupInstructions = ({
</div>
) : activeTab === "html" ? (
<div className="prose prose-slate">
<p className="mt-6 -mb-1 text-sm text-slate-700">
<p className="-mb-1 mt-6 text-sm text-slate-700">
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
</p>
<div>
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys}
</CodeBlock>
</div>
@@ -1,12 +1,12 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { WEBAPP_URL } from "@/lib/constants";
import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react";
import Link from "next/link";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
interface ConnectPageProps {
params: Promise<{
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
channel={channel}
/>
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}`}>
@@ -1,7 +1,7 @@
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/errors";
const OnboardingLayout = async (props) => {
@@ -1,4 +1,4 @@
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
import { TProject } from "@formbricks/types/project";
import { TXMTemplate } from "@formbricks/types/templates";
@@ -1,8 +1,13 @@
import { getDefaultEndingCard } from "@/app/lib/templates";
import {
buildCTAQuestion,
buildNPSQuestion,
buildOpenTextQuestion,
buildRatingQuestion,
getDefaultEndingCard,
} from "@/app/lib/survey-builder";
import { createId } from "@paralleldrive/cuid2";
import { TFnType } from "@tolgee/react";
import { logger } from "@formbricks/logger";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
@@ -26,35 +31,26 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.nps_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: t("templates.nps_survey_question_1_headline") },
buildNPSQuestion({
headline: t("templates.nps_survey_question_1_headline"),
required: true,
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
upperLabel: t("templates.nps_survey_question_1_upper_label"),
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_2_headline") },
t,
}),
buildOpenTextQuestion({
headline: t("templates.nps_survey_question_2_headline"),
required: false,
inputType: "text",
charLimit: {
enabled: false,
},
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_3_headline") },
t,
}),
buildOpenTextQuestion({
headline: t("templates.nps_survey_question_3_headline"),
required: false,
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -67,9 +63,8 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.star_rating_survey_name"),
questions: [
{
buildRatingQuestion({
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -102,16 +97,15 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "number",
headline: { default: t("templates.star_rating_survey_question_1_headline") },
headline: t("templates.star_rating_survey_question_1_headline"),
required: true,
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
t,
}),
buildCTAQuestion({
id: reusableQuestionIds[1],
html: { default: t("templates.star_rating_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
html: t("templates.star_rating_survey_question_2_html"),
logic: [
{
id: createId(),
@@ -138,25 +132,23 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: { default: t("templates.star_rating_survey_question_2_headline") },
headline: t("templates.star_rating_survey_question_2_headline"),
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
buttonLabel: t("templates.star_rating_survey_question_2_button_label"),
buttonExternal: true,
},
{
t,
}),
buildOpenTextQuestion({
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.star_rating_survey_question_3_headline") },
headline: t("templates.star_rating_survey_question_3_headline"),
required: true,
subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
subheader: t("templates.star_rating_survey_question_3_subheader"),
buttonLabel: t("templates.star_rating_survey_question_3_button_label"),
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -169,9 +161,8 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.csat_survey_name"),
questions: [
{
buildRatingQuestion({
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -204,15 +195,14 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: t("templates.csat_survey_question_1_headline") },
headline: t("templates.csat_survey_question_1_headline"),
required: true,
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
upperLabel: t("templates.csat_survey_question_1_upper_label"),
t,
}),
buildOpenTextQuestion({
id: reusableQuestionIds[1],
type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{
id: createId(),
@@ -239,25 +229,20 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: { default: t("templates.csat_survey_question_2_headline") },
headline: t("templates.csat_survey_question_2_headline"),
required: false,
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
placeholder: t("templates.csat_survey_question_2_placeholder"),
inputType: "text",
charLimit: {
enabled: false,
},
},
{
t,
}),
buildOpenTextQuestion({
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.csat_survey_question_3_headline") },
headline: t("templates.csat_survey_question_3_headline"),
required: false,
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
placeholder: t("templates.csat_survey_question_3_placeholder"),
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -267,28 +252,22 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.cess_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
buildRatingQuestion({
range: 5,
scale: "number",
headline: { default: t("templates.cess_survey_question_1_headline") },
headline: t("templates.cess_survey_question_1_headline"),
required: true,
lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.cess_survey_question_2_headline") },
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
upperLabel: t("templates.cess_survey_question_1_upper_label"),
t,
}),
buildOpenTextQuestion({
headline: t("templates.cess_survey_question_2_headline"),
required: true,
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
placeholder: t("templates.cess_survey_question_2_placeholder"),
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -301,9 +280,8 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.smileys_survey_name"),
questions: [
{
buildRatingQuestion({
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -336,16 +314,15 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: t("templates.smileys_survey_question_1_headline") },
headline: t("templates.smileys_survey_question_1_headline"),
required: true,
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
t,
}),
buildCTAQuestion({
id: reusableQuestionIds[1],
html: { default: t("templates.smileys_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
html: t("templates.smileys_survey_question_2_html"),
logic: [
{
id: createId(),
@@ -372,25 +349,23 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: { default: t("templates.smileys_survey_question_2_headline") },
headline: t("templates.smileys_survey_question_2_headline"),
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
buttonLabel: t("templates.smileys_survey_question_2_button_label"),
buttonExternal: true,
},
{
t,
}),
buildOpenTextQuestion({
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.smileys_survey_question_3_headline") },
headline: t("templates.smileys_survey_question_3_headline"),
required: true,
subheader: { default: t("templates.smileys_survey_question_3_subheader") },
buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
subheader: t("templates.smileys_survey_question_3_subheader"),
buttonLabel: t("templates.smileys_survey_question_3_button_label"),
placeholder: t("templates.smileys_survey_question_3_placeholder"),
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -400,37 +375,26 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.enps_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: t("templates.enps_survey_question_1_headline"),
},
buildNPSQuestion({
headline: t("templates.enps_survey_question_1_headline"),
required: false,
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
upperLabel: t("templates.enps_survey_question_1_upper_label"),
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_2_headline") },
t,
}),
buildOpenTextQuestion({
headline: t("templates.enps_survey_question_2_headline"),
required: false,
inputType: "text",
charLimit: {
enabled: false,
},
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_3_headline") },
t,
}),
buildOpenTextQuestion({
headline: t("templates.enps_survey_question_3_headline"),
required: false,
inputType: "text",
charLimit: {
enabled: false,
},
},
t,
}),
],
};
};
@@ -1,4 +1,7 @@
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { getEnvironment } from "@/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
import { getUser } from "@/lib/user/service";
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button";
@@ -7,9 +10,6 @@ import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import Link from "next/link";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
interface XMTemplatePageProps {
params: Promise<{
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
<XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}/surveys`}>
@@ -1,12 +1,12 @@
"use server";
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
import { cache } from "@/lib/cache";
import { teamCache } from "@/lib/cache/team";
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
@@ -2,6 +2,8 @@
import { formbricksLogout } from "@/app/lib/formbricks";
import FBLogo from "@/images/formbricks-wordmark.svg";
import { cn } from "@/lib/cn";
import { capitalizeFirstLetter } from "@/lib/utils/strings";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
import {
@@ -24,8 +26,6 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
@@ -80,7 +80,7 @@ export const LandingSidebar = ({
<DropdownMenuTrigger
asChild
id="userDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center space-x-3")}>
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
<>
@@ -112,7 +112,7 @@ export const LandingSidebar = ({
{/* Dropdown Items */}
{dropdownNavigation.map((link) => (
<Link href={link.href} target={link.target} className="flex w-full items-center">
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center">
<DropdownMenuItem>
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
{link.label}
@@ -1,9 +1,9 @@
import { getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getUserProjects } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getUserProjects } from "@formbricks/lib/project/service";
const LandingLayout = async (props) => {
const params = await props.params;
@@ -1,11 +1,11 @@
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
import { getOrganizationsByUserId } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { notFound, redirect } from "next/navigation";
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
const Page = async (props) => {
const params = await props.params;
@@ -1,19 +1,19 @@
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import "@testing-library/jest-dom/vitest";
import { act, cleanup, render, screen } from "@testing-library/react";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import ProjectOnboardingLayout from "./layout";
// Mock all the modules and functions that this layout uses:
vi.mock("@formbricks/lib/constants", () => ({
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
POSTHOG_API_KEY: "mock-posthog-api-key",
POSTHOG_HOST: "mock-posthog-host",
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
vi.mock("@formbricks/lib/organization/auth", () => ({
vi.mock("@/lib/organization/auth", () => ({
canUserAccessOrganization: vi.fn(),
}));
vi.mock("@formbricks/lib/organization/service", () => ({
vi.mock("@/lib/organization/service", () => ({
getOrganization: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
vi.mock("@/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
cleanup();
});
it("redirects to /auth/login if there is no session", async () => {
test("redirects to /auth/login if there is no session", async () => {
// Mock no session
vi.mocked(getServerSession).mockResolvedValueOnce(null);
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
expect(layoutElement).toBeUndefined();
});
it("throws an error if user does not exist", async () => {
test("throws an error if user does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({
user: { id: "user-123" },
});
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.user_not_found");
});
it("throws AuthorizationError if user cannot access organization", async () => {
test("throws AuthorizationError if user cannot access organization", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.not_authorized");
});
it("throws an error if organization does not exist", async () => {
test("throws an error if organization does not exist", async () => {
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
).rejects.toThrow("common.organization_not_found");
});
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
// Provide valid data
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
@@ -1,13 +1,13 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
import { canUserAccessOrganization } from "@/lib/organization/auth";
import { getOrganization } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
const ProjectOnboardingLayout = async (props) => {
@@ -1,4 +1,5 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ChannelPageProps {
params: Promise<{
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -1,12 +1,12 @@
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
const OnboardingLayout = async (props) => {
const params = await props.params;
@@ -1,4 +1,5 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getUserProjects } from "@/lib/project/service";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
interface ModePageProps {
params: Promise<{
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -2,6 +2,7 @@
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
@@ -26,7 +27,6 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import {
TProjectConfigChannel,
TProjectConfigIndustry,
@@ -218,7 +218,7 @@ export const ProjectSettings = ({
</FormProvider>
</div>
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow-sm">
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
{logoUrl && (
<Image
src={logoUrl}
@@ -1,5 +1,7 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
import { getUserProjects } from "@/lib/project/service";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
import { Button } from "@/modules/ui/components/button";
@@ -8,8 +10,6 @@ import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react";
import Link from "next/link";
import { redirect } from "next/navigation";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getUserProjects } from "@formbricks/lib/project/service";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
interface ProjectSettingsPageProps {
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
/>
{projects.length >= 1 && (
<Button
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -1,9 +1,9 @@
import { getEnvironment } from "@/lib/environment/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import { cleanup, render, screen } from "@testing-library/react";
import { Session } from "next-auth";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganization } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
@@ -28,7 +28,7 @@ vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
vi.mock("@/modules/environments/lib/utils", () => ({
environmentIdLayoutChecks: vi.fn(),
}));
vi.mock("@formbricks/lib/environment/service", () => ({
vi.mock("@/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("next/navigation", () => ({
@@ -41,7 +41,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
vi.clearAllMocks();
});
it("renders successfully when environment is found", async () => {
test("renders successfully when environment is found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
session: { user: { id: "user1" } } as Session,
@@ -62,7 +62,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
});
it("throws an error when environment is not found", async () => {
test("throws an error when environment is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
@@ -79,7 +79,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
).rejects.toThrow("common.environment_not_found");
});
it("calls redirect when session is null", async () => {
test("calls redirect when session is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: undefined as unknown as Session,
@@ -98,7 +98,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
).rejects.toThrow("Redirect called");
});
it("throws error if user is null", async () => {
test("throws error if user is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
@@ -1,8 +1,8 @@
import { getEnvironment } from "@/lib/environment/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
import { redirect } from "next/navigation";
import { getEnvironment } from "@formbricks/lib/environment/service";
const SurveyEditorEnvironmentLayout = async (props) => {
const params = await props.params;
@@ -1,5 +1,5 @@
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { cn } from "@formbricks/lib/cn";
import { cn } from "@/lib/cn";
export const LoadingCard = ({
title,
@@ -1,5 +1,8 @@
"use server";
import { getOrganization } from "@/lib/organization/service";
import { getOrganizationProjectsCount } from "@/lib/project/service";
import { updateUser } from "@/lib/user/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import {
@@ -8,9 +11,6 @@ import {
} from "@/modules/ee/license-check/lib/utils";
import { createProject } from "@/modules/projects/settings/lib/project";
import { z } from "zod";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
import { updateUser } from "@formbricks/lib/user/service";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { ZProjectUpdateInput } from "@formbricks/types/project";
@@ -1,12 +1,12 @@
"use server";
import { deleteActionClass, getActionClass, updateActionClass } from "@/lib/actionClass/service";
import { cache } from "@/lib/cache";
import { getSurveysByActionClassId } from "@/lib/survey/service";
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
import { z } from "zod";
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
import { cache } from "@formbricks/lib/cache";
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
import { ZActionClassInput } from "@formbricks/types/action-classes";
import { ZId } from "@formbricks/types/common";
import { ResourceNotFoundError } from "@formbricks/types/errors";
@@ -1,7 +1,9 @@
"use client";
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
import { convertDateTimeStringShort } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { capitalizeFirstLetter } from "@/lib/utils/strings";
import { createActionClassAction } from "@/modules/survey/editor/actions";
import { Button } from "@/modules/ui/components/button";
import { ErrorComponent } from "@/modules/ui/components/error-component";
@@ -10,8 +12,6 @@ import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
import { useTranslate } from "@tolgee/react";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
import { TEnvironment } from "@formbricks/types/environment";
import { getActiveInactiveSurveysAction } from "../actions";
@@ -36,7 +36,7 @@ export const ActionClassesTable = ({
return (
<>
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
{TableHeading}
<div id="actionClassesWrapper" className="flex flex-col">
{actionClasses.length > 0 ? (
@@ -1,5 +1,5 @@
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
import { timeSince } from "@formbricks/lib/time";
import { timeSince } from "@/lib/time";
import { TActionClass } from "@formbricks/types/action-classes";
import { TUserLocale } from "@formbricks/types/user";
@@ -14,7 +14,9 @@ export const ActionClassDataRow = ({
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-5 w-5 shrink-0 text-slate-500">{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}</div>
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">{actionClass.name}</div>
<div className="text-xs text-slate-400">{actionClass.description}</div>
@@ -10,7 +10,7 @@ const Loading = () => {
<>
<PageContentWrapper>
<PageHeader pageTitle={t("common.actions")} />
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<span className="sr-only">{t("common.edit")}</span>
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
@@ -22,7 +22,7 @@ const Loading = () => {
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-4 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="h-6 w-6 shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
@@ -33,7 +33,7 @@ const Loading = () => {
</div>
</div>
</div>
<div className="col-span-2 my-auto flex justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
@@ -2,15 +2,15 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
import { getActionClasses } from "@/lib/actionClass/service";
import { getEnvironments } from "@/lib/environment/service";
import { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getEnvironments } from "@formbricks/lib/environment/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
export const metadata: Metadata = {
title: "Actions",
@@ -1,5 +1,17 @@
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
getOrganizationsByUserId,
} from "@/lib/organization/service";
import { getUserProjects } from "@/lib/project/service";
import { getUser } from "@/lib/user/service";
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
@@ -7,18 +19,6 @@ import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-bann
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
import { getTranslate } from "@/tolgee/server";
import type { Session } from "next-auth";
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
getOrganizationsByUserId,
} from "@formbricks/lib/organization/service";
import { getUserProjects } from "@formbricks/lib/project/service";
import { getUser } from "@formbricks/lib/user/service";
interface EnvironmentLayoutProps {
environmentId: string;
@@ -1,7 +1,7 @@
"use client";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import { useEffect } from "react";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
interface EnvironmentStorageHandlerProps {
environmentId: string;
@@ -1,11 +1,11 @@
"use client";
import { cn } from "@/lib/cn";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
interface EnvironmentSwitchProps {
@@ -4,6 +4,9 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
import { formbricksLogout } from "@/app/lib/formbricks";
import FBLogo from "@/images/formbricks-wordmark.svg";
import { cn } from "@/lib/cn";
import { getAccessFlags } from "@/lib/membership/utils";
import { capitalizeFirstLetter } from "@/lib/utils/strings";
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
import { ProfileAvatar } from "@/modules/ui/components/avatars";
@@ -45,9 +48,6 @@ import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
@@ -265,7 +265,7 @@ export const MainNavigation = ({
size="icon"
onClick={toggleSidebar}
className={cn(
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-hidden"
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
)}>
{isCollapsed ? (
<PanelLeftOpenIcon strokeWidth={1.5} />
@@ -332,7 +332,7 @@ export const MainNavigation = ({
<DropdownMenuTrigger
asChild
id="userDropdownTrigger"
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
<div
tabIndex={0}
className={cn(
@@ -1,7 +1,7 @@
import { cn } from "@/lib/cn";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import Link from "next/link";
import React from "react";
import { cn } from "@formbricks/lib/cn";
interface NavigationLinkProps {
href: string;
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom/vitest";
import { cleanup, render } from "@testing-library/react";
import { Session } from "next-auth";
import { usePostHog } from "posthog-js/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TOrganizationBilling } from "@formbricks/types/organizations";
import { TUser } from "@formbricks/types/user";
import { PosthogIdentify } from "./PosthogIdentify";
@@ -18,7 +18,7 @@ describe("PosthogIdentify", () => {
cleanup();
});
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
@@ -72,7 +72,7 @@ describe("PosthogIdentify", () => {
});
});
it("does nothing if isPosthogEnabled is false", () => {
test("does nothing if isPosthogEnabled is false", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
@@ -95,7 +95,7 @@ describe("PosthogIdentify", () => {
expect(mockGroup).not.toHaveBeenCalled();
});
it("does nothing if session user is missing", () => {
test("does nothing if session user is missing", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
@@ -120,7 +120,7 @@ describe("PosthogIdentify", () => {
expect(mockGroup).not.toHaveBeenCalled();
});
it("identifies user but does not group if environmentId/organizationId not provided", () => {
test("identifies user but does not group if environmentId/organizationId not provided", () => {
const mockIdentify = vi.fn();
const mockGroup = vi.fn();
@@ -18,7 +18,7 @@ export const TopControlBar = ({
}: SideBarProps) => {
return (
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
<div className="z-10 shadow-2xs">
<div className="shadow-xs z-10">
<div className="flex w-fit items-center space-x-2 py-2">
<TopControlButtons
environment={environment}
@@ -1,6 +1,7 @@
"use client";
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
import { getAccessFlags } from "@/lib/membership/utils";
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
import { Button } from "@/modules/ui/components/button";
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TEnvironment } from "@formbricks/types/environment";
import { TOrganizationRole } from "@formbricks/types/memberships";
@@ -1,10 +1,10 @@
"use client";
import { cn } from "@/lib/cn";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
interface WidgetStatusIndicatorProps {
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
<currentStatus.icon />
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
@@ -1,5 +1,6 @@
"use server";
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import {
@@ -9,7 +10,6 @@ import {
getProjectIdFromIntegrationId,
} from "@/lib/utils/helper";
import { z } from "zod";
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
import { ZId } from "@formbricks/types/common";
import { ZIntegrationInput } from "@formbricks/types/integration";
@@ -4,6 +4,8 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
import AirtableLogo from "@/images/airtableLogo.svg";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
@@ -23,8 +25,6 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationAirtable,
@@ -0,0 +1,151 @@
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock(
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal",
() => ({
AddIntegrationModal: ({ open, setOpenWithStates }) =>
open ? (
<div data-testid="add-modal">
<button onClick={() => setOpenWithStates(false)}>close</button>
</div>
) : null,
})
);
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
environmentId: "env1",
setIsConnected: vi.fn(),
surveys: [],
airtableArray: [],
locale: "en-US" as const,
};
describe("ManageIntegration", () => {
afterEach(() => {
cleanup();
});
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
expect(screen.getByText(/link_new_table/)).toBeInTheDocument();
});
test("open add modal", async () => {
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/link_new_table/));
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
});
test("list integrations and open edit modal", async () => {
const item = {
baseId: "b",
tableId: "t",
surveyId: "s",
surveyName: "S",
tableName: "T",
questions: "Q",
questionIds: ["x"],
createdAt: new Date(),
includeVariables: false,
includeHiddenFields: false,
includeMetadata: false,
includeCreatedAt: false,
};
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [item] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalled();
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
airtableIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
} as TIntegrationAirtable
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalled();
});
});
@@ -5,6 +5,7 @@ import {
AddIntegrationModal,
IntegrationModalInputs,
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -13,7 +14,6 @@ import { useTranslate } from "@tolgee/react";
import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -98,17 +98,17 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
{integrationData.length ? (
<div className="mt-6 w-full rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
{tableHeaders.map((header, idx) => (
<div key={idx} className={`col-span-2 hidden text-center sm:block`}>
{tableHeaders.map((header) => (
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
{t(header)}
</div>
))}
</div>
{integrationData.map((data, index) => (
<div
key={index}
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg hover:bg-slate-100"
<button
key={`${index}-${data.baseId}-${data.tableId}-${data.surveyId}`}
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => {
setDefaultValues({
base: data.baseId,
@@ -129,7 +129,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), props.locale)}
</div>
</div>
</button>
))}
</div>
) : (
@@ -1,15 +1,15 @@
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { getAirtableTables } from "@/lib/airtable/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
import { getIntegrations } from "@/lib/integration/service";
import { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { redirect } from "next/navigation";
import { getAirtableTables } from "@formbricks/lib/airtable/service";
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationItem } from "@formbricks/types/integration";
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
@@ -1,10 +1,10 @@
"use server";
import { getSpreadsheetNameById } from "@/lib/googleSheet/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
const ZGetSpreadsheetNameByIdAction = z.object({
@@ -8,7 +8,9 @@ import {
isValidGoogleSheetsUrl,
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
@@ -21,8 +23,6 @@ import Image from "next/image";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import {
TIntegrationGoogleSheets,
TIntegrationGoogleSheetsConfigData,
@@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
<div className="space-y-4">
<div>
<Label htmlFor="Surveys">{t("common.questions")}</Label>
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
<div key={question.id} className="my-1 flex items-center space-x-2">
@@ -0,0 +1,162 @@
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock("react-hot-toast", () => ({
default: { success: vi.fn(), error: vi.fn() },
}));
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
}));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
locale: "en-US" as const,
} as const;
describe("ManageIntegration (Google Sheets)", () => {
afterEach(() => {
cleanup();
});
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument();
});
test("click link new sheet", async () => {
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/link_new_sheet/));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("list integrations and open edit", async () => {
const item = {
spreadsheetId: "sid",
spreadsheetName: "SheetName",
surveyId: "s1",
surveyName: "Survey1",
questionIds: ["q1"],
questions: "Q",
createdAt: new Date(),
};
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [item] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
expect(screen.getByText("Survey1")).toBeInTheDocument();
await userEvent.click(screen.getByText("Survey1"));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({
...item,
index: 0,
});
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { default: toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
googleSheetIntegration={
{
id: "1",
config: { email: "a@b.com", data: [] },
} as unknown as TIntegrationGoogleSheets
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { default: toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
});
});
@@ -1,6 +1,7 @@
"use client";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
import { Trash2Icon } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import { timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import {
TIntegrationGoogleSheets,
@@ -36,11 +36,10 @@ export const ManageIntegration = ({
}: ManageIntegrationProps) => {
const { t } = useTranslate();
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const integrationArray = googleSheetIntegration
? googleSheetIntegration.config.data
? googleSheetIntegration.config.data
: []
: [];
let integrationArray: TIntegrationGoogleSheetsConfigData[] = [];
if (googleSheetIntegration?.config.data) {
integrationArray = googleSheetIntegration.config.data;
}
const [isDeleting, setisDeleting] = useState(false);
const handleDeleteIntegration = async () => {
@@ -112,9 +111,9 @@ export const ManageIntegration = ({
{integrationArray &&
integrationArray.map((data, index) => {
return (
<div
key={index}
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
<button
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
@@ -124,7 +123,7 @@ export const ManageIntegration = ({
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</div>
</button>
);
})}
</div>
@@ -1,19 +1,19 @@
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import {
GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL,
WEBAPP_URL,
} from "@/lib/constants";
import { getIntegrations } from "@/lib/integration/service";
import { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { redirect } from "next/navigation";
import {
GOOGLE_SHEETS_CLIENT_ID,
GOOGLE_SHEETS_CLIENT_SECRET,
GOOGLE_SHEETS_REDIRECT_URL,
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
const Page = async (props) => {
@@ -1,12 +1,12 @@
import "server-only";
import { cache } from "@/lib/cache";
import { surveyCache } from "@/lib/survey/cache";
import { selectSurvey } from "@/lib/survey/service";
import { transformPrismaSurvey } from "@/lib/survey/utils";
import { validateInputs } from "@/lib/utils/validate";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { selectSurvey } from "@formbricks/lib/survey/service";
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { logger } from "@formbricks/logger";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
@@ -1,9 +1,9 @@
import { cache } from "@/lib/cache";
import { webhookCache } from "@/lib/cache/webhook";
import { validateInputs } from "@/lib/utils/validate";
import { Prisma, Webhook } from "@prisma/client";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { DatabaseError } from "@formbricks/types/errors";
@@ -7,6 +7,9 @@ import {
UNSUPPORTED_TYPES_BY_NOTION,
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
import NotionLogo from "@/images/notion.png";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { structuredClone } from "@/lib/pollyfills/structuredClone";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { getQuestionTypes } from "@/modules/survey/lib/questions";
import { Button } from "@/modules/ui/components/button";
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
@@ -18,9 +21,6 @@ import Image from "next/image";
import React, { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TIntegrationInput } from "@formbricks/types/integration";
import {
TIntegrationNotion,
@@ -0,0 +1,91 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import type {
TIntegrationNotion,
TIntegrationNotionConfig,
TIntegrationNotionConfigData,
TIntegrationNotionCredential,
} from "@formbricks/types/integration/notion";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("react-hot-toast", () => ({ success: vi.fn(), error: vi.fn() }));
vi.mock("@/lib/time", () => ({ timeSince: () => "ago" }));
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
describe("ManageIntegration", () => {
afterEach(() => {
cleanup();
});
const defaultProps = {
environment: {} as any,
locale: "en-US" as const,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
handleNotionAuthorization: vi.fn(),
};
test("shows empty state when no databases", () => {
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: {
data: [] as TIntegrationNotionConfigData[],
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
} as TIntegrationNotionConfig,
} as TIntegrationNotion
}
/>
);
expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument();
});
test("renders list and handles clicks", async () => {
const data = [
{ surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" },
] as unknown as TIntegrationNotionConfigData[];
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: { data, key: { workspace_name: "ws" } as TIntegrationNotionCredential },
} as TIntegrationNotion
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 });
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
});
test("update and link new buttons invoke handlers", async () => {
render(
<ManageIntegration
{...defaultProps}
notionIntegration={
{
id: "1",
config: {
data: [],
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
} as TIntegrationNotionConfig,
} as TIntegrationNotion
}
/>
);
await userEvent.click(screen.getByText("environments.integrations.notion.update_connection"));
expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled();
await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database"));
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
});
});
@@ -1,6 +1,7 @@
"use client";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -10,7 +11,6 @@ import { useTranslate } from "@tolgee/react";
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
import { TUserLocale } from "@formbricks/types/user";
@@ -39,11 +39,11 @@ export const ManageIntegration = ({
const { t } = useTranslate();
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const [isDeleting, setisDeleting] = useState(false);
const integrationArray = notionIntegration
? notionIntegration.config.data
? notionIntegration.config.data
: []
: [];
let integrationArray: TIntegrationNotionConfigData[] = [];
if (notionIntegration?.config.data) {
integrationArray = notionIntegration.config.data;
}
const handleDeleteIntegration = async () => {
setisDeleting(true);
@@ -121,9 +121,9 @@ export const ManageIntegration = ({
{integrationArray &&
integrationArray.map((data, index) => {
return (
<div
key={index}
className="m-2 grid h-16 cursor-pointer grid-cols-6 content-center rounded-lg hover:bg-slate-100"
<button
key={`${index}-${data.databaseId}`}
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
@@ -132,7 +132,7 @@ export const ManageIntegration = ({
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</div>
</button>
);
})}
</div>
@@ -25,6 +25,8 @@ export const TYPE_MAPPING = {
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
[TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"],
[TSurveyQuestionTypeEnum.Ranking]: ["rich_text"],
};
export const UNSUPPORTED_TYPES_BY_NOTION = [
@@ -1,21 +1,21 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { redirect } from "next/navigation";
import {
NOTION_AUTH_URL,
NOTION_OAUTH_CLIENT_ID,
NOTION_OAUTH_CLIENT_SECRET,
NOTION_REDIRECT_URI,
WEBAPP_URL,
} from "@formbricks/lib/constants";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { getNotionDatabases } from "@formbricks/lib/notion/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
} from "@/lib/constants";
import { getIntegrationByType } from "@/lib/integration/service";
import { getNotionDatabases } from "@/lib/notion/service";
import { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { redirect } from "next/navigation";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
const Page = async (props) => {
@@ -9,6 +9,7 @@ import notionLogo from "@/images/notion.png";
import SlackLogo from "@/images/slacklogo.png";
import WebhookLogo from "@/images/webhook.png";
import ZapierLogo from "@/images/zapier-small.png";
import { getIntegrations } from "@/lib/integration/service";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { Card } from "@/modules/ui/components/integration-card";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -16,7 +17,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import Image from "next/image";
import { redirect } from "next/navigation";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { TIntegrationType } from "@formbricks/types/integration";
const Page = async (props) => {
@@ -1,10 +1,10 @@
"use server";
import { getSlackChannels } from "@/lib/slack/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
import { z } from "zod";
import { getSlackChannels } from "@formbricks/lib/slack/service";
import { ZId } from "@formbricks/types/common";
const ZGetSlackChannelsAction = z.object({
@@ -2,6 +2,8 @@
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import SlackLogo from "@/images/slacklogo.png";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { replaceHeadlineRecall } from "@/lib/utils/recall";
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
import { Button } from "@/modules/ui/components/button";
import { Checkbox } from "@/modules/ui/components/checkbox";
@@ -15,8 +17,6 @@ import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TIntegrationItem } from "@formbricks/types/integration";
import {
TIntegrationSlack,
@@ -0,0 +1,158 @@
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
import { ManageIntegration } from "./ManageIntegration";
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
deleteIntegrationAction: vi.fn(),
}));
vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
vi.mock("@/modules/ui/components/delete-dialog", () => ({
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
open ? (
<div data-testid="delete-dialog">
<button onClick={onDelete}>confirm</button>
<button onClick={() => setOpen(false)}>cancel</button>
</div>
) : null,
}));
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
}));
const baseProps = {
environment: { id: "env1" } as TEnvironment,
setOpenAddIntegrationModal: vi.fn(),
setIsConnected: vi.fn(),
setSelectedIntegration: vi.fn(),
refreshChannels: vi.fn(),
handleSlackAuthorization: vi.fn(),
showReconnectButton: false,
locale: "en-US" as const,
};
describe("ManageIntegration (Slack)", () => {
afterEach(() => cleanup());
test("empty state", () => {
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument();
expect(screen.getByText(/link_channel/)).toBeInTheDocument();
});
test("link channel triggers handlers", async () => {
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/link_channel/));
expect(baseProps.refreshChannels).toHaveBeenCalled();
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("show reconnect button and triggers authorization", async () => {
render(
<ManageIntegration
{...baseProps}
showReconnectButton={true}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "Team" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument();
await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button"));
expect(baseProps.handleSlackAuthorization).toHaveBeenCalled();
});
test("list integrations and open edit", async () => {
const item = {
surveyName: "S",
channelName: "C",
questions: "Q",
createdAt: new Date().toISOString(),
surveyId: "s",
channelId: "c",
} as unknown as TIntegrationSlackConfigData;
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [item], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
expect(screen.getByText("S")).toBeInTheDocument();
await userEvent.click(screen.getByText("S"));
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 });
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
});
test("delete integration success", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
await userEvent.click(screen.getByText("confirm"));
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
const { default: toast } = await import("react-hot-toast");
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
});
test("delete integration error", async () => {
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
render(
<ManageIntegration
{...baseProps}
slackIntegration={
{
id: "1",
config: { data: [], key: { team: { name: "team name" } } },
} as unknown as TIntegrationSlack
}
/>
);
await userEvent.click(screen.getByText(/delete_integration/));
await userEvent.click(screen.getByText("confirm"));
const { default: toast } = await import("react-hot-toast");
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
});
});
@@ -1,16 +1,15 @@
"use client";
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
import { timeSince } from "@/lib/time";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
import { useTranslate } from "@tolgee/react";
import { T } from "@tolgee/react";
import { T, useTranslate } from "@tolgee/react";
import { Trash2Icon } from "lucide-react";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { timeSince } from "@formbricks/lib/time";
import { TEnvironment } from "@formbricks/types/environment";
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
import { TUserLocale } from "@formbricks/types/user";
@@ -43,11 +42,10 @@ export const ManageIntegration = ({
const { t } = useTranslate();
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const [isDeleting, setisDeleting] = useState(false);
const integrationArray = slackIntegration
? slackIntegration.config.data
? slackIntegration.config.data
: []
: [];
let integrationArray: TIntegrationSlackConfigData[] = [];
if (slackIntegration?.config.data) {
integrationArray = slackIntegration.config.data;
}
const handleDeleteIntegration = async () => {
setisDeleting(true);
@@ -129,9 +127,9 @@ export const ManageIntegration = ({
{integrationArray &&
integrationArray.map((data, index) => {
return (
<div
key={index}
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg text-slate-700 hover:cursor-pointer hover:bg-slate-100"
<button
key={`${index}-${data.surveyName}-${data.channelName}`}
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 text-slate-700 hover:cursor-pointer hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
@@ -141,7 +139,7 @@ export const ManageIntegration = ({
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</div>
</button>
);
})}
</div>
@@ -1,14 +1,14 @@
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
import { getIntegrationByType } from "@/lib/integration/service";
import { findMatchingLocale } from "@/lib/utils/locale";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { GoBackButton } from "@/modules/ui/components/go-back-button";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { redirect } from "next/navigation";
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { getIntegrationByType } from "@formbricks/lib/integration/service";
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
const Page = async (props) => {
@@ -1,10 +1,10 @@
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import { cleanup, render, screen } from "@testing-library/react";
import { Session } from "next-auth";
import { redirect } from "next/navigation";
import { afterEach, describe, expect, it, vi } from "vitest";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TMembership } from "@formbricks/types/memberships";
import { TOrganization } from "@formbricks/types/organizations";
import { TProject } from "@formbricks/types/project";
@@ -41,10 +41,10 @@ vi.mock("./components/EnvironmentStorageHandler", () => ({
vi.mock("@/modules/environments/lib/utils", () => ({
environmentIdLayoutChecks: vi.fn(),
}));
vi.mock("@formbricks/lib/project/service", () => ({
vi.mock("@/lib/project/service", () => ({
getProjectByEnvironmentId: vi.fn(),
}));
vi.mock("@formbricks/lib/membership/service", () => ({
vi.mock("@/lib/membership/service", () => ({
getMembershipByUserIdOrganizationId: vi.fn(),
}));
@@ -53,7 +53,7 @@ describe("EnvLayout", () => {
cleanup();
});
it("renders successfully when all dependencies return valid data", async () => {
test("renders successfully when all dependencies return valid data", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
session: { user: { id: "user1" } } as Session,
@@ -77,7 +77,7 @@ describe("EnvLayout", () => {
expect(screen.getByTestId("child")).toHaveTextContent("Content");
});
it("throws error if project is not found", async () => {
test("throws error if project is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
@@ -97,7 +97,7 @@ describe("EnvLayout", () => {
).rejects.toThrow("common.project_not_found");
});
it("throws error if membership is not found", async () => {
test("throws error if membership is not found", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
@@ -115,7 +115,7 @@ describe("EnvLayout", () => {
).rejects.toThrow("common.membership_not_found");
});
it("calls redirect when session is null", async () => {
test("calls redirect when session is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: undefined as unknown as Session,
@@ -134,7 +134,7 @@ describe("EnvLayout", () => {
).rejects.toThrow("Redirect called");
});
it("throws error if user is null", async () => {
test("throws error if user is null", async () => {
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
t: ((key: string) => key) as any,
session: { user: { id: "user1" } } as Session,
@@ -1,9 +1,9 @@
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
import { redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
const EnvLayout = async (props: {
@@ -1,7 +1,7 @@
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
import { getAccessFlags } from "@/lib/membership/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
const EnvironmentPage = async (props) => {
const params = await props.params;
@@ -1,8 +1,8 @@
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const AccountSettingsLayout = async (props) => {
const params = await props.params;
@@ -1,8 +1,8 @@
"use server";
import { updateUser } from "@/lib/user/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { z } from "zod";
import { updateUser } from "@formbricks/lib/user/service";
import { ZUserNotificationSettings } from "@formbricks/types/user";
const ZUpdateNotificationSettingsAction = z.object({
@@ -56,7 +56,7 @@ export const EditAlerts = ({
<TooltipTrigger>
<div className="col-span-1 flex cursor-default items-center justify-center space-x-2">
<span>{t("environments.settings.notifications.every_response")}</span>
<HelpCircleIcon className="h-4 w-4 shrink-0 text-slate-500" />
<HelpCircleIcon className="h-4 w-4 flex-shrink-0 text-slate-500" />
</div>
</TooltipTrigger>
<TooltipContent>
@@ -99,7 +99,7 @@ export const EditAlerts = ({
))}
</div>
) : (
<div className="m-2 flex h-16 items-center justify-center rounded-sm bg-slate-50 text-sm text-slate-500">
<div className="m-2 flex h-16 items-center justify-center rounded bg-slate-50 text-sm text-slate-500">
<p>{t("common.no_surveys_found")}</p>
</div>
)}
@@ -11,7 +11,7 @@ export const IntegrationsTip = ({ environmentId }: IntegrationsTipProps) => {
const { t } = useTranslate();
return (
<div>
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-xs md:space-y-0 md:text-base">
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
<SlackIcon className="mr-3 h-4 w-4 text-blue-400" />
<p className="text-sm">
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
@@ -1,12 +1,12 @@
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
import { getUser } from "@/lib/user/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { prisma } from "@formbricks/database";
import { getUser } from "@formbricks/lib/user/service";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { EditAlerts } from "./components/EditAlerts";
import { EditWeeklySummary } from "./components/EditWeeklySummary";
@@ -1,10 +1,10 @@
"use server";
import { deleteFile } from "@/lib/storage/service";
import { getFileNameWithIdFromUrl } from "@/lib/storage/utils";
import { updateUser } from "@/lib/user/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { z } from "zod";
import { deleteFile } from "@formbricks/lib/storage/service";
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
import { updateUser } from "@formbricks/lib/user/service";
import { ZId } from "@formbricks/types/common";
import { ZUserUpdateInput } from "@formbricks/types/user";
@@ -105,7 +105,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
<div>
<div className="relative h-10 w-10 overflow-hidden rounded-full">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
@@ -1,5 +1,6 @@
"use client";
import { appLanguages } from "@/lib/i18n/utils";
import { Button } from "@/modules/ui/components/button";
import {
DropdownMenu,
@@ -23,7 +24,6 @@ import { ChevronDownIcon } from "lucide-react";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { appLanguages } from "@formbricks/lib/i18n/utils";
import { TUser, ZUser } from "@formbricks/types/user";
import { updateUserAction } from "../actions";
@@ -1,5 +1,8 @@
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
import { getUser } from "@/lib/user/service";
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
@@ -7,9 +10,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
import { SettingsId } from "@/modules/ui/components/settings-id";
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
import { getTranslate } from "@/tolgee/server";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteAccount } from "./components/DeleteAccount";
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
@@ -1,5 +1,5 @@
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import Loading from "@/modules/organization/settings/api-keys/loading";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
export default function LoadingPage() {
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
@@ -1,8 +1,8 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
const Loading = async () => {
const t = await getTranslate();
@@ -1,9 +1,9 @@
"use client";
import { getAccessFlags } from "@/lib/membership/utils";
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
import { useTranslate } from "@tolgee/react";
import { usePathname } from "next/navigation";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TOrganizationRole } from "@formbricks/types/memberships";
interface OrganizationSettingsNavbarProps {
@@ -22,7 +22,7 @@ export const OrganizationSettingsNavbar = ({
loading,
}: OrganizationSettingsNavbarProps) => {
const pathname = usePathname();
const { isMember } = getAccessFlags(membershipRole);
const { isMember, isOwner } = getAccessFlags(membershipRole);
const isPricingDisabled = isMember;
const { t } = useTranslate();
@@ -59,6 +59,7 @@ export const OrganizationSettingsNavbar = ({
label: t("common.api_keys"),
href: `/environments/${environmentId}/settings/api-keys`,
current: pathname?.includes("/api-keys"),
hidden: !isOwner,
},
];
@@ -1,8 +1,8 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
const Loading = async () => {
const t = await getTranslate();
@@ -1,4 +1,5 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { Button } from "@/modules/ui/components/button";
@@ -8,7 +9,6 @@ import { getTranslate } from "@/tolgee/server";
import { CheckIcon } from "lucide-react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
const Page = async (props) => {
const params = await props.params;
@@ -58,11 +58,6 @@ const Page = async (props) => {
comingSoon: false,
onRequest: false,
},
{
title: t("environments.settings.enterprise.ai"),
comingSoon: false,
onRequest: true,
},
{
title: t("environments.settings.enterprise.audit_logs"),
comingSoon: false,
@@ -97,7 +92,7 @@ const Page = async (props) => {
</PageHeader>
{isEnterpriseEdition ? (
<div>
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
<div className="space-y-4 p-8">
<div className="flex items-center gap-x-2">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
@@ -152,7 +147,7 @@ const Page = async (props) => {
</p>
</div>
</div>
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
<div className="p-8">
<h2 className="mr-2 inline-flex text-2xl font-bold text-slate-700">
{t("environments.settings.enterprise.enterprise_features")}
@@ -1,10 +1,10 @@
"use server";
import { deleteOrganization, updateOrganization } from "@/lib/organization/service";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
import { z } from "zod";
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/service";
import { ZId } from "@formbricks/types/common";
import { OperationNotAllowedError } from "@formbricks/types/errors";
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
@@ -1,96 +0,0 @@
"use client";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { updateOrganizationAIEnabledAction } from "@/modules/ee/insights/actions";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import { TOrganization } from "@formbricks/types/organizations";
interface AIToggleProps {
environmentId: string;
organization: TOrganization;
isOwnerOrManager: boolean;
}
export const AIToggle = ({ organization, isOwnerOrManager }: AIToggleProps) => {
const { t } = useTranslate();
const [isAIEnabled, setIsAIEnabled] = useState(organization.isAIEnabled);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleUpdateOrganization = async (data) => {
try {
setIsAIEnabled(data.enabled);
setIsSubmitting(true);
const updatedOrganizationResponse = await updateOrganizationAIEnabledAction({
organizationId: organization.id,
data: {
isAIEnabled: data.enabled,
},
});
if (updatedOrganizationResponse?.data) {
if (data.enabled) {
toast.success(t("environments.settings.general.formbricks_ai_enable_success_message"));
} else {
toast.success(t("environments.settings.general.formbricks_ai_disable_success_message"));
}
} else {
const errorMessage = getFormattedErrorMessage(updatedOrganizationResponse);
toast.error(errorMessage);
}
} catch (err) {
toast.error(`Error: ${err.message}`);
} finally {
setIsSubmitting(false);
if (typeof window !== "undefined") {
setTimeout(() => {
window.location.reload();
}, 500);
}
}
};
return (
<>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Label htmlFor="formbricks-ai-toggle" className="cursor-pointer">
{t("environments.settings.general.enable_formbricks_ai")}
</Label>
<Switch
id="formbricks-ai-toggle"
disabled={!isOwnerOrManager || isSubmitting}
checked={isAIEnabled}
onClick={(e) => {
e.stopPropagation();
handleUpdateOrganization({ enabled: !organization.isAIEnabled });
}}
/>
</div>
<div className="mt-3 text-xs text-slate-600">
{t("environments.settings.general.formbricks_ai_privacy_policy_text")}{" "}
<Link
className="underline"
href={"https://formbricks.com/privacy-policy"}
rel="noreferrer"
target="_blank">
{t("common.privacy_policy")}
</Link>
.
</div>
</div>
{!isOwnerOrManager && (
<Alert variant="warning" className="mt-4">
<AlertDescription>
{t("environments.settings.general.only_org_owner_can_perform_action")}
</AlertDescription>
</Alert>
)}
</>
);
};
@@ -1,6 +1,7 @@
"use client";
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
import { TOrganization } from "@formbricks/types/organizations";
type DeleteOrganizationProps = {
@@ -1,6 +1,7 @@
"use client";
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
import { getAccessFlags } from "@/lib/membership/utils";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
import { Button } from "@/modules/ui/components/button";
@@ -18,7 +19,6 @@ import { useTranslate } from "@tolgee/react";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TOrganizationRole } from "@formbricks/types/memberships";
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
@@ -1,9 +1,9 @@
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { getTranslate } from "@/tolgee/server";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
const Loading = async () => {
const t = await getTranslate();
@@ -1,17 +1,13 @@
import {
getIsMultiOrgEnabled,
getIsOrganizationAIReady,
getWhiteLabelPermission,
} from "@/modules/ee/license-check/lib/utils";
import { getUser } from "@/lib/user/service";
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
import { getTranslate } from "@/tolgee/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getUser } from "@formbricks/lib/user/service";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { TUser } from "@formbricks/types/user";
import Page from "./page";
vi.mock("@formbricks/lib/constants", () => ({
vi.mock("@/lib/constants", () => ({
IS_FORMBRICKS_CLOUD: false,
IS_PRODUCTION: false,
FB_LOGO_URL: "https://example.com/mock-logo.png",
@@ -33,12 +29,6 @@ vi.mock("@formbricks/lib/constants", () => ({
WEBAPP_URL: "mock-webapp-url",
SMTP_HOST: "mock-smtp-host",
SMTP_PORT: "mock-smtp-port",
AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name",
AI_AZURE_LLM_API_KEY: "mock-ai",
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id",
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name",
AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key",
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id",
}));
vi.mock("next-auth", () => ({
@@ -49,7 +39,7 @@ vi.mock("@/tolgee/server", () => ({
getTranslate: vi.fn(),
}));
vi.mock("@formbricks/lib/user/service", () => ({
vi.mock("@/lib/user/service", () => ({
getUser: vi.fn(),
}));
@@ -59,7 +49,6 @@ vi.mock("@/modules/environments/lib/utils", () => ({
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
getIsMultiOrgEnabled: vi.fn(),
getIsOrganizationAIReady: vi.fn(),
getWhiteLabelPermission: vi.fn(),
}));
@@ -80,11 +69,10 @@ describe("Page", () => {
vi.mocked(getUser).mockResolvedValue(mockUser);
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true);
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
});
it("renders the page with organization settings", async () => {
test("renders the page with organization settings", async () => {
const props = {
params: Promise.resolve({ environmentId: "env-123" }),
};
@@ -94,7 +82,7 @@ describe("Page", () => {
expect(result).toBeTruthy();
});
it("renders if session user id empty", async () => {
test("renders if session user id empty", async () => {
mockEnvironmentAuth.session.user.id = "";
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
@@ -108,7 +96,7 @@ describe("Page", () => {
expect(result).toBeTruthy();
});
it("handles getEnvironmentAuth error", async () => {
test("handles getEnvironmentAuth error", async () => {
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
const props = {
@@ -1,18 +1,13 @@
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
import {
getIsMultiOrgEnabled,
getIsOrganizationAIReady,
getWhiteLabelPermission,
} from "@/modules/ee/license-check/lib/utils";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
import { getUser } from "@/lib/user/service";
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
import { PageHeader } from "@/modules/ui/components/page-header";
import { SettingsId } from "@/modules/ui/components/settings-id";
import { getTranslate } from "@/tolgee/server";
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getUser } from "@formbricks/lib/user/service";
import { SettingsCard } from "../../components/SettingsCard";
import { DeleteOrganization } from "./components/DeleteOrganization";
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
@@ -35,8 +30,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
const isOwnerOrManager = isManager || isOwner;
const isOrganizationAIReady = await getIsOrganizationAIReady(organization.billing.plan);
return (
<PageContentWrapper>
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
@@ -56,17 +49,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
membershipRole={currentUserMembership?.role}
/>
</SettingsCard>
{isOrganizationAIReady && (
<SettingsCard
title={t("environments.settings.general.formbricks_ai")}
description={t("environments.settings.general.formbricks_ai_description")}>
<AIToggle
environmentId={params.environmentId}
organization={organization}
isOwnerOrManager={isOwnerOrManager}
/>
</SettingsCard>
)}
<EmailCustomizationSettings
organization={organization}
hasWhiteLabelPermission={hasWhiteLabelPermission}
@@ -1,8 +1,8 @@
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
import { getProjectByEnvironmentId } from "@/lib/project/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
const Layout = async (props) => {
const params = await props.params;
@@ -1,8 +1,8 @@
"use client";
import { cn } from "@/lib/cn";
import { Badge } from "@/modules/ui/components/badge";
import { useTranslate } from "@tolgee/react";
import { cn } from "@formbricks/lib/cn";
export const SettingsCard = ({
title,
@@ -25,7 +25,7 @@ export const SettingsCard = ({
return (
<div
className={cn(
"relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-xs",
"relative my-4 w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 text-left shadow-sm",
className
)}
id={title}>

Some files were not shown because too many files have changed in this diff Show More