mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-25 07:50:19 -06:00
Compare commits
14 Commits
fix-hidden
...
feat/heic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa7f52a72b | ||
|
|
5306aecec4 | ||
|
|
cdc332d44e | ||
|
|
2e58f337b0 | ||
|
|
1390b071ea | ||
|
|
d5a4a4736b | ||
|
|
57725783e8 | ||
|
|
8f5527ac34 | ||
|
|
3632c9ec94 | ||
|
|
1acf334ad3 | ||
|
|
a526bb08be | ||
|
|
787cf2d56f | ||
|
|
12b7dbd567 | ||
|
|
423466d30a |
@@ -1,56 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
**/node_modules
|
||||
# **/node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
# testing
|
||||
**/coverage
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
**/.next/
|
||||
**/out/
|
||||
**/.next
|
||||
**/out
|
||||
**/build
|
||||
|
||||
# node
|
||||
**/dist/
|
||||
**/dist
|
||||
|
||||
# misc
|
||||
**/.DS_Store
|
||||
.DS_Store
|
||||
*.pem
|
||||
Zone.Identifier
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
**/.env
|
||||
**/.env.local
|
||||
**/.env.development.local
|
||||
**/.env.test.local
|
||||
**/.env.production.local
|
||||
!packages/database/.env
|
||||
!apps/web/.env
|
||||
|
||||
# build tools
|
||||
# turbo
|
||||
.turbo
|
||||
**/*vite.config.*.timestamp-*
|
||||
|
||||
# environment specific
|
||||
# nixos stuff
|
||||
.direnv
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.vscode
|
||||
.github
|
||||
**/.turbo
|
||||
|
||||
# project specific
|
||||
packages/lib/uploads
|
||||
apps/web/public/js
|
||||
packages/database/migrations
|
||||
branch.json
|
||||
.env
|
||||
|
||||
26
.env.example
26
.env.example
@@ -25,9 +25,6 @@ NEXTAUTH_SECRET=
|
||||
# You can use: `openssl rand -hex 32` to generate a secure one
|
||||
CRON_SECRET=
|
||||
|
||||
# Set the minimum log level(debug, info, warn, error, fatal)
|
||||
LOG_LEVEL=info
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
@@ -42,7 +39,6 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu
|
||||
# See optional configurations below if you want to disable these features.
|
||||
|
||||
MAIL_FROM=noreply@example.com
|
||||
MAIL_FROM_NAME=Formbricks
|
||||
SMTP_HOST=localhost
|
||||
SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
@@ -100,9 +96,6 @@ PASSWORD_RESET_DISABLED=1
|
||||
# Organization Invite. Disable the ability for invited users to create an account.
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
# Docker cron jobs. Disable the supercronic cron jobs in the Docker image (useful for cluster setups).
|
||||
# DOCKER_CRON_ENABLED=1
|
||||
|
||||
##########
|
||||
# Other #
|
||||
##########
|
||||
@@ -137,9 +130,6 @@ AZUREAD_TENANT_ID=
|
||||
# OIDC_DISPLAY_NAME=
|
||||
# OIDC_SIGNING_ALGORITHM=
|
||||
|
||||
# Configure SAML SSO
|
||||
# SAML_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/formbricks-saml
|
||||
|
||||
# Configure this when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||
# ASSET_PREFIX_URL=
|
||||
|
||||
@@ -191,16 +181,11 @@ ENTERPRISE_LICENSE_KEY=
|
||||
UNSPLASH_ACCESS_KEY=
|
||||
|
||||
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
|
||||
# You can also add more configuration to Redis using the redis.conf file in the root directory
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_DEFAULT_TTL=86400 # 1 day
|
||||
# REDIS_URL=redis://localhost:6379
|
||||
|
||||
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
|
||||
# REDIS_HTTP_URL:
|
||||
|
||||
# The below is used for Rate Limiting for management API
|
||||
UNKEY_ROOT_KEY=
|
||||
|
||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||
# CUSTOM_CACHE_DISABLED=1
|
||||
|
||||
@@ -210,10 +195,5 @@ UNKEY_ROOT_KEY=
|
||||
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||
|
||||
# INTERCOM_APP_ID=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
|
||||
# Enable Prometheus metrics
|
||||
# PROMETHEUS_ENABLED=
|
||||
# PROMETHEUS_EXPORTER_PORT=
|
||||
|
||||
# NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# INTERCOM_SECRET_KEY=
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: Bug report
|
||||
description: "Found a bug? Please fill out the sections below. \U0001F44D"
|
||||
type: bug
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-summary
|
||||
|
||||
4
.github/actions/cache-build-web/action.yml
vendored
4
.github/actions/cache-build-web/action.yml
vendored
@@ -56,6 +56,10 @@ runs:
|
||||
- name: Fill ENCRYPTION_KEY, ENTERPRISE_LICENSE_KEY and E2E_TESTING in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "E2E_TESTING=${{ inputs.e2e_testing_mode }}" >> .env
|
||||
shell: bash
|
||||
|
||||
|
||||
10
.github/workflows/apply-issue-labels-to-pr.yml
vendored
10
.github/workflows/apply-issue-labels-to-pr.yml
vendored
@@ -5,9 +5,6 @@ on:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
label_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,13 +15,8 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Apply labels from linked issue to PR
|
||||
uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
7
.github/workflows/build-web.yml
vendored
7
.github/workflows/build-web.yml
vendored
@@ -12,12 +12,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Build & Cache Web Binaries
|
||||
|
||||
13
.github/workflows/chromatic.yml
vendored
13
.github/workflows/chromatic.yml
vendored
@@ -11,24 +11,19 @@ jobs:
|
||||
name: Run Chromatic
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
- name: Run Chromatic
|
||||
uses: chromaui/action@c93e0bc3a63aa176e14a75b61a31847cbfdd341c # latest
|
||||
uses: chromaui/action@latest
|
||||
with:
|
||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
|
||||
@@ -18,11 +18,6 @@ jobs:
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
|
||||
7
.github/workflows/cron-weeklySummary.yml
vendored
7
.github/workflows/cron-weeklySummary.yml
vendored
@@ -7,9 +7,6 @@ on:
|
||||
schedule:
|
||||
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
|
||||
- cron: "0 8 * * 1"
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cron-weeklySummary:
|
||||
permissions:
|
||||
@@ -19,10 +16,6 @@ jobs:
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
|
||||
27
.github/workflows/dependency-review.yml
vendored
27
.github/workflows/dependency-review.yml
vendored
@@ -1,27 +0,0 @@
|
||||
# Dependency Review Action
|
||||
#
|
||||
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||
# Once installed, if the workflow run is marked as required,
|
||||
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||
#
|
||||
# Source repository: https://github.com/actions/dependency-review-action
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||
64
.github/workflows/deploy-formbricks-cloud.yml
vendored
64
.github/workflows/deploy-formbricks-cloud.yml
vendored
@@ -1,64 +0,0 @@
|
||||
name: Formbricks Cloud Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
VERSION:
|
||||
description: 'The version of the Docker image to release'
|
||||
required: true
|
||||
type: string
|
||||
REPOSITORY:
|
||||
description: 'The repository to use for the Docker image'
|
||||
required: false
|
||||
type: string
|
||||
default: 'ghcr.io/formbricks/formbricks'
|
||||
workflow_call:
|
||||
inputs:
|
||||
VERSION:
|
||||
description: 'The version of the Docker image to release'
|
||||
required: true
|
||||
type: string
|
||||
REPOSITORY:
|
||||
description: 'The repository to use for the Docker image'
|
||||
required: false
|
||||
type: string
|
||||
default: 'ghcr.io/formbricks/formbricks'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
helmfile-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
|
||||
aws-region: "eu-central-1"
|
||||
|
||||
- name: Setup Cluster Access
|
||||
run: |
|
||||
aws eks update-kubeconfig --name formbricks-prod-eks --region eu-central-1
|
||||
env:
|
||||
AWS_REGION: eu-central-1
|
||||
|
||||
- uses: helmfile/helmfile-action@v2
|
||||
env:
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||
FORMBRICKS_S3_BUCKET: ${{ secrets.FORMBRICKS_S3_BUCKET }}
|
||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
||||
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
||||
with:
|
||||
helm-plugins: >
|
||||
https://github.com/databus23/helm-diff,
|
||||
https://github.com/jkroepke/helm-secrets
|
||||
helmfile-args: apply
|
||||
helmfile-auto-init: "false"
|
||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||
|
||||
27
.github/workflows/e2e.yml
vendored
27
.github/workflows/e2e.yml
vendored
@@ -43,21 +43,16 @@ jobs:
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
@@ -89,7 +84,7 @@ jobs:
|
||||
|
||||
- name: Run App
|
||||
run: |
|
||||
NODE_ENV=test pnpm start --filter=@formbricks/web | tee app.log 2>&1 &
|
||||
NODE_ENV=test pnpm start --filter=@formbricks/web &
|
||||
sleep 10 # Optional: gives some buffer for the app to start
|
||||
for attempt in {1..10}; do
|
||||
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
|
||||
@@ -117,7 +112,7 @@ jobs:
|
||||
|
||||
- name: Azure login
|
||||
if: env.AZURE_ENABLED == 'true'
|
||||
uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
@@ -135,19 +130,9 @@ jobs:
|
||||
run: |
|
||||
pnpm test:e2e
|
||||
|
||||
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
if: failure()
|
||||
with:
|
||||
name: app-logs
|
||||
path: app.log
|
||||
|
||||
- name: Output App Logs
|
||||
if: failure()
|
||||
run: cat app.log
|
||||
|
||||
33
.github/workflows/formbricks-release.yml
vendored
33
.github/workflows/formbricks-release.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Build, release & deploy Formbricks images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
docker-build:
|
||||
name: Build & release stable docker image
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/release-docker-github.yml
|
||||
secrets: inherit
|
||||
|
||||
helm-chart-release:
|
||||
name: Release Helm Chart
|
||||
uses: ./.github/workflows/release-helm-chart.yml
|
||||
secrets: inherit
|
||||
needs:
|
||||
- docker-build
|
||||
with:
|
||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||
|
||||
deploy-formbricks-cloud:
|
||||
name: Deploy Helm Chart to Formbricks Cloud
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/deploy-formbricks-cloud.yml
|
||||
needs:
|
||||
- docker-build
|
||||
- helm-chart-release
|
||||
with:
|
||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||
10
.github/workflows/labeler.yml
vendored
10
.github/workflows/labeler.yml
vendored
@@ -4,9 +4,6 @@ on:
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
name: Pull Request Labeler
|
||||
@@ -15,12 +12,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/labeler@ac9175f8a1f3625fd0d4fb234536d26811351594 # v4.3.0
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
|
||||
5
.github/workflows/lint.yml
vendored
5
.github/workflows/lint.yml
vendored
@@ -12,11 +12,6 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
|
||||
4
.github/workflows/pr.yml
vendored
4
.github/workflows/pr.yml
vendored
@@ -50,10 +50,6 @@ jobs:
|
||||
checks: write
|
||||
statuses: write
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
run: exit 1
|
||||
|
||||
62
.github/workflows/prepare-release.yml
vendored
Normal file
62
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Prepare release
|
||||
run-name: Prepare release ${{ inputs.next_version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
next_version:
|
||||
required: true
|
||||
type: string
|
||||
description: "Version name"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --local user.email "github-actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
cd apps/web
|
||||
pnpm version ${{ inputs.next_version }} --no-workspaces-update
|
||||
|
||||
- name: Commit changes and create a branch
|
||||
run: |
|
||||
branch_name="release-v${{ inputs.next_version }}"
|
||||
git checkout -b "$branch_name"
|
||||
git add .
|
||||
git commit -m "chore: release v${{ inputs.next_version }}"
|
||||
git push origin "$branch_name"
|
||||
|
||||
- name: Create pull request
|
||||
run: |
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "release-v${{ inputs.next_version }}" \
|
||||
--title "chore: bump version to v${{ inputs.next_version }}" \
|
||||
--body "This PR contains the changes for the v${{ inputs.next_version }} release."
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
13
.github/workflows/release-changesets.yml
vendored
13
.github/workflows/release-changesets.yml
vendored
@@ -26,28 +26,23 @@ jobs:
|
||||
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
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
|
||||
uses: pnpm/action-setup@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
|
||||
uses: changesets/action@v1
|
||||
with:
|
||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||
publish: pnpm release
|
||||
|
||||
@@ -17,9 +17,6 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -31,28 +28,23 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@v3 # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -62,7 +54,7 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@v5 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -70,7 +62,7 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: tw0fqmsx3c
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
|
||||
45
.github/workflows/release-docker-github.yml
vendored
45
.github/workflows/release-docker-github.yml
vendored
@@ -6,11 +6,10 @@ name: Docker Release to Github
|
||||
# documentation.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
VERSION:
|
||||
description: release version
|
||||
value: ${{ jobs.build.outputs.VERSION }}
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
@@ -21,9 +20,6 @@ env:
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -34,45 +30,24 @@ jobs:
|
||||
# with sigstore/fulcio when running outside of PRs.
|
||||
id-token: write
|
||||
|
||||
outputs:
|
||||
VERSION: ${{ steps.extract_release_tag.outputs.VERSION }}
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: extract_release_tag
|
||||
run: |
|
||||
TAG=${{ github.ref }}
|
||||
TAG=${TAG#refs/tags/v}
|
||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||
echo "VERSION=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Update package.json version
|
||||
run: |
|
||||
sed -i "s/\"version\": \"0.0.0\"/\"version\": \"${{ env.RELEASE_TAG }}\"/" ./apps/web/package.json
|
||||
cat ./apps/web/package.json | grep version
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Depot CLI
|
||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||
uses: depot/setup-action@v1
|
||||
|
||||
# Install the cosign tool except on PR
|
||||
# https://github.com/sigstore/cosign-installer
|
||||
- name: Install cosign
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@v3 # v3.0.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -82,7 +57,7 @@ jobs:
|
||||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@v5 # v5.0.0
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
@@ -90,7 +65,7 @@ jobs:
|
||||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
|
||||
uses: depot/build-push-action@v1
|
||||
with:
|
||||
project: tw0fqmsx3c
|
||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||
|
||||
46
.github/workflows/release-docker.yml
vendored
Normal file
46
.github/workflows/release-docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Release on Dockerhub
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release-image-on-dockerhub:
|
||||
name: Release on Dockerhub
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Get Release Tag
|
||||
id: extract_release_tag
|
||||
run: |
|
||||
TAG=${{ github.ref }}
|
||||
TAG=${TAG#refs/tags/v}
|
||||
echo "RELEASE_TAG=$TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/web/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
|
||||
54
.github/workflows/release-helm-chart.yml
vendored
54
.github/workflows/release-helm-chart.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Publish Helm Chart
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
VERSION:
|
||||
description: 'The version of the Helm chart to release'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Extract release version
|
||||
run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3.5
|
||||
with:
|
||||
version: latest
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Install YQ
|
||||
uses: dcarbone/install-yq-action@4075b4dca348d74bd83f2bf82d30f25d7c54539b # v1.3.1
|
||||
|
||||
- name: Update Chart.yaml with new version
|
||||
run: |
|
||||
yq -i ".version = \"${{ inputs.VERSION }}\"" helm-chart/Chart.yaml
|
||||
yq -i ".appVersion = \"v${{ inputs.VERSION }}\"" helm-chart/Chart.yaml
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
helm package ./helm-chart
|
||||
|
||||
- name: Push Helm chart to GitHub Container Registry
|
||||
run: |
|
||||
helm push formbricks-${{ inputs.VERSION }}.tgz oci://ghcr.io/formbricks/helm-charts
|
||||
7
.github/workflows/scorecard.yml
vendored
7
.github/workflows/scorecard.yml
vendored
@@ -34,11 +34,6 @@ jobs:
|
||||
# actions: read
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
@@ -76,6 +71,6 @@ jobs:
|
||||
# Upload the results to GitHub's code scanning dashboard (optional).
|
||||
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
11
.github/workflows/semantic-pull-requests.yml
vendored
11
.github/workflows/semantic-pull-requests.yml
vendored
@@ -16,12 +16,7 @@ jobs:
|
||||
name: PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -40,7 +35,7 @@ jobs:
|
||||
revert
|
||||
ossgg
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
# condition you can continue the execution with the populated error message.
|
||||
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
||||
@@ -59,7 +54,7 @@ jobs:
|
||||
|
||||
# Delete a previous comment when the issue has been resolved
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
||||
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
|
||||
12
.github/workflows/sonarqube.yml
vendored
12
.github/workflows/sonarqube.yml
vendored
@@ -6,7 +6,6 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
merge_group:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
@@ -14,12 +13,7 @@ jobs:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
@@ -46,7 +40,11 @@ jobs:
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd apps/web
|
||||
pnpm test:coverage
|
||||
cd ../../
|
||||
# The Vitest coverage config is in your vite.config.mts
|
||||
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203
|
||||
env:
|
||||
|
||||
75
.github/workflows/terrafrom-plan-and-apply.yml
vendored
75
.github/workflows/terrafrom-plan-and-apply.yml
vendored
@@ -1,75 +0,0 @@
|
||||
name: 'Terraform'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# TODO: enable it back when migration is completed.
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# pull_request:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
terraform:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ASSUME_ROLE_ARN }}
|
||||
aws-region: "eu-central-1"
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||
|
||||
- name: Terraform Format
|
||||
id: fmt
|
||||
run: terraform fmt -check -recursive
|
||||
continue-on-error: true
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Terraform Init
|
||||
id: init
|
||||
run: terraform init
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Terraform Validate
|
||||
id: validate
|
||||
run: terraform validate
|
||||
working-directory: infra/terraform
|
||||
|
||||
- name: Terraform Plan
|
||||
id: plan
|
||||
run: terraform plan -out .planfile
|
||||
working-directory: infra/terraform
|
||||
|
||||
# - name: Post PR comment
|
||||
# uses: borchero/terraform-plan-comment@3399d8dbae8b05185e815e02361ede2949cd99c4 # v2.4.0
|
||||
# if: always() && github.ref != 'refs/heads/main' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure')
|
||||
# with:
|
||||
# token: ${{ github.token }}
|
||||
# planfile: .planfile
|
||||
# working-directory: "infra/terraform"
|
||||
# skip-comment: true
|
||||
|
||||
- name: Terraform Apply
|
||||
id: apply
|
||||
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
run: terraform apply .planfile
|
||||
working-directory: "infra/terraform"
|
||||
|
||||
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -1,9 +1,6 @@
|
||||
name: Tests
|
||||
on:
|
||||
workflow_call:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Unit Tests
|
||||
@@ -13,21 +10,16 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v3.8.2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
18
.github/workflows/tolgee-missing-key-check.yml
vendored
18
.github/workflows/tolgee-missing-key-check.yml
vendored
@@ -5,30 +5,18 @@ permissions:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
check-missing-translations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
|
||||
59
.github/workflows/tolgee.yml
vendored
59
.github/workflows/tolgee.yml
vendored
@@ -3,8 +3,7 @@ permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -12,36 +11,13 @@ jobs:
|
||||
tag-production-keys:
|
||||
name: Tag Production Keys
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0 # This ensures we get the full git history
|
||||
|
||||
- name: Get source branch name
|
||||
id: branch-name
|
||||
run: |
|
||||
RAW_BRANCH="${{ github.head_ref }}"
|
||||
SOURCE_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9._\/-]//g')
|
||||
|
||||
|
||||
# Safely add to environment variables using GitHub's recommended method
|
||||
# This prevents environment variable injection attacks
|
||||
echo "SOURCE_BRANCH<<EOF" >> $GITHUB_ENV
|
||||
echo "$SOURCE_BRANCH" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
echo "Detected source branch: $SOURCE_BRANCH"
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18 # Ensure compatibility with your project
|
||||
|
||||
@@ -50,38 +26,17 @@ jobs:
|
||||
|
||||
- name: Tag Production Keys
|
||||
run: |
|
||||
BRANCH_NAME=${GITHUB_REF##*/}
|
||||
npx tolgee tag \
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-extracted \
|
||||
--filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--filter-tag "draft: ${BRANCH_NAME}" \
|
||||
--tag production \
|
||||
--untag "draft:${SOURCE_BRANCH}"
|
||||
--untag "draft: ${BRANCH_NAME}"
|
||||
|
||||
- name: Tag unused production keys as Deprecated
|
||||
- name: Tag Deprecated Keys
|
||||
run: |
|
||||
npx tolgee tag \
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag production \
|
||||
--tag deprecated --untag production
|
||||
|
||||
- name: Tag unused draft:current-branch keys as Deprecated
|
||||
run: |
|
||||
npx tolgee tag \
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--filter-not-extracted --filter-tag "draft:${SOURCE_BRANCH}" \
|
||||
--tag deprecated --untag "draft:${SOURCE_BRANCH}"
|
||||
|
||||
- name: Sync with backup
|
||||
run: |
|
||||
npx tolgee sync \
|
||||
--api-key ${{ secrets.TOLGEE_API_KEY }} \
|
||||
--backup ./tolgee-backup \
|
||||
--continue-on-warning \
|
||||
--yes
|
||||
|
||||
- name: Upload backup as artifact
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: tolgee-backup-${{ github.sha }}
|
||||
path: ./tolgee-backup
|
||||
retention-days: 90
|
||||
|
||||
@@ -17,12 +17,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
if: github.event.action == 'opened'
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/first-interaction@3c71ce730280171fd1cfb57c00c774f8998586f7 # v1
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |-
|
||||
|
||||
58
.gitignore
vendored
58
.gitignore
vendored
@@ -1,26 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
**/node_modules
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store/
|
||||
|
||||
# testing
|
||||
**/coverage
|
||||
coverage
|
||||
|
||||
# next.js
|
||||
**/.next/
|
||||
**/out/
|
||||
**/build
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# node
|
||||
**/dist/
|
||||
dist/
|
||||
|
||||
# misc
|
||||
**/.DS_Store
|
||||
.DS_Store
|
||||
*.pem
|
||||
Zone.Identifier
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
@@ -28,47 +27,36 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
**/.env
|
||||
**/.env.local
|
||||
**/.env.development.local
|
||||
**/.env.test.local
|
||||
**/.env.production.local
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
!packages/database/.env
|
||||
!apps/web/.env
|
||||
|
||||
# build tools
|
||||
# turbo
|
||||
.turbo
|
||||
**/*vite.config.*.timestamp-*
|
||||
|
||||
# environment specific
|
||||
# nixos stuff
|
||||
.direnv
|
||||
|
||||
Zone.Identifier
|
||||
|
||||
# Playwright
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
||||
# project specific
|
||||
# uploads
|
||||
packages/lib/uploads
|
||||
|
||||
# Vite Timestamps
|
||||
*vite.config.*.timestamp-*
|
||||
|
||||
# js compiled assets
|
||||
apps/web/public/js
|
||||
packages/database/migrations
|
||||
branch.json
|
||||
.vercel
|
||||
|
||||
# Terraform
|
||||
infra/terraform/.terraform/
|
||||
**/.terraform.lock.hcl
|
||||
**/terraform.tfstate
|
||||
**/terraform.tfstate.*
|
||||
**/crash.log
|
||||
**/override.tf
|
||||
**/override.tf.json
|
||||
**/*.tfvars
|
||||
**/*.tfvars.json
|
||||
**/.terraformrc
|
||||
**/terraform.rc
|
||||
|
||||
# IntelliJ IDEA
|
||||
/.idea/
|
||||
/*.iml
|
||||
packages/database/migrations
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
images=($(yq eval '.services.*.image' docker-compose.dev.yml))
|
||||
images=($(yq eval '.services.*.image' packages/database/docker-compose.yml))
|
||||
|
||||
pull_image() {
|
||||
docker pull "$1"
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ./branch.json
|
||||
prettier --write ./branch.json
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
||||
1
.husky/post-commit
Normal file
1
.husky/post-commit
Normal file
@@ -0,0 +1 @@
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
||||
@@ -1,21 +1,5 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
# Load environment variables from .env files
|
||||
if [ -f .env ]; then
|
||||
set -a
|
||||
. .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
pnpm lint-staged
|
||||
pnpm tolgee-pull || true
|
||||
echo "{\"branchName\": \"main\"}" > ../branch.json
|
||||
git add branch.json packages/lib/messages/*.json
|
||||
|
||||
# Run tolgee-pull if branch.json exists and NEXT_PUBLIC_TOLGEE_API_KEY is not set
|
||||
if [ -f branch.json ]; then
|
||||
if [ -z "$NEXT_PUBLIC_TOLGEE_API_KEY" ]; then
|
||||
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
|
||||
else
|
||||
pnpm run tolgee-pull
|
||||
git add packages/lib/messages
|
||||
fi
|
||||
fi
|
||||
@@ -27,10 +27,6 @@
|
||||
{
|
||||
"language": "zh-Hant-TW",
|
||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||
},
|
||||
{
|
||||
"language": "pt-PT",
|
||||
"path": "./packages/lib/messages/pt-PT.json"
|
||||
}
|
||||
],
|
||||
"forceMode": "OVERRIDE"
|
||||
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -6,8 +6,6 @@
|
||||
"dbaeumer.vscode-eslint", // eslint plugin
|
||||
"esbenp.prettier-vscode", // prettier plugin
|
||||
"Prisma.prisma", // syntax|format|completion for prisma
|
||||
"yzhang.markdown-all-in-one", // nicer markdown support
|
||||
"vitest.explorer", // run tests directly from the code window
|
||||
"sonarsource.sonarlint-vscode" // sonarqube linter for vscode
|
||||
"yzhang.markdown-all-in-one" // nicer markdown support
|
||||
]
|
||||
}
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
|
||||
- All content that resides under the "packages/js/", "packages/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 content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<h3 align="center">Formbricks</h3>
|
||||
|
||||
<p align="center">
|
||||
The Open Source Qualtrics Alternative
|
||||
Harvest user-insights, build irresistible experiences.
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a>
|
||||
</p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"lucide-react": "0.468.0",
|
||||
"next": "15.2.3",
|
||||
"next": "15.1.2",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
},
|
||||
|
||||
@@ -9,12 +9,6 @@ declare const window: Window;
|
||||
export default function AppPage(): React.JSX.Element {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const router = useRouter();
|
||||
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
|
||||
const userAttributes = {
|
||||
"Attribute 1": "one",
|
||||
"Attribute 2": "two",
|
||||
"Attribute 3": "three",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
@@ -39,9 +33,18 @@ export default function AppPage(): React.JSX.Element {
|
||||
addFormbricksDebugParam();
|
||||
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
void formbricks.setup({
|
||||
const userId = "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING";
|
||||
const userInitAttributes = {
|
||||
language: "de",
|
||||
"Init Attribute 1": "eight",
|
||||
"Init Attribute 2": "two",
|
||||
};
|
||||
|
||||
void formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
appUrl: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
apiHost: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
||||
userId,
|
||||
attributes: userInitAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -123,19 +126,19 @@ export default function AppPage(): React.JSX.Element {
|
||||
<div className="md:grid md:grid-cols-3">
|
||||
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Set a user ID / pull data from Formbricks app
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
On formbricks.setUserId() the user state will <strong>be fetched from Formbricks</strong> and
|
||||
the local state gets <strong>updated with the user state</strong>.
|
||||
On formbricks.reset() the local state will <strong>be deleted</strong> and formbricks gets{" "}
|
||||
<strong>reinitialized</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void formbricks.setUserId(userId);
|
||||
void formbricks.reset();
|
||||
}}>
|
||||
Set user ID
|
||||
Reset
|
||||
</button>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Reset' and
|
||||
@@ -155,7 +158,7 @@ export default function AppPage(): React.JSX.Element {
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sends a{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
|
||||
href="https://formbricks.com/docs/actions/no-code"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500"
|
||||
target="_blank">
|
||||
@@ -163,7 +166,7 @@ export default function AppPage(): React.JSX.Element {
|
||||
</a>{" "}
|
||||
as long as you created it beforehand in the Formbricks App.{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-no-code-actions"
|
||||
href="https://formbricks.com/docs/actions/no-code"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="underline dark:text-blue-500">
|
||||
@@ -172,7 +175,6 @@ export default function AppPage(): React.JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
@@ -188,7 +190,7 @@ export default function AppPage(): React.JSX.Element {
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
|
||||
href="https://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500">
|
||||
@@ -213,7 +215,7 @@ export default function AppPage(): React.JSX.Element {
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
|
||||
href="https://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500">
|
||||
@@ -238,7 +240,7 @@ export default function AppPage(): React.JSX.Element {
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification"
|
||||
href="https://formbricks.com/docs/attributes/identify-users"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500">
|
||||
@@ -248,110 +250,6 @@ export default function AppPage(): React.JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void formbricks.setAttributes(userAttributes);
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Multiple Attributes
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/user-identification#setting-custom-user-attributes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500">
|
||||
user attributes
|
||||
</a>{" "}
|
||||
to 'one', 'two', 'three'.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
void formbricks.setLanguage("de");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
|
||||
Set Language to 'de'
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/general-features/multi-language-surveys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500">
|
||||
language
|
||||
</a>{" "}
|
||||
to 'de'.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
onClick={() => {
|
||||
void formbricks.track("code");
|
||||
}}>
|
||||
Code Action
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button sends a{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
|
||||
rel="noopener noreferrer"
|
||||
className="underline dark:text-blue-500"
|
||||
target="_blank">
|
||||
Code Action
|
||||
</a>{" "}
|
||||
as long as you created it beforehand in the Formbricks App.{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/actions#setting-up-code-actions"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="underline dark:text-blue-500">
|
||||
Here are instructions on how to do it.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||
onClick={() => {
|
||||
void formbricks.logout();
|
||||
}}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||
This button logs out the user and syncs the local state with Formbricks. (Only works if a
|
||||
userId is set)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
"@typescript-eslint/eslint-plugin": "8.18.0",
|
||||
"@typescript-eslint/parser": "8.18.0",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"esbuild": "0.25.1",
|
||||
"esbuild": "0.25.0",
|
||||
"eslint-plugin-storybook": "0.11.1",
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "8.4.7",
|
||||
"tsup": "8.3.5",
|
||||
"vite": "6.0.12"
|
||||
"vite": "6.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -48,6 +48,3 @@ uploads/
|
||||
|
||||
# Sentry Config File
|
||||
.sentryclirc
|
||||
|
||||
# SAML Preloaded Connections
|
||||
saml-connection/
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
||||
FROM node:22-alpine3.20 AS base
|
||||
|
||||
#
|
||||
## step 1: Prune monorepo
|
||||
@@ -24,12 +24,15 @@ RUN corepack enable
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
|
||||
|
||||
# Set hardcoded environment variables
|
||||
ENV DATABASE_URL="postgresql://placeholder:for@build:5432/gets_overwritten_at_runtime?schema=public"
|
||||
ENV NEXTAUTH_SECRET="placeholder_for_next_auth_of_64_chars_get_overwritten_at_runtime"
|
||||
ENV ENCRYPTION_KEY="placeholder_for_build_key_of_64_chars_get_overwritten_at_runtime"
|
||||
ENV CRON_SECRET="placeholder_for_cron_secret_of_64_chars_get_overwritten_at_runtime"
|
||||
|
||||
ARG NEXT_PUBLIC_SENTRY_DSN
|
||||
ARG SENTRY_AUTH_TOKEN
|
||||
|
||||
# Increase Node.js memory limit
|
||||
# ENV NODE_OPTIONS="--max_old_space_size=4096"
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -38,17 +41,19 @@ WORKDIR /app
|
||||
# COPY --from=builder /app/out/json/ .
|
||||
# COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Install the dependencies
|
||||
# RUN pnpm install
|
||||
|
||||
# Prepare the build
|
||||
COPY . .
|
||||
|
||||
# Create a .env file
|
||||
RUN touch apps/web/.env
|
||||
|
||||
# Install the dependencies
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project
|
||||
RUN NODE_OPTIONS="--max_old_space_size=4096" pnpm build --filter=@formbricks/web...
|
||||
# RUN pnpm post-install --filter=@formbricks/web...
|
||||
RUN pnpm build --filter=@formbricks/web...
|
||||
|
||||
# Extract Prisma version
|
||||
RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_version.txt
|
||||
@@ -71,7 +76,6 @@ WORKDIR /home/nextjs
|
||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||
COPY --from=installer /app/apps/web/package.json .
|
||||
# Leverage output traces to reduce image size
|
||||
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
||||
@@ -79,8 +83,6 @@ COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
|
||||
# Copy Prisma-specific generated files
|
||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
@@ -89,32 +91,20 @@ COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_mod
|
||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
||||
COPY /docker/cronjobs /app/docker/cronjobs
|
||||
|
||||
# Copy required dependencies
|
||||
# Copy only @paralleldrive/cuid2 and @noble/hashes
|
||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
||||
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
|
||||
RUN npm install -g tsx typescript prisma pino-pretty
|
||||
RUN npm install -g tsx typescript prisma
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
ENV NODE_ENV="production"
|
||||
# USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
# Prepare volume for SAML preloaded connection
|
||||
RUN mkdir -p /home/nextjs/apps/web/saml-connection
|
||||
VOLUME /home/nextjs/apps/web/saml-connection
|
||||
|
||||
CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
|
||||
echo "Starting cron jobs..."; \
|
||||
supercronic -quiet /app/docker/cronjobs & \
|
||||
else \
|
||||
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
|
||||
fi; \
|
||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
(cd packages/database && npm run db:migrate:deploy) && \
|
||||
(cd packages/database && npm run db:create-saml-database:deploy) && \
|
||||
exec node apps/web/server.js
|
||||
exec node apps/web/server.js
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
|
||||
// Mock react-hot-toast so we can assert that a success message is shown
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Set up a spy for navigator.clipboard.writeText so it becomes a ViTest spy.
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: {
|
||||
// Using a mockResolvedValue resolves the promise as writeText is async.
|
||||
writeText: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("OnboardingSetupInstructions", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
// Provide some default props for testing
|
||||
const defaultProps = {
|
||||
environmentId: "env-123",
|
||||
webAppUrl: "https://example.com",
|
||||
channel: "app" as const, // Assuming channel is either "app" or "website"
|
||||
widgetSetupCompleted: false,
|
||||
};
|
||||
|
||||
test("renders HTML tab content by default", () => {
|
||||
render(<OnboardingSetupInstructions {...defaultProps} />);
|
||||
|
||||
// Since the default active tab is "html", we check for a unique text
|
||||
expect(
|
||||
screen.getByText(/environments.connect.insert_this_code_into_the_head_tag_of_your_website/i)
|
||||
).toBeInTheDocument();
|
||||
|
||||
// The HTML snippet contains a marker comment
|
||||
expect(screen.getByText("START")).toBeInTheDocument();
|
||||
|
||||
// Verify the "Copy Code" button is present
|
||||
expect(screen.getByRole("button", { name: /common.copy_code/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders NPM tab content when selected", async () => {
|
||||
render(<OnboardingSetupInstructions {...defaultProps} />);
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Click on the "NPM" tab to switch views.
|
||||
const npmTab = screen.getByText("NPM");
|
||||
await user.click(npmTab);
|
||||
|
||||
// Check that the install commands are present
|
||||
expect(screen.getByText(/npm install @formbricks\/js/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/yarn add @formbricks\/js/)).toBeInTheDocument();
|
||||
|
||||
// Verify the "Read Docs" link has the correct URL (based on channel prop)
|
||||
const readDocsLink = screen.getByRole("link", { name: /common.read_docs/i });
|
||||
expect(readDocsLink).toHaveAttribute("href", "https://formbricks.com/docs/app-surveys/framework-guides");
|
||||
});
|
||||
|
||||
test("copies HTML snippet to clipboard and shows success toast when Copy Code button is clicked", async () => {
|
||||
render(<OnboardingSetupInstructions {...defaultProps} />);
|
||||
const user = userEvent.setup();
|
||||
|
||||
const writeTextSpy = vi.spyOn(navigator.clipboard, "writeText");
|
||||
|
||||
// Click the "Copy Code" button
|
||||
const copyButton = screen.getByRole("button", { name: /common.copy_code/i });
|
||||
await user.click(copyButton);
|
||||
|
||||
// Ensure navigator.clipboard.writeText was called.
|
||||
expect(writeTextSpy).toHaveBeenCalled();
|
||||
const writtenText = (navigator.clipboard.writeText as any).mock.calls[0][0] as string;
|
||||
|
||||
// Check that the pasted snippet contains the expected environment values
|
||||
expect(writtenText).toContain('var appUrl = "https://example.com"');
|
||||
expect(writtenText).toContain('var environmentId = "env-123"');
|
||||
|
||||
// Verify that a success toast was shown
|
||||
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
||||
});
|
||||
|
||||
test("renders step-by-step manual link with correct URL in HTML tab", () => {
|
||||
render(<OnboardingSetupInstructions {...defaultProps} />);
|
||||
const manualLink = screen.getByRole("link", { name: /common.step_by_step_manual/i });
|
||||
expect(manualLink).toHaveAttribute(
|
||||
"href",
|
||||
"https://formbricks.com/docs/app-surveys/framework-guides#html"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -34,9 +34,10 @@ export const OnboardingSetupInstructions = ({
|
||||
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){
|
||||
var appUrl = "${webAppUrl}";
|
||||
var apiHost = "${webAppUrl}";
|
||||
var environmentId = "${environmentId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var userId = "testUser";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
@@ -44,9 +45,9 @@ export const OnboardingSetupInstructions = ({
|
||||
const htmlSnippetForWebsiteSurveys = `<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){
|
||||
var appUrl = "${webAppUrl}";
|
||||
var apiHost = "${webAppUrl}";
|
||||
var environmentId = "${environmentId}";
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=appUrl+"/js/formbricks.umd.cjs",t.onload=function(){window.formbricks?window.formbricks.setup({environmentId:environmentId,appUrl:appUrl}):console.error("Formbricks library failed to load properly. The formbricks object is not available.");};var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e)}();
|
||||
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/js/formbricks.umd.cjs";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->
|
||||
`;
|
||||
@@ -55,9 +56,10 @@ export const OnboardingSetupInstructions = ({
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.setup({
|
||||
formbricks.init({
|
||||
environmentId: "${environmentId}",
|
||||
appUrl: "${webAppUrl}",
|
||||
apiHost: "${webAppUrl}",
|
||||
userId: "testUser",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,9 +75,9 @@ export const OnboardingSetupInstructions = ({
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.setup({
|
||||
formbricks.init({
|
||||
environmentId: "${environmentId}",
|
||||
appUrl: "${webAppUrl}",
|
||||
apiHost: "${webAppUrl}",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { getDefaultEndingCard } from "@/app/lib/templates";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { TFnType } from "@tolgee/react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
|
||||
function logError(error: Error, context: string) {
|
||||
console.error(`Error in ${context}:`, error);
|
||||
}
|
||||
|
||||
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
||||
try {
|
||||
return {
|
||||
@@ -16,7 +19,7 @@ export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to create default XM survey template");
|
||||
logError(error, "getXMSurveyDefault");
|
||||
throw error; // Re-throw after logging
|
||||
}
|
||||
};
|
||||
@@ -446,7 +449,7 @@ export const getXMTemplates = (t: TFnType): TXMTemplate[] => {
|
||||
enpsSurvey(t),
|
||||
];
|
||||
} catch (error) {
|
||||
logger.error(error, "Unable to load XM templates, returning empty array");
|
||||
logError(error, "getXMTemplates");
|
||||
return []; // Return an empty array or handle as needed
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
||||
import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, organization } = await getOrganizationAuth(params.organizationId);
|
||||
|
||||
if (!session?.user) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) return notFound();
|
||||
|
||||
const organization = await getOrganization(params.organizationId);
|
||||
if (!organization) return notFound();
|
||||
|
||||
const organizations = await getOrganizationsByUserId(session.user.id);
|
||||
|
||||
const { features } = await getEnterpriseLicense();
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import ProjectOnboardingLayout from "./layout";
|
||||
|
||||
// Mock all the modules and functions that this layout uses:
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
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,
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/organization/auth", () => ({
|
||||
canUserAccessOrganization: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||
getOrganization: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: vi.fn(() => {
|
||||
// Return a mock translator that just returns the key
|
||||
return (key: string) => key;
|
||||
}),
|
||||
}));
|
||||
|
||||
// mock the child components
|
||||
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="toaster-client" />,
|
||||
}));
|
||||
|
||||
describe("ProjectOnboardingLayout", () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("redirects to /auth/login if there is no session", async () => {
|
||||
// Mock no session
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
const layoutElement = await ProjectOnboardingLayout({
|
||||
params: { organizationId: "org-123" },
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
});
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
// Layout returns nothing after redirect
|
||||
expect(layoutElement).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws an error if user does not exist", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||
user: { id: "user-123" },
|
||||
});
|
||||
vi.mocked(getUser).mockResolvedValueOnce(null); // no user in DB
|
||||
|
||||
await expect(
|
||||
ProjectOnboardingLayout({
|
||||
params: { organizationId: "org-123" },
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
})
|
||||
).rejects.toThrow("common.user_not_found");
|
||||
});
|
||||
|
||||
it("throws AuthorizationError if user cannot access organization", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
||||
|
||||
await expect(
|
||||
ProjectOnboardingLayout({
|
||||
params: { organizationId: "org-123" },
|
||||
children: <div data-testid="child-content">Child</div>,
|
||||
})
|
||||
).rejects.toThrow("common.not_authorized");
|
||||
});
|
||||
|
||||
it("throws an error if organization does not exist", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
||||
vi.mocked(getOrganization).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
ProjectOnboardingLayout({
|
||||
params: { organizationId: "org-123" },
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
})
|
||||
).rejects.toThrow("common.organization_not_found");
|
||||
});
|
||||
|
||||
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
||||
// Provide valid data
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
||||
vi.mocked(getOrganization).mockResolvedValueOnce({
|
||||
id: "org-123",
|
||||
name: "Test Org",
|
||||
billing: {
|
||||
plan: "enterprise",
|
||||
},
|
||||
} as TOrganization);
|
||||
|
||||
let layoutElement: React.ReactNode;
|
||||
// Because it's an async server component, do it in an act
|
||||
await act(async () => {
|
||||
layoutElement = await ProjectOnboardingLayout({
|
||||
params: { organizationId: "org-123" },
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
});
|
||||
render(layoutElement);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello!");
|
||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("toaster-client")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
@@ -17,8 +16,7 @@ const ProjectOnboardingLayout = async (props) => {
|
||||
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
@@ -28,9 +26,8 @@ const ProjectOnboardingLayout = async (props) => {
|
||||
}
|
||||
|
||||
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
|
||||
|
||||
if (!isAuthorized) {
|
||||
throw new AuthorizationError(t("common.not_authorized"));
|
||||
throw AuthorizationError;
|
||||
}
|
||||
|
||||
const organization = await getOrganization(params.organizationId);
|
||||
@@ -46,7 +43,6 @@ const ProjectOnboardingLayout = async (props) => {
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
||||
/>
|
||||
<ToasterClient />
|
||||
{children}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
@@ -16,10 +17,8 @@ interface ChannelPageProps {
|
||||
|
||||
const Page = async (props: ChannelPageProps) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { session } = await getOrganizationAuth(params.organizationId);
|
||||
|
||||
if (!session?.user) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
@@ -16,10 +17,8 @@ interface ModePageProps {
|
||||
|
||||
const Page = async (props: ModePageProps) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { session } = await getOrganizationAuth(params.organizationId);
|
||||
|
||||
if (!session?.user) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,6 @@ export const ProjectSettings = ({
|
||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||
<div className="z-0 h-3/4 w-3/4">
|
||||
<SurveyInline
|
||||
isPreviewMode={true}
|
||||
survey={previewSurvey(projectName || "my Product", t)}
|
||||
styling={{ brandColor: { light: brandColor } }}
|
||||
isBrandingEnabled={false}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Header } from "@/modules/ui/components/header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||
|
||||
@@ -27,20 +29,25 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
const { session, organization } = await getOrganizationAuth(params.organizationId);
|
||||
|
||||
if (!session?.user) {
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
const channel = searchParams.channel ?? null;
|
||||
const industry = searchParams.industry ?? null;
|
||||
const mode = searchParams.mode ?? "surveys";
|
||||
const channel = searchParams.channel || null;
|
||||
const industry = searchParams.industry || null;
|
||||
const mode = searchParams.mode || "surveys";
|
||||
const projects = await getUserProjects(session.user.id, params.organizationId);
|
||||
|
||||
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
|
||||
|
||||
const organization = await getOrganization(params.organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const canDoRoleManagement = await getRoleManagementPermission(organization.billing.plan);
|
||||
|
||||
if (!organizationTeams) {
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { 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 { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import SurveyEditorEnvironmentLayout from "./layout";
|
||||
|
||||
// mock all dependencies
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
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,
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("redirects to /auth/login if there is no session", async () => {
|
||||
// Mock no session
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
const layoutElement = await SurveyEditorEnvironmentLayout({
|
||||
params: { environmentId: "env-123" },
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
});
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
// No JSX is returned after redirect
|
||||
expect(layoutElement).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws error if user does not exist in DB", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
||||
|
||||
await expect(
|
||||
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);
|
||||
|
||||
await expect(
|
||||
SurveyEditorEnvironmentLayout({
|
||||
params: { environmentId: "env-123" },
|
||||
children: <div>Child</div>,
|
||||
})
|
||||
).rejects.toThrow("common.environment_not_found");
|
||||
});
|
||||
|
||||
it("renders environment layout if everything is valid", async () => {
|
||||
// Provide all valid data
|
||||
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(true);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
||||
id: "env-123",
|
||||
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);
|
||||
});
|
||||
|
||||
// Now confirm we got the child plus all the mocked sub-components
|
||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("dev-environment-banner")).toHaveTextContent("env-123");
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
@@ -21,8 +20,7 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
||||
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
@@ -48,23 +46,24 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
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">
|
||||
<DevEnvironmentBanner environment={environment} />
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
</ResponseFilterProvider>
|
||||
<>
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
environmentId={params.environmentId}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
/>
|
||||
<FormbricksClient userId={user.id} email={user.email} />
|
||||
<ToasterClient />
|
||||
<div className="flex h-screen flex-col">
|
||||
<DevEnvironmentBanner environment={environment} />
|
||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||
</div>
|
||||
</ResponseFilterProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { FormbricksClient } from "./FormbricksClient";
|
||||
|
||||
// Mock next/navigation hooks.
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: () => "/test-path",
|
||||
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.
|
||||
vi.mock("@/app/lib/formbricks", () => ({
|
||||
formbricksEnabled: true,
|
||||
}));
|
||||
|
||||
// Mock the Formbricks SDK module.
|
||||
vi.mock("@formbricks/js", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
setup: vi.fn(),
|
||||
setUserId: vi.fn(),
|
||||
setEmail: vi.fn(),
|
||||
registerRouteChange: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("FormbricksClient", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("calls setup, setUserId, setEmail and registerRouteChange on mount when enabled", () => {
|
||||
const mockSetup = vi.spyOn(formbricks, "setup");
|
||||
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||
|
||||
render(<FormbricksClient userId="user-123" email="test@example.com" />);
|
||||
|
||||
// Expect the first effect to call setup and assign the provided user details.
|
||||
expect(mockSetup).toHaveBeenCalledWith({
|
||||
environmentId: "env-test",
|
||||
appUrl: "https://api.test.com",
|
||||
});
|
||||
expect(mockSetUserId).toHaveBeenCalledWith("user-123");
|
||||
expect(mockSetEmail).toHaveBeenCalledWith("test@example.com");
|
||||
|
||||
// And the second effect should always register the route change when Formbricks is enabled.
|
||||
expect(mockRegisterRouteChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does not call setup, setUserId, or setEmail if userId is not provided yet still calls registerRouteChange", () => {
|
||||
const mockSetup = vi.spyOn(formbricks, "setup");
|
||||
const mockSetUserId = vi.spyOn(formbricks, "setUserId");
|
||||
const mockSetEmail = vi.spyOn(formbricks, "setEmail");
|
||||
const mockRegisterRouteChange = vi.spyOn(formbricks, "registerRouteChange");
|
||||
|
||||
render(<FormbricksClient userId="" email="test@example.com" />);
|
||||
|
||||
// Since userId is falsy, the first effect should not call setup or assign user details.
|
||||
expect(mockSetup).not.toHaveBeenCalled();
|
||||
expect(mockSetUserId).not.toHaveBeenCalled();
|
||||
expect(mockSetEmail).not.toHaveBeenCalled();
|
||||
|
||||
// The second effect only checks formbricksEnabled, so registerRouteChange should be called.
|
||||
expect(mockRegisterRouteChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -12,12 +12,12 @@ export const FormbricksClient = ({ userId, email }: { userId: string; email: str
|
||||
|
||||
useEffect(() => {
|
||||
if (formbricksEnabled && userId) {
|
||||
formbricks.setup({
|
||||
formbricks.init({
|
||||
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
||||
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
||||
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
||||
userId,
|
||||
});
|
||||
|
||||
formbricks.setUserId(userId);
|
||||
formbricks.setEmail(email);
|
||||
}
|
||||
}, [userId, email]);
|
||||
|
||||
@@ -2,14 +2,21 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
|
||||
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -18,24 +25,51 @@ export const metadata: Metadata = {
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
|
||||
const { isReadOnly, project, isBilling, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
const t = await getTranslate();
|
||||
|
||||
const [actionClasses] = await Promise.all([getActionClasses(params.environmentId)]);
|
||||
|
||||
const [actionClasses, organization, project] = await Promise.all([
|
||||
getActionClasses(params.environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getProjectByEnvironmentId(params.environmentId),
|
||||
]);
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const environments = await getEnvironments(project.id);
|
||||
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
|
||||
|
||||
if (!currentEnvironment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
|
||||
|
||||
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, project.id);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
}
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
const renderAddActionButton = () => (
|
||||
<AddActionModal
|
||||
environmentId={params.environmentId}
|
||||
@@ -48,7 +82,7 @@ const Page = async (props) => {
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
|
||||
<ActionClassesTable
|
||||
environment={environment}
|
||||
environment={currentEnvironment}
|
||||
otherEnvironment={otherEnvironment}
|
||||
otherEnvActionClasses={otherEnvActionClasses}
|
||||
environmentId={params.environmentId}
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
// PosthogIdentify.test.tsx
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
import { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { PosthogIdentify } from "./PosthogIdentify";
|
||||
|
||||
type PartialPostHog = Partial<ReturnType<typeof usePostHog>>;
|
||||
|
||||
vi.mock("posthog-js/react", () => ({
|
||||
usePostHog: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("PosthogIdentify", () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
const mockPostHog: PartialPostHog = {
|
||||
identify: mockIdentify,
|
||||
group: mockGroup,
|
||||
};
|
||||
|
||||
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
|
||||
|
||||
render(
|
||||
<PosthogIdentify
|
||||
session={{ user: { id: "user-123" } } as Session}
|
||||
user={
|
||||
{
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "engineer",
|
||||
objective: "increase_conversion",
|
||||
} as TUser
|
||||
}
|
||||
environmentId="env-456"
|
||||
organizationId="org-789"
|
||||
organizationName="Test Org"
|
||||
organizationBilling={
|
||||
{
|
||||
plan: "enterprise",
|
||||
limits: { monthly: { responses: 1000, miu: 5000 }, projects: 10 },
|
||||
} as TOrganizationBilling
|
||||
}
|
||||
isPosthogEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
// verify that identify is called with the session user id + extra info
|
||||
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: "engineer",
|
||||
objective: "increase_conversion",
|
||||
});
|
||||
|
||||
// environment + organization groups
|
||||
expect(mockGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mockGroup).toHaveBeenCalledWith("environment", "env-456", { name: "env-456" });
|
||||
expect(mockGroup).toHaveBeenCalledWith("organization", "org-789", {
|
||||
name: "Test Org",
|
||||
plan: "enterprise",
|
||||
responseLimit: 1000,
|
||||
miuLimit: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it("does nothing if isPosthogEnabled is false", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
const mockPostHog: PartialPostHog = {
|
||||
identify: mockIdentify,
|
||||
group: mockGroup,
|
||||
};
|
||||
|
||||
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
|
||||
|
||||
render(
|
||||
<PosthogIdentify
|
||||
session={{ user: { id: "user-123" } } as Session}
|
||||
user={{ name: "Test User", email: "test@example.com" } as TUser}
|
||||
isPosthogEnabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(mockIdentify).not.toHaveBeenCalled();
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if session user is missing", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
const mockPostHog: PartialPostHog = {
|
||||
identify: mockIdentify,
|
||||
group: mockGroup,
|
||||
};
|
||||
|
||||
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
|
||||
|
||||
render(
|
||||
<PosthogIdentify
|
||||
// no user in session
|
||||
session={{} as any}
|
||||
user={{ name: "Test User", email: "test@example.com" } as TUser}
|
||||
isPosthogEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
// Because there's no session.user, we skip identify
|
||||
expect(mockIdentify).not.toHaveBeenCalled();
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||
const mockIdentify = vi.fn();
|
||||
const mockGroup = vi.fn();
|
||||
|
||||
const mockPostHog: PartialPostHog = {
|
||||
identify: mockIdentify,
|
||||
group: mockGroup,
|
||||
};
|
||||
|
||||
vi.mocked(usePostHog).mockReturnValue(mockPostHog as ReturnType<typeof usePostHog>);
|
||||
|
||||
render(
|
||||
<PosthogIdentify
|
||||
session={{ user: { id: "user-123" } } as Session}
|
||||
user={{ name: "Test User", email: "test@example.com" } as TUser}
|
||||
isPosthogEnabled
|
||||
/>
|
||||
);
|
||||
|
||||
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
|
||||
name: "Test User",
|
||||
email: "test@example.com",
|
||||
role: undefined,
|
||||
objective: undefined,
|
||||
});
|
||||
// No environmentId or organizationId => no group calls
|
||||
expect(mockGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,12 @@
|
||||
import type { Session } from "next-auth";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { useEffect } from "react";
|
||||
import { env } from "@formbricks/lib/env";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
const posthogEnabled = env.NEXT_PUBLIC_POSTHOG_API_KEY && env.NEXT_PUBLIC_POSTHOG_API_HOST;
|
||||
|
||||
interface PosthogIdentifyProps {
|
||||
session: Session;
|
||||
user: TUser;
|
||||
@@ -13,7 +16,6 @@ interface PosthogIdentifyProps {
|
||||
organizationId?: string;
|
||||
organizationName?: string;
|
||||
organizationBilling?: TOrganizationBilling;
|
||||
isPosthogEnabled: boolean;
|
||||
}
|
||||
|
||||
export const PosthogIdentify = ({
|
||||
@@ -23,12 +25,11 @@ export const PosthogIdentify = ({
|
||||
organizationId,
|
||||
organizationName,
|
||||
organizationBilling,
|
||||
isPosthogEnabled,
|
||||
}: PosthogIdentifyProps) => {
|
||||
const posthog = usePostHog();
|
||||
|
||||
useEffect(() => {
|
||||
if (isPosthogEnabled && session.user && posthog) {
|
||||
if (posthogEnabled && session.user && posthog) {
|
||||
posthog.identify(session.user.id, {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
@@ -58,7 +59,6 @@ export const PosthogIdentify = ({
|
||||
user.email,
|
||||
user.role,
|
||||
user.objective,
|
||||
isPosthogEnabled,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
import { getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
export interface FilterValue {
|
||||
questionType: Partial<QuestionOption>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TopControlButtons } from "@/app/(app)/environments/[environmentId]/components/TopControlButtons";
|
||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
|
||||
@@ -23,6 +24,7 @@ export const TopControlBar = ({
|
||||
<TopControlButtons
|
||||
environment={environment}
|
||||
environments={environments}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={membershipRole}
|
||||
projectPermission={projectPermission}
|
||||
/>
|
||||
|
||||
@@ -6,9 +6,9 @@ import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CircleUserIcon, MessageCircleQuestionIcon, PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
@@ -16,6 +16,7 @@ import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||
interface TopControlButtonsProps {
|
||||
environment: TEnvironment;
|
||||
environments: TEnvironment[];
|
||||
isFormbricksCloud: boolean;
|
||||
membershipRole?: TOrganizationRole;
|
||||
projectPermission: TTeamPermission | null;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ interface TopControlButtonsProps {
|
||||
export const TopControlButtons = ({
|
||||
environment,
|
||||
environments,
|
||||
isFormbricksCloud,
|
||||
membershipRole,
|
||||
projectPermission,
|
||||
}: TopControlButtonsProps) => {
|
||||
@@ -36,15 +38,19 @@ export const TopControlButtons = ({
|
||||
return (
|
||||
<div className="z-50 flex items-center space-x-2">
|
||||
{!isBilling && <EnvironmentSwitch environment={environment} environments={environments} />}
|
||||
|
||||
<TooltipRenderer tooltipContent={t("common.share_feedback")}>
|
||||
<Button variant="ghost" size="icon" className="h-fit w-fit bg-slate-50 p-1" asChild>
|
||||
<Link href="https://github.com/formbricks/formbricks/issues/new/choose" target="_blank">
|
||||
<BugIcon />
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
|
||||
{isFormbricksCloud && (
|
||||
<TooltipRenderer tooltipContent={t("common.share_feedback")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-fit w-fit bg-slate-50 p-1"
|
||||
onClick={() => {
|
||||
formbricks.track("Top Menu: Product Feedback");
|
||||
}}>
|
||||
<MessageCircleQuestionIcon />
|
||||
</Button>
|
||||
</TooltipRenderer>
|
||||
)}
|
||||
<TooltipRenderer tooltipContent={t("common.account")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TIntegrationAirtableTables } from "@formbricks/types/integration/airtable";
|
||||
|
||||
export const fetchTables = async (environmentId: string, baseId: string) => {
|
||||
@@ -18,8 +17,7 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
logger.error({ errorText }, "authorize: Could not fetch airtable config");
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -17,25 +24,48 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||
|
||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [surveys, integrations] = await Promise.all([
|
||||
const [session, surveys, integrations, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
|
||||
(integration): integration is TIntegrationAirtable => integration.type === "airtable"
|
||||
);
|
||||
|
||||
let airtableArray: TIntegrationItem[] = [];
|
||||
if (airtableIntegration?.config.key) {
|
||||
if (airtableIntegration && airtableIntegration.config.key) {
|
||||
airtableArray = await getAirtableTables(params.environmentId);
|
||||
}
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
if (isReadOnly) {
|
||||
redirect("./");
|
||||
}
|
||||
|
||||
@@ -1,41 +1,25 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
|
||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||
googleSheetIntegration: ZIntegrationGoogleSheets,
|
||||
environmentId: z.string(),
|
||||
spreadsheetId: z.string(),
|
||||
});
|
||||
export async function getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration: TIntegrationGoogleSheets,
|
||||
environmentId: string,
|
||||
spreadsheetId: string
|
||||
) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
export const getSpreadsheetNameByIdAction = authenticatedActionClient
|
||||
.schema(ZGetSpreadsheetNameByIdAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
{
|
||||
type: "projectTeam",
|
||||
projectId: await getProjectIdFromEnvironmentId(parsedInput.environmentId),
|
||||
minPermission: "readWrite",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const integrationData = structuredClone(parsedInput.googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
|
||||
return await getSpreadsheetNameById(integrationData, parsedInput.spreadsheetId);
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
const integrationData = structuredClone(googleSheetIntegration);
|
||||
integrationData.config.data.forEach((data) => {
|
||||
data.createdAt = new Date(data.createdAt);
|
||||
});
|
||||
return await getSpreadsheetNameById(integrationData, spreadsheetId);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
isValidGoogleSheetsUrl,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
@@ -116,18 +115,11 @@ export const AddIntegrationModal = ({
|
||||
throw new Error(t("environments.integrations.select_at_least_one_question_error"));
|
||||
}
|
||||
const spreadsheetId = extractSpreadsheetIdFromUrl(spreadsheetUrl);
|
||||
const spreadsheetNameResponse = await getSpreadsheetNameByIdAction({
|
||||
const spreadsheetName = await getSpreadsheetNameByIdAction(
|
||||
googleSheetIntegration,
|
||||
environmentId,
|
||||
spreadsheetId,
|
||||
});
|
||||
|
||||
if (!spreadsheetNameResponse?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(spreadsheetNameResponse);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const spreadsheetName = spreadsheetNameResponse.data;
|
||||
spreadsheetId
|
||||
);
|
||||
|
||||
setIsLinkingSheet(true);
|
||||
integrationData.spreadsheetId = spreadsheetId;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
||||
const res = await fetch(`${apiHost}/api/google-sheet`, {
|
||||
method: "GET",
|
||||
@@ -7,8 +5,7 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
logger.error({ errorText }, "authorize: Could not fetch google sheet config");
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
GOOGLE_SHEETS_CLIENT_ID,
|
||||
@@ -12,7 +14,12 @@ import {
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
|
||||
@@ -20,20 +27,43 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
|
||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [surveys, integrations] = await Promise.all([
|
||||
const [session, surveys, integrations, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
|
||||
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
|
||||
);
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
if (isReadOnly) {
|
||||
redirect("./");
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import "server-only";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { selectSurvey } from "@formbricks/lib/survey/service";
|
||||
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
|
||||
export const getSurveys = reactCache(
|
||||
async (environmentId: string): Promise<TSurvey[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
try {
|
||||
const surveysPrisma = await prisma.survey.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
status: {
|
||||
not: "completed",
|
||||
},
|
||||
},
|
||||
select: selectSurvey,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return surveysPrisma.map((surveyPrisma) => transformPrismaSurvey<TSurvey>(surveyPrisma));
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
logger.error({ error }, "getSurveys: Could not fetch surveys");
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getSurveys-${environmentId}`],
|
||||
{
|
||||
tags: [surveyCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
UNSUPPORTED_TYPES_BY_NOTION,
|
||||
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
||||
import NotionLogo from "@/images/notion.png";
|
||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
@@ -20,6 +19,7 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/integrations/notion`, {
|
||||
method: "GET",
|
||||
@@ -7,8 +5,7 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
logger.error({ errorText }, "authorize: Could not fetch notion config");
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
NOTION_AUTH_URL,
|
||||
@@ -13,8 +15,13 @@ import {
|
||||
NOTION_REDIRECT_URI,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||
|
||||
@@ -27,20 +34,44 @@ const Page = async (props) => {
|
||||
NOTION_AUTH_URL &&
|
||||
NOTION_REDIRECT_URI
|
||||
);
|
||||
|
||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [surveys, notionIntegration] = await Promise.all([
|
||||
const [session, surveys, notionIntegration, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "notion"),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
let databasesArray: TIntegrationNotionDatabase[] = [];
|
||||
if (notionIntegration && (notionIntegration as TIntegrationNotion).config.key?.bot_id) {
|
||||
databasesArray = (await getNotionDatabases(environment.id)) ?? [];
|
||||
}
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
if (isReadOnly) {
|
||||
redirect("./");
|
||||
}
|
||||
|
||||
@@ -9,40 +9,71 @@ import notionLogo from "@/images/notion.png";
|
||||
import SlackLogo from "@/images/slacklogo.png";
|
||||
import WebhookLogo from "@/images/webhook.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { Card } from "@/modules/ui/components/integration-card";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Image from "next/image";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { TIntegrationType } from "@formbricks/types/integration";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const environmentId = params.environmentId;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { isReadOnly, environment, isBilling } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [
|
||||
environment,
|
||||
integrations,
|
||||
organization,
|
||||
session,
|
||||
userWebhookCount,
|
||||
zapierWebhookCount,
|
||||
makeWebhookCount,
|
||||
n8nwebhookCount,
|
||||
activePiecesWebhookCount,
|
||||
] = await Promise.all([
|
||||
getIntegrations(params.environmentId),
|
||||
getWebhookCountBySource(params.environmentId, "user"),
|
||||
getWebhookCountBySource(params.environmentId, "zapier"),
|
||||
getWebhookCountBySource(params.environmentId, "make"),
|
||||
getWebhookCountBySource(params.environmentId, "n8n"),
|
||||
getWebhookCountBySource(params.environmentId, "activepieces"),
|
||||
getEnvironment(environmentId),
|
||||
getIntegrations(environmentId),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
getWebhookCountBySource(environmentId, "user"),
|
||||
getWebhookCountBySource(environmentId, "zapier"),
|
||||
getWebhookCountBySource(environmentId, "make"),
|
||||
getWebhookCountBySource(environmentId, "n8n"),
|
||||
getWebhookCountBySource(environmentId, "activepieces"),
|
||||
]);
|
||||
|
||||
const isIntegrationConnected = (type: TIntegrationType) =>
|
||||
integrations.some((integration) => integration.type === type);
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
if (isBilling) {
|
||||
return redirect(`/environments/${params.environmentId}/settings/billing`);
|
||||
@@ -213,7 +244,7 @@ const Page = async (props) => {
|
||||
docsHref: "https://formbricks.com/docs/app-surveys/quickstart",
|
||||
docsText: t("common.docs"),
|
||||
docsNewTab: true,
|
||||
connectHref: `/environments/${params.environmentId}/project/app-connection`,
|
||||
connectHref: `/environments/${environmentId}/project/app-connection`,
|
||||
connectText: t("common.connect"),
|
||||
connectNewTab: false,
|
||||
label: "Javascript SDK",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
export const authorize = async (environmentId: string, apiHost: string): Promise<string> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/integrations/slack`, {
|
||||
method: "GET",
|
||||
@@ -7,8 +5,7 @@ export const authorize = async (environmentId: string, apiHost: string): Promise
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorText = await res.text();
|
||||
logger.error({ errorText }, "authorize: Could not fetch slack config");
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJSON = await res.json();
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
|
||||
@@ -16,16 +23,40 @@ const Page = async (props) => {
|
||||
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
|
||||
|
||||
const t = await getTranslate();
|
||||
|
||||
const { isReadOnly, environment } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const [surveys, slackIntegration] = await Promise.all([
|
||||
const [session, surveys, slackIntegration, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "slack"),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
const project = await getProjectByEnvironmentId(params.environmentId);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(
|
||||
session?.user.id,
|
||||
project.organizationId
|
||||
);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
if (isReadOnly) {
|
||||
redirect("./");
|
||||
}
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import React from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TMembership } from "@formbricks/types/memberships";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import EnvLayout from "./layout";
|
||||
|
||||
// mock all the dependencies
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||
POSTHOG_HOST: "mock-posthog-host",
|
||||
IS_POSTHOG_CONFIGURED: true,
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "test-githubID",
|
||||
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,
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: vi.fn(() => {
|
||||
return (key: string) => {
|
||||
return key;
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("next-auth", () => ({
|
||||
getServerSession: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/environment/auth", () => ({
|
||||
hasUserEnvironmentAccess: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/membership/service", () => ({
|
||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||
getOrganizationByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/project/service", () => ({
|
||||
getProjectByEnvironmentId: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
vi.mock("@formbricks/lib/aiModels", () => ({
|
||||
llmModel: {},
|
||||
}));
|
||||
|
||||
// mock all the components that are rendered in the layout
|
||||
|
||||
vi.mock("./components/PosthogIdentify", () => ({
|
||||
PosthogIdentify: () => <div data-testid="posthog-identify" />,
|
||||
}));
|
||||
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
||||
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
||||
}));
|
||||
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
||||
ToasterClient: () => <div data-testid="mock-toaster" />,
|
||||
}));
|
||||
vi.mock("./components/EnvironmentStorageHandler", () => ({
|
||||
default: () => <div data-testid="mock-storage-handler" />,
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
||||
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="mock-response-filter-provider">{children}</div>
|
||||
),
|
||||
}));
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/components/EnvironmentLayout", () => ({
|
||||
EnvironmentLayout: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="mock-environment-result">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("EnvLayout", () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it("redirects to /auth/login if there is no session", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||
|
||||
// Since it's an async server component, call EnvLayout yourself:
|
||||
const layoutElement = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
});
|
||||
|
||||
// Because we have no session, we expect a redirect to "/auth/login"
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
|
||||
// If your code calls redirect() early and returns no JSX,
|
||||
// layoutElement might be undefined or null.
|
||||
expect(layoutElement).toBeUndefined();
|
||||
});
|
||||
|
||||
it("redirects to /auth/login if user does not exist in DB", async () => {
|
||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||
user: { id: "user-123" },
|
||||
});
|
||||
|
||||
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
||||
|
||||
const layoutElement = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div data-testid="child-content">Hello!</div>,
|
||||
});
|
||||
|
||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
||||
expect(layoutElement).toBeUndefined();
|
||||
});
|
||||
|
||||
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(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ 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",
|
||||
email: "test@example.com",
|
||||
} as TUser);
|
||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div data-testid="child-content">Hello from children!</div>,
|
||||
})
|
||||
).rejects.toThrow("common.organization_not_found");
|
||||
});
|
||||
|
||||
it("throws if no project is found", 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(true);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div>Child</div>,
|
||||
})
|
||||
).rejects.toThrow("project_not_found");
|
||||
});
|
||||
|
||||
it("calls notFound if membership is missing", 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(true);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj-111" } as TProject);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce(null);
|
||||
|
||||
await expect(
|
||||
EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div>Child</div>,
|
||||
})
|
||||
).rejects.toThrow("membership_not_found");
|
||||
});
|
||||
|
||||
it("renders environment layout if everything is valid", 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(true);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
||||
vi.mocked(getProjectByEnvironmentId).mockResolvedValueOnce({ id: "proj-111" } as TProject);
|
||||
vi.mocked(getMembershipByUserIdOrganizationId).mockResolvedValueOnce({
|
||||
id: "membership-123",
|
||||
} as unknown as TMembership);
|
||||
|
||||
let layoutElement: React.ReactNode;
|
||||
|
||||
await act(async () => {
|
||||
layoutElement = await EnvLayout({
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
children: <div data-testid="child-content">Hello from children!</div>,
|
||||
});
|
||||
|
||||
// Now render the fully resolved layout
|
||||
render(layoutElement);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-storage-handler")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("mock-environment-result")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,8 +4,7 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
@@ -26,8 +25,7 @@ const EnvLayout = async (props: {
|
||||
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session?.user) {
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
@@ -51,29 +49,27 @@ const EnvLayout = async (props: {
|
||||
}
|
||||
|
||||
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
|
||||
|
||||
if (!membership) {
|
||||
throw new Error(t("common.membership_not_found"));
|
||||
}
|
||||
if (!membership) return notFound();
|
||||
|
||||
return (
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
environmentId={params.environmentId}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
||||
/>
|
||||
<FormbricksClient userId={user.id} email={user.email} />
|
||||
<ToasterClient />
|
||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||
{children}
|
||||
</EnvironmentLayout>
|
||||
</ResponseFilterProvider>
|
||||
<>
|
||||
<ResponseFilterProvider>
|
||||
<PosthogIdentify
|
||||
session={session}
|
||||
user={user}
|
||||
environmentId={params.environmentId}
|
||||
organizationId={organization.id}
|
||||
organizationName={organization.name}
|
||||
organizationBilling={organization.billing}
|
||||
/>
|
||||
<FormbricksClient userId={user.id} email={user.email} />
|
||||
<ToasterClient />
|
||||
<EnvironmentStorageHandler environmentId={params.environmentId} />
|
||||
<EnvironmentLayout environmentId={params.environmentId} session={session}>
|
||||
{children}
|
||||
</EnvironmentLayout>
|
||||
</ResponseFilterProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
|
||||
const EnvironmentPage = async (props) => {
|
||||
const params = await props.params;
|
||||
const { session, organization } = await getEnvironmentAuth(params.environmentId);
|
||||
const session = await getServerSession(authOptions);
|
||||
const t = await getTranslate();
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
return redirect(`/auth/login`);
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
@@ -27,7 +27,7 @@ export const EditAlerts = ({
|
||||
return (
|
||||
<>
|
||||
{memberships.map((membership) => (
|
||||
<div key={membership.organization.id}>
|
||||
<>
|
||||
<div className="mb-5 grid grid-cols-6 items-center space-x-3">
|
||||
<div className="col-span-3 flex items-center space-x-3">
|
||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||
@@ -110,7 +110,7 @@ export const EditAlerts = ({
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
|
||||
return (
|
||||
<>
|
||||
{memberships.map((membership) => (
|
||||
<div key={membership.organization.id}>
|
||||
<>
|
||||
<div className="mb-5 flex items-center space-x-3 text-sm font-medium">
|
||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||
|
||||
@@ -52,7 +52,7 @@ export const EditWeeklySummary = ({ memberships, user, environmentId }: EditAler
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -157,10 +157,6 @@ const Page = async (props) => {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
if (!memberships) {
|
||||
throw new Error(t("common.membership_not_found"));
|
||||
}
|
||||
|
||||
if (user?.notificationSettings) {
|
||||
user.notificationSettings = setCompleteNotificationSettings(user.notificationSettings, memberships);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteAccount } from "./components/DeleteAccount";
|
||||
@@ -21,16 +23,20 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const { environmentId } = params;
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
const { session } = await getEnvironmentAuth(params.environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(session.user.id);
|
||||
|
||||
const user = session?.user ? await getUser(session.user.id) : null;
|
||||
|
||||
if (!user) {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
const user = session && session.user ? await getUser(session.user.id) : null;
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
@@ -65,9 +71,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
description={t("environments.settings.profile.two_factor_authentication_description")}
|
||||
buttons={[
|
||||
{
|
||||
text: IS_FORMBRICKS_CLOUD
|
||||
? t("common.start_free_trial")
|
||||
: t("common.request_trial_license"),
|
||||
text: t("common.start_free_trial"),
|
||||
href: IS_FORMBRICKS_CLOUD
|
||||
? `/environments/${params.environmentId}/settings/billing`
|
||||
: "https://formbricks.com/upgrade-self-hosting-license",
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
@@ -17,8 +21,20 @@ const Page = async (props) => {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const { isMember, currentUserMembership } = await getEnvironmentAuth(params.environmentId);
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
const isPricingDisabled = isMember;
|
||||
|
||||
if (isPricingDisabled) {
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsOrganizationAIReady,
|
||||
getWhiteLabelPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import Page from "./page";
|
||||
|
||||
vi.mock("@formbricks/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: false,
|
||||
IS_PRODUCTION: false,
|
||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||
ENCRYPTION_KEY: "mock-encryption-key",
|
||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
||||
GITHUB_ID: "mock-github-id",
|
||||
GITHUB_SECRET: "mock-github-secret",
|
||||
GOOGLE_CLIENT_ID: "mock-google-client-id",
|
||||
GOOGLE_CLIENT_SECRET: "mock-google-client-secret",
|
||||
AZUREAD_CLIENT_ID: "mock-azuread-client-id",
|
||||
AZUREAD_CLIENT_SECRET: "mock-azure-client-secret",
|
||||
AZUREAD_TENANT_ID: "mock-azuread-tenant-id",
|
||||
OIDC_CLIENT_ID: "mock-oidc-client-id",
|
||||
OIDC_CLIENT_SECRET: "mock-oidc-client-secret",
|
||||
OIDC_ISSUER: "mock-oidc-issuer",
|
||||
OIDC_DISPLAY_NAME: "mock-oidc-display-name",
|
||||
OIDC_SIGNING_ALGORITHM: "mock-oidc-signing-algorithm",
|
||||
SAML_DATABASE_URL: "mock-saml-database-url",
|
||||
WEBAPP_URL: "mock-webapp-url",
|
||||
SMTP_HOST: "mock-smtp-host",
|
||||
SMTP_PORT: "mock-smtp-port",
|
||||
AI_AZURE_LLM_RESSOURCE_NAME: "mock-azure-llm-resource-name",
|
||||
AI_AZURE_LLM_API_KEY: "mock-azure-llm-api-key",
|
||||
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-azure-llm-deployment-id",
|
||||
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-azure-embeddings-resource-name",
|
||||
AI_AZURE_EMBEDDINGS_API_KEY: "mock-azure-embeddings-api-key",
|
||||
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-azure-embeddings-deployment-id",
|
||||
}));
|
||||
|
||||
vi.mock("@/tolgee/server", () => ({
|
||||
getTranslate: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/lib/user/service", () => ({
|
||||
getUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||
getEnvironmentAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
getIsMultiOrgEnabled: vi.fn(),
|
||||
getIsOrganizationAIReady: vi.fn(),
|
||||
getWhiteLabelPermission: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("Page", () => {
|
||||
let mockEnvironmentAuth = {
|
||||
session: { user: { id: "test-user-id" } },
|
||||
currentUserMembership: { role: "owner" },
|
||||
organization: { id: "test-organization-id", billing: { plan: "free" } },
|
||||
isOwner: true,
|
||||
isManager: false,
|
||||
} as unknown as TEnvironmentAuth;
|
||||
|
||||
const mockUser = { id: "test-user-id" } as TUser;
|
||||
const mockTranslate = vi.fn((key) => key);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getTranslate).mockResolvedValue(mockTranslate);
|
||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true);
|
||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("renders the page with organization settings", async () => {
|
||||
const props = {
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
|
||||
const result = await Page(props);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders if session user id empty", async () => {
|
||||
mockEnvironmentAuth.session.user.id = "";
|
||||
|
||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||
|
||||
const props = {
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
|
||||
const result = await Page(props);
|
||||
|
||||
expect(result).toBeTruthy();
|
||||
});
|
||||
|
||||
it("handles getEnvironmentAuth error", async () => {
|
||||
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
||||
|
||||
const props = {
|
||||
params: Promise.resolve({ environmentId: "env-123" }),
|
||||
};
|
||||
|
||||
await expect(Page(props)).rejects.toThrow("Authentication error");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,21 @@
|
||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import {
|
||||
getIsMultiOrgEnabled,
|
||||
getIsOrganizationAIReady,
|
||||
getWhiteLabelPermission,
|
||||
} from "@/modules/ee/license-check/lib/utils";
|
||||
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { SettingsCard } from "../../components/SettingsCard";
|
||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||
@@ -20,13 +24,20 @@ import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm"
|
||||
const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
|
||||
const { session, currentUserMembership, organization, isOwner, isManager } = await getEnvironmentAuth(
|
||||
params.environmentId
|
||||
);
|
||||
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
const user = session?.user?.id ? await getUser(session.user.id) : null;
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isOwner, isManager } = getAccessFlags(currentUserMembership?.role);
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
const hasWhiteLabelPermission = await getWhiteLabelPermission(organization.billing.plan);
|
||||
|
||||
@@ -73,7 +84,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
environmentId={params.environmentId}
|
||||
isReadOnly={!isOwnerOrManager}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
fbLogoUrl={FB_LOGO_URL}
|
||||
user={user}
|
||||
/>
|
||||
{isMultiOrgEnabled && (
|
||||
@@ -88,7 +98,7 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
<SettingsId title={t("common.organization_id")} id={organization.id}></SettingsId>
|
||||
<SettingsId title={t("common.organization")} id={organization.id}></SettingsId>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
||||
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
|
||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||
import { ResponseBadges } from "@/modules/ui/components/response-badges";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
@@ -13,6 +12,7 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { processResponseData } from "@formbricks/lib/responses";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
|
||||
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TResponseTableData } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
@@ -200,6 +200,13 @@ export const generateResponseTableColumns = (
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="font-normal">
|
||||
{t("environments.surveys.responses.how_to_identify_users")}
|
||||
<Link
|
||||
className="underline underline-offset-2 hover:text-slate-900"
|
||||
href="https://formbricks.com/docs/link-surveys/user-identification"
|
||||
target="_blank">
|
||||
{t("common.link_surveys")}
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<Link
|
||||
className="underline underline-offset-2 hover:text-slate-900"
|
||||
href="https://formbricks.com/docs/app-surveys/user-identification"
|
||||
|
||||
@@ -3,16 +3,24 @@ import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[
|
||||
import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner";
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getTranslate } from "@/tolgee/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import {
|
||||
MAX_RESPONSES_FOR_INSIGHT_GENERATION,
|
||||
RESPONSES_PER_PAGE,
|
||||
WEBAPP_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
@@ -22,25 +30,47 @@ import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
const [survey, environment] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||
|
||||
const survey = await getSurvey(params.surveyId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
if (!survey) {
|
||||
throw new Error(t("common.survey_not_found"));
|
||||
}
|
||||
const project = await getProjectByEnvironmentId(environment.id);
|
||||
if (!project) {
|
||||
throw new Error(t("common.project_not_found"));
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
|
||||
if (!user) {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const permission = await getProjectPermissionByUserId(session.user.id, project.id);
|
||||
const { hasReadAccess } = getTeamPermissionFlags(permission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
const isAIEnabled = await getIsAIEnabled({
|
||||
isAIEnabled: organization.isAIEnabled,
|
||||
billing: organization.billing,
|
||||
|
||||
@@ -51,9 +51,9 @@ export const HiddenFieldsSummary = ({ environment, questionSummary, locale }: Hi
|
||||
<div className="col-span-2 pl-4 md:pl-6">{t("common.response")}</div>
|
||||
<div className="px-4 md:px-6">{t("common.time")}</div>
|
||||
</div>
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response, idx) => (
|
||||
{questionSummary.samples.slice(0, visibleResponses).map((response) => (
|
||||
<div
|
||||
key={`${response.value}-${idx}`}
|
||||
key={response.value}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.contact ? (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import type { JSX } from "react";
|
||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export const ShareEmbedSurvey = ({
|
||||
|
||||
const [activeId, setActiveId] = useState(survey.type === "link" ? tabs[0].id : tabs[3].id);
|
||||
const [showView, setShowView] = useState<"start" | "embed" | "panel">("start");
|
||||
const [surveyUrl, setSurveyUrl] = useState("");
|
||||
const [surveyUrl, setSurveyUrl] = useState(webAppUrl + "/s/" + survey.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (survey.type !== "link") {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { getQuestionIcon } from "@/modules/survey/lib/questions";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { TimerIcon } from "lucide-react";
|
||||
import { JSX } from "react";
|
||||
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { ShareEmbedSurvey } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ShareEmbedSurvey";
|
||||
import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage";
|
||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
||||
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
|
||||
import { Badge } from "@/modules/ui/components/badge";
|
||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
@@ -51,7 +49,6 @@ export const SurveyAnalysisCTA = ({
|
||||
});
|
||||
|
||||
const surveyUrl = useMemo(() => `${webAppUrl}/s/${survey.id}`, [survey.id, webAppUrl]);
|
||||
const { refreshSingleUseId } = useSingleUseId(survey);
|
||||
|
||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||
|
||||
@@ -74,11 +71,8 @@ export const SurveyAnalysisCTA = ({
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
refreshSingleUseId()
|
||||
.then((newId) => {
|
||||
const linkToCopy = copySurveyLink(surveyUrl, newId);
|
||||
return navigator.clipboard.writeText(linkToCopy);
|
||||
})
|
||||
navigator.clipboard
|
||||
.writeText(surveyUrl)
|
||||
.then(() => {
|
||||
toast.success(t("common.copied_to_clipboard"));
|
||||
})
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { MobileAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/MobileAppTab";
|
||||
import { WebAppTab } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/shareEmbedModal/WebAppTab";
|
||||
import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
export const AppTab = () => {
|
||||
export const AppTab = ({ environmentId }) => {
|
||||
const { t } = useTranslate();
|
||||
const [selectedTab, setSelectedTab] = useState("webapp");
|
||||
|
||||
@@ -21,7 +20,79 @@ export const AppTab = () => {
|
||||
handleOptionChange={(value) => setSelectedTab(value)}
|
||||
/>
|
||||
|
||||
<div className="mt-4">{selectedTab === "webapp" ? <WebAppTab /> : <MobileAppTab />}</div>
|
||||
<div className="mt-4">
|
||||
{selectedTab === "webapp" ? <WebAppTab environmentId={environmentId} /> : <MobileAppTab />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MobileAppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-slate-800">
|
||||
{t("environments.surveys.summary.how_to_embed_a_survey_on_your_react_native_app")}
|
||||
</p>
|
||||
<ol className="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-700">
|
||||
<li>
|
||||
{t("common.follow_these")}{" "}
|
||||
<Link
|
||||
href="https://formbricks.com/docs/developer-docs/react-native-in-app-surveys"
|
||||
target="_blank"
|
||||
className="decoration-brand-dark font-medium underline underline-offset-2">
|
||||
{t("environments.surveys.summary.setup_instructions_for_react_native_apps")}
|
||||
</Link>{" "}
|
||||
{t("environments.surveys.summary.to_connect_your_app_with_formbricks")}
|
||||
</li>
|
||||
</ol>
|
||||
<div className="mt-2 text-sm italic text-slate-700">
|
||||
{t("environments.surveys.summary.were_working_on_sdks_for_flutter_swift_and_kotlin")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WebAppTab = ({ environmentId }) => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-slate-800">
|
||||
{t("environments.surveys.summary.how_to_embed_a_survey_on_your_web_app")}
|
||||
</p>
|
||||
<ol className="mt-4 list-decimal space-y-2 pl-5 text-sm text-slate-700">
|
||||
<li>
|
||||
{t("common.follow_these")}{" "}
|
||||
<Link
|
||||
href={`/environments/${environmentId}/project/app-connection`}
|
||||
target="_blank"
|
||||
className="decoration-brand-dark font-medium underline underline-offset-2">
|
||||
{t("environments.surveys.summary.setup_instructions")}
|
||||
</Link>{" "}
|
||||
{t("environments.surveys.summary.to_connect_your_web_app_with_formbricks")}
|
||||
</li>
|
||||
<li>
|
||||
{t("environments.surveys.summary.learn_how_to")}{" "}
|
||||
<Link
|
||||
href="https://formbricks.com/docs/app-surveys/user-identification"
|
||||
target="_blank"
|
||||
className="decoration-brand-dark font-medium underline underline-offset-2">
|
||||
{t("environments.surveys.summary.identify_users_and_set_attributes")}
|
||||
</Link>{" "}
|
||||
{t("environments.surveys.summary.to_run_highly_targeted_surveys")}.
|
||||
</li>
|
||||
<li>
|
||||
{t("environments.surveys.summary.make_sure_the_survey_type_is_set_to")}{" "}
|
||||
<b>{t("common.app_survey")}</b>
|
||||
</li>
|
||||
<li>{t("environments.surveys.summary.define_when_and_where_the_survey_should_pop_up")}</li>
|
||||
</ol>
|
||||
<div className="mt-4">
|
||||
<video autoPlay loop muted className="w-full rounded-xl border border-slate-200">
|
||||
<source src="/video/tooltips/change-survey-type-app.mp4" type="video/mp4" />
|
||||
{t("environments.surveys.summary.unsupported_video_tag_warning")}
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,7 +88,7 @@ export const EmbedView = ({
|
||||
locale={locale}
|
||||
/>
|
||||
) : activeId === "app" ? (
|
||||
<AppTab />
|
||||
<AppTab environmentId={environmentId} />
|
||||
) : null}
|
||||
<div className="mt-2 rounded-md p-3 text-center lg:hidden">
|
||||
{tabs.slice(0, 2).map((tab) => (
|
||||
|
||||
@@ -16,8 +16,12 @@ interface LinkTabProps {
|
||||
|
||||
export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }: LinkTabProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const docsLinks = [
|
||||
{
|
||||
title: t("environments.surveys.summary.identify_users"),
|
||||
description: t("environments.surveys.summary.identify_users_description"),
|
||||
link: "https://formbricks.com/docs/link-surveys/user-identification",
|
||||
},
|
||||
{
|
||||
title: t("environments.surveys.summary.data_prefilling"),
|
||||
description: t("environments.surveys.summary.data_prefilling_description"),
|
||||
@@ -49,7 +53,6 @@ export const LinkTab = ({ survey, webAppUrl, surveyUrl, setSurveyUrl, locale }:
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<p className="pt-2 font-semibold text-slate-700">
|
||||
{t("environments.surveys.summary.you_can_do_a_lot_more_with_links_surveys")} 💡
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const MobileAppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTitle>{t("environments.surveys.summary.quickstart_mobile_apps")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.summary.quickstart_mobile_apps_description")}
|
||||
<Button asChild className="w-fit" size="sm" variant="link">
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/framework-guides"
|
||||
target="_blank">
|
||||
{t("common.learn_more")}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
@@ -85,10 +85,8 @@ export const PanelInfoView = ({ disableBack, handleInitialPageButton }: PanelInf
|
||||
</p>
|
||||
</div>
|
||||
<Button className="justify-center" asChild>
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/link-surveys/market-research-panel"
|
||||
target="_blank">
|
||||
{t("common.learn_more")}
|
||||
<Link href="https://formbricks.com/docs/link-surveys/market-research-panel" target="_blank">
|
||||
{t("common.get_started")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const WebAppTab = () => {
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<Alert>
|
||||
<AlertTitle>{t("environments.surveys.summary.quickstart_web_apps")}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("environments.surveys.summary.quickstart_web_apps_description")}
|
||||
<Button asChild className="w-fit" size="sm" variant="link">
|
||||
<Link
|
||||
href="https://formbricks.com/docs/xm-and-surveys/surveys/website-app-surveys/quickstart"
|
||||
target="_blank">
|
||||
{t("common.learn_more")}
|
||||
</Link>
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user