mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 03:03:25 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7540c64fdf |
+9
-14
@@ -120,10 +120,6 @@ IMPRINT_ADDRESS=
|
|||||||
# TURNSTILE_SITE_KEY=
|
# TURNSTILE_SITE_KEY=
|
||||||
# TURNSTILE_SECRET_KEY=
|
# TURNSTILE_SECRET_KEY=
|
||||||
|
|
||||||
# Google reCAPTCHA v3 keys
|
|
||||||
RECAPTCHA_SITE_KEY=
|
|
||||||
RECAPTCHA_SECRET_KEY=
|
|
||||||
|
|
||||||
# Configure Github Login
|
# Configure Github Login
|
||||||
GITHUB_ID=
|
GITHUB_ID=
|
||||||
GITHUB_SECRET=
|
GITHUB_SECRET=
|
||||||
@@ -159,8 +155,9 @@ STRIPE_SECRET_KEY=
|
|||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
# Configure Formbricks usage within Formbricks
|
# Configure Formbricks usage within Formbricks
|
||||||
FORMBRICKS_API_HOST=
|
NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
||||||
FORMBRICKS_ENVIRONMENT_ID=
|
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
||||||
|
|
||||||
# Oauth credentials for Google sheet integration
|
# Oauth credentials for Google sheet integration
|
||||||
GOOGLE_SHEETS_CLIENT_ID=
|
GOOGLE_SHEETS_CLIENT_ID=
|
||||||
@@ -210,6 +207,12 @@ UNKEY_ROOT_KEY=
|
|||||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||||
# CUSTOM_CACHE_DISABLED=1
|
# 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_APP_ID=
|
||||||
# INTERCOM_SECRET_KEY=
|
# INTERCOM_SECRET_KEY=
|
||||||
|
|
||||||
@@ -217,11 +220,3 @@ UNKEY_ROOT_KEY=
|
|||||||
# PROMETHEUS_ENABLED=
|
# PROMETHEUS_ENABLED=
|
||||||
# PROMETHEUS_EXPORTER_PORT=
|
# 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
|
|
||||||
@@ -8,14 +8,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: "0"
|
default: "0"
|
||||||
|
|
||||||
inputs:
|
|
||||||
turbo_token:
|
|
||||||
description: "Turborepo token"
|
|
||||||
required: false
|
|
||||||
turbo_team:
|
|
||||||
description: "Turborepo team"
|
|
||||||
required: false
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -70,8 +62,6 @@ runs:
|
|||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
pnpm build --filter=@formbricks/web...
|
pnpm build --filter=@formbricks/web...
|
||||||
|
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ inputs.turbo_token }}
|
|
||||||
TURBO_TEAM: ${{ inputs.turbo_team }}
|
|
||||||
|
|||||||
@@ -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:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Formbricks-web
|
name: Build Formbricks-web
|
||||||
@@ -13,11 +13,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Build & Cache Web Binaries
|
- name: Build & Cache Web Binaries
|
||||||
@@ -25,5 +25,3 @@ jobs:
|
|||||||
id: cache-build-web
|
id: cache-build-web
|
||||||
with:
|
with:
|
||||||
e2e_testing_mode: "0"
|
e2e_testing_mode: "0"
|
||||||
turbo_token: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
turbo_team: ${{ vars.TURBO_TEAM }}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
|
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||||
|
|||||||
@@ -12,13 +12,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
ENVIRONMENT:
|
|
||||||
description: 'The environment to deploy to'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- stage
|
|
||||||
- prod
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
VERSION:
|
||||||
@@ -30,10 +23,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
ENVIRONMENT:
|
|
||||||
description: 'The environment to deploy to'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -44,14 +33,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- 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
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||||
@@ -66,8 +48,6 @@ jobs:
|
|||||||
AWS_REGION: eu-central-1
|
AWS_REGION: eu-central-1
|
||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
- 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:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
@@ -78,23 +58,7 @@ jobs:
|
|||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
helmfile-args: apply -l environment=prod
|
helmfile-args: apply
|
||||||
helmfile-auto-init: "false"
|
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
|
||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
|
||||||
name: Deploy Formbricks Cloud Stage
|
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
|
|
||||||
env:
|
|
||||||
VERSION: ${{ inputs.VERSION }}
|
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
|
||||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
|
||||||
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
|
||||||
with:
|
|
||||||
helm-plugins: >
|
|
||||||
https://github.com/databus23/helm-diff,
|
|
||||||
https://github.com/jkroepke/helm-secrets
|
|
||||||
helmfile-args: apply -l environment=stage
|
|
||||||
helmfile-auto-init: "false"
|
helmfile-auto-init: "false"
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-docker-build:
|
validate-docker-build:
|
||||||
name: Validate Docker Build
|
name: Validate Docker Build
|
||||||
@@ -40,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TELEMETRY_DISABLED: 1
|
TELEMETRY_DISABLED: 1
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -46,11 +44,11 @@ jobs:
|
|||||||
--health-retries=5
|
--health-retries=5
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
|
|||||||
@@ -31,4 +31,3 @@ jobs:
|
|||||||
- helm-chart-release
|
- helm-chart-release
|
||||||
with:
|
with:
|
||||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||||
ENVIRONMENT: "prod"
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
|
|||||||
@@ -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:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
- name: Set up Depot CLI
|
||||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||||
@@ -45,13 +45,13 @@ jobs:
|
|||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
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
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -82,6 +82,8 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
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.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
|
|
||||||
- name: Get Release Tag
|
- name: Get Release Tag
|
||||||
id: extract_release_tag
|
id: extract_release_tag
|
||||||
@@ -65,13 +65,13 @@ jobs:
|
|||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
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
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -102,6 +102,8 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
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.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request_target:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
merge_group:
|
merge_group:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
+1
-8
@@ -26,20 +26,13 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
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
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -14,11 +14,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
if: github.event.action == 'opened'
|
if: github.event.action == 'opened'
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- 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:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -72,4 +72,3 @@ infra/terraform/.terraform/
|
|||||||
# IntelliJ IDEA
|
# IntelliJ IDEA
|
||||||
/.idea/
|
/.idea/
|
||||||
/*.iml
|
/*.iml
|
||||||
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
|
|
||||||
|
|||||||
+1
-1
@@ -16,6 +16,6 @@ if [ -f branch.json ]; then
|
|||||||
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
|
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
|
||||||
else
|
else
|
||||||
pnpm run tolgee-pull
|
pnpm run tolgee-pull
|
||||||
git add apps/web/locales
|
git add packages/lib/messages
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
+7
-7
@@ -4,33 +4,33 @@
|
|||||||
"patterns": ["./apps/web/**/*.ts?(x)"],
|
"patterns": ["./apps/web/**/*.ts?(x)"],
|
||||||
"projectId": 10304,
|
"projectId": 10304,
|
||||||
"pull": {
|
"pull": {
|
||||||
"path": "./apps/web/locales"
|
"path": "./packages/lib/messages"
|
||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"language": "en-US",
|
"language": "en-US",
|
||||||
"path": "./apps/web/locales/en-US.json"
|
"path": "./packages/lib/messages/en-US.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "de-DE",
|
"language": "de-DE",
|
||||||
"path": "./apps/web/locales/de-DE.json"
|
"path": "./packages/lib/messages/de-DE.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "fr-FR",
|
"language": "fr-FR",
|
||||||
"path": "./apps/web/locales/fr-FR.json"
|
"path": "./packages/lib/messages/fr-FR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-BR",
|
"language": "pt-BR",
|
||||||
"path": "./apps/web/locales/pt-BR.json"
|
"path": "./packages/lib/messages/pt-BR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "zh-Hant-TW",
|
"language": "zh-Hant-TW",
|
||||||
"path": "./apps/web/locales/zh-Hant-TW.json"
|
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-PT",
|
"language": "pt-PT",
|
||||||
"path": "./apps/web/locales/pt-PT.json"
|
"path": "./packages/lib/messages/pt-PT.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"forceMode": "OVERRIDE"
|
"forceMode": "OVERRIDE"
|
||||||
|
|||||||
Vendored
+1
-7
@@ -1,10 +1,4 @@
|
|||||||
{
|
{
|
||||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
|
||||||
"sonarlint.connectedMode.project": {
|
|
||||||
"connectionId": "formbricks",
|
|
||||||
"projectKey": "formbricks_formbricks"
|
|
||||||
},
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib",
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
|
|||||||
Portions of this software are licensed as follows:
|
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 "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.
|
- 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.
|
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
|
||||||
|
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["@formbricks/eslint-config/react.js"],
|
||||||
|
parserOptions: {
|
||||||
|
project: "tsconfig.json",
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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 |
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = function babel(api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: ["babel-preset-expo"],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { registerRootComponent } from "expo";
|
||||||
|
import { LogBox } from "react-native";
|
||||||
|
import App from "./src/app";
|
||||||
|
|
||||||
|
registerRootComponent(App);
|
||||||
|
|
||||||
|
LogBox.ignoreAllLogs();
|
||||||
@@ -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;
|
||||||
@@ -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.78.2",
|
||||||
|
"react-native-webview": "13.12.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "7.26.0",
|
||||||
|
"@types/react": "18.3.18",
|
||||||
|
"typescript": "5.7.2"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"extends": "expo/tsconfig.base"
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ const secondaryNavigation = [
|
|||||||
|
|
||||||
export function Sidebar(): React.JSX.Element {
|
export function Sidebar(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="flex grow flex-col overflow-y-auto bg-cyan-700 pt-5 pb-4">
|
<div className="flex grow flex-col overflow-y-auto bg-cyan-700 pb-4 pt-5">
|
||||||
<nav
|
<nav
|
||||||
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
|
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
|
||||||
aria-label="Sidebar">
|
aria-label="Sidebar">
|
||||||
@@ -38,7 +38,7 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
|
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
|
||||||
"group flex items-center rounded-md px-2 py-2 text-sm leading-6 font-medium"
|
"group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6"
|
||||||
)}
|
)}
|
||||||
aria-current={item.current ? "page" : undefined}>
|
aria-current={item.current ? "page" : undefined}>
|
||||||
<item.icon className="mr-4 h-6 w-6 shrink-0 text-cyan-200" aria-hidden="true" />
|
<item.icon className="mr-4 h-6 w-6 shrink-0 text-cyan-200" aria-hidden="true" />
|
||||||
@@ -52,7 +52,7 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
<a
|
<a
|
||||||
key={item.name}
|
key={item.name}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="group flex items-center rounded-md px-2 py-2 text-sm leading-6 font-medium text-cyan-100 hover:bg-cyan-600 hover:text-white">
|
className="group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6 text-cyan-100 hover:bg-cyan-600 hover:text-white">
|
||||||
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
|
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
|
||||||
{item.name}
|
{item.name}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
"lucide-react": "0.486.0",
|
"lucide-react": "0.486.0",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"postcss": "8.5.3",
|
"postcss": "8.5.3",
|
||||||
"react": "19.1.0",
|
"react": "19.0.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.0.0",
|
||||||
"tailwindcss": "4.1.3"
|
"tailwindcss": "4.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ export default function AppPage(): React.JSX.Element {
|
|||||||
<p className="text-slate-700 dark:text-slate-300">
|
<p className="text-slate-700 dark:text-slate-300">
|
||||||
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
||||||
</p>
|
</p>
|
||||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded-xs" priority />
|
<Image src={fbsetup} alt="fb setup" className="rounded-xs mt-4" priority />
|
||||||
|
|
||||||
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
|
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
|
||||||
<p className="mb-1 sm:mr-2 sm:mb-0">You're connected with env:</p>
|
<p className="mb-1 sm:mb-0 sm:mr-2">You're connected with env:</p>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<strong className="w-32 truncate sm:w-auto">
|
<strong className="w-32 truncate sm:w-auto">
|
||||||
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
"@storybook/react": "8.6.12",
|
"@storybook/react": "8.6.12",
|
||||||
"@storybook/react-vite": "8.6.12",
|
"@storybook/react-vite": "8.6.12",
|
||||||
"@storybook/test": "8.6.12",
|
"@storybook/test": "8.6.12",
|
||||||
"@typescript-eslint/eslint-plugin": "8.29.1",
|
"@typescript-eslint/eslint-plugin": "8.29.0",
|
||||||
"@typescript-eslint/parser": "8.29.1",
|
"@typescript-eslint/parser": "8.29.0",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.3.4",
|
||||||
"esbuild": "0.25.2",
|
"esbuild": "0.25.2",
|
||||||
"eslint-plugin-storybook": "0.12.0",
|
"eslint-plugin-storybook": "0.12.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"storybook": "8.6.12",
|
"storybook": "8.6.12",
|
||||||
"tsup": "8.4.0",
|
"tsup": "8.4.0",
|
||||||
"vite": "6.2.5"
|
"vite": "6.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
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
@@ -50,4 +50,4 @@ uploads/
|
|||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
# SAML Preloaded Connections
|
# SAML Preloaded Connections
|
||||||
saml-connection/
|
saml-connection/
|
||||||
+26
-33
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-alpine3.21 AS base
|
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
## step 1: Prune monorepo
|
## step 1: Prune monorepo
|
||||||
@@ -81,20 +81,13 @@ RUN corepack enable
|
|||||||
RUN apk add --no-cache curl \
|
RUN apk add --no-cache curl \
|
||||||
&& apk add --no-cache supercronic \
|
&& apk add --no-cache supercronic \
|
||||||
# && addgroup --system --gid 1001 nodejs \
|
# && addgroup --system --gid 1001 nodejs \
|
||||||
&& addgroup -S nextjs \
|
&& adduser --system --uid 1001 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
|
WORKDIR /home/nextjs
|
||||||
|
|
||||||
# Ensure no write permissions are assigned to the copied resources
|
# Ensure no write permissions are assigned to the copied resources
|
||||||
COPY --from=installer /app/apps/web/.next/standalone ./
|
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
||||||
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
RUN chmod -R 755 ./
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||||
RUN chmod 644 ./next.config.mjs
|
RUN chmod 644 ./next.config.mjs
|
||||||
@@ -102,38 +95,38 @@ RUN chmod 644 ./next.config.mjs
|
|||||||
COPY --from=installer /app/apps/web/package.json .
|
COPY --from=installer /app/apps/web/package.json .
|
||||||
RUN chmod 644 ./package.json
|
RUN chmod 644 ./package.json
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
|
RUN chmod -R 755 ./apps/web/.next/static
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/public ./apps/web/public
|
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||||
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
RUN chmod -R 755 ./apps/web/public
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||||
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
RUN chmod 644 ./packages/database/schema.prisma
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
||||||
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
RUN chmod 644 ./packages/database/package.json
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
RUN chmod -R 755 ./packages/database/migration
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/src ./packages/database/src
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
RUN chmod -R 755 ./packages/database/src
|
||||||
|
|
||||||
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
RUN chmod -R 755 ./packages/database/node_modules
|
||||||
|
|
||||||
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||||
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
RUN chmod -R 755 ./node_modules/@prisma/client
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
RUN chmod -R 755 ./node_modules/.prisma
|
||||||
|
|
||||||
COPY --from=installer /prisma_version.txt .
|
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
||||||
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
RUN chmod 644 ./prisma_version.txt
|
||||||
|
|
||||||
COPY /docker/cronjobs /app/docker/cronjobs
|
COPY /docker/cronjobs /app/docker/cronjobs
|
||||||
RUN chmod -R 755 /app/docker/cronjobs
|
RUN chmod -R 755 /app/docker/cronjobs
|
||||||
|
|||||||
+1
-1
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
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 { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
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 {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/environments/${environment.id}`}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
|
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
|
|||||||
+136
-100
@@ -1,13 +1,8 @@
|
|||||||
import {
|
import { getDefaultEndingCard } from "@/app/lib/templates";
|
||||||
buildCTAQuestion,
|
|
||||||
buildNPSQuestion,
|
|
||||||
buildOpenTextQuestion,
|
|
||||||
buildRatingQuestion,
|
|
||||||
getDefaultEndingCard,
|
|
||||||
} from "@/app/lib/survey-builder";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { TFnType } from "@tolgee/react";
|
import { TFnType } from "@tolgee/react";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
||||||
@@ -31,26 +26,35 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.nps_survey_name"),
|
name: t("templates.nps_survey_name"),
|
||||||
questions: [
|
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,
|
required: true,
|
||||||
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.nps_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
id: createId(),
|
||||||
headline: t("templates.nps_survey_question_2_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.nps_survey_question_2_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
headline: t("templates.nps_survey_question_3_headline"),
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.nps_survey_question_3_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -63,8 +67,9 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.star_rating_survey_name"),
|
name: t("templates.star_rating_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -97,15 +102,16 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: t("templates.star_rating_survey_question_1_headline"),
|
headline: { default: t("templates.star_rating_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildCTAQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
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: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
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,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
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,
|
buttonExternal: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
|
||||||
id: reusableQuestionIds[2],
|
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,
|
required: true,
|
||||||
subheader: t("templates.star_rating_survey_question_3_subheader"),
|
subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
|
||||||
buttonLabel: t("templates.star_rating_survey_question_3_button_label"),
|
buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
|
||||||
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -161,8 +169,9 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.csat_survey_name"),
|
name: t("templates.csat_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -195,14 +204,15 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: t("templates.csat_survey_question_1_headline"),
|
headline: { default: t("templates.csat_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.csat_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildOpenTextQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
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,
|
required: false,
|
||||||
placeholder: t("templates.csat_survey_question_2_placeholder"),
|
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
id: reusableQuestionIds[2],
|
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,
|
required: false,
|
||||||
placeholder: t("templates.csat_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -252,22 +267,28 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.cess_survey_name"),
|
name: t("templates.cess_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: t("templates.cess_survey_question_1_headline"),
|
headline: { default: t("templates.cess_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.cess_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildOpenTextQuestion({
|
{
|
||||||
headline: t("templates.cess_survey_question_2_headline"),
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.cess_survey_question_2_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: t("templates.cess_survey_question_2_placeholder"),
|
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -280,8 +301,9 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.smileys_survey_name"),
|
name: t("templates.smileys_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -314,15 +336,16 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: t("templates.smileys_survey_question_1_headline"),
|
headline: { default: t("templates.smileys_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildCTAQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
html: t("templates.smileys_survey_question_2_html"),
|
html: { default: t("templates.smileys_survey_question_2_html") },
|
||||||
|
type: TSurveyQuestionTypeEnum.CTA,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
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,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
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,
|
buttonExternal: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
|
||||||
id: reusableQuestionIds[2],
|
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,
|
required: true,
|
||||||
subheader: t("templates.smileys_survey_question_3_subheader"),
|
subheader: { default: t("templates.smileys_survey_question_3_subheader") },
|
||||||
buttonLabel: t("templates.smileys_survey_question_3_button_label"),
|
buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
|
||||||
placeholder: t("templates.smileys_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -375,26 +400,37 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.enps_survey_name"),
|
name: t("templates.enps_survey_name"),
|
||||||
questions: [
|
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,
|
required: false,
|
||||||
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.enps_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
id: createId(),
|
||||||
headline: t("templates.enps_survey_question_2_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.enps_survey_question_2_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
headline: t("templates.enps_survey_question_3_headline"),
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.enps_survey_question_3_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
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 { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +7,9 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
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 {
|
interface XMTemplatePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||||
{projects.length >= 2 && (
|
{projects.length >= 2 && (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/environments/${environment.id}/surveys`}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { teamCache } from "@/lib/cache/team";
|
import { teamCache } from "@/lib/cache/team";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
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 { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
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 { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +24,8 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
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 { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export const LandingSidebar = ({
|
|||||||
{/* Dropdown Items */}
|
{/* Dropdown Items */}
|
||||||
|
|
||||||
{dropdownNavigation.map((link) => (
|
{dropdownNavigation.map((link) => (
|
||||||
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center">
|
<Link href={link.href} target={link.target} className="flex w-full items-center">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||||
{link.label}
|
{link.label}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getEnvironments } from "@/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
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 LandingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
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 { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { notFound, redirect } from "next/navigation";
|
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 Page = async (props) => {
|
||||||
const params = await props.params;
|
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 "@testing-library/jest-dom/vitest";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||||
|
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import ProjectOnboardingLayout from "./layout";
|
import ProjectOnboardingLayout from "./layout";
|
||||||
|
|
||||||
// Mock all the modules and functions that this layout uses:
|
// Mock all the modules and functions that this layout uses:
|
||||||
|
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@formbricks/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
POSTHOG_HOST: "mock-posthog-host",
|
||||||
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
|
|||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/auth", () => ({
|
vi.mock("@formbricks/lib/organization/auth", () => ({
|
||||||
canUserAccessOrganization: vi.fn(),
|
canUserAccessOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/service", () => ({
|
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||||
getOrganization: vi.fn(),
|
getOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/user/service", () => ({
|
vi.mock("@formbricks/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/tolgee/server", () => ({
|
vi.mock("@/tolgee/server", () => ({
|
||||||
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("redirects to /auth/login if there is no session", async () => {
|
it("redirects to /auth/login if there is no session", async () => {
|
||||||
// Mock no session
|
// Mock no session
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
expect(layoutElement).toBeUndefined();
|
expect(layoutElement).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws an error if user does not exist", async () => {
|
it("throws an error if user does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
user: { id: "user-123" },
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.user_not_found");
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws AuthorizationError if user cannot access organization", async () => {
|
it("throws AuthorizationError if user cannot access organization", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
||||||
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.not_authorized");
|
).rejects.toThrow("common.not_authorized");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws an error if organization does not exist", async () => {
|
it("throws an error if organization does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
||||||
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.organization_not_found");
|
).rejects.toThrow("common.organization_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
||||||
// Provide valid data
|
// Provide valid data
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
import { 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 { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
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";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const ProjectOnboardingLayout = async (props) => {
|
const ProjectOnboardingLayout = async (props) => {
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,4 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
interface ChannelPageProps {
|
interface ChannelPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
+4
-4
@@ -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 { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
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 OnboardingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,4 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
interface ModePageProps {
|
interface ModePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||||
import { previewSurvey } from "@/app/lib/templates";
|
import { previewSurvey } from "@/app/lib/templates";
|
||||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
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 { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||||
import {
|
import {
|
||||||
TProjectConfigChannel,
|
TProjectConfigChannel,
|
||||||
TProjectConfigIndustry,
|
TProjectConfigIndustry,
|
||||||
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={256}
|
width={256}
|
||||||
height={56}
|
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>
|
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,5 @@
|
|||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
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 { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +8,8 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
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";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
|
|
||||||
interface ProjectSettingsPageProps {
|
interface ProjectSettingsPageProps {
|
||||||
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<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"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,120 +1,191 @@
|
|||||||
import { getEnvironment } from "@/lib/environment/service";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { getServerSession } from "next-auth";
|
||||||
import { Session } from "next-auth";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import React from "react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||||
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import SurveyEditorEnvironmentLayout from "./layout";
|
import SurveyEditorEnvironmentLayout from "./layout";
|
||||||
|
|
||||||
// Mock sub-components to render identifiable elements
|
// mock all dependencies
|
||||||
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
|
|
||||||
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
|
vi.mock("@formbricks/lib/constants", () => ({
|
||||||
<div data-testid="EnvironmentIdBaseLayout">
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
{environmentId}
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
{children}
|
POSTHOG_HOST: "mock-posthog-host",
|
||||||
</div>
|
IS_POSTHOG_CONFIGURED: true,
|
||||||
),
|
ENCRYPTION_KEY: "mock-encryption-key",
|
||||||
}));
|
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||||
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
GITHUB_ID: "mock-github-id",
|
||||||
DevEnvironmentBanner: ({ environment }: any) => (
|
GITHUB_SECRET: "test-githubID",
|
||||||
<div data-testid="DevEnvironmentBanner">{environment.id}</div>
|
GOOGLE_CLIENT_ID: "test-google-client-id",
|
||||||
),
|
GOOGLE_CLIENT_SECRET: "test-google-client-secret",
|
||||||
|
AZUREAD_CLIENT_ID: "test-azuread-client-id",
|
||||||
|
AZUREAD_CLIENT_SECRET: "test-azure",
|
||||||
|
AZUREAD_TENANT_ID: "test-azuread-tenant-id",
|
||||||
|
OIDC_DISPLAY_NAME: "test-oidc-display-name",
|
||||||
|
OIDC_CLIENT_ID: "test-oidc-client-id",
|
||||||
|
OIDC_ISSUER: "test-oidc-issuer",
|
||||||
|
OIDC_CLIENT_SECRET: "test-oidc-client-secret",
|
||||||
|
OIDC_SIGNING_ALGORITHM: "test-oidc-signing-algorithm",
|
||||||
|
WEBAPP_URL: "test-webapp-url",
|
||||||
|
IS_PRODUCTION: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mocks for dependencies
|
vi.mock("next-auth", () => ({
|
||||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
getServerSession: vi.fn(),
|
||||||
environmentIdLayoutChecks: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@/lib/environment/service", () => ({
|
|
||||||
getEnvironment: vi.fn(),
|
|
||||||
}));
|
}));
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
vi.mock("@formbricks/lib/environment/auth", () => ({
|
||||||
|
hasUserEnvironmentAccess: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@formbricks/lib/environment/service", () => ({
|
||||||
|
getEnvironment: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||||
|
getOrganizationByEnvironmentId: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@formbricks/lib/user/service", () => ({
|
||||||
|
getUser: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@/tolgee/server", () => ({
|
||||||
|
getTranslate: vi.fn(() => {
|
||||||
|
return (key: string) => key; // trivial translator returning the key
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// mock child components rendered by the layout:
|
||||||
|
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
||||||
|
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({
|
||||||
|
PosthogIdentify: () => <div data-testid="posthog-identify" />,
|
||||||
|
}));
|
||||||
|
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
||||||
|
ToasterClient: () => <div data-testid="mock-toaster" />,
|
||||||
|
}));
|
||||||
|
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
||||||
|
DevEnvironmentBanner: ({ environment }: { environment: TEnvironment }) => (
|
||||||
|
<div data-testid="dev-environment-banner">{environment?.id || "no-env"}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||||
|
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<div data-testid="mock-response-filter-provider">{children}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("SurveyEditorEnvironmentLayout", () => {
|
describe("SurveyEditorEnvironmentLayout", () => {
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders successfully when environment is found", async () => {
|
it("redirects to /auth/login if there is no session", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
// Mock no session
|
||||||
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||||
session: { user: { id: "user1" } } as Session,
|
|
||||||
user: { id: "user1", email: "user1@example.com" } as TUser,
|
|
||||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
|
||||||
});
|
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment);
|
|
||||||
|
|
||||||
const result = await SurveyEditorEnvironmentLayout({
|
const layoutElement = await SurveyEditorEnvironmentLayout({
|
||||||
params: Promise.resolve({ environmentId: "env1" }),
|
params: { environmentId: "env-123" },
|
||||||
children: <div data-testid="child">Survey Editor Content</div>,
|
children: <div data-testid="child-content">Hello!</div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
render(result);
|
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||||
|
// No JSX is returned after redirect
|
||||||
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
|
expect(layoutElement).toBeUndefined();
|
||||||
expect(screen.getByTestId("DevEnvironmentBanner")).toHaveTextContent("env1");
|
|
||||||
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("throws an error when environment is not found", async () => {
|
it("throws error if user does not exist in DB", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
t: ((key: string) => key) as any,
|
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
||||||
session: { user: { id: "user1" } } as Session,
|
|
||||||
user: { id: "user1", email: "user1@example.com" } as TUser,
|
await expect(
|
||||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
SurveyEditorEnvironmentLayout({
|
||||||
});
|
params: { environmentId: "env-123" },
|
||||||
|
children: <div data-testid="child-content">Hello!</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow("common.user_not_found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws AuthorizationError if user does not have environment access", async () => {
|
||||||
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
||||||
|
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
SurveyEditorEnvironmentLayout({
|
||||||
|
params: { environmentId: "env-123" },
|
||||||
|
children: <div>Child</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow(AuthorizationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws if no organization is found", async () => {
|
||||||
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
|
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
||||||
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
SurveyEditorEnvironmentLayout({
|
||||||
|
params: { environmentId: "env-123" },
|
||||||
|
children: <div data-testid="child-content">Hello from children!</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow("common.organization_not_found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws if no environment is found", async () => {
|
||||||
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
|
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
||||||
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
SurveyEditorEnvironmentLayout({
|
SurveyEditorEnvironmentLayout({
|
||||||
params: Promise.resolve({ environmentId: "env1" }),
|
params: { environmentId: "env-123" },
|
||||||
children: <div>Content</div>,
|
children: <div>Child</div>,
|
||||||
})
|
})
|
||||||
).rejects.toThrow("common.environment_not_found");
|
).rejects.toThrow("common.environment_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("calls redirect when session is null", async () => {
|
it("renders environment layout if everything is valid", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
// Provide all valid data
|
||||||
t: ((key: string) => key) as any,
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
session: undefined as unknown as Session,
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
||||||
user: undefined as unknown as TUser,
|
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
||||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||||
});
|
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||||
vi.mocked(redirect).mockImplementationOnce(() => {
|
id: "env-123",
|
||||||
throw new Error("Redirect called");
|
name: "My Test Environment",
|
||||||
|
} as unknown as TEnvironment);
|
||||||
|
|
||||||
|
// Because it's an async server component, we typically wrap in act(...)
|
||||||
|
let layoutElement: React.ReactNode;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
layoutElement = await SurveyEditorEnvironmentLayout({
|
||||||
|
params: { environmentId: "env-123" },
|
||||||
|
children: <div data-testid="child-content">Hello from children!</div>,
|
||||||
|
});
|
||||||
|
render(layoutElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
// Now confirm we got the child plus all the mocked sub-components
|
||||||
SurveyEditorEnvironmentLayout({
|
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
||||||
params: Promise.resolve({ environmentId: "env1" }),
|
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
||||||
children: <div>Content</div>,
|
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
||||||
})
|
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
||||||
).rejects.toThrow("Redirect called");
|
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
||||||
});
|
expect(screen.getByTestId("dev-environment-banner")).toHaveTextContent("env-123");
|
||||||
|
|
||||||
test("throws error if user is null", async () => {
|
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
|
||||||
t: ((key: string) => key) as any,
|
|
||||||
session: { user: { id: "user1" } } as Session,
|
|
||||||
user: undefined as unknown as TUser,
|
|
||||||
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(redirect).mockImplementationOnce(() => {
|
|
||||||
throw new Error("Redirect called");
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
SurveyEditorEnvironmentLayout({
|
|
||||||
params: Promise.resolve({ environmentId: "env1" }),
|
|
||||||
children: <div>Content</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.user_not_found");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,46 @@
|
|||||||
import { getEnvironment } from "@/lib/environment/service";
|
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||||
|
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||||
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||||
|
import { getTranslate } from "@/tolgee/server";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
||||||
|
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||||
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const SurveyEditorEnvironmentLayout = async (props) => {
|
const SurveyEditorEnvironmentLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
const t = await getTranslate();
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
|
||||||
if (!session) {
|
if (!session?.user) {
|
||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await getUser(session.user.id);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new AuthorizationError(t("common.not_authorized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new Error(t("common.organization_not_found"));
|
||||||
|
}
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
@@ -26,16 +48,23 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnvironmentIdBaseLayout
|
<ResponseFilterProvider>
|
||||||
environmentId={params.environmentId}
|
<PosthogIdentify
|
||||||
session={session}
|
session={session}
|
||||||
user={user}
|
user={user}
|
||||||
organization={organization}>
|
environmentId={params.environmentId}
|
||||||
|
organizationId={organization.id}
|
||||||
|
organizationName={organization.name}
|
||||||
|
organizationBilling={organization.billing}
|
||||||
|
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
||||||
|
/>
|
||||||
|
<FormbricksClient userId={user.id} email={user.email} />
|
||||||
|
<ToasterClient />
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<DevEnvironmentBanner environment={environment} />
|
<DevEnvironmentBanner environment={environment} />
|
||||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</EnvironmentIdBaseLayout>
|
</ResponseFilterProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
import { FormbricksClient } from "./FormbricksClient";
|
import { FormbricksClient } from "./FormbricksClient";
|
||||||
|
|
||||||
@@ -9,6 +9,14 @@ vi.mock("next/navigation", () => ({
|
|||||||
useSearchParams: () => new URLSearchParams("foo=bar"),
|
useSearchParams: () => new URLSearchParams("foo=bar"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock the environment variables.
|
||||||
|
vi.mock("@formbricks/lib/env", () => ({
|
||||||
|
env: {
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: "env-test",
|
||||||
|
NEXT_PUBLIC_FORMBRICKS_API_HOST: "https://api.test.com",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
// Mock the flag that enables Formbricks.
|
// Mock the flag that enables Formbricks.
|
||||||
vi.mock("@/app/lib/formbricks", () => ({
|
vi.mock("@/app/lib/formbricks", () => ({
|
||||||
formbricksEnabled: true,
|
formbricksEnabled: true,
|
||||||
@@ -26,21 +34,17 @@ vi.mock("@formbricks/js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("FormbricksClient", () => {
|
describe("FormbricksClient", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
|
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
|
||||||
const mockSetup = vi.spyOn(formbricks, "setup");
|
const mockSetup = vi.spyOn(formbricks, "setup");
|
||||||
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
||||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||||
|
|
||||||
render(
|
render(<FormbricksClient userId="user-123" email="test@example.com" />);
|
||||||
<FormbricksClient
|
|
||||||
userId="user-123"
|
|
||||||
email="test@example.com"
|
|
||||||
formbricksEnvironmentId="env-test"
|
|
||||||
formbricksApiHost="https://api.test.com"
|
|
||||||
formbricksEnabled={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Expect the first effect to call setup and assign the provided user details.
|
// Expect the first effect to call setup and assign the provided user details.
|
||||||
expect(mockSetup).toHaveBeenCalledWith({
|
expect(mockSetup).toHaveBeenCalledWith({
|
||||||
@@ -60,15 +64,7 @@ describe("FormbricksClient", () => {
|
|||||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||||
|
|
||||||
render(
|
render(<FormbricksClient userId="" email="test@example.com" />);
|
||||||
<FormbricksClient
|
|
||||||
userId=""
|
|
||||||
email="test@example.com"
|
|
||||||
formbricksEnvironmentId="env-test"
|
|
||||||
formbricksApiHost="https://api.test.com"
|
|
||||||
formbricksEnabled={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Since userId is falsy, the first effect should not call setup or assign user details.
|
// Since userId is falsy, the first effect should not call setup or assign user details.
|
||||||
expect(mockSetup).not.toHaveBeenCalled();
|
expect(mockSetup).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -1,44 +1,32 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { formbricksEnabled } from "@/app/lib/formbricks";
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import formbricks from "@formbricks/js";
|
import formbricks from "@formbricks/js";
|
||||||
|
import { env } from "@formbricks/lib/env";
|
||||||
|
|
||||||
interface FormbricksClientProps {
|
export const FormbricksClient = ({ userId, email }: { userId: string; email: string }) => {
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
formbricksEnvironmentId?: string;
|
|
||||||
formbricksApiHost?: string;
|
|
||||||
formbricksEnabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormbricksClient = ({
|
|
||||||
userId,
|
|
||||||
email,
|
|
||||||
formbricksEnvironmentId,
|
|
||||||
formbricksApiHost,
|
|
||||||
formbricksEnabled,
|
|
||||||
}: FormbricksClientProps) => {
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formbricksEnabled && userId) {
|
if (formbricksEnabled && userId) {
|
||||||
formbricks.setup({
|
formbricks.setup({
|
||||||
environmentId: formbricksEnvironmentId ?? "",
|
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
||||||
appUrl: formbricksApiHost ?? "",
|
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
formbricks.setUserId(userId);
|
formbricks.setUserId(userId);
|
||||||
formbricks.setEmail(email);
|
formbricks.setEmail(email);
|
||||||
}
|
}
|
||||||
}, [userId, email, formbricksEnvironmentId, formbricksApiHost, formbricksEnabled]);
|
}, [userId, email]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formbricksEnabled) {
|
if (formbricksEnabled) {
|
||||||
formbricks.registerRouteChange();
|
formbricks.registerRouteChange();
|
||||||
}
|
}
|
||||||
}, [pathname, searchParams, formbricksEnabled]);
|
}, [pathname, searchParams]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
export const LoadingCard = ({
|
export const LoadingCard = ({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"use server";
|
"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 { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +8,9 @@ import {
|
|||||||
} from "@/modules/ee/license-check/lib/utils";
|
} from "@/modules/ee/license-check/lib/utils";
|
||||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||||
import { z } from "zod";
|
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 { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"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 { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
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 { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
import { convertDateTimeStringShort } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
|
||||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
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 { useTranslate } from "@tolgee/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
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 { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { getActiveInactiveSurveysAction } from "../actions";
|
import { getActiveInactiveSurveysAction } from "../actions";
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
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 { TActionClass } from "@formbricks/types/action-classes";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const ActionClassDataRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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)}
|
{timeSince(actionClass.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></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 { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
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 { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Actions",
|
title: "Actions",
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
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 { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
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 { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import type { Session } from "next-auth";
|
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 {
|
interface EnvironmentLayoutProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
||||||
|
|
||||||
interface EnvironmentStorageHandlerProps {
|
interface EnvironmentStorageHandlerProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
import { Switch } from "@/modules/ui/components/switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface EnvironmentSwitchProps {
|
interface EnvironmentSwitchProps {
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
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 { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
@@ -48,6 +45,9 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
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 { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
@@ -265,7 +265,7 @@ export const MainNavigation = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
|
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
|
||||||
)}>
|
)}>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
interface NavigationLinkProps {
|
interface NavigationLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom/vitest";
|
|||||||
import { cleanup, render } from "@testing-library/react";
|
import { cleanup, render } from "@testing-library/react";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { PosthogIdentify } from "./PosthogIdentify";
|
import { PosthogIdentify } from "./PosthogIdentify";
|
||||||
@@ -18,7 +18,7 @@ describe("PosthogIdentify", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ describe("PosthogIdentify", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does nothing if isPosthogEnabled is false", () => {
|
it("does nothing if isPosthogEnabled is false", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("does nothing if session user is missing", () => {
|
it("does nothing if session user is missing", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("identifies user but does not group if environmentId/organizationId not provided", () => {
|
it("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
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 { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +9,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface WidgetStatusIndicatorProps {
|
interface WidgetStatusIndicatorProps {
|
||||||
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
<currentStatus.icon />
|
<currentStatus.icon />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
<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" && (
|
{status === "notImplemented" && (
|
||||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||||
<RotateCcwIcon />
|
<RotateCcwIcon />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
|
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
getProjectIdFromIntegrationId,
|
getProjectIdFromIntegrationId,
|
||||||
} from "@/lib/utils/helper";
|
} from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,8 +4,6 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
|
|||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
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 { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -25,6 +23,8 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
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 { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationAirtable,
|
TIntegrationAirtable,
|
||||||
|
|||||||
-151
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+7
-7
@@ -5,7 +5,6 @@ import {
|
|||||||
AddIntegrationModal,
|
AddIntegrationModal,
|
||||||
IntegrationModalInputs,
|
IntegrationModalInputs,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -14,6 +13,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { timeSince } from "@formbricks/lib/time";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
@@ -98,17 +98,17 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
{integrationData.length ? (
|
{integrationData.length ? (
|
||||||
<div className="mt-6 w-full rounded-lg border border-slate-200">
|
<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">
|
<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) => (
|
{tableHeaders.map((header, idx) => (
|
||||||
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
|
<div key={idx} className={`col-span-2 hidden text-center sm:block`}>
|
||||||
{t(header)}
|
{t(header)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{integrationData.map((data, index) => (
|
{integrationData.map((data, index) => (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.baseId}-${data.tableId}-${data.surveyId}`}
|
key={index}
|
||||||
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDefaultValues({
|
setDefaultValues({
|
||||||
base: data.baseId,
|
base: data.baseId,
|
||||||
@@ -129,7 +129,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), props.locale)}
|
{timeSince(data.createdAt.toString(), props.locale)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
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 { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
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 { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getSpreadsheetNameById } from "@/lib/googleSheet/service";
|
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
||||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||||
|
|||||||
+3
-3
@@ -8,9 +8,7 @@ import {
|
|||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -23,6 +21,8 @@ import Image from "next/image";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
@@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
<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">
|
<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) => (
|
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||||
|
|||||||
-162
@@ -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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+10
-9
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -10,6 +9,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { timeSince } from "@formbricks/lib/time";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
@@ -36,10 +36,11 @@ export const ManageIntegration = ({
|
|||||||
}: ManageIntegrationProps) => {
|
}: ManageIntegrationProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||||
let integrationArray: TIntegrationGoogleSheetsConfigData[] = [];
|
const integrationArray = googleSheetIntegration
|
||||||
if (googleSheetIntegration?.config.data) {
|
? googleSheetIntegration.config.data
|
||||||
integrationArray = googleSheetIntegration.config.data;
|
? googleSheetIntegration.config.data
|
||||||
}
|
: []
|
||||||
|
: [];
|
||||||
const [isDeleting, setisDeleting] = useState(false);
|
const [isDeleting, setisDeleting] = useState(false);
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
@@ -111,9 +112,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
|
key={index}
|
||||||
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -123,7 +124,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
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 { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
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";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import "server-only";
|
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 { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
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 { logger } from "@formbricks/logger";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { webhookCache } from "@/lib/cache/webhook";
|
import { webhookCache } from "@/lib/cache/webhook";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
|
||||||
import { Prisma, Webhook } from "@prisma/client";
|
import { Prisma, Webhook } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
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 { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user