Compare commits
149 Commits
fix/sonarq
...
formbricks
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ca3aecd6d | ||
|
|
c4aa83492c | ||
|
|
b9d62f6af2 | ||
|
|
f7ac38953b | ||
|
|
6441c0aa31 | ||
|
|
16479eb6cf | ||
|
|
69472c21c2 | ||
|
|
c270688e8f | ||
|
|
00c86c7082 | ||
|
|
e95e9f9fda | ||
|
|
1588c2f47b | ||
|
|
53850c96db | ||
|
|
ae2cb15055 | ||
|
|
8bf1e096c0 | ||
|
|
0052dc88f0 | ||
|
|
d67d62df45 | ||
|
|
5d45de6bc4 | ||
|
|
cf5bc51e94 | ||
|
|
9a7d24ea4e | ||
|
|
649f28ff8d | ||
|
|
bc5a81d146 | ||
|
|
7dce35bde4 | ||
|
|
f30ebc32ec | ||
|
|
027bc20975 | ||
|
|
3b1cddb9ce | ||
|
|
bd22aaaa86 | ||
|
|
e0e42d2eed | ||
|
|
616210f1bf | ||
|
|
ff2e7f6cc7 | ||
|
|
d1ce037f7d | ||
|
|
91f87f4b7b | ||
|
|
61657b9f9a | ||
|
|
476d032642 | ||
|
|
7538e570c5 | ||
|
|
66fcf4b79b | ||
|
|
21371b1815 | ||
|
|
a53c13d6ed | ||
|
|
1a0c6e72b2 | ||
|
|
ba7c8b79b1 | ||
|
|
d7b504eed0 | ||
|
|
a1df10eb09 | ||
|
|
92be409d4f | ||
|
|
665c7c6bf1 | ||
|
|
6c2ff7ee08 | ||
|
|
295a1bf402 | ||
|
|
3e6f558b08 | ||
|
|
aad5a59e82 | ||
|
|
36d02480b2 | ||
|
|
99454ac57b | ||
|
|
e2915f878e | ||
|
|
710a813e9b | ||
|
|
8bdb818995 | ||
|
|
20466c3800 | ||
|
|
faf6c2d062 | ||
|
|
a760a3c341 | ||
|
|
94e6d2f215 | ||
|
|
a6f1c0f63d | ||
|
|
c653996cbb | ||
|
|
da44fef89d | ||
|
|
4dc2c5e3df | ||
|
|
1797c2ae20 | ||
|
|
3b5da01c0a | ||
|
|
0f1bdce002 | ||
|
|
7c8f3e826f | ||
|
|
f21d63bb55 | ||
|
|
f223bb3d3f | ||
|
|
51001d07b6 | ||
|
|
a9eedd3c7a | ||
|
|
b0aa08fe4e | ||
|
|
8d45d24d55 | ||
|
|
8c1b9f81b9 | ||
|
|
71fad1c22b | ||
|
|
292266c597 | ||
|
|
54e589a6a0 | ||
|
|
fb3f425c27 | ||
|
|
1aaa30c6e9 | ||
|
|
8611410b21 | ||
|
|
40fa7a69c0 | ||
|
|
5eca30e513 | ||
|
|
4b78493782 | ||
|
|
2ce44b734f | ||
|
|
85d8f8c3ae | ||
|
|
3f16291137 | ||
|
|
a5958d5653 | ||
|
|
fdbdf8207a | ||
|
|
630e5489ec | ||
|
|
36943bb786 | ||
|
|
e1bbb0a10f | ||
|
|
27da540846 | ||
|
|
7d7f6ed04a | ||
|
|
ff01bc342d | ||
|
|
cd8b40b569 | ||
|
|
31c742f7a8 | ||
|
|
d6a7a2c21f | ||
|
|
499ecab691 | ||
|
|
df06540f1b | ||
|
|
a32b213ca5 | ||
|
|
6120f992a4 | ||
|
|
389a551a69 | ||
|
|
8ddbdc0e1e | ||
|
|
302c6a90c0 | ||
|
|
18e597d8a3 | ||
|
|
81d717ccff | ||
|
|
2e979c7323 | ||
|
|
4dfd15d6dd | ||
|
|
5b9bf3ff43 | ||
|
|
d2f7485098 | ||
|
|
f8fee1fba7 | ||
|
|
19249ca00f | ||
|
|
01e5700340 | ||
|
|
ff2f7660a6 | ||
|
|
2bc05e2b4a | ||
|
|
137c6447b7 | ||
|
|
ebc8f0c917 | ||
|
|
5a8d10b5b4 | ||
|
|
875815fb62 | ||
|
|
cdf526e130 | ||
|
|
b685032b34 | ||
|
|
a171f9cb00 | ||
|
|
c452f05ec2 | ||
|
|
93d91f80f2 | ||
|
|
7b764c8427 | ||
|
|
016289c8cb | ||
|
|
93a9575389 | ||
|
|
9e265adf14 | ||
|
|
eb08a0ed14 | ||
|
|
c533f37983 | ||
|
|
ca4f8385e4 | ||
|
|
3eb9aa74ed | ||
|
|
637b51464c | ||
|
|
fd9585a66e | ||
|
|
49ecbcb0c9 | ||
|
|
1132bdd66a | ||
|
|
c7d6ed9ea3 | ||
|
|
782528f169 | ||
|
|
104c78275f | ||
|
|
d9d88f7175 | ||
|
|
bf7e24cf11 | ||
|
|
c8aba01db3 | ||
|
|
a896c7e46e | ||
|
|
8018ec14a2 | ||
|
|
9c3208c860 | ||
|
|
e1063964cf | ||
|
|
38568738cc | ||
|
|
15b8358b14 | ||
|
|
2173cb2610 | ||
|
|
87b925d622 | ||
|
|
885b06cc26 | ||
|
|
adb6a5f41e |
30
.env.example
@@ -93,10 +93,6 @@ EMAIL_VERIFICATION_DISABLED=1
|
|||||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||||
PASSWORD_RESET_DISABLED=1
|
PASSWORD_RESET_DISABLED=1
|
||||||
|
|
||||||
# Signup. Disable the ability for new users to create an account.
|
|
||||||
# Note: This variable is only available to the SaaS setup of Formbricks Cloud. Signup is disable by default for self-hosting.
|
|
||||||
# SIGNUP_DISABLED=1
|
|
||||||
|
|
||||||
# Email login. Disable the ability for users to login with email.
|
# Email login. Disable the ability for users to login with email.
|
||||||
# EMAIL_AUTH_DISABLED=1
|
# EMAIL_AUTH_DISABLED=1
|
||||||
|
|
||||||
@@ -120,6 +116,10 @@ IMPRINT_ADDRESS=
|
|||||||
# TURNSTILE_SITE_KEY=
|
# TURNSTILE_SITE_KEY=
|
||||||
# TURNSTILE_SECRET_KEY=
|
# TURNSTILE_SECRET_KEY=
|
||||||
|
|
||||||
|
# Google reCAPTCHA v3 keys
|
||||||
|
RECAPTCHA_SITE_KEY=
|
||||||
|
RECAPTCHA_SECRET_KEY=
|
||||||
|
|
||||||
# Configure Github Login
|
# Configure Github Login
|
||||||
GITHUB_ID=
|
GITHUB_ID=
|
||||||
GITHUB_SECRET=
|
GITHUB_SECRET=
|
||||||
@@ -154,11 +154,6 @@ NOTION_OAUTH_CLIENT_SECRET=
|
|||||||
STRIPE_SECRET_KEY=
|
STRIPE_SECRET_KEY=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
|
|
||||||
# Configure Formbricks usage within Formbricks
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
|
||||||
|
|
||||||
# Oauth credentials for Google sheet integration
|
# Oauth credentials for Google sheet integration
|
||||||
GOOGLE_SHEETS_CLIENT_ID=
|
GOOGLE_SHEETS_CLIENT_ID=
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET=
|
GOOGLE_SHEETS_CLIENT_SECRET=
|
||||||
@@ -177,8 +172,9 @@ ENTERPRISE_LICENSE_KEY=
|
|||||||
# Automatically assign new users to a specific organization and role within that organization
|
# Automatically assign new users to a specific organization and role within that organization
|
||||||
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
# Insert an existing organization id or generate a valid CUID for a new one at https://www.getuniqueid.com/cuid (e.g. cjld2cjxh0000qzrmn831i7rn)
|
||||||
# (Role Management is an Enterprise feature)
|
# (Role Management is an Enterprise feature)
|
||||||
# DEFAULT_ORGANIZATION_ID=
|
|
||||||
# DEFAULT_ORGANIZATION_ROLE=owner
|
# DEFAULT_ORGANIZATION_ROLE=owner
|
||||||
|
# AUTH_SSO_DEFAULT_TEAM_ID=
|
||||||
|
# AUTH_SKIP_INVITE_FOR_SSO=
|
||||||
|
|
||||||
# Send new users to Brevo
|
# Send new users to Brevo
|
||||||
# BREVO_API_KEY=
|
# BREVO_API_KEY=
|
||||||
@@ -207,12 +203,6 @@ UNKEY_ROOT_KEY=
|
|||||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||||
# CUSTOM_CACHE_DISABLED=1
|
# CUSTOM_CACHE_DISABLED=1
|
||||||
|
|
||||||
# Azure AI settings
|
|
||||||
# AI_AZURE_RESSOURCE_NAME=
|
|
||||||
# AI_AZURE_API_KEY=
|
|
||||||
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
|
||||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
|
||||||
|
|
||||||
# INTERCOM_APP_ID=
|
# INTERCOM_APP_ID=
|
||||||
# INTERCOM_SECRET_KEY=
|
# INTERCOM_SECRET_KEY=
|
||||||
|
|
||||||
@@ -220,3 +210,11 @@ UNKEY_ROOT_KEY=
|
|||||||
# PROMETHEUS_ENABLED=
|
# PROMETHEUS_ENABLED=
|
||||||
# PROMETHEUS_EXPORTER_PORT=
|
# PROMETHEUS_EXPORTER_PORT=
|
||||||
|
|
||||||
|
# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry.
|
||||||
|
# SENTRY_DSN=
|
||||||
|
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
|
||||||
|
# It's used automatically by Sentry during the build for authentication when uploading source maps.
|
||||||
|
# SENTRY_AUTH_TOKEN=
|
||||||
|
|
||||||
|
# Disable the user management from UI
|
||||||
|
# DISABLE_USER_MANAGEMENT=1
|
||||||
14
.github/actions/cache-build-web/action.yml
vendored
@@ -8,6 +8,14 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
default: "0"
|
default: "0"
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
turbo_token:
|
||||||
|
description: "Turborepo token"
|
||||||
|
required: false
|
||||||
|
turbo_team:
|
||||||
|
description: "Turborepo team"
|
||||||
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -41,7 +49,7 @@ runs:
|
|||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -62,6 +70,8 @@ runs:
|
|||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
pnpm build --filter=@formbricks/web...
|
pnpm build --filter=@formbricks/web...
|
||||||
|
|
||||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ inputs.turbo_token }}
|
||||||
|
TURBO_TEAM: ${{ inputs.turbo_team }}
|
||||||
|
|||||||
31
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# Testing Instructions
|
||||||
|
|
||||||
|
When generating test files inside the "/app/web" path, follow these rules:
|
||||||
|
|
||||||
|
- You are an experienced senior software engineer
|
||||||
|
- Use vitest
|
||||||
|
- Ensure 100% code coverage
|
||||||
|
- Add as few comments as possible
|
||||||
|
- The test file should be located in the same folder as the original file
|
||||||
|
- Use the `test` function instead of `it`
|
||||||
|
- Follow the same test pattern used for other files in the package where the file is located
|
||||||
|
- All imports should be at the top of the file, not inside individual tests
|
||||||
|
- For mocking inside "test" blocks use "vi.mocked"
|
||||||
|
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file. Do this only when the test file is created.
|
||||||
|
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
|
||||||
|
- When using "screen.getByText" check for the tolgee string if it is being used in the file.
|
||||||
|
- The types for mocked variables can be found in the "packages/types" path. Be sure that every imported type exists before using it. Don't create types that are not already in the codebase.
|
||||||
|
- When mocking data check if the properties added are part of the type of the object being mocked. Only specify known properties, don't use properties that are not part of the type.
|
||||||
|
|
||||||
|
If it's a test for a ".tsx" file, follow these extra instructions:
|
||||||
|
|
||||||
|
- Add this code inside the "describe" block and before any test:
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
- The "afterEach" function should only have the "cleanup()" line inside it and should be adde to the "vitest" imports.
|
||||||
|
- For click events, import userEvent from "@testing-library/user-event"
|
||||||
|
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
|
||||||
|
- You don't need to mock @tolgee/react
|
||||||
@@ -19,12 +19,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Apply labels from linked issue to PR
|
- name: Apply labels from linked issue to PR
|
||||||
uses: actions/github-script@211cb3fefb35a799baa5156f9321bb774fe56294 # v5.2.0
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
8
.github/workflows/build-web.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build Formbricks-web
|
name: Build Formbricks-web
|
||||||
@@ -13,11 +13,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Build & Cache Web Binaries
|
- name: Build & Cache Web Binaries
|
||||||
@@ -25,3 +25,5 @@ jobs:
|
|||||||
id: cache-build-web
|
id: cache-build-web
|
||||||
with:
|
with:
|
||||||
e2e_testing_mode: "0"
|
e2e_testing_mode: "0"
|
||||||
|
turbo_token: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
turbo_team: ${{ vars.TURBO_TEAM }}
|
||||||
|
|||||||
2
.github/workflows/chromatic.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/dependency-review.yml
vendored
@@ -17,11 +17,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 'Checkout Repository'
|
- name: 'Checkout Repository'
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
|
||||||
|
|||||||
44
.github/workflows/deploy-formbricks-cloud.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
VERSION:
|
||||||
description: 'The version of the Docker image to release'
|
description: 'The version of the Docker image to release, full image tag if image tag is v0.0.0 enter v0.0.0.'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
REPOSITORY:
|
REPOSITORY:
|
||||||
@@ -12,6 +12,13 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
|
ENVIRONMENT:
|
||||||
|
description: 'The environment to deploy to'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- stage
|
||||||
|
- prod
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
VERSION:
|
||||||
@@ -23,6 +30,10 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
|
ENVIRONMENT:
|
||||||
|
description: 'The environment to deploy to'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -33,7 +44,14 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
|
- name: Tailscale
|
||||||
|
uses: tailscale/github-action@v3
|
||||||
|
with:
|
||||||
|
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||||
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||||
|
tags: tag:github
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||||
@@ -48,6 +66,8 @@ jobs:
|
|||||||
AWS_REGION: eu-central-1
|
AWS_REGION: eu-central-1
|
||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
- uses: helmfile/helmfile-action@v2
|
||||||
|
name: Deploy Formbricks Cloud Prod
|
||||||
|
if: inputs.ENVIRONMENT == 'prod'
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
@@ -55,10 +75,28 @@ jobs:
|
|||||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.FORMBRICKS_INGRESS_CERT_ARN }}
|
||||||
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
FORMBRICKS_ROLE_ARN: ${{ secrets.FORMBRICKS_ROLE_ARN }}
|
||||||
with:
|
with:
|
||||||
|
helmfile-version: 'v1.0.0'
|
||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
helmfile-args: apply
|
helmfile-args: apply -l environment=prod
|
||||||
|
helmfile-auto-init: "false"
|
||||||
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
- uses: helmfile/helmfile-action@v2
|
||||||
|
name: Deploy Formbricks Cloud Stage
|
||||||
|
if: inputs.ENVIRONMENT == 'stage'
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.VERSION }}
|
||||||
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
|
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
||||||
|
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
||||||
|
with:
|
||||||
|
helmfile-version: 'v1.0.0'
|
||||||
|
helm-plugins: >
|
||||||
|
https://github.com/databus23/helm-diff,
|
||||||
|
https://github.com/jkroepke/helm-secrets
|
||||||
|
helmfile-args: apply -l environment=stage
|
||||||
helmfile-auto-init: "false"
|
helmfile-auto-init: "false"
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-docker-build:
|
validate-docker-build:
|
||||||
name: Validate Docker Build
|
name: Validate Docker Build
|
||||||
@@ -36,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|||||||
6
.github/workflows/e2e.yml
vendored
@@ -16,6 +16,8 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TELEMETRY_DISABLED: 1
|
TELEMETRY_DISABLED: 1
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -44,11 +46,11 @@ jobs:
|
|||||||
--health-retries=5
|
--health-retries=5
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
|
|||||||
3
.github/workflows/formbricks-release.yml
vendored
@@ -30,4 +30,5 @@ jobs:
|
|||||||
- docker-build
|
- docker-build
|
||||||
- helm-chart-release
|
- helm-chart-release
|
||||||
with:
|
with:
|
||||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
VERSION: v${{ needs.docker-build.outputs.VERSION }}
|
||||||
|
ENVIRONMENT: "prod"
|
||||||
|
|||||||
27
.github/workflows/labeler.yml
vendored
@@ -1,27 +0,0 @@
|
|||||||
name: "Pull Request Labeler"
|
|
||||||
on:
|
|
||||||
- pull_request_target
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
labeler:
|
|
||||||
name: Pull Request Labeler
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
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
|
|
||||||
with:
|
|
||||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
|
||||||
sync-labels: ""
|
|
||||||
4
.github/workflows/lint.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|||||||
2
.github/workflows/pr.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
|||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: fail if conditional jobs failed
|
- name: fail if conditional jobs failed
|
||||||
|
|||||||
56
.github/workflows/release-changesets.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: Release Changesets
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
#push:
|
|
||||||
# branches:
|
|
||||||
# - main
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
|
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
|
||||||
steps:
|
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- name: Checkout Repo
|
|
||||||
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
|
||||||
|
|
||||||
- name: Setup Node.js 18.x
|
|
||||||
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
|
|
||||||
with:
|
|
||||||
node-version: 18.x
|
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
|
||||||
|
|
||||||
- name: Create Release Pull Request or Publish to npm
|
|
||||||
id: changesets
|
|
||||||
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
|
|
||||||
with:
|
|
||||||
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
|
||||||
publish: pnpm release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
||||||
@@ -31,12 +31,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up Depot CLI
|
- name: Set up Depot CLI
|
||||||
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
|
||||||
@@ -45,13 +45,13 @@ jobs:
|
|||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -82,8 +82,6 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
10
.github/workflows/release-docker-github.yml
vendored
@@ -38,12 +38,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Get Release Tag
|
- name: Get Release Tag
|
||||||
id: extract_release_tag
|
id: extract_release_tag
|
||||||
@@ -65,13 +65,13 @@ jobs:
|
|||||||
# https://github.com/sigstore/cosign-installer
|
# https://github.com/sigstore/cosign-installer
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
|
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -102,8 +102,6 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
2
.github/workflows/release-helm-chart.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/scorecard.yml
vendored
@@ -35,12 +35,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: "Checkout code"
|
- name: "Checkout code"
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/semantic-pull-requests.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/sonarqube.yml
vendored
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
merge_group:
|
merge_group:
|
||||||
permissions:
|
permissions:
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ jobs:
|
|||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|||||||
@@ -26,13 +26,20 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Tailscale
|
||||||
|
uses: tailscale/github-action@v3
|
||||||
|
with:
|
||||||
|
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
|
||||||
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||||
|
tags: tag:github
|
||||||
|
|
||||||
- name: Configure AWS Credentials
|
- name: Configure AWS Credentials
|
||||||
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
|
||||||
with:
|
with:
|
||||||
4
.github/workflows/test.yml
vendored
@@ -14,11 +14,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
|
||||||
- name: Setup Node.js 20.x
|
- name: Setup Node.js 20.x
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/tolgee.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
if: github.event.action == 'opened'
|
if: github.event.action == 'opened'
|
||||||
steps:
|
steps:
|
||||||
- name: Harden the runner (Audit all outbound calls)
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -72,3 +72,4 @@ infra/terraform/.terraform/
|
|||||||
# IntelliJ IDEA
|
# IntelliJ IDEA
|
||||||
/.idea/
|
/.idea/
|
||||||
/*.iml
|
/*.iml
|
||||||
|
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ if [ -f branch.json ]; then
|
|||||||
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
|
echo "Skipping tolgee-pull: NEXT_PUBLIC_TOLGEE_API_KEY is not set"
|
||||||
else
|
else
|
||||||
pnpm run tolgee-pull
|
pnpm run tolgee-pull
|
||||||
git add packages/lib/messages
|
git add apps/web/locales
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -4,33 +4,33 @@
|
|||||||
"patterns": ["./apps/web/**/*.ts?(x)"],
|
"patterns": ["./apps/web/**/*.ts?(x)"],
|
||||||
"projectId": 10304,
|
"projectId": 10304,
|
||||||
"pull": {
|
"pull": {
|
||||||
"path": "./packages/lib/messages"
|
"path": "./apps/web/locales"
|
||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"language": "en-US",
|
"language": "en-US",
|
||||||
"path": "./packages/lib/messages/en-US.json"
|
"path": "./apps/web/locales/en-US.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "de-DE",
|
"language": "de-DE",
|
||||||
"path": "./packages/lib/messages/de-DE.json"
|
"path": "./apps/web/locales/de-DE.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "fr-FR",
|
"language": "fr-FR",
|
||||||
"path": "./packages/lib/messages/fr-FR.json"
|
"path": "./apps/web/locales/fr-FR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-BR",
|
"language": "pt-BR",
|
||||||
"path": "./packages/lib/messages/pt-BR.json"
|
"path": "./apps/web/locales/pt-BR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "zh-Hant-TW",
|
"language": "zh-Hant-TW",
|
||||||
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
"path": "./apps/web/locales/zh-Hant-TW.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-PT",
|
"language": "pt-PT",
|
||||||
"path": "./packages/lib/messages/pt-PT.json"
|
"path": "./apps/web/locales/pt-PT.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"forceMode": "OVERRIDE"
|
"forceMode": "OVERRIDE"
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
|
"sonarlint.connectedMode.project": {
|
||||||
|
"connectionId": "formbricks",
|
||||||
|
"projectKey": "formbricks_formbricks"
|
||||||
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.tsdk": "node_modules/typescript/lib"
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||||
}
|
}
|
||||||
|
|||||||
2
LICENSE
@@ -3,7 +3,7 @@ Copyright (c) 2024 Formbricks GmbH
|
|||||||
Portions of this software are licensed as follows:
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
|
- All content that resides under the "apps/web/modules/ee" directory of this repository, if these directories exist, is licensed under the license defined in "apps/web/modules/ee/LICENSE".
|
||||||
- All content that resides under the "packages/js/", "packages/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/android/", "packages/ios/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||||
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
||||||
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
|
|
||||||
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: ["@formbricks/eslint-config/react.js"],
|
|
||||||
parserOptions: {
|
|
||||||
project: "tsconfig.json",
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
35
apps/demo-react-native/.gitignore
vendored
@@ -1,35 +0,0 @@
|
|||||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Expo
|
|
||||||
.expo/
|
|
||||||
dist/
|
|
||||||
web-build/
|
|
||||||
|
|
||||||
# Native
|
|
||||||
*.orig.*
|
|
||||||
*.jks
|
|
||||||
*.p8
|
|
||||||
*.p12
|
|
||||||
*.key
|
|
||||||
*.mobileprovision
|
|
||||||
|
|
||||||
# Metro
|
|
||||||
.metro-health-check*
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.*
|
|
||||||
yarn-debug.*
|
|
||||||
yarn-error.*
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
"expo": {
|
|
||||||
"android": {
|
|
||||||
"adaptiveIcon": {
|
|
||||||
"backgroundColor": "#ffffff",
|
|
||||||
"foregroundImage": "./assets/adaptive-icon.png"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"assetBundlePatterns": ["**/*"],
|
|
||||||
"icon": "./assets/icon.png",
|
|
||||||
"ios": {
|
|
||||||
"infoPlist": {
|
|
||||||
"NSCameraUsageDescription": "Take pictures for certain activities.",
|
|
||||||
"NSMicrophoneUsageDescription": "Need microphone access for recording videos.",
|
|
||||||
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities."
|
|
||||||
},
|
|
||||||
"supportsTablet": true
|
|
||||||
},
|
|
||||||
"jsEngine": "hermes",
|
|
||||||
"name": "react-native-demo",
|
|
||||||
"newArchEnabled": true,
|
|
||||||
"orientation": "portrait",
|
|
||||||
"slug": "react-native-demo",
|
|
||||||
"splash": {
|
|
||||||
"backgroundColor": "#ffffff",
|
|
||||||
"image": "./assets/splash.png",
|
|
||||||
"resizeMode": "contain"
|
|
||||||
},
|
|
||||||
"userInterfaceStyle": "light",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"web": {
|
|
||||||
"favicon": "./assets/favicon.png"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 46 KiB |
@@ -1,6 +0,0 @@
|
|||||||
module.exports = function babel(api) {
|
|
||||||
api.cache(true);
|
|
||||||
return {
|
|
||||||
presets: ["babel-preset-expo"],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { registerRootComponent } from "expo";
|
|
||||||
import { LogBox } from "react-native";
|
|
||||||
import App from "./src/app";
|
|
||||||
|
|
||||||
registerRootComponent(App);
|
|
||||||
|
|
||||||
LogBox.ignoreAllLogs();
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
|
||||||
const path = require("node:path");
|
|
||||||
const { getDefaultConfig } = require("expo/metro-config");
|
|
||||||
|
|
||||||
// Find the workspace root, this can be replaced with `find-yarn-workspace-root`
|
|
||||||
const workspaceRoot = path.resolve(__dirname, "../..");
|
|
||||||
const projectRoot = __dirname;
|
|
||||||
|
|
||||||
const config = getDefaultConfig(projectRoot);
|
|
||||||
|
|
||||||
// 1. Watch all files within the monorepo
|
|
||||||
config.watchFolders = [workspaceRoot];
|
|
||||||
// 2. Let Metro know where to resolve packages, and in what order
|
|
||||||
config.resolver.nodeModulesPaths = [
|
|
||||||
path.resolve(projectRoot, "node_modules"),
|
|
||||||
path.resolve(workspaceRoot, "node_modules"),
|
|
||||||
];
|
|
||||||
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
|
|
||||||
config.resolver.disableHierarchicalLookup = true;
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@formbricks/demo-react-native",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "./index.js",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "expo start",
|
|
||||||
"android": "expo start --android",
|
|
||||||
"ios": "expo start --ios",
|
|
||||||
"web": "expo start --web",
|
|
||||||
"eject": "expo eject",
|
|
||||||
"clean": "rimraf .turbo node_modules .expo"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@formbricks/js": "workspace:*",
|
|
||||||
"@formbricks/react-native": "workspace:*",
|
|
||||||
"@react-native-async-storage/async-storage": "2.1.0",
|
|
||||||
"expo": "52.0.28",
|
|
||||||
"expo-status-bar": "2.0.1",
|
|
||||||
"react": "18.3.1",
|
|
||||||
"react-dom": "18.3.1",
|
|
||||||
"react-native": "0.78.2",
|
|
||||||
"react-native-webview": "13.12.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "7.26.0",
|
|
||||||
"@types/react": "18.3.18",
|
|
||||||
"typescript": "5.7.2"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { StatusBar } from "expo-status-bar";
|
|
||||||
import React, { type JSX } from "react";
|
|
||||||
import { Button, LogBox, StyleSheet, Text, View } from "react-native";
|
|
||||||
import Formbricks, {
|
|
||||||
logout,
|
|
||||||
setAttribute,
|
|
||||||
setAttributes,
|
|
||||||
setLanguage,
|
|
||||||
setUserId,
|
|
||||||
track,
|
|
||||||
} from "@formbricks/react-native";
|
|
||||||
|
|
||||||
LogBox.ignoreAllLogs();
|
|
||||||
|
|
||||||
export default function App(): JSX.Element {
|
|
||||||
if (!process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID) {
|
|
||||||
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.EXPO_PUBLIC_APP_URL) {
|
|
||||||
throw new Error("EXPO_PUBLIC_APP_URL is required");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<Text>Formbricks React Native SDK Demo</Text>
|
|
||||||
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 10,
|
|
||||||
}}>
|
|
||||||
<Button
|
|
||||||
title="Trigger Code Action"
|
|
||||||
onPress={() => {
|
|
||||||
track("code").catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error tracking event:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Set User Id"
|
|
||||||
onPress={() => {
|
|
||||||
setUserId("random-user-id").catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error setting user id:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Set User Attributess (multiple)"
|
|
||||||
onPress={() => {
|
|
||||||
setAttributes({
|
|
||||||
testAttr: "attr-test",
|
|
||||||
testAttr2: "attr-test-2",
|
|
||||||
testAttr3: "attr-test-3",
|
|
||||||
testAttr4: "attr-test-4",
|
|
||||||
}).catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error setting user attributes:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Set User Attributes (single)"
|
|
||||||
onPress={() => {
|
|
||||||
setAttribute("testSingleAttr", "testSingleAttr").catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error setting user attributes:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Logout"
|
|
||||||
onPress={() => {
|
|
||||||
logout().catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error logging out:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
title="Set Language (de)"
|
|
||||||
onPress={() => {
|
|
||||||
setLanguage("de").catch((error: unknown) => {
|
|
||||||
// eslint-disable-next-line no-console -- logging is allowed in demo apps
|
|
||||||
console.error("Error setting language:", error);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<StatusBar style="auto" />
|
|
||||||
|
|
||||||
<Formbricks
|
|
||||||
appUrl={process.env.EXPO_PUBLIC_APP_URL as string}
|
|
||||||
environmentId={process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"strict": true
|
|
||||||
},
|
|
||||||
"extends": "expo/tsconfig.base"
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
NEXT_PUBLIC_FORMBRICKS_API_HOST=http://localhost:3000
|
|
||||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=YOUR_ENVIRONMENT_ID
|
|
||||||
|
|
||||||
# Copy the environment ID for the URL of your Formbricks App and
|
|
||||||
# paste it above to connect your Formbricks App with the Demo App.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
extends: ["@formbricks/eslint-config/next.js"],
|
|
||||||
parserOptions: {
|
|
||||||
project: "tsconfig.json",
|
|
||||||
tsconfigRootDir: __dirname,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
36
apps/demo/.gitignore
vendored
@@ -1,36 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Sidebar } from "./sidebar";
|
|
||||||
|
|
||||||
export function LayoutApp({ children }: { children: React.ReactNode }): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="min-h-full">
|
|
||||||
{/* Static sidebar for desktop */}
|
|
||||||
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
|
|
||||||
<Sidebar />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-1 flex-col lg:pl-64">{children}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import {
|
|
||||||
ClockIcon,
|
|
||||||
CogIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
FileBarChartIcon,
|
|
||||||
HelpCircleIcon,
|
|
||||||
HomeIcon,
|
|
||||||
ScaleIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
UsersIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { classNames } from "../lib/utils";
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{ name: "Home", href: "#", icon: HomeIcon, current: true },
|
|
||||||
{ name: "History", href: "#", icon: ClockIcon, current: false },
|
|
||||||
{ name: "Balances", href: "#", icon: ScaleIcon, current: false },
|
|
||||||
{ name: "Cards", href: "#", icon: CreditCardIcon, current: false },
|
|
||||||
{ name: "Recipients", href: "#", icon: UsersIcon, current: false },
|
|
||||||
{ name: "Reports", href: "#", icon: FileBarChartIcon, current: false },
|
|
||||||
];
|
|
||||||
const secondaryNavigation = [
|
|
||||||
{ name: "Settings", href: "#", icon: CogIcon },
|
|
||||||
{ name: "Help", href: "#", icon: HelpCircleIcon },
|
|
||||||
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Sidebar(): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<div className="flex grow flex-col overflow-y-auto bg-cyan-700 pb-4 pt-5">
|
|
||||||
<nav
|
|
||||||
className="mt-5 flex flex-1 flex-col divide-y divide-cyan-800 overflow-y-auto"
|
|
||||||
aria-label="Sidebar">
|
|
||||||
<div className="space-y-1 px-2">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={classNames(
|
|
||||||
item.current ? "bg-cyan-800 text-white" : "text-cyan-100 hover:bg-cyan-600 hover:text-white",
|
|
||||||
"group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6"
|
|
||||||
)}
|
|
||||||
aria-current={item.current ? "page" : undefined}>
|
|
||||||
<item.icon className="mr-4 h-6 w-6 shrink-0 text-cyan-200" aria-hidden="true" />
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 pt-6">
|
|
||||||
<div className="space-y-1 px-2">
|
|
||||||
{secondaryNavigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="group flex items-center rounded-md px-2 py-2 text-sm font-medium leading-6 text-cyan-100 hover:bg-cyan-600 hover:text-white">
|
|
||||||
<item.icon className="mr-4 h-6 w-6 text-cyan-200" aria-hidden="true" />
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
@plugin '@tailwindcss/forms';
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
|
||||||
|
|
||||||
/*
|
|
||||||
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
|
||||||
so we've added these compatibility styles to make sure everything still
|
|
||||||
looks the same as it did with Tailwind CSS v3.
|
|
||||||
|
|
||||||
If we ever want to remove these styles, we need to add an explicit border
|
|
||||||
color utility to any element that depends on these defaults.
|
|
||||||
*/
|
|
||||||
@layer base {
|
|
||||||
*,
|
|
||||||
::after,
|
|
||||||
::before,
|
|
||||||
::backdrop,
|
|
||||||
::file-selector-button {
|
|
||||||
border-color: var(--color-gray-200, currentcolor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export function classNames(...classes: string[]): string {
|
|
||||||
return classes.filter(Boolean).join(" ");
|
|
||||||
}
|
|
||||||
5
apps/demo/next-env.d.ts
vendored
@@ -1,5 +0,0 @@
|
|||||||
/// <reference types="next" />
|
|
||||||
/// <reference types="next/image-types/global" />
|
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
|
||||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
images: {
|
|
||||||
remotePatterns: [
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "tailwindui.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
protocol: "https",
|
|
||||||
hostname: "images.unsplash.com",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@formbricks/demo",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"clean": "rimraf .turbo node_modules .next",
|
|
||||||
"dev": "next dev -p 3002 --turbopack",
|
|
||||||
"go": "next dev -p 3002 --turbopack",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@formbricks/js": "workspace:*",
|
|
||||||
"@tailwindcss/forms": "0.5.9",
|
|
||||||
"@tailwindcss/postcss": "4.1.3",
|
|
||||||
"lucide-react": "0.486.0",
|
|
||||||
"next": "15.2.4",
|
|
||||||
"postcss": "8.5.3",
|
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"tailwindcss": "4.1.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
|
||||||
"@formbricks/eslint-config": "workspace:*"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import type { AppProps } from "next/app";
|
|
||||||
import Head from "next/head";
|
|
||||||
import "../globals.css";
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Demo App</title>
|
|
||||||
</Head>
|
|
||||||
{(!process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID ||
|
|
||||||
!process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) && (
|
|
||||||
<div className="w-full bg-red-500 p-3 text-center text-sm text-white">
|
|
||||||
Please set Formbricks environment variables in apps/demo/.env
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Head, Html, Main, NextScript } from "next/document";
|
|
||||||
|
|
||||||
export default function Document(): React.JSX.Element {
|
|
||||||
return (
|
|
||||||
<Html lang="en" className="h-full bg-slate-50">
|
|
||||||
<Head />
|
|
||||||
<body className="h-full">
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import formbricks from "@formbricks/js";
|
|
||||||
import fbsetup from "../public/fb-setup.png";
|
|
||||||
|
|
||||||
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) {
|
|
||||||
document.body.classList.add("dark");
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove("dark");
|
|
||||||
}
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initFormbricks = () => {
|
|
||||||
// enable Formbricks debug mode by adding formbricksDebug=true GET parameter
|
|
||||||
const addFormbricksDebugParam = (): void => {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
if (!urlParams.has("formbricksDebug")) {
|
|
||||||
urlParams.set("formbricksDebug", "true");
|
|
||||||
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
|
|
||||||
window.history.replaceState({}, "", newUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
addFormbricksDebugParam();
|
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
|
||||||
void formbricks.setup({
|
|
||||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
|
||||||
appUrl: process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect next.js router to Formbricks
|
|
||||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
|
||||||
const handleRouteChange = formbricks.registerRouteChange;
|
|
||||||
|
|
||||||
router.events.on("routeChangeComplete", () => {
|
|
||||||
void handleRouteChange();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off("routeChangeComplete", () => {
|
|
||||||
void handleRouteChange();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initFormbricks();
|
|
||||||
}, [router.events]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
|
||||||
<div className="flex flex-col justify-between md:flex-row">
|
|
||||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
|
||||||
Formbricks In-product Survey Demo App
|
|
||||||
</h1>
|
|
||||||
<p className="text-slate-700 dark:text-slate-300">
|
|
||||||
This app helps you test your app surveys. You can create and test user actions, create and
|
|
||||||
update user attributes, etc.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-2 rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
|
|
||||||
onClick={() => {
|
|
||||||
setDarkMode(!darkMode);
|
|
||||||
}}>
|
|
||||||
{darkMode ? "Toggle Light Mode" : "Toggle Dark Mode"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<div className="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 text-slate-900 dark:text-white">1. Setup .env</h3>
|
|
||||||
<p className="text-slate-700 dark:text-slate-300">
|
|
||||||
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
|
||||||
</p>
|
|
||||||
<Image src={fbsetup} alt="fb setup" className="rounded-xs mt-4" priority />
|
|
||||||
|
|
||||||
<div className="mt-4 flex-col items-start text-sm text-slate-700 sm:flex sm:items-center sm:text-base dark:text-slate-300">
|
|
||||||
<p className="mb-1 sm:mb-0 sm:mr-2">You're connected with env:</p>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<strong className="w-32 truncate sm:w-auto">
|
|
||||||
{process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID}
|
|
||||||
</strong>
|
|
||||||
<span className="relative ml-2 flex h-3 w-3">
|
|
||||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75" />
|
|
||||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 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 text-slate-900 dark:text-white">2. Widget Logs</h3>
|
|
||||||
<p className="text-slate-700 dark:text-slate-300">
|
|
||||||
Look at the logs to understand how the widget works.{" "}
|
|
||||||
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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
|
|
||||||
</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>.
|
|
||||||
</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);
|
|
||||||
}}>
|
|
||||||
Set user ID
|
|
||||||
</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
|
|
||||||
try again.
|
|
||||||
</p>
|
|
||||||
</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">
|
|
||||||
No-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-no-code-actions"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="underline dark:text-blue-500"
|
|
||||||
target="_blank">
|
|
||||||
No 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-no-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"
|
|
||||||
onClick={() => {
|
|
||||||
void formbricks.setAttribute("Plan", "Free");
|
|
||||||
}}
|
|
||||||
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 Plan to 'Free'
|
|
||||||
</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">
|
|
||||||
attribute
|
|
||||||
</a>{" "}
|
|
||||||
'Plan' to 'Free'. If the attribute does not exist, it creates it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-6">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
void formbricks.setAttribute("Plan", "Paid");
|
|
||||||
}}
|
|
||||||
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 Plan to 'Paid'
|
|
||||||
</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">
|
|
||||||
attribute
|
|
||||||
</a>{" "}
|
|
||||||
'Plan' to 'Paid'. If the attribute does not exist, it creates it.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-6">
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
void formbricks.setEmail("test@web.com");
|
|
||||||
}}
|
|
||||||
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 Email
|
|
||||||
</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"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="underline dark:text-blue-500">
|
|
||||||
user email
|
|
||||||
</a>{" "}
|
|
||||||
'test@web.com'
|
|
||||||
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="31" fill="none"><g opacity=".9"><path fill="url(#a)" d="M13 .4v29.3H7V6.3h-.2L0 10.5V5L7.2.4H13Z"/><path fill="url(#b)" d="M28.8 30.1c-2.2 0-4-.3-5.7-1-1.7-.8-3-1.8-4-3.1a7.7 7.7 0 0 1-1.4-4.6h6.2c0 .8.3 1.4.7 2 .4.5 1 .9 1.7 1.2.7.3 1.6.4 2.5.4 1 0 1.7-.2 2.5-.5.7-.3 1.3-.8 1.7-1.4.4-.6.6-1.2.6-2s-.2-1.5-.7-2.1c-.4-.6-1-1-1.8-1.4-.8-.4-1.8-.5-2.9-.5h-2.7v-4.6h2.7a6 6 0 0 0 2.5-.5 4 4 0 0 0 1.7-1.3c.4-.6.6-1.3.6-2a3.5 3.5 0 0 0-2-3.3 5.6 5.6 0 0 0-4.5 0 4 4 0 0 0-1.7 1.2c-.4.6-.6 1.2-.6 2h-6c0-1.7.6-3.2 1.5-4.5 1-1.3 2.2-2.3 3.8-3C25 .4 26.8 0 28.8 0s3.8.4 5.3 1.1c1.5.7 2.7 1.7 3.6 3a7.2 7.2 0 0 1 1.2 4.2c0 1.6-.5 3-1.5 4a7 7 0 0 1-4 2.2v.2c2.2.3 3.8 1 5 2.2a6.4 6.4 0 0 1 1.6 4.6c0 1.7-.5 3.1-1.4 4.4a9.7 9.7 0 0 1-4 3.1c-1.7.8-3.7 1.1-5.8 1.1Z"/></g><defs><linearGradient id="a" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient><linearGradient id="b" x1="20" x2="20" y1="0" y2="30.1" gradientUnits="userSpaceOnUse"><stop/><stop offset="1" stop-color="#3D3D3D"/></linearGradient></defs></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 629 B |
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"extends": "@formbricks/config-typescript/nextjs.json",
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,12 @@
|
|||||||
"clean": "rimraf .turbo node_modules dist storybook-static"
|
"clean": "rimraf .turbo node_modules dist storybook-static"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-plugin-react-refresh": "0.4.19",
|
"eslint-plugin-react-refresh": "0.4.20",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chromatic-com/storybook": "3.2.6",
|
"@chromatic-com/storybook": "3.2.6",
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
|
||||||
"@storybook/addon-a11y": "8.6.12",
|
"@storybook/addon-a11y": "8.6.12",
|
||||||
"@storybook/addon-essentials": "8.6.12",
|
"@storybook/addon-essentials": "8.6.12",
|
||||||
"@storybook/addon-interactions": "8.6.12",
|
"@storybook/addon-interactions": "8.6.12",
|
||||||
@@ -27,14 +26,13 @@
|
|||||||
"@storybook/react": "8.6.12",
|
"@storybook/react": "8.6.12",
|
||||||
"@storybook/react-vite": "8.6.12",
|
"@storybook/react-vite": "8.6.12",
|
||||||
"@storybook/test": "8.6.12",
|
"@storybook/test": "8.6.12",
|
||||||
"@typescript-eslint/eslint-plugin": "8.29.0",
|
"@typescript-eslint/eslint-plugin": "8.32.0",
|
||||||
"@typescript-eslint/parser": "8.29.0",
|
"@typescript-eslint/parser": "8.32.0",
|
||||||
"@vitejs/plugin-react": "4.3.4",
|
"@vitejs/plugin-react": "4.4.1",
|
||||||
"esbuild": "0.25.2",
|
"esbuild": "0.25.4",
|
||||||
"eslint-plugin-storybook": "0.12.0",
|
"eslint-plugin-storybook": "0.12.0",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"storybook": "8.6.12",
|
"storybook": "8.6.12",
|
||||||
"tsup": "8.4.0",
|
"vite": "6.3.5"
|
||||||
"vite": "6.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
||||||
|
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["locales/*.json"],
|
||||||
|
plugins: ["i18n-json"],
|
||||||
|
rules: {
|
||||||
|
"i18n-json/identical-keys": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
filePath: require("path").join(__dirname, "locales", "en-US.json"),
|
||||||
|
checkExtraKeys: false,
|
||||||
|
checkMissingKeys: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
2
apps/web/.gitignore
vendored
@@ -50,4 +50,4 @@ uploads/
|
|||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
# SAML Preloaded Connections
|
# SAML Preloaded Connections
|
||||||
saml-connection/
|
saml-connection/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
FROM node:22-alpine3.21 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
## step 1: Prune monorepo
|
## step 1: Prune monorepo
|
||||||
@@ -18,8 +18,9 @@ FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a1
|
|||||||
FROM base AS installer
|
FROM base AS installer
|
||||||
|
|
||||||
# Enable corepack and prepare pnpm
|
# Enable corepack and prepare pnpm
|
||||||
RUN npm install -g corepack@latest
|
RUN npm install --ignore-scripts -g corepack@latest
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@9.15.9 --activate
|
||||||
|
|
||||||
# Install necessary build tools and compilers
|
# Install necessary build tools and compilers
|
||||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||||
@@ -59,7 +60,7 @@ COPY . .
|
|||||||
RUN touch apps/web/.env
|
RUN touch apps/web/.env
|
||||||
|
|
||||||
# Install the dependencies
|
# Install the dependencies
|
||||||
RUN pnpm install
|
RUN pnpm install --ignore-scripts
|
||||||
|
|
||||||
# Build the project using our secret reader script
|
# Build the project using our secret reader script
|
||||||
# This mounts the secrets only during this build step without storing them in layers
|
# This mounts the secrets only during this build step without storing them in layers
|
||||||
@@ -75,19 +76,14 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
|
|||||||
#
|
#
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
|
|
||||||
RUN npm install -g corepack@latest
|
RUN addgroup -S nextjs \
|
||||||
RUN corepack enable
|
&& adduser -S -u 1001 -G nextjs nextjs
|
||||||
|
|
||||||
RUN apk add --no-cache curl \
|
|
||||||
&& apk add --no-cache supercronic \
|
|
||||||
# && addgroup --system --gid 1001 nodejs \
|
|
||||||
&& adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
WORKDIR /home/nextjs
|
WORKDIR /home/nextjs
|
||||||
|
|
||||||
# Ensure no write permissions are assigned to the copied resources
|
# Ensure no write permissions are assigned to the copied resources
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
|
COPY --from=installer /app/apps/web/.next/standalone ./
|
||||||
RUN chmod -R 755 ./
|
RUN chown -R nextjs:nextjs ./ && chmod -R 755 ./
|
||||||
|
|
||||||
COPY --from=installer /app/apps/web/next.config.mjs .
|
COPY --from=installer /app/apps/web/next.config.mjs .
|
||||||
RUN chmod 644 ./next.config.mjs
|
RUN chmod 644 ./next.config.mjs
|
||||||
@@ -95,57 +91,16 @@ RUN chmod 644 ./next.config.mjs
|
|||||||
COPY --from=installer /app/apps/web/package.json .
|
COPY --from=installer /app/apps/web/package.json .
|
||||||
RUN chmod 644 ./package.json
|
RUN chmod 644 ./package.json
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
RUN chmod -R 755 ./apps/web/.next/static
|
RUN chown -R nextjs:nextjs ./apps/web/.next/static && chmod -R 755 ./apps/web/.next/static
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
COPY --from=installer /app/apps/web/public ./apps/web/public
|
||||||
RUN chmod -R 755 ./apps/web/public
|
RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
|
||||||
RUN chmod 644 ./packages/database/schema.prisma
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
|
||||||
RUN chmod 644 ./packages/database/package.json
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
|
||||||
RUN chmod -R 755 ./packages/database/migration
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
|
||||||
RUN chmod -R 755 ./packages/database/src
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
|
||||||
RUN chmod -R 755 ./packages/database/node_modules
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
|
||||||
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
|
||||||
RUN chmod -R 755 ./node_modules/@prisma/client
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
|
||||||
RUN chmod -R 755 ./node_modules/.prisma
|
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
|
||||||
RUN chmod 644 ./prisma_version.txt
|
|
||||||
|
|
||||||
COPY /docker/cronjobs /app/docker/cronjobs
|
|
||||||
RUN chmod -R 755 /app/docker/cronjobs
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
|
|
||||||
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes
|
|
||||||
RUN chmod -R 755 ./node_modules/@noble/hashes
|
|
||||||
|
|
||||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
|
||||||
RUN chmod -R 755 ./node_modules/zod
|
|
||||||
|
|
||||||
RUN npm install -g tsx typescript prisma pino-pretty
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV HOSTNAME "0.0.0.0"
|
ENV HOSTNAME "0.0.0.0"
|
||||||
ENV NODE_ENV="production"
|
ENV NODE_ENV="production"
|
||||||
# USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
# Prepare volume for uploads
|
# Prepare volume for uploads
|
||||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||||
@@ -155,12 +110,4 @@ VOLUME /home/nextjs/apps/web/uploads/
|
|||||||
RUN mkdir -p /home/nextjs/apps/web/saml-connection
|
RUN mkdir -p /home/nextjs/apps/web/saml-connection
|
||||||
VOLUME /home/nextjs/apps/web/saml-connection
|
VOLUME /home/nextjs/apps/web/saml-connection
|
||||||
|
|
||||||
CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
|
CMD ["node", "apps/web/server.js"]
|
||||||
echo "Starting cron jobs..."; \
|
|
||||||
supercronic -quiet /app/docker/cronjobs & \
|
|
||||||
else \
|
|
||||||
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
|
|
||||||
fi; \
|
|
||||||
(cd packages/database && npm run db:migrate:deploy) && \
|
|
||||||
(cd packages/database && npm run db:create-saml-database:deploy) && \
|
|
||||||
exec node apps/web/server.js
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||||
|
import { WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ConnectPageProps {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/environments/${environment.id}`}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { getDefaultEndingCard } from "@/app/lib/templates";
|
import {
|
||||||
|
buildCTAQuestion,
|
||||||
|
buildNPSQuestion,
|
||||||
|
buildOpenTextQuestion,
|
||||||
|
buildRatingQuestion,
|
||||||
|
getDefaultEndingCard,
|
||||||
|
} from "@/app/lib/survey-builder";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { TFnType } from "@tolgee/react";
|
import { TFnType } from "@tolgee/react";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
||||||
@@ -26,35 +31,26 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.nps_survey_name"),
|
name: t("templates.nps_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildNPSQuestion({
|
||||||
id: createId(),
|
headline: t("templates.nps_survey_question_1_headline"),
|
||||||
type: TSurveyQuestionTypeEnum.NPS,
|
|
||||||
headline: { default: t("templates.nps_survey_question_1_headline") },
|
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
|
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
|
upperLabel: t("templates.nps_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
},
|
t,
|
||||||
{
|
}),
|
||||||
id: createId(),
|
buildOpenTextQuestion({
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
headline: t("templates.nps_survey_question_2_headline"),
|
||||||
headline: { default: t("templates.nps_survey_question_2_headline") },
|
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
buildOpenTextQuestion({
|
||||||
},
|
headline: t("templates.nps_survey_question_3_headline"),
|
||||||
{
|
|
||||||
id: createId(),
|
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
|
||||||
headline: { default: t("templates.nps_survey_question_3_headline") },
|
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -67,9 +63,8 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.star_rating_survey_name"),
|
name: t("templates.star_rating_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildRatingQuestion({
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
type: TSurveyQuestionTypeEnum.Rating,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -102,16 +97,15 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: { default: t("templates.star_rating_survey_question_1_headline") },
|
headline: t("templates.star_rating_survey_question_1_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
|
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
|
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: false,
|
t,
|
||||||
},
|
}),
|
||||||
{
|
buildCTAQuestion({
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
html: { default: t("templates.star_rating_survey_question_2_html") },
|
html: t("templates.star_rating_survey_question_2_html"),
|
||||||
type: TSurveyQuestionTypeEnum.CTA,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -138,25 +132,23 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: { default: t("templates.star_rating_survey_question_2_headline") },
|
headline: t("templates.star_rating_survey_question_2_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
buttonUrl: "https://formbricks.com/github",
|
||||||
buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
|
buttonLabel: t("templates.star_rating_survey_question_2_button_label"),
|
||||||
buttonExternal: true,
|
buttonExternal: true,
|
||||||
},
|
t,
|
||||||
{
|
}),
|
||||||
|
buildOpenTextQuestion({
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
headline: t("templates.star_rating_survey_question_3_headline"),
|
||||||
headline: { default: t("templates.star_rating_survey_question_3_headline") },
|
|
||||||
required: true,
|
required: true,
|
||||||
subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
|
subheader: t("templates.star_rating_survey_question_3_subheader"),
|
||||||
buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
|
buttonLabel: t("templates.star_rating_survey_question_3_button_label"),
|
||||||
placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
|
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -169,9 +161,8 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.csat_survey_name"),
|
name: t("templates.csat_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildRatingQuestion({
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
type: TSurveyQuestionTypeEnum.Rating,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -204,15 +195,14 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: { default: t("templates.csat_survey_question_1_headline") },
|
headline: t("templates.csat_survey_question_1_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
|
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
|
upperLabel: t("templates.csat_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: false,
|
t,
|
||||||
},
|
}),
|
||||||
{
|
buildOpenTextQuestion({
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -239,25 +229,20 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: { default: t("templates.csat_survey_question_2_headline") },
|
headline: t("templates.csat_survey_question_2_headline"),
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
|
placeholder: t("templates.csat_survey_question_2_placeholder"),
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
buildOpenTextQuestion({
|
||||||
},
|
|
||||||
{
|
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
headline: t("templates.csat_survey_question_3_headline"),
|
||||||
headline: { default: t("templates.csat_survey_question_3_headline") },
|
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
|
placeholder: t("templates.csat_survey_question_3_placeholder"),
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -267,28 +252,22 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.cess_survey_name"),
|
name: t("templates.cess_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildRatingQuestion({
|
||||||
id: createId(),
|
|
||||||
type: TSurveyQuestionTypeEnum.Rating,
|
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: { default: t("templates.cess_survey_question_1_headline") },
|
headline: t("templates.cess_survey_question_1_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
|
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
|
upperLabel: t("templates.cess_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: false,
|
t,
|
||||||
},
|
}),
|
||||||
{
|
buildOpenTextQuestion({
|
||||||
id: createId(),
|
headline: t("templates.cess_survey_question_2_headline"),
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
|
||||||
headline: { default: t("templates.cess_survey_question_2_headline") },
|
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
|
placeholder: t("templates.cess_survey_question_2_placeholder"),
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -301,9 +280,8 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.smileys_survey_name"),
|
name: t("templates.smileys_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildRatingQuestion({
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
type: TSurveyQuestionTypeEnum.Rating,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -336,16 +314,15 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: { default: t("templates.smileys_survey_question_1_headline") },
|
headline: t("templates.smileys_survey_question_1_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
|
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
|
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: false,
|
t,
|
||||||
},
|
}),
|
||||||
{
|
buildCTAQuestion({
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
html: { default: t("templates.smileys_survey_question_2_html") },
|
html: t("templates.smileys_survey_question_2_html"),
|
||||||
type: TSurveyQuestionTypeEnum.CTA,
|
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -372,25 +349,23 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: { default: t("templates.smileys_survey_question_2_headline") },
|
headline: t("templates.smileys_survey_question_2_headline"),
|
||||||
required: true,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
buttonUrl: "https://formbricks.com/github",
|
||||||
buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
|
buttonLabel: t("templates.smileys_survey_question_2_button_label"),
|
||||||
buttonExternal: true,
|
buttonExternal: true,
|
||||||
},
|
t,
|
||||||
{
|
}),
|
||||||
|
buildOpenTextQuestion({
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
headline: t("templates.smileys_survey_question_3_headline"),
|
||||||
headline: { default: t("templates.smileys_survey_question_3_headline") },
|
|
||||||
required: true,
|
required: true,
|
||||||
subheader: { default: t("templates.smileys_survey_question_3_subheader") },
|
subheader: t("templates.smileys_survey_question_3_subheader"),
|
||||||
buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
|
buttonLabel: t("templates.smileys_survey_question_3_button_label"),
|
||||||
placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
|
placeholder: t("templates.smileys_survey_question_3_placeholder"),
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -400,37 +375,26 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.enps_survey_name"),
|
name: t("templates.enps_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
{
|
buildNPSQuestion({
|
||||||
id: createId(),
|
headline: t("templates.enps_survey_question_1_headline"),
|
||||||
type: TSurveyQuestionTypeEnum.NPS,
|
|
||||||
headline: {
|
|
||||||
default: t("templates.enps_survey_question_1_headline"),
|
|
||||||
},
|
|
||||||
required: false,
|
required: false,
|
||||||
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
|
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
|
||||||
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
|
upperLabel: t("templates.enps_survey_question_1_upper_label"),
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
},
|
t,
|
||||||
{
|
}),
|
||||||
id: createId(),
|
buildOpenTextQuestion({
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
headline: t("templates.enps_survey_question_2_headline"),
|
||||||
headline: { default: t("templates.enps_survey_question_2_headline") },
|
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
buildOpenTextQuestion({
|
||||||
},
|
headline: t("templates.enps_survey_question_3_headline"),
|
||||||
{
|
|
||||||
id: createId(),
|
|
||||||
type: TSurveyQuestionTypeEnum.OpenText,
|
|
||||||
headline: { default: t("templates.enps_survey_question_3_headline") },
|
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
charLimit: {
|
t,
|
||||||
enabled: false,
|
}),
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -7,9 +10,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
interface XMTemplatePageProps {
|
interface XMTemplatePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||||
{projects.length >= 2 && (
|
{projects.length >= 2 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/environments/${environment.id}/surveys`}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
import { teamCache } from "@/lib/cache/team";
|
import { teamCache } from "@/lib/cache/team";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
import {
|
import {
|
||||||
@@ -24,8 +25,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ export const LandingSidebar = ({
|
|||||||
{/* Dropdown Items */}
|
{/* Dropdown Items */}
|
||||||
|
|
||||||
{dropdownNavigation.map((link) => (
|
{dropdownNavigation.map((link) => (
|
||||||
<Link href={link.href} target={link.target} className="flex w-full items-center">
|
<Link id={link.href} href={link.href} target={link.target} className="flex w-full items-center">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
<link.icon className="mr-2 h-4 w-4" strokeWidth={1.5} />
|
||||||
{link.label}
|
{link.label}
|
||||||
@@ -125,7 +124,6 @@ export const LandingSidebar = ({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await signOut({ callbackUrl: "/auth/login" });
|
await signOut({ callbackUrl: "/auth/login" });
|
||||||
await formbricksLogout();
|
|
||||||
}}
|
}}
|
||||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||||
{t("common.logout")}
|
{t("common.logout")}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const LandingLayout = async (props) => {
|
const LandingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||||
|
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import ProjectOnboardingLayout from "./layout";
|
import ProjectOnboardingLayout from "./layout";
|
||||||
|
|
||||||
// Mock all the modules and functions that this layout uses:
|
// Mock all the modules and functions that this layout uses:
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
POSTHOG_HOST: "mock-posthog-host",
|
||||||
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
|
|||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/organization/auth", () => ({
|
vi.mock("@/lib/organization/auth", () => ({
|
||||||
canUserAccessOrganization: vi.fn(),
|
canUserAccessOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
vi.mock("@/lib/organization/service", () => ({
|
||||||
getOrganization: vi.fn(),
|
getOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
vi.mock("@/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/tolgee/server", () => ({
|
vi.mock("@/tolgee/server", () => ({
|
||||||
@@ -71,7 +71,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if there is no session", async () => {
|
test("redirects to /auth/login if there is no session", async () => {
|
||||||
// Mock no session
|
// Mock no session
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
expect(layoutElement).toBeUndefined();
|
expect(layoutElement).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if user does not exist", async () => {
|
test("throws an error if user does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({
|
vi.mocked(getServerSession).mockResolvedValueOnce({
|
||||||
user: { id: "user-123" },
|
user: { id: "user-123" },
|
||||||
});
|
});
|
||||||
@@ -99,7 +99,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.user_not_found");
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws AuthorizationError if user cannot access organization", async () => {
|
test("throws AuthorizationError if user cannot access organization", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(false);
|
||||||
@@ -112,7 +112,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.not_authorized");
|
).rejects.toThrow("common.not_authorized");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error if organization does not exist", async () => {
|
test("throws an error if organization does not exist", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
||||||
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
vi.mocked(canUserAccessOrganization).mockResolvedValueOnce(true);
|
||||||
@@ -126,7 +126,7 @@ describe("ProjectOnboardingLayout", () => {
|
|||||||
).rejects.toThrow("common.organization_not_found");
|
).rejects.toThrow("common.organization_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
test("renders child content plus PosthogIdentify & ToasterClient if everything is valid", async () => {
|
||||||
// Provide valid data
|
// Provide valid data
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", name: "Test User" } as TUser);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
||||||
|
import { IS_POSTHOG_CONFIGURED } from "@/lib/constants";
|
||||||
|
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
|
||||||
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const ProjectOnboardingLayout = async (props) => {
|
const ProjectOnboardingLayout = async (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ChannelPageProps {
|
interface ChannelPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -6,7 +7,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ModePageProps {
|
interface ModePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||||
import { previewSurvey } from "@/app/lib/templates";
|
import { previewSurvey } from "@/app/lib/templates";
|
||||||
|
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||||
@@ -26,7 +27,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
|
||||||
import {
|
import {
|
||||||
TProjectConfigChannel,
|
TProjectConfigChannel,
|
||||||
TProjectConfigIndustry,
|
TProjectConfigIndustry,
|
||||||
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={256}
|
width={256}
|
||||||
height={56}
|
height={56}
|
||||||
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||||
|
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -8,8 +10,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
|
|
||||||
interface ProjectSettingsPageProps {
|
interface ProjectSettingsPageProps {
|
||||||
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,191 +1,120 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { getServerSession } from "next-auth";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import { Session } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import SurveyEditorEnvironmentLayout from "./layout";
|
import SurveyEditorEnvironmentLayout from "./layout";
|
||||||
|
|
||||||
// mock all dependencies
|
// Mock sub-components to render identifiable elements
|
||||||
|
vi.mock("@/modules/ui/components/environmentId-base-layout", () => ({
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
EnvironmentIdBaseLayout: ({ children, environmentId }: any) => (
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
<div data-testid="EnvironmentIdBaseLayout">
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
{environmentId}
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
{children}
|
||||||
IS_POSTHOG_CONFIGURED: true,
|
</div>
|
||||||
ENCRYPTION_KEY: "mock-encryption-key",
|
),
|
||||||
ENTERPRISE_LICENSE_KEY: "mock-enterprise-license-key",
|
}));
|
||||||
GITHUB_ID: "mock-github-id",
|
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
||||||
GITHUB_SECRET: "test-githubID",
|
DevEnvironmentBanner: ({ environment }: any) => (
|
||||||
GOOGLE_CLIENT_ID: "test-google-client-id",
|
<div data-testid="DevEnvironmentBanner">{environment.id}</div>
|
||||||
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", () => ({
|
// Mocks for dependencies
|
||||||
getServerSession: vi.fn(),
|
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||||
|
environmentIdLayoutChecks: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/environment/service", () => ({
|
||||||
|
getEnvironment: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/environment/auth", () => ({
|
|
||||||
hasUserEnvironmentAccess: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/environment/service", () => ({
|
|
||||||
getEnvironment: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/organization/service", () => ({
|
|
||||||
getOrganizationByEnvironmentId: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
|
||||||
getUser: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("@/tolgee/server", () => ({
|
|
||||||
getTranslate: vi.fn(() => {
|
|
||||||
return (key: string) => key; // trivial translator returning the key
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// mock child components rendered by the layout:
|
|
||||||
vi.mock("@/app/(app)/components/FormbricksClient", () => ({
|
|
||||||
FormbricksClient: () => <div data-testid="formbricks-client" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/PosthogIdentify", () => ({
|
|
||||||
PosthogIdentify: () => <div data-testid="posthog-identify" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/modules/ui/components/toaster-client", () => ({
|
|
||||||
ToasterClient: () => <div data-testid="mock-toaster" />,
|
|
||||||
}));
|
|
||||||
vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
|
||||||
DevEnvironmentBanner: ({ environment }: { environment: TEnvironment }) => (
|
|
||||||
<div data-testid="dev-environment-banner">{environment?.id || "no-env"}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/components/ResponseFilterContext", () => ({
|
|
||||||
ResponseFilterProvider: ({ children }: { children: React.ReactNode }) => (
|
|
||||||
<div data-testid="mock-response-filter-provider">{children}</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("SurveyEditorEnvironmentLayout", () => {
|
describe("SurveyEditorEnvironmentLayout", () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects to /auth/login if there is no session", async () => {
|
test("renders successfully when environment is found", async () => {
|
||||||
// Mock no session
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce(null);
|
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
|
});
|
||||||
|
vi.mocked(getEnvironment).mockResolvedValueOnce({ id: "env1" } as TEnvironment);
|
||||||
|
|
||||||
const layoutElement = await SurveyEditorEnvironmentLayout({
|
const result = await SurveyEditorEnvironmentLayout({
|
||||||
params: { environmentId: "env-123" },
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
children: <div data-testid="child">Survey Editor Content</div>,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(redirect).toHaveBeenCalledWith("/auth/login");
|
render(result);
|
||||||
// No JSX is returned after redirect
|
|
||||||
expect(layoutElement).toBeUndefined();
|
expect(screen.getByTestId("EnvironmentIdBaseLayout")).toHaveTextContent("env1");
|
||||||
|
expect(screen.getByTestId("DevEnvironmentBanner")).toHaveTextContent("env1");
|
||||||
|
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if user does not exist in DB", async () => {
|
test("throws an error when environment is not found", async () => {
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getUser).mockResolvedValueOnce(null); // user not found
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
await expect(
|
user: { id: "user1", email: "user1@example.com" } as TUser,
|
||||||
SurveyEditorEnvironmentLayout({
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
params: { environmentId: "env-123" },
|
});
|
||||||
children: <div data-testid="child-content">Hello!</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.user_not_found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws AuthorizationError if user does not have environment access", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(false);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
SurveyEditorEnvironmentLayout({
|
|
||||||
params: { environmentId: "env-123" },
|
|
||||||
children: <div>Child</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow(AuthorizationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no organization is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
SurveyEditorEnvironmentLayout({
|
|
||||||
params: { environmentId: "env-123" },
|
|
||||||
children: <div data-testid="child-content">Hello from children!</div>,
|
|
||||||
})
|
|
||||||
).rejects.toThrow("common.organization_not_found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws if no environment is found", async () => {
|
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123" } as TUser);
|
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
vi.mocked(getEnvironment).mockResolvedValueOnce(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
SurveyEditorEnvironmentLayout({
|
SurveyEditorEnvironmentLayout({
|
||||||
params: { environmentId: "env-123" },
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
children: <div>Child</div>,
|
children: <div>Content</div>,
|
||||||
})
|
})
|
||||||
).rejects.toThrow("common.environment_not_found");
|
).rejects.toThrow("common.environment_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders environment layout if everything is valid", async () => {
|
test("calls redirect when session is null", async () => {
|
||||||
// Provide all valid data
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
vi.mocked(getServerSession).mockResolvedValueOnce({ user: { id: "user-123" } });
|
t: ((key: string) => key) as any,
|
||||||
vi.mocked(getUser).mockResolvedValueOnce({ id: "user-123", email: "test@example.com" } as TUser);
|
session: undefined as unknown as Session,
|
||||||
vi.mocked(hasUserEnvironmentAccess).mockResolvedValueOnce(true);
|
user: undefined as unknown as TUser,
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValueOnce({ id: "org-999" } as TOrganization);
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
vi.mocked(getEnvironment).mockResolvedValueOnce({
|
});
|
||||||
id: "env-123",
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
name: "My Test Environment",
|
throw new Error("Redirect called");
|
||||||
} 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
|
await expect(
|
||||||
expect(screen.getByTestId("child-content")).toHaveTextContent("Hello from children!");
|
SurveyEditorEnvironmentLayout({
|
||||||
expect(screen.getByTestId("posthog-identify")).toBeInTheDocument();
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
expect(screen.getByTestId("formbricks-client")).toBeInTheDocument();
|
children: <div>Content</div>,
|
||||||
expect(screen.getByTestId("mock-toaster")).toBeInTheDocument();
|
})
|
||||||
expect(screen.getByTestId("mock-response-filter-provider")).toBeInTheDocument();
|
).rejects.toThrow("Redirect called");
|
||||||
expect(screen.getByTestId("dev-environment-banner")).toHaveTextContent("env-123");
|
});
|
||||||
|
|
||||||
|
test("throws error if user is null", async () => {
|
||||||
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
|
t: ((key: string) => key) as any,
|
||||||
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
user: undefined as unknown as TUser,
|
||||||
|
organization: { id: "org1", name: "Org1", billing: {} } as TOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(redirect).mockImplementationOnce(() => {
|
||||||
|
throw new Error("Redirect called");
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
SurveyEditorEnvironmentLayout({
|
||||||
|
params: Promise.resolve({ environmentId: "env1" }),
|
||||||
|
children: <div>Content</div>,
|
||||||
|
})
|
||||||
|
).rejects.toThrow("common.user_not_found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,46 +1,24 @@
|
|||||||
import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
import { ToasterClient } from "@/modules/ui/components/toaster-client";
|
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { IS_POSTHOG_CONFIGURED } from "@formbricks/lib/constants";
|
|
||||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
|
||||||
|
|
||||||
const SurveyEditorEnvironmentLayout = async (props) => {
|
const SurveyEditorEnvironmentLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const t = await getTranslate();
|
const { t, session, user, organization } = await environmentIdLayoutChecks(params.environmentId);
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session) {
|
||||||
return redirect(`/auth/login`);
|
return redirect(`/auth/login`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(session.user.id);
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
|
|
||||||
if (!hasAccess) {
|
|
||||||
throw new AuthorizationError(t("common.not_authorized"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await getOrganizationByEnvironmentId(params.environmentId);
|
|
||||||
if (!organization) {
|
|
||||||
throw new Error(t("common.organization_not_found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const environment = await getEnvironment(params.environmentId);
|
const environment = await getEnvironment(params.environmentId);
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
@@ -48,23 +26,16 @@ const SurveyEditorEnvironmentLayout = async (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponseFilterProvider>
|
<EnvironmentIdBaseLayout
|
||||||
<PosthogIdentify
|
environmentId={params.environmentId}
|
||||||
session={session}
|
session={session}
|
||||||
user={user}
|
user={user}
|
||||||
environmentId={params.environmentId}
|
organization={organization}>
|
||||||
organizationId={organization.id}
|
|
||||||
organizationName={organization.name}
|
|
||||||
organizationBilling={organization.billing}
|
|
||||||
isPosthogEnabled={IS_POSTHOG_CONFIGURED}
|
|
||||||
/>
|
|
||||||
<FormbricksClient userId={user.id} email={user.email} />
|
|
||||||
<ToasterClient />
|
|
||||||
<div className="flex h-screen flex-col">
|
<div className="flex h-screen flex-col">
|
||||||
<DevEnvironmentBanner environment={environment} />
|
<DevEnvironmentBanner environment={environment} />
|
||||||
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</ResponseFilterProvider>
|
</EnvironmentIdBaseLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { formbricksEnabled } from "@/app/lib/formbricks";
|
|
||||||
import { usePathname, useSearchParams } from "next/navigation";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import formbricks from "@formbricks/js";
|
|
||||||
import { env } from "@formbricks/lib/env";
|
|
||||||
|
|
||||||
export const FormbricksClient = ({ userId, email }: { userId: string; email: string }) => {
|
|
||||||
const pathname = usePathname();
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formbricksEnabled && userId) {
|
|
||||||
formbricks.setup({
|
|
||||||
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
|
||||||
appUrl: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
|
||||||
});
|
|
||||||
|
|
||||||
formbricks.setUserId(userId);
|
|
||||||
formbricks.setEmail(email);
|
|
||||||
}
|
|
||||||
}, [userId, email]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formbricksEnabled) {
|
|
||||||
formbricks.registerRouteChange();
|
|
||||||
}
|
|
||||||
}, [pathname, searchParams]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
|
|
||||||
export const LoadingCard = ({
|
export const LoadingCard = ({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { SingleContactPage } from "@/modules/ee/contacts/[contactId]/page";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import Page from "./page";
|
||||||
|
|
||||||
|
// mock constants
|
||||||
|
vi.mock("@/lib/constants", () => ({
|
||||||
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
|
ENCRYPTION_KEY: "test",
|
||||||
|
ENTERPRISE_LICENSE_KEY: "test",
|
||||||
|
GITHUB_ID: "test",
|
||||||
|
GITHUB_SECRET: "test",
|
||||||
|
GOOGLE_CLIENT_ID: "test",
|
||||||
|
GOOGLE_CLIENT_SECRET: "test",
|
||||||
|
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",
|
||||||
|
WEBAPP_URL: "mock-webapp-url",
|
||||||
|
IS_PRODUCTION: true,
|
||||||
|
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||||
|
SMTP_HOST: "mock-smtp-host",
|
||||||
|
SMTP_PORT: "mock-smtp-port",
|
||||||
|
IS_POSTHOG_CONFIGURED: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Contact Page Re-export", () => {
|
||||||
|
test("should re-export SingleContactPage", () => {
|
||||||
|
expect(Page).toBe(SingleContactPage);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ContactsPage } from "@/modules/ee/contacts/page";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
import Page from "./page";
|
||||||
|
|
||||||
|
// Mock the actual ContactsPage component
|
||||||
|
vi.mock("@/modules/ee/contacts/page", () => ({
|
||||||
|
ContactsPage: () => <div data-testid="contacts-page">Mock Contacts Page</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Contacts Page Re-export", () => {
|
||||||
|
test("should re-export ContactsPage from the EE module", () => {
|
||||||
|
// Assert that the default export 'Page' is the same as the mocked 'ContactsPage'
|
||||||
|
expect(Page).toBe(ContactsPage);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import SegmentsPageWrapper from "./page";
|
||||||
|
|
||||||
|
vi.mock("@/modules/ee/contacts/segments/page", () => ({
|
||||||
|
SegmentsPage: vi.fn(() => <div>SegmentsPageMock</div>),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("SegmentsPageWrapper", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders the SegmentsPage component", () => {
|
||||||
|
render(<SegmentsPageWrapper params={{ environmentId: "test-env" } as any} />);
|
||||||
|
expect(screen.getByText("SegmentsPageMock")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getOrganization } from "@/lib/organization/service";
|
||||||
|
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -8,9 +11,6 @@ import {
|
|||||||
} from "@/modules/ee/license-check/lib/utils";
|
} from "@/modules/ee/license-check/lib/utils";
|
||||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
|
||||||
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteActionClass, getActionClass, updateActionClass } from "@/lib/actionClass/service";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
|
import { getSurveysByActionClassId } from "@/lib/survey/service";
|
||||||
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
|
||||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { getActiveInactiveSurveysAction } from "../actions";
|
||||||
|
import { ActionActivityTab } from "./ActionActivityTab";
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/actions/utils", () => ({
|
||||||
|
ACTION_TYPE_ICON_LOOKUP: {
|
||||||
|
noCode: <div>NoCodeIcon</div>,
|
||||||
|
automatic: <div>AutomaticIcon</div>,
|
||||||
|
code: <div>CodeIcon</div>,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/time", () => ({
|
||||||
|
convertDateTimeStringShort: (dateString: string) => `formatted-${dateString}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/helper", () => ({
|
||||||
|
getFormattedErrorMessage: (error: any) => `Formatted error: ${error?.message || "Unknown error"}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/utils/strings", () => ({
|
||||||
|
capitalizeFirstLetter: (str: string) => str.charAt(0).toUpperCase() + str.slice(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/survey/editor/actions", () => ({
|
||||||
|
createActionClassAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/button", () => ({
|
||||||
|
Button: ({ children, onClick, variant, ...props }: any) => (
|
||||||
|
<button onClick={onClick} data-variant={variant} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/error-component", () => ({
|
||||||
|
ErrorComponent: () => <div>ErrorComponent</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/label", () => ({
|
||||||
|
Label: ({ children, ...props }: any) => <label {...props}>{children}</label>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/loading-spinner", () => ({
|
||||||
|
LoadingSpinner: () => <div>LoadingSpinner</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../actions", () => ({
|
||||||
|
getActiveInactiveSurveysAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockActionClass = {
|
||||||
|
id: "action1",
|
||||||
|
createdAt: new Date("2023-01-01T10:00:00Z"),
|
||||||
|
updatedAt: new Date("2023-01-10T11:00:00Z"),
|
||||||
|
name: "Test Action",
|
||||||
|
description: "Test Description",
|
||||||
|
type: "noCode",
|
||||||
|
environmentId: "env1_dev",
|
||||||
|
noCodeConfig: {
|
||||||
|
/* ... */
|
||||||
|
} as any,
|
||||||
|
} as unknown as TActionClass;
|
||||||
|
|
||||||
|
const mockEnvironmentDev = {
|
||||||
|
id: "env1_dev",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
type: "development",
|
||||||
|
} as unknown as TEnvironment;
|
||||||
|
|
||||||
|
const mockEnvironmentProd = {
|
||||||
|
id: "env1_prod",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
type: "production",
|
||||||
|
} as unknown as TEnvironment;
|
||||||
|
|
||||||
|
const mockOtherEnvActionClasses: TActionClass[] = [
|
||||||
|
{
|
||||||
|
id: "action2",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: "Existing Action Prod",
|
||||||
|
type: "noCode",
|
||||||
|
environmentId: "env1_prod",
|
||||||
|
} as unknown as TActionClass,
|
||||||
|
{
|
||||||
|
id: "action3",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: "Existing Code Action Prod",
|
||||||
|
type: "code",
|
||||||
|
key: "existing-key",
|
||||||
|
environmentId: "env1_prod",
|
||||||
|
} as unknown as TActionClass,
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("ActionActivityTab", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(getActiveInactiveSurveysAction).mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
activeSurveys: ["Active Survey 1"],
|
||||||
|
inactiveSurveys: ["Inactive Survey 1", "Inactive Survey 2"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders loading state initially", () => {
|
||||||
|
// Don't resolve the promise immediately
|
||||||
|
vi.mocked(getActiveInactiveSurveysAction).mockReturnValue(new Promise(() => {}));
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("LoadingSpinner")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders error state if fetching surveys fails", async () => {
|
||||||
|
vi.mocked(getActiveInactiveSurveysAction).mockResolvedValue({
|
||||||
|
data: undefined,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// Wait for the component to update after the promise resolves
|
||||||
|
await screen.findByText("ErrorComponent");
|
||||||
|
expect(screen.getByText("ErrorComponent")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders survey lists and action details correctly", async () => {
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for loading to finish
|
||||||
|
await screen.findByText("common.active_surveys");
|
||||||
|
|
||||||
|
// Check survey lists
|
||||||
|
expect(screen.getByText("Active Survey 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Inactive Survey 1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Inactive Survey 2")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check action details
|
||||||
|
// Use the actual Date.toString() output that the mock receives
|
||||||
|
expect(screen.getByText(`formatted-${mockActionClass.createdAt.toString()}`)).toBeInTheDocument(); // Created on
|
||||||
|
expect(screen.getByText(`formatted-${mockActionClass.updatedAt.toString()}`)).toBeInTheDocument(); // Last updated
|
||||||
|
expect(screen.getByText("NoCodeIcon")).toBeInTheDocument(); // Type icon
|
||||||
|
expect(screen.getByText("NoCode")).toBeInTheDocument(); // Type text
|
||||||
|
expect(screen.getByText("Development")).toBeInTheDocument(); // Environment
|
||||||
|
expect(screen.getByText("Copy to Production")).toBeInTheDocument(); // Copy button text
|
||||||
|
});
|
||||||
|
|
||||||
|
test("calls copyAction with correct data on button click", async () => {
|
||||||
|
vi.mocked(createActionClassAction).mockResolvedValue({ data: { id: "newAction" } as any });
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("Copy to Production");
|
||||||
|
const copyButton = screen.getByText("Copy to Production");
|
||||||
|
await userEvent.click(copyButton);
|
||||||
|
|
||||||
|
expect(createActionClassAction).toHaveBeenCalledTimes(1);
|
||||||
|
// Include the extra properties that the component sends due to spreading mockActionClass
|
||||||
|
const expectedActionInput = {
|
||||||
|
...mockActionClass, // Spread the original object
|
||||||
|
name: "Test Action", // Keep the original name as it doesn't conflict
|
||||||
|
environmentId: "env1_prod", // Target environment ID
|
||||||
|
};
|
||||||
|
// Remove properties not expected by the action call itself, even if sent by component
|
||||||
|
delete (expectedActionInput as any).id;
|
||||||
|
delete (expectedActionInput as any).createdAt;
|
||||||
|
delete (expectedActionInput as any).updatedAt;
|
||||||
|
|
||||||
|
// The assertion now checks against the structure sent by the component
|
||||||
|
expect(createActionClassAction).toHaveBeenCalledWith({
|
||||||
|
action: {
|
||||||
|
...mockActionClass, // Include id, createdAt, updatedAt etc.
|
||||||
|
name: "Test Action",
|
||||||
|
environmentId: "env1_prod",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("environments.actions.action_copied_successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles name conflict during copy", async () => {
|
||||||
|
vi.mocked(createActionClassAction).mockResolvedValue({ data: { id: "newAction" } as any });
|
||||||
|
const conflictingActionClass = { ...mockActionClass, name: "Existing Action Prod" };
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={conflictingActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("Copy to Production");
|
||||||
|
const copyButton = screen.getByText("Copy to Production");
|
||||||
|
await userEvent.click(copyButton);
|
||||||
|
|
||||||
|
expect(createActionClassAction).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// The assertion now checks against the structure sent by the component
|
||||||
|
expect(createActionClassAction).toHaveBeenCalledWith({
|
||||||
|
action: {
|
||||||
|
...conflictingActionClass, // Include id, createdAt, updatedAt etc.
|
||||||
|
name: "Existing Action Prod (copy)",
|
||||||
|
environmentId: "env1_prod",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("environments.actions.action_copied_successfully");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles key conflict during copy for 'code' type", async () => {
|
||||||
|
const codeActionClass: TActionClass = {
|
||||||
|
...mockActionClass,
|
||||||
|
id: "codeAction1",
|
||||||
|
type: "code",
|
||||||
|
key: "existing-key", // Conflicting key
|
||||||
|
noCodeConfig: {
|
||||||
|
/* ... */
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={codeActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("Copy to Production");
|
||||||
|
const copyButton = screen.getByText("Copy to Production");
|
||||||
|
await userEvent.click(copyButton);
|
||||||
|
|
||||||
|
expect(createActionClassAction).not.toHaveBeenCalled();
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("environments.actions.action_with_key_already_exists");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error if copy action fails server-side", async () => {
|
||||||
|
vi.mocked(createActionClassAction).mockResolvedValue({ data: undefined });
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("Copy to Production");
|
||||||
|
const copyButton = screen.getByText("Copy to Production");
|
||||||
|
await userEvent.click(copyButton);
|
||||||
|
|
||||||
|
expect(createActionClassAction).toHaveBeenCalledTimes(1);
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("environments.actions.action_copy_failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error and prevents copy if user is read-only", async () => {
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={mockActionClass}
|
||||||
|
environmentId="env1_dev"
|
||||||
|
environment={mockEnvironmentDev}
|
||||||
|
otherEnvActionClasses={mockOtherEnvActionClasses}
|
||||||
|
otherEnvironment={mockEnvironmentProd}
|
||||||
|
isReadOnly={true} // Set to read-only
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText("Copy to Production");
|
||||||
|
const copyButton = screen.getByText("Copy to Production");
|
||||||
|
await userEvent.click(copyButton);
|
||||||
|
|
||||||
|
expect(createActionClassAction).not.toHaveBeenCalled();
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("common.you_are_not_authorised_to_perform_this_action");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders correct copy button text for production environment", async () => {
|
||||||
|
render(
|
||||||
|
<ActionActivityTab
|
||||||
|
actionClass={{ ...mockActionClass, environmentId: "env1_prod" }}
|
||||||
|
environmentId="env1_prod"
|
||||||
|
environment={mockEnvironmentProd} // Current env is Production
|
||||||
|
otherEnvActionClasses={[]} // Assume dev env has no actions for simplicity
|
||||||
|
otherEnvironment={mockEnvironmentDev} // Target env is Development
|
||||||
|
isReadOnly={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await screen.findByText("Copy to Development");
|
||||||
|
expect(screen.getByText("Copy to Development")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Production")).toBeInTheDocument(); // Environment text
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
|
import { convertDateTimeStringShort } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
||||||
@@ -10,8 +12,6 @@ import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
|||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { getActiveInactiveSurveysAction } from "../actions";
|
import { getActiveInactiveSurveysAction } from "../actions";
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { ActionClassesTable } from "./ActionClassesTable";
|
||||||
|
|
||||||
|
// Mock the ActionDetailModal
|
||||||
|
vi.mock("./ActionDetailModal", () => ({
|
||||||
|
ActionDetailModal: ({ open, actionClass, setOpen }: any) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="action-detail-modal">
|
||||||
|
Modal for {actionClass.name}
|
||||||
|
<button onClick={() => setOpen(false)}>Close Modal</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockActionClasses: TActionClass[] = [
|
||||||
|
{ id: "1", name: "Action 1", type: "noCode", environmentId: "env1" } as TActionClass,
|
||||||
|
{ id: "2", name: "Action 2", type: "code", environmentId: "env1" } as TActionClass,
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockEnvironment: TEnvironment = {
|
||||||
|
id: "env1",
|
||||||
|
name: "Test Environment",
|
||||||
|
type: "development",
|
||||||
|
} as unknown as TEnvironment;
|
||||||
|
const mockOtherEnvironment: TEnvironment = {
|
||||||
|
id: "env2",
|
||||||
|
name: "Other Environment",
|
||||||
|
type: "production",
|
||||||
|
} as unknown as TEnvironment;
|
||||||
|
|
||||||
|
const mockTableHeading = <div data-testid="table-heading">Table Heading</div>;
|
||||||
|
const mockActionRows = mockActionClasses.map((action) => (
|
||||||
|
<div key={action.id} data-testid={`action-row-${action.id}`}>
|
||||||
|
{action.name} Row
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
describe("ActionClassesTable", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders table heading and action rows when actions exist", () => {
|
||||||
|
render(
|
||||||
|
<ActionClassesTable
|
||||||
|
environmentId="env1"
|
||||||
|
actionClasses={mockActionClasses}
|
||||||
|
environment={mockEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
otherEnvActionClasses={[]}
|
||||||
|
otherEnvironment={mockOtherEnvironment}>
|
||||||
|
{[mockTableHeading, mockActionRows]}
|
||||||
|
</ActionClassesTable>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("table-heading")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("action-row-1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("action-row-2")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("No actions found")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders 'No actions found' message when no actions exist", () => {
|
||||||
|
render(
|
||||||
|
<ActionClassesTable
|
||||||
|
environmentId="env1"
|
||||||
|
actionClasses={[]}
|
||||||
|
environment={mockEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
otherEnvActionClasses={[]}
|
||||||
|
otherEnvironment={mockOtherEnvironment}>
|
||||||
|
{[mockTableHeading, []]}
|
||||||
|
</ActionClassesTable>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("table-heading")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("No actions found")).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("action-row-1")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("opens ActionDetailModal with correct action when a row is clicked", async () => {
|
||||||
|
render(
|
||||||
|
<ActionClassesTable
|
||||||
|
environmentId="env1"
|
||||||
|
actionClasses={mockActionClasses}
|
||||||
|
environment={mockEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
otherEnvActionClasses={[]}
|
||||||
|
otherEnvironment={mockOtherEnvironment}>
|
||||||
|
{[mockTableHeading, mockActionRows]}
|
||||||
|
</ActionClassesTable>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Modal should not be open initially
|
||||||
|
expect(screen.queryByTestId("action-detail-modal")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Find the button wrapping the first action row
|
||||||
|
const actionButton1 = screen.getByTitle("Action 1");
|
||||||
|
await userEvent.click(actionButton1);
|
||||||
|
|
||||||
|
// Modal should now be open with the correct action name
|
||||||
|
const modal = screen.getByTestId("action-detail-modal");
|
||||||
|
expect(modal).toBeInTheDocument();
|
||||||
|
expect(modal).toHaveTextContent("Modal for Action 1");
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
await userEvent.click(screen.getByText("Close Modal"));
|
||||||
|
expect(screen.queryByTestId("action-detail-modal")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
// Click the second action button
|
||||||
|
const actionButton2 = screen.getByTitle("Action 2");
|
||||||
|
await userEvent.click(actionButton2);
|
||||||
|
|
||||||
|
// Modal should open for the second action
|
||||||
|
const modal2 = screen.getByTestId("action-detail-modal");
|
||||||
|
expect(modal2).toBeInTheDocument();
|
||||||
|
expect(modal2).toHaveTextContent("Modal for Action 2");
|
||||||
|
});
|
||||||
|
});
|
||||||