Compare commits

..

2 Commits

Author SHA1 Message Date
pandeymangg 9c0306c27b Merge branch 'main' into fix/bulk-contacts 2025-04-18 15:03:11 +05:30
pandeymangg 62960686a1 fix: bulk contacts refactors 2025-04-08 11:32:00 +05:30
1182 changed files with 29428 additions and 41572 deletions
+6 -12
View File
@@ -120,10 +120,6 @@ IMPRINT_ADDRESS=
# TURNSTILE_SITE_KEY=
# TURNSTILE_SECRET_KEY=
# Google reCAPTCHA v3 keys
RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
# Configure Github Login
GITHUB_ID=
GITHUB_SECRET=
@@ -210,6 +206,12 @@ 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=
@@ -217,11 +219,3 @@ 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=
# Disable the user management from UI
# DISABLE_USER_MANAGEMENT
-30
View File
@@ -1,30 +0,0 @@
# Testing Instructions
When generating test files inside the "/app/web" path, follow these rules:
- You are an experienced senior software engineer
- 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. Do this only when the test file is created.
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
- When mocking data check if the properties added are part of the type of the object being mocked. Don't add properties that are not part of the type.
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 the "cleanup()" line 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.
- You don't need to mock @tolgee/react
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Build & Cache Web Binaries
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+2 -2
View File
@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
+2 -38
View File
@@ -12,13 +12,6 @@ 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:
@@ -30,10 +23,6 @@ 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
@@ -44,14 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
- name: Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:github
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
@@ -66,8 +48,6 @@ 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 }}
@@ -78,23 +58,7 @@ jobs:
helm-plugins: >
https://github.com/databus23/helm-diff,
https://github.com/jkroepke/helm-secrets
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-args: apply
helmfile-auto-init: "false"
helmfile-workdirectory: infra/formbricks-cloud-helm
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+2 -2
View File
@@ -46,11 +46,11 @@ jobs:
--health-retries=5
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
-1
View File
@@ -31,4 +31,3 @@ jobs:
- helm-chart-release
with:
VERSION: ${{ needs.docker-build.outputs.VERSION }}
ENVIRONMENT: "prod"
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
statuses: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
with:
egress-policy: audit
- name: fail if conditional jobs failed
+56
View File
@@ -0,0 +1,56 @@
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 }}
@@ -31,12 +31,12 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Set up Depot CLI
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
@@ -45,13 +45,13 @@ jobs:
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -82,6 +82,8 @@ 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
+6 -4
View File
@@ -38,12 +38,12 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- name: Get Release Tag
id: extract_release_tag
@@ -65,13 +65,13 @@ jobs:
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -102,6 +102,8 @@ 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 -1
View File
@@ -19,7 +19,7 @@ jobs:
contents: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+2 -2
View File
@@ -35,12 +35,12 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@@ -26,20 +26,13 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:github
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
+2 -2
View File
@@ -14,11 +14,11 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/dangerous-git-checkout
- name: Setup Node.js 20.x
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@@ -18,7 +18,7 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
-1
View File
@@ -72,4 +72,3 @@ 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 apps/web/locales
git add packages/lib/messages
fi
fi
+7 -7
View File
@@ -4,33 +4,33 @@
"patterns": ["./apps/web/**/*.ts?(x)"],
"projectId": 10304,
"pull": {
"path": "./apps/web/locales"
"path": "./packages/lib/messages"
},
"push": {
"files": [
{
"language": "en-US",
"path": "./apps/web/locales/en-US.json"
"path": "./packages/lib/messages/en-US.json"
},
{
"language": "de-DE",
"path": "./apps/web/locales/de-DE.json"
"path": "./packages/lib/messages/de-DE.json"
},
{
"language": "fr-FR",
"path": "./apps/web/locales/fr-FR.json"
"path": "./packages/lib/messages/fr-FR.json"
},
{
"language": "pt-BR",
"path": "./apps/web/locales/pt-BR.json"
"path": "./packages/lib/messages/pt-BR.json"
},
{
"language": "zh-Hant-TW",
"path": "./apps/web/locales/zh-Hant-TW.json"
"path": "./packages/lib/messages/zh-Hant-TW.json"
},
{
"language": "pt-PT",
"path": "./apps/web/locales/pt-PT.json"
"path": "./packages/lib/messages/pt-PT.json"
}
],
"forceMode": "OVERRIDE"
+6 -3
View File
@@ -1,10 +1,13 @@
{
"javascript.updateImportsOnFileMove.enabled": "always",
"github.copilot.chat.codeGeneration.instructions": [
{
"text": "When generating tests, always use vitest and use the `test` function instead of `it`."
}
],
"sonarlint.connectedMode.project": {
"connectionId": "formbricks",
"projectKey": "formbricks_formbricks"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.updateImportsOnFileMove.enabled": "always"
"typescript.tsdk": "node_modules/typescript/lib"
}
+1 -1
View File
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
Portions of this software are licensed as follows:
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
- All content that resides under the "packages/js/", "packages/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All content that resides under the "packages/js/", "packages/react-native/", "packages/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
+2
View File
@@ -0,0 +1,2 @@
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1
@@ -1,5 +1,5 @@
module.exports = {
extends: ["@formbricks/eslint-config/library.js"],
extends: ["@formbricks/eslint-config/react.js"],
parserOptions: {
project: "tsconfig.json",
tsconfigRootDir: __dirname,
+35
View File
@@ -0,0 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
View File
+35
View File
@@ -0,0 +1,35 @@
{
"expo": {
"android": {
"adaptiveIcon": {
"backgroundColor": "#ffffff",
"foregroundImage": "./assets/adaptive-icon.png"
}
},
"assetBundlePatterns": ["**/*"],
"icon": "./assets/icon.png",
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Take pictures for certain activities.",
"NSMicrophoneUsageDescription": "Need microphone access for recording videos.",
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities."
},
"supportsTablet": true
},
"jsEngine": "hermes",
"name": "react-native-demo",
"newArchEnabled": true,
"orientation": "portrait",
"slug": "react-native-demo",
"splash": {
"backgroundColor": "#ffffff",
"image": "./assets/splash.png",
"resizeMode": "contain"
},
"userInterfaceStyle": "light",
"version": "1.0.0",
"web": {
"favicon": "./assets/favicon.png"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

+6
View File
@@ -0,0 +1,6 @@
module.exports = function babel(api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};
+7
View File
@@ -0,0 +1,7 @@
import { registerRootComponent } from "expo";
import { LogBox } from "react-native";
import App from "./src/app";
registerRootComponent(App);
LogBox.ignoreAllLogs();
+21
View File
@@ -0,0 +1,21 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const path = require("node:path");
const { getDefaultConfig } = require("expo/metro-config");
// Find the workspace root, this can be replaced with `find-yarn-workspace-root`
const workspaceRoot = path.resolve(__dirname, "../..");
const projectRoot = __dirname;
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages, and in what order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, "node_modules"),
path.resolve(workspaceRoot, "node_modules"),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;
module.exports = config;
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@formbricks/demo-react-native",
"version": "1.0.0",
"main": "./index.js",
"scripts": {
"dev": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"clean": "rimraf .turbo node_modules .expo"
},
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/react-native": "workspace:*",
"@react-native-async-storage/async-storage": "2.1.0",
"expo": "52.0.28",
"expo-status-bar": "2.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
"react-native-webview": "13.12.5"
},
"devDependencies": {
"@babel/core": "7.26.0",
"@types/react": "18.3.18",
"typescript": "5.7.2"
},
"private": true
}
+117
View File
@@ -0,0 +1,117 @@
import { StatusBar } from "expo-status-bar";
import React, { type JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, {
logout,
setAttribute,
setAttributes,
setLanguage,
setUserId,
track,
} from "@formbricks/react-native";
LogBox.ignoreAllLogs();
export default function App(): JSX.Element {
if (!process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID) {
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
}
if (!process.env.EXPO_PUBLIC_APP_URL) {
throw new Error("EXPO_PUBLIC_APP_URL is required");
}
return (
<View style={styles.container}>
<Text>Formbricks React Native SDK Demo</Text>
<View
style={{
display: "flex",
flexDirection: "column",
gap: 10,
}}>
<Button
title="Trigger Code Action"
onPress={() => {
track("code").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error tracking event:", error);
});
}}
/>
<Button
title="Set User Id"
onPress={() => {
setUserId("random-user-id").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user id:", error);
});
}}
/>
<Button
title="Set User Attributess (multiple)"
onPress={() => {
setAttributes({
testAttr: "attr-test",
testAttr2: "attr-test-2",
testAttr3: "attr-test-3",
testAttr4: "attr-test-4",
}).catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Set User Attributes (single)"
onPress={() => {
setAttribute("testSingleAttr", "testSingleAttr").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Logout"
onPress={() => {
logout().catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error logging out:", error);
});
}}
/>
<Button
title="Set Language (de)"
onPress={() => {
setLanguage("de").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting language:", error);
});
}}
/>
</View>
<StatusBar style="auto" />
<Formbricks
appUrl={process.env.EXPO_PUBLIC_APP_URL as string}
environmentId={process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
+6
View File
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"strict": true
},
"extends": "expo/tsconfig.base"
}
-17
View File
@@ -1,20 +1,3 @@
module.exports = {
extends: ["@formbricks/eslint-config/legacy-next.js"],
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
overrides: [
{
files: ["locales/*.json"],
plugins: ["i18n-json"],
rules: {
"i18n-json/identical-keys": [
"error",
{
filePath: require("path").join(__dirname, "locales", "en-US.json"),
checkExtraKeys: false,
checkMissingKeys: true,
},
],
},
},
],
};
+1 -1
View File
@@ -50,4 +50,4 @@ uploads/
.sentryclirc
# SAML Preloaded Connections
saml-connection/
saml-connection/
+1 -7
View File
@@ -1,4 +1,4 @@
FROM node:22-alpine3.21 AS base
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
#
## step 1: Prune monorepo
@@ -84,12 +84,6 @@ RUN apk add --no-cache curl \
&& addgroup -S nextjs \
&& adduser -S -u 1001 -G nextjs nextjs
# In the runner stage
RUN apk update && \
apk upgrade && \
# This explicitly removes old package versions
rm -rf /var/cache/apk/*
WORKDIR /home/nextjs
# Ensure no write permissions are assigned to the copied resources
@@ -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";
@@ -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 right-5 top-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 "@/lib/utils/templates";
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
import { TProject } from "@formbricks/types/project";
import { TXMTemplate } from "@formbricks/types/templates";
@@ -1,13 +1,8 @@
import {
buildCTAQuestion,
buildNPSQuestion,
buildOpenTextQuestion,
buildRatingQuestion,
getDefaultEndingCard,
} from "@/app/lib/survey-builder";
import { getDefaultEndingCard } from "@/app/lib/templates";
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 => {
@@ -31,26 +26,35 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.nps_survey_name"),
questions: [
buildNPSQuestion({
headline: t("templates.nps_survey_question_1_headline"),
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: t("templates.nps_survey_question_1_headline") },
required: true,
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
upperLabel: t("templates.nps_survey_question_1_upper_label"),
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
isColorCodingEnabled: true,
t,
}),
buildOpenTextQuestion({
headline: t("templates.nps_survey_question_2_headline"),
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_2_headline") },
required: false,
inputType: "text",
t,
}),
buildOpenTextQuestion({
headline: t("templates.nps_survey_question_3_headline"),
charLimit: {
enabled: false,
},
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.nps_survey_question_3_headline") },
required: false,
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -63,8 +67,9 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.star_rating_survey_name"),
questions: [
buildRatingQuestion({
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -97,15 +102,16 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "number",
headline: t("templates.star_rating_survey_question_1_headline"),
headline: { default: t("templates.star_rating_survey_question_1_headline") },
required: true,
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
t,
}),
buildCTAQuestion({
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: t("templates.star_rating_survey_question_2_html"),
html: { default: t("templates.star_rating_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
id: createId(),
@@ -132,23 +138,25 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: t("templates.star_rating_survey_question_2_headline"),
headline: { default: t("templates.star_rating_survey_question_2_headline") },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: t("templates.star_rating_survey_question_2_button_label"),
buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
buttonExternal: true,
t,
}),
buildOpenTextQuestion({
},
{
id: reusableQuestionIds[2],
headline: t("templates.star_rating_survey_question_3_headline"),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.star_rating_survey_question_3_headline") },
required: true,
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"),
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") },
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -161,8 +169,9 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.csat_survey_name"),
questions: [
buildRatingQuestion({
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -195,14 +204,15 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: t("templates.csat_survey_question_1_headline"),
headline: { default: t("templates.csat_survey_question_1_headline") },
required: true,
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
upperLabel: t("templates.csat_survey_question_1_upper_label"),
t,
}),
buildOpenTextQuestion({
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{
id: createId(),
@@ -229,20 +239,25 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: t("templates.csat_survey_question_2_headline"),
headline: { default: t("templates.csat_survey_question_2_headline") },
required: false,
placeholder: t("templates.csat_survey_question_2_placeholder"),
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
inputType: "text",
t,
}),
buildOpenTextQuestion({
charLimit: {
enabled: false,
},
},
{
id: reusableQuestionIds[2],
headline: t("templates.csat_survey_question_3_headline"),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.csat_survey_question_3_headline") },
required: false,
placeholder: t("templates.csat_survey_question_3_placeholder"),
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -252,22 +267,28 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.cess_survey_name"),
questions: [
buildRatingQuestion({
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: t("templates.cess_survey_question_1_headline"),
headline: { default: t("templates.cess_survey_question_1_headline") },
required: true,
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"),
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") },
required: true,
placeholder: t("templates.cess_survey_question_2_placeholder"),
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -280,8 +301,9 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
...defaultSurvey,
name: t("templates.smileys_survey_name"),
questions: [
buildRatingQuestion({
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
@@ -314,15 +336,16 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: t("templates.smileys_survey_question_1_headline"),
headline: { default: t("templates.smileys_survey_question_1_headline") },
required: true,
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
t,
}),
buildCTAQuestion({
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: t("templates.smileys_survey_question_2_html"),
html: { default: t("templates.smileys_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
id: createId(),
@@ -349,23 +372,25 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
],
},
],
headline: t("templates.smileys_survey_question_2_headline"),
headline: { default: t("templates.smileys_survey_question_2_headline") },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: t("templates.smileys_survey_question_2_button_label"),
buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
buttonExternal: true,
t,
}),
buildOpenTextQuestion({
},
{
id: reusableQuestionIds[2],
headline: t("templates.smileys_survey_question_3_headline"),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.smileys_survey_question_3_headline") },
required: true,
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"),
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") },
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -375,26 +400,37 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
...getXMSurveyDefault(t),
name: t("templates.enps_survey_name"),
questions: [
buildNPSQuestion({
headline: t("templates.enps_survey_question_1_headline"),
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: t("templates.enps_survey_question_1_headline"),
},
required: false,
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
upperLabel: t("templates.enps_survey_question_1_upper_label"),
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
isColorCodingEnabled: true,
t,
}),
buildOpenTextQuestion({
headline: t("templates.enps_survey_question_2_headline"),
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_2_headline") },
required: false,
inputType: "text",
t,
}),
buildOpenTextQuestion({
headline: t("templates.enps_survey_question_3_headline"),
charLimit: {
enabled: false,
},
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: t("templates.enps_survey_question_3_headline") },
required: false,
inputType: "text",
t,
}),
charLimit: {
enabled: false,
},
},
],
};
};
@@ -1,7 +1,4 @@
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";
@@ -10,6 +7,9 @@ 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 right-5 top-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,8 +2,6 @@
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 {
@@ -26,6 +24,8 @@ 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";
@@ -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, test, 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 { 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("@/lib/constants", () => ({
vi.mock("@formbricks/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("@/lib/organization/auth", () => ({
vi.mock("@formbricks/lib/organization/auth", () => ({
canUserAccessOrganization: vi.fn(),
}));
vi.mock("@/lib/organization/service", () => ({
vi.mock("@formbricks/lib/organization/service", () => ({
getOrganization: vi.fn(),
}));
vi.mock("@/lib/user/service", () => ({
vi.mock("@formbricks/lib/user/service", () => ({
getUser: vi.fn(),
}));
vi.mock("@/tolgee/server", () => ({
@@ -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,5 +1,4 @@
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";
@@ -7,6 +6,7 @@ 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 right-5 top-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,5 +1,4 @@
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";
@@ -7,6 +6,7 @@ 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 right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -2,7 +2,6 @@
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";
@@ -27,6 +26,7 @@ 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,
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
alt="Logo"
width={256}
height={56}
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">{t("common.preview")}</p>
@@ -1,7 +1,5 @@
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";
@@ -10,6 +8,8 @@ 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 right-5 top-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, test, vi } from "vitest";
import { getEnvironment } from "@formbricks/lib/environment/service";
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("@/lib/environment/service", () => ({
vi.mock("@formbricks/lib/environment/service", () => ({
getEnvironment: vi.fn(),
}));
vi.mock("next/navigation", () => ({
@@ -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 "@/lib/cn";
import { cn } from "@formbricks/lib/cn";
export const LoadingCard = ({
title,
@@ -1,8 +1,5 @@
"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 {
@@ -11,6 +8,9 @@ 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,9 +1,7 @@
"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";
@@ -12,6 +10,8 @@ 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";
@@ -1,5 +1,5 @@
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
import { timeSince } from "@/lib/time";
import { timeSince } from "@formbricks/lib/time";
import { TActionClass } from "@formbricks/types/action-classes";
import { TUserLocale } from "@formbricks/types/user";
@@ -23,7 +23,7 @@ export const ActionClassDataRow = ({
</div>
</div>
</div>
<div className="col-span-2 my-auto text-center text-sm whitespace-nowrap text-slate-500">
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
{timeSince(actionClass.createdAt.toString(), locale)}
</div>
<div className="text-center"></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,17 +1,5 @@
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";
@@ -19,6 +7,18 @@ 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,9 +4,6 @@ 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";
@@ -48,6 +45,9 @@ 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";
@@ -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;
@@ -1,7 +1,6 @@
"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";
@@ -10,6 +9,7 @@ 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-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
@@ -1,6 +1,5 @@
"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 {
@@ -10,6 +9,7 @@ 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,8 +4,6 @@ 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";
@@ -25,6 +23,8 @@ 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,
@@ -1,151 +0,0 @@
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,7 +5,6 @@ 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";
@@ -14,6 +13,7 @@ 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) => (
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
{tableHeaders.map((header, idx) => (
<div key={idx} className={`col-span-2 hidden text-center sm:block`}>
{t(header)}
</div>
))}
</div>
{integrationData.map((data, index) => (
<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"
<div
key={index}
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg 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>
</button>
</div>
))}
</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,9 +8,7 @@ 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";
@@ -23,6 +21,8 @@ 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-x-hidden overflow-y-auto rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden 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">
@@ -1,162 +0,0 @@
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,7 +1,6 @@
"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,6 +9,7 @@ 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,10 +36,11 @@ export const ManageIntegration = ({
}: ManageIntegrationProps) => {
const { t } = useTranslate();
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
let integrationArray: TIntegrationGoogleSheetsConfigData[] = [];
if (googleSheetIntegration?.config.data) {
integrationArray = googleSheetIntegration.config.data;
}
const integrationArray = googleSheetIntegration
? googleSheetIntegration.config.data
? googleSheetIntegration.config.data
: []
: [];
const [isDeleting, setisDeleting] = useState(false);
const handleDeleteIntegration = async () => {
@@ -111,9 +112,9 @@ export const ManageIntegration = ({
{integrationArray &&
integrationArray.map((data, index) => {
return (
<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"
<div
key={index}
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
onClick={() => {
editIntegration(index);
}}>
@@ -123,7 +124,7 @@ export const ManageIntegration = ({
<div className="col-span-2 text-center">
{timeSince(data.createdAt.toString(), locale)}
</div>
</button>
</div>
);
})}
</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,9 +7,6 @@ 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";
@@ -21,6 +18,9 @@ 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,
@@ -1,91 +0,0 @@
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,7 +1,6 @@
"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";
@@ -11,6 +10,7 @@ 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);
let integrationArray: TIntegrationNotionConfigData[] = [];
if (notionIntegration?.config.data) {
integrationArray = notionIntegration.config.data;
}
const integrationArray = notionIntegration
? notionIntegration.config.data
? notionIntegration.config.data
: []
: [];
const handleDeleteIntegration = async () => {
setisDeleting(true);
@@ -121,9 +121,9 @@ export const ManageIntegration = ({
{integrationArray &&
integrationArray.map((data, index) => {
return (
<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"
<div
key={index}
className="m-2 grid h-16 cursor-pointer grid-cols-6 content-center rounded-lg 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>
</button>
</div>
);
})}
</div>
@@ -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 {
NOTION_AUTH_URL,
NOTION_OAUTH_CLIENT_ID,
NOTION_OAUTH_CLIENT_SECRET,
NOTION_REDIRECT_URI,
WEBAPP_URL,
} 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 {
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";
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
const Page = async (props) => {
@@ -9,7 +9,6 @@ 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";
@@ -17,6 +16,7 @@ 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,8 +2,6 @@
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";
@@ -17,6 +15,8 @@ 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,
@@ -1,158 +0,0 @@
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));
});
});

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