mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-21 00:46:39 -05:00
Compare commits
63 Commits
fix/tailwi
...
response-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
11
.env.example
11
.env.example
@@ -206,12 +206,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=
|
||||||
|
|
||||||
@@ -219,3 +213,8 @@ 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=
|
||||||
|
|||||||
26
.github/copilot-instructions.md
vendored
Normal file
26
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Testing Instructions
|
||||||
|
|
||||||
|
When generating test files inside the "/app/web" path, follow these rules:
|
||||||
|
|
||||||
|
- Use vitest
|
||||||
|
- Ensure 100% code coverage
|
||||||
|
- Add as few comments as possible
|
||||||
|
- The test file should be located in the same folder as the original file
|
||||||
|
- Use the `test` function instead of `it`
|
||||||
|
- Follow the same test pattern used for other files in the package where the file is located
|
||||||
|
- All imports should be at the top of the file, not inside individual tests
|
||||||
|
- For mocking inside "test" blocks use "vi.mocked"
|
||||||
|
- Add the original file path to the "test.coverage.include"array in the "apps/web/vite.config.mts" file
|
||||||
|
- Don't mock functions that are already mocked in the "apps/web/vitestSetup.ts" file
|
||||||
|
|
||||||
|
If it's a test for a ".tsx" file, follow these extra instructions:
|
||||||
|
|
||||||
|
- Add this code inside the "describe" block and before any test:
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
- the "afterEach" function should only have "cleanup()" inside it and should be adde to the "vitest" imports
|
||||||
|
- For click events, import userEvent from "@testing-library/user-event"
|
||||||
|
- Mock other components that can make the text more complex and but at the same time mocking it wouldn't make the test flaky. It's ok to leave basic and simple components.
|
||||||
31
.github/workflows/deploy-formbricks-cloud.yml
vendored
31
.github/workflows/deploy-formbricks-cloud.yml
vendored
@@ -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
|
||||||
@@ -48,6 +59,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: (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && github.event.inputs.ENVIRONMENT == 'prod'
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
@@ -58,7 +71,23 @@ jobs:
|
|||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
helmfile-args: apply
|
helmfile-args: apply -l environment=prod
|
||||||
|
helmfile-auto-init: "false"
|
||||||
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
- uses: helmfile/helmfile-action@v2
|
||||||
|
name: Deploy Formbricks Cloud Stage
|
||||||
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.VERSION }}
|
||||||
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
|
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
||||||
|
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
||||||
|
with:
|
||||||
|
helm-plugins: >
|
||||||
|
https://github.com/databus23/helm-diff,
|
||||||
|
https://github.com/jkroepke/helm-secrets
|
||||||
|
helmfile-args: apply -l environment=stage
|
||||||
helmfile-auto-init: "false"
|
helmfile-auto-init: "false"
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/formbricks-release.yml
vendored
1
.github/workflows/formbricks-release.yml
vendored
@@ -31,3 +31,4 @@ jobs:
|
|||||||
- helm-chart-release
|
- helm-chart-release
|
||||||
with:
|
with:
|
||||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||||
|
ENVIRONMENT: "prod"
|
||||||
|
|||||||
56
.github/workflows/release-changesets.yml
vendored
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 }}
|
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
# 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
|
||||||
|
|||||||
4
.github/workflows/release-docker-github.yml
vendored
4
.github/workflows/release-docker-github.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
|||||||
# 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
|
||||||
|
|||||||
1
.gitignore
vendored
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"
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
"sonarlint.connectedMode.project": {
|
"sonarlint.connectedMode.project": {
|
||||||
"connectionId": "formbricks",
|
"connectionId": "formbricks",
|
||||||
"projectKey": "formbricks_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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: ["lib/messages/**/*.json"],
|
||||||
|
plugins: ["i18n-json"],
|
||||||
|
rules: {
|
||||||
|
"i18n-json/identical-keys": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
filePath: require("path").join(__dirname, "messages", "en-US.json"),
|
||||||
|
checkExtraKeys: false,
|
||||||
|
checkMissingKeys: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
2
apps/web/.gitignore
vendored
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
|
||||||
@@ -81,13 +81,14 @@ RUN corepack enable
|
|||||||
RUN apk add --no-cache curl \
|
RUN apk add --no-cache curl \
|
||||||
&& apk add --no-cache supercronic \
|
&& apk add --no-cache supercronic \
|
||||||
# && addgroup --system --gid 1001 nodejs \
|
# && addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 nextjs
|
&& addgroup -S nextjs \
|
||||||
|
&& adduser -S -u 1001 -G nextjs 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,38 +96,38 @@ RUN chmod 644 ./next.config.mjs
|
|||||||
COPY --from=installer /app/apps/web/package.json .
|
COPY --from=installer /app/apps/web/package.json .
|
||||||
RUN chmod 644 ./package.json
|
RUN chmod 644 ./package.json
|
||||||
|
|
||||||
COPY --from=installer --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
|
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||||
RUN chmod 644 ./packages/database/schema.prisma
|
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json
|
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
||||||
RUN chmod 644 ./packages/database/package.json
|
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration
|
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
||||||
RUN chmod -R 755 ./packages/database/migration
|
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src
|
COPY --from=installer /app/packages/database/src ./packages/database/src
|
||||||
RUN chmod -R 755 ./packages/database/src
|
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/node_modules ./packages/database/node_modules
|
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
||||||
RUN chmod -R 755 ./packages/database/node_modules
|
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
RUN chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||||
RUN chmod -R 755 ./node_modules/@prisma/client
|
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma
|
COPY --from=installer /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
RUN chmod -R 755 ./node_modules/.prisma
|
RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules/.prisma
|
||||||
|
|
||||||
COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt .
|
COPY --from=installer /prisma_version.txt .
|
||||||
RUN chmod 644 ./prisma_version.txt
|
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
|
||||||
|
|
||||||
COPY /docker/cronjobs /app/docker/cronjobs
|
COPY /docker/cronjobs /app/docker/cronjobs
|
||||||
RUN chmod -R 755 /app/docker/cronjobs
|
RUN chmod -R 755 /app/docker/cronjobs
|
||||||
|
|||||||
@@ -1,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";
|
||||||
|
|||||||
@@ -101,17 +101,17 @@ export const OnboardingSetupInstructions = ({
|
|||||||
<div>
|
<div>
|
||||||
{activeTab === "npm" ? (
|
{activeTab === "npm" ? (
|
||||||
<div className="prose prose-slate w-full">
|
<div className="prose prose-slate w-full">
|
||||||
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
|
||||||
npm install @formbricks/js
|
npm install @formbricks/js
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
<p>{t("common.or")}</p>
|
<p>{t("common.or")}</p>
|
||||||
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="sh">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
|
||||||
yarn add @formbricks/js
|
yarn add @formbricks/js
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
<p className="text-sm text-slate-700">
|
<p className="text-sm text-slate-700">
|
||||||
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
|
||||||
</p>
|
</p>
|
||||||
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||||
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
|
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
<Button id="onboarding-inapp-connect-read-npm-docs" className="mt-3" variant="secondary" asChild>
|
<Button id="onboarding-inapp-connect-read-npm-docs" className="mt-3" variant="secondary" asChild>
|
||||||
@@ -125,11 +125,11 @@ export const OnboardingSetupInstructions = ({
|
|||||||
</div>
|
</div>
|
||||||
) : activeTab === "html" ? (
|
) : activeTab === "html" ? (
|
||||||
<div className="prose prose-slate">
|
<div className="prose prose-slate">
|
||||||
<p className="mt-6 -mb-1 text-sm text-slate-700">
|
<p className="-mb-1 mt-6 text-sm text-slate-700">
|
||||||
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<CodeBlock customEditorClass="bg-white! border border-slate-200" language="js">
|
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
|
||||||
{channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys}
|
{channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys}
|
||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||||
|
import { WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
interface ConnectPageProps {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
|
className="absolute 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 top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
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";
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
import {
|
import {
|
||||||
@@ -24,8 +26,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";
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export const LandingSidebar = ({
|
|||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
asChild
|
asChild
|
||||||
id="userDropdownTrigger"
|
id="userDropdownTrigger"
|
||||||
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
|
className="w-full rounded-br-xl border-t py-4 pl-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||||
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center space-x-3")}>
|
<div tabIndex={0} className={cn("flex cursor-pointer flex-row items-center space-x-3")}>
|
||||||
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
|
<ProfileAvatar userId={user.id} imageUrl={user.imageUrl} />
|
||||||
<>
|
<>
|
||||||
@@ -112,7 +112,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}
|
||||||
|
|||||||
@@ -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 top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
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<{
|
||||||
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -218,7 +218,7 @@ export const ProjectSettings = ({
|
|||||||
</FormProvider>
|
</FormProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow-sm">
|
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
|
||||||
{logoUrl && (
|
{logoUrl && (
|
||||||
<Image
|
<Image
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
|
|||||||
@@ -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 top-5 right-5 mt-0! text-slate-500 hover:text-slate-700"
|
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
@@ -28,7 +28,7 @@ vi.mock("@/modules/ui/components/dev-environment-banner", () => ({
|
|||||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||||
environmentIdLayoutChecks: vi.fn(),
|
environmentIdLayoutChecks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/environment/service", () => ({
|
vi.mock("@/lib/environment/service", () => ({
|
||||||
getEnvironment: vi.fn(),
|
getEnvironment: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
@@ -41,7 +41,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders successfully when environment is found", async () => {
|
test("renders successfully when environment is found", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
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,
|
session: { user: { id: "user1" } } as Session,
|
||||||
@@ -62,7 +62,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
|||||||
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
expect(screen.getByTestId("child")).toHaveTextContent("Survey Editor Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws an error when environment is not found", async () => {
|
test("throws an error when environment is not found", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: { user: { id: "user1" } } as Session,
|
session: { user: { id: "user1" } } as Session,
|
||||||
@@ -79,7 +79,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
|||||||
).rejects.toThrow("common.environment_not_found");
|
).rejects.toThrow("common.environment_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls redirect when session is null", async () => {
|
test("calls redirect when session is null", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: undefined as unknown as Session,
|
session: undefined as unknown as Session,
|
||||||
@@ -98,7 +98,7 @@ describe("SurveyEditorEnvironmentLayout", () => {
|
|||||||
).rejects.toThrow("Redirect called");
|
).rejects.toThrow("Redirect called");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if user is null", async () => {
|
test("throws error if user is null", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: { user: { id: "user1" } } as Session,
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getEnvironment } from "@/lib/environment/service";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
|
||||||
|
|
||||||
const SurveyEditorEnvironmentLayout = async (props) => {
|
const SurveyEditorEnvironmentLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ActionClassesTable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
|
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||||
{TableHeading}
|
{TableHeading}
|
||||||
<div id="actionClassesWrapper" className="flex flex-col">
|
<div id="actionClassesWrapper" className="flex flex-col">
|
||||||
{actionClasses.length > 0 ? (
|
{actionClasses.length > 0 ? (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
import { timeSince } from "@/lib/time";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -14,7 +14,9 @@ export const ActionClassDataRow = ({
|
|||||||
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="h-5 w-5 shrink-0 text-slate-500">{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}</div>
|
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
|
||||||
|
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
|
||||||
|
</div>
|
||||||
<div className="ml-4 text-left">
|
<div className="ml-4 text-left">
|
||||||
<div className="font-medium text-slate-900">{actionClass.name}</div>
|
<div className="font-medium text-slate-900">{actionClass.name}</div>
|
||||||
<div className="text-xs text-slate-400">{actionClass.description}</div>
|
<div className="text-xs text-slate-400">{actionClass.description}</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const Loading = () => {
|
|||||||
<>
|
<>
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.actions")} />
|
<PageHeader pageTitle={t("common.actions")} />
|
||||||
<div className="rounded-xl border border-slate-200 bg-white shadow-xs">
|
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||||
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-6 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
|
||||||
<span className="sr-only">{t("common.edit")}</span>
|
<span className="sr-only">{t("common.edit")}</span>
|
||||||
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
|
<div className="col-span-4 pl-6">{t("environments.actions.user_actions")}</div>
|
||||||
@@ -22,7 +22,7 @@ const Loading = () => {
|
|||||||
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="h-6 w-6 shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
|
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-slate-200 text-slate-500" />
|
||||||
<div className="ml-4 text-left">
|
<div className="ml-4 text-left">
|
||||||
<div className="font-medium text-slate-900">
|
<div className="font-medium text-slate-900">
|
||||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
@@ -33,7 +33,7 @@ const Loading = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto flex justify-center text-center text-sm whitespace-nowrap text-slate-500">
|
<div className="col-span-2 my-auto flex justify-center whitespace-nowrap text-center text-sm text-slate-500">
|
||||||
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
<div className="h-4 w-28 animate-pulse rounded-full bg-slate-200"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
|
|||||||
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||||
|
import { getActionClasses } from "@/lib/actionClass/service";
|
||||||
|
import { getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
|
||||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Actions",
|
title: "Actions",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||||
|
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
|
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import {
|
||||||
|
getMonthlyActiveOrganizationPeopleCount,
|
||||||
|
getMonthlyOrganizationResponseCount,
|
||||||
|
getOrganizationByEnvironmentId,
|
||||||
|
getOrganizationsByUserId,
|
||||||
|
} from "@/lib/organization/service";
|
||||||
|
import { getUserProjects } from "@/lib/project/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
@@ -7,18 +19,6 @@ import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-bann
|
|||||||
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import {
|
|
||||||
getMonthlyActiveOrganizationPeopleCount,
|
|
||||||
getMonthlyOrganizationResponseCount,
|
|
||||||
getOrganizationByEnvironmentId,
|
|
||||||
getOrganizationsByUserId,
|
|
||||||
} from "@formbricks/lib/organization/service";
|
|
||||||
import { getUserProjects } from "@formbricks/lib/project/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
|
|
||||||
interface EnvironmentLayoutProps {
|
interface EnvironmentLayoutProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
|
||||||
|
|
||||||
interface EnvironmentStorageHandlerProps {
|
interface EnvironmentStorageHandlerProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
import { Switch } from "@/modules/ui/components/switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface EnvironmentSwitchProps {
|
interface EnvironmentSwitchProps {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
@@ -45,9 +48,6 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
@@ -265,7 +265,7 @@ export const MainNavigation = ({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={toggleSidebar}
|
onClick={toggleSidebar}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-hidden"
|
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
|
||||||
)}>
|
)}>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<PanelLeftOpenIcon strokeWidth={1.5} />
|
<PanelLeftOpenIcon strokeWidth={1.5} />
|
||||||
@@ -332,7 +332,7 @@ export const MainNavigation = ({
|
|||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
asChild
|
asChild
|
||||||
id="userDropdownTrigger"
|
id="userDropdownTrigger"
|
||||||
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-hidden">
|
className="w-full rounded-br-xl border-t py-4 transition-colors duration-200 hover:bg-slate-50 focus:outline-none">
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
|
|
||||||
interface NavigationLinkProps {
|
interface NavigationLinkProps {
|
||||||
href: string;
|
href: string;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "@testing-library/jest-dom/vitest";
|
|||||||
import { cleanup, render } from "@testing-library/react";
|
import { cleanup, render } from "@testing-library/react";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { usePostHog } from "posthog-js/react";
|
import { usePostHog } from "posthog-js/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { PosthogIdentify } from "./PosthogIdentify";
|
import { PosthogIdentify } from "./PosthogIdentify";
|
||||||
@@ -18,7 +18,7 @@ describe("PosthogIdentify", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
test("identifies the user and sets groups when isPosthogEnabled is true", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ describe("PosthogIdentify", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if isPosthogEnabled is false", () => {
|
test("does nothing if isPosthogEnabled is false", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if session user is missing", () => {
|
test("does nothing if session user is missing", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ describe("PosthogIdentify", () => {
|
|||||||
expect(mockGroup).not.toHaveBeenCalled();
|
expect(mockGroup).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("identifies user but does not group if environmentId/organizationId not provided", () => {
|
test("identifies user but does not group if environmentId/organizationId not provided", () => {
|
||||||
const mockIdentify = vi.fn();
|
const mockIdentify = vi.fn();
|
||||||
const mockGroup = vi.fn();
|
const mockGroup = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const TopControlBar = ({
|
|||||||
}: SideBarProps) => {
|
}: SideBarProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
|
<div className="fixed inset-0 top-0 z-30 flex h-14 w-full items-center justify-end bg-slate-50 px-6">
|
||||||
<div className="z-10 shadow-2xs">
|
<div className="shadow-xs z-10">
|
||||||
<div className="flex w-fit items-center space-x-2 py-2">
|
<div className="flex w-fit items-center space-x-2 py-2">
|
||||||
<TopControlButtons
|
<TopControlButtons
|
||||||
environment={environment}
|
environment={environment}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/cn";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface WidgetStatusIndicatorProps {
|
interface WidgetStatusIndicatorProps {
|
||||||
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
<currentStatus.icon />
|
<currentStatus.icon />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||||
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
|
||||||
{status === "notImplemented" && (
|
{status === "notImplemented" && (
|
||||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||||
<RotateCcwIcon />
|
<RotateCcwIcon />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +10,6 @@ import {
|
|||||||
getProjectIdFromIntegrationId,
|
getProjectIdFromIntegrationId,
|
||||||
} from "@/lib/utils/helper";
|
} from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
|
|||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -23,8 +25,6 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationAirtable,
|
TIntegrationAirtable,
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable";
|
||||||
|
import { ManageIntegration } from "./ManageIntegration";
|
||||||
|
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||||
|
deleteIntegrationAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock(
|
||||||
|
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal",
|
||||||
|
() => ({
|
||||||
|
AddIntegrationModal: ({ open, setOpenWithStates }) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="add-modal">
|
||||||
|
<button onClick={() => setOpenWithStates(false)}>close</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
||||||
|
DeleteDialog: ({ open, setOpen, onDelete }) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="delete-dialog">
|
||||||
|
<button onClick={onDelete}>confirm</button>
|
||||||
|
<button onClick={() => setOpen(false)}>cancel</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
environment: { id: "env1" } as TEnvironment,
|
||||||
|
environmentId: "env1",
|
||||||
|
setIsConnected: vi.fn(),
|
||||||
|
surveys: [],
|
||||||
|
airtableArray: [],
|
||||||
|
locale: "en-US" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ManageIntegration", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty state", () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
airtableIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
||||||
|
} as TIntegrationAirtable
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/link_new_table/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("open add modal", async () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
airtableIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
||||||
|
} as TIntegrationAirtable
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/link_new_table/));
|
||||||
|
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list integrations and open edit modal", async () => {
|
||||||
|
const item = {
|
||||||
|
baseId: "b",
|
||||||
|
tableId: "t",
|
||||||
|
surveyId: "s",
|
||||||
|
surveyName: "S",
|
||||||
|
tableName: "T",
|
||||||
|
questions: "Q",
|
||||||
|
questionIds: ["x"],
|
||||||
|
createdAt: new Date(),
|
||||||
|
includeVariables: false,
|
||||||
|
includeHiddenFields: false,
|
||||||
|
includeMetadata: false,
|
||||||
|
includeCreatedAt: false,
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
airtableIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [item] } as unknown as TIntegrationAirtableConfig,
|
||||||
|
} as TIntegrationAirtable
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("S")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("S"));
|
||||||
|
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration success", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
airtableIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
||||||
|
} as TIntegrationAirtable
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
||||||
|
const { toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.success).toHaveBeenCalled();
|
||||||
|
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration error", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
airtableIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
||||||
|
} as TIntegrationAirtable
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
const { toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
AddIntegrationModal,
|
AddIntegrationModal,
|
||||||
IntegrationModalInputs,
|
IntegrationModalInputs,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -13,7 +14,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
@@ -98,17 +98,17 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
{integrationData.length ? (
|
{integrationData.length ? (
|
||||||
<div className="mt-6 w-full rounded-lg border border-slate-200">
|
<div className="mt-6 w-full rounded-lg border border-slate-200">
|
||||||
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
{tableHeaders.map((header, idx) => (
|
{tableHeaders.map((header) => (
|
||||||
<div key={idx} className={`col-span-2 hidden text-center sm:block`}>
|
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
|
||||||
{t(header)}
|
{t(header)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{integrationData.map((data, index) => (
|
{integrationData.map((data, index) => (
|
||||||
<div
|
<button
|
||||||
key={index}
|
key={`${index}-${data.baseId}-${data.tableId}-${data.surveyId}`}
|
||||||
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDefaultValues({
|
setDefaultValues({
|
||||||
base: data.baseId,
|
base: data.baseId,
|
||||||
@@ -129,7 +129,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), props.locale)}
|
{timeSince(data.createdAt.toString(), props.locale)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
|
import { getAirtableTables } from "@/lib/airtable/service";
|
||||||
|
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
|
||||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getSpreadsheetNameById } from "@/lib/googleSheet/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service";
|
|
||||||
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { ZIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const ZGetSpreadsheetNameByIdAction = z.object({
|
const ZGetSpreadsheetNameByIdAction = z.object({
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import {
|
|||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -21,8 +23,6 @@ import Image from "next/image";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
@@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
|
||||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
import { ManageIntegration } from "./ManageIntegration";
|
||||||
|
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||||
|
deleteIntegrationAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("react-hot-toast", () => ({
|
||||||
|
default: { success: vi.fn(), error: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
||||||
|
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="delete-dialog">
|
||||||
|
<button onClick={onDelete}>confirm</button>
|
||||||
|
<button onClick={() => setOpen(false)}>cancel</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
|
||||||
|
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
environment: { id: "env1" } as TEnvironment,
|
||||||
|
setOpenAddIntegrationModal: vi.fn(),
|
||||||
|
setIsConnected: vi.fn(),
|
||||||
|
setSelectedIntegration: vi.fn(),
|
||||||
|
locale: "en-US" as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
describe("ManageIntegration (Google Sheets)", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("empty state", () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
googleSheetIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] },
|
||||||
|
} as unknown as TIntegrationGoogleSheets
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("click link new sheet", async () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
googleSheetIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] },
|
||||||
|
} as unknown as TIntegrationGoogleSheets
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText(/link_new_sheet/));
|
||||||
|
|
||||||
|
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
|
||||||
|
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list integrations and open edit", async () => {
|
||||||
|
const item = {
|
||||||
|
spreadsheetId: "sid",
|
||||||
|
spreadsheetName: "SheetName",
|
||||||
|
surveyId: "s1",
|
||||||
|
surveyName: "Survey1",
|
||||||
|
questionIds: ["q1"],
|
||||||
|
questions: "Q",
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
googleSheetIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [item] },
|
||||||
|
} as unknown as TIntegrationGoogleSheets
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Survey1")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText("Survey1"));
|
||||||
|
|
||||||
|
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({
|
||||||
|
...item,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration success", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
googleSheetIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] },
|
||||||
|
} as unknown as TIntegrationGoogleSheets
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
|
||||||
|
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
||||||
|
|
||||||
|
const { default: toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
||||||
|
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration error", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
googleSheetIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { email: "a@b.com", data: [] },
|
||||||
|
} as unknown as TIntegrationGoogleSheets
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
|
||||||
|
const { default: toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
@@ -36,11 +36,10 @@ export const ManageIntegration = ({
|
|||||||
}: ManageIntegrationProps) => {
|
}: ManageIntegrationProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||||
const integrationArray = googleSheetIntegration
|
let integrationArray: TIntegrationGoogleSheetsConfigData[] = [];
|
||||||
? googleSheetIntegration.config.data
|
if (googleSheetIntegration?.config.data) {
|
||||||
? googleSheetIntegration.config.data
|
integrationArray = googleSheetIntegration.config.data;
|
||||||
: []
|
}
|
||||||
: [];
|
|
||||||
const [isDeleting, setisDeleting] = useState(false);
|
const [isDeleting, setisDeleting] = useState(false);
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
@@ -112,9 +111,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={index}
|
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
|
||||||
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -124,7 +123,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
|
import {
|
||||||
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
|
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||||
|
GOOGLE_SHEETS_REDIRECT_URL,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@/lib/constants";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {
|
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
|
||||||
GOOGLE_SHEETS_REDIRECT_URL,
|
|
||||||
WEBAPP_URL,
|
|
||||||
} from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
import { cache } from "@/lib/cache";
|
||||||
|
import { surveyCache } from "@/lib/survey/cache";
|
||||||
|
import { selectSurvey } from "@/lib/survey/service";
|
||||||
|
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
|
||||||
import { selectSurvey } from "@formbricks/lib/survey/service";
|
|
||||||
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import { cache } from "@/lib/cache";
|
||||||
import { webhookCache } from "@/lib/cache/webhook";
|
import { webhookCache } from "@/lib/cache/webhook";
|
||||||
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma, Webhook } from "@prisma/client";
|
import { Prisma, Webhook } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { cache } from "@formbricks/lib/cache";
|
|
||||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import {
|
|||||||
UNSUPPORTED_TYPES_BY_NOTION,
|
UNSUPPORTED_TYPES_BY_NOTION,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
} from "@/app/(app)/environments/[environmentId]/integrations/notion/constants";
|
||||||
import NotionLogo from "@/images/notion.png";
|
import NotionLogo from "@/images/notion.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { structuredClone } from "@/lib/pollyfills/structuredClone";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
import { getQuestionTypes } from "@/modules/survey/lib/questions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||||
@@ -18,9 +21,6 @@ import Image from "next/image";
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationNotion,
|
TIntegrationNotion,
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import type {
|
||||||
|
TIntegrationNotion,
|
||||||
|
TIntegrationNotionConfig,
|
||||||
|
TIntegrationNotionConfigData,
|
||||||
|
TIntegrationNotionCredential,
|
||||||
|
} from "@formbricks/types/integration/notion";
|
||||||
|
import { ManageIntegration } from "./ManageIntegration";
|
||||||
|
|
||||||
|
vi.mock("react-hot-toast", () => ({ success: vi.fn(), error: vi.fn() }));
|
||||||
|
vi.mock("@/lib/time", () => ({ timeSince: () => "ago" }));
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||||
|
deleteIntegrationAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("ManageIntegration", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
environment: {} as any,
|
||||||
|
locale: "en-US" as const,
|
||||||
|
setOpenAddIntegrationModal: vi.fn(),
|
||||||
|
setIsConnected: vi.fn(),
|
||||||
|
setSelectedIntegration: vi.fn(),
|
||||||
|
handleNotionAuthorization: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
test("shows empty state when no databases", () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...defaultProps}
|
||||||
|
notionIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: {
|
||||||
|
data: [] as TIntegrationNotionConfigData[],
|
||||||
|
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
|
||||||
|
} as TIntegrationNotionConfig,
|
||||||
|
} as TIntegrationNotion
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders list and handles clicks", async () => {
|
||||||
|
const data = [
|
||||||
|
{ surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" },
|
||||||
|
] as unknown as TIntegrationNotionConfigData[];
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...defaultProps}
|
||||||
|
notionIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data, key: { workspace_name: "ws" } as TIntegrationNotionCredential },
|
||||||
|
} as TIntegrationNotion
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("S")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("S"));
|
||||||
|
expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 });
|
||||||
|
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("update and link new buttons invoke handlers", async () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...defaultProps}
|
||||||
|
notionIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: {
|
||||||
|
data: [],
|
||||||
|
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
|
||||||
|
} as TIntegrationNotionConfig,
|
||||||
|
} as TIntegrationNotion
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText("environments.integrations.notion.update_connection"));
|
||||||
|
expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled();
|
||||||
|
await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database"));
|
||||||
|
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -10,7 +11,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
|
import { RefreshCcwIcon, Trash2Icon } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
@@ -39,11 +39,11 @@ export const ManageIntegration = ({
|
|||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||||
const [isDeleting, setisDeleting] = useState(false);
|
const [isDeleting, setisDeleting] = useState(false);
|
||||||
const integrationArray = notionIntegration
|
|
||||||
? notionIntegration.config.data
|
let integrationArray: TIntegrationNotionConfigData[] = [];
|
||||||
? notionIntegration.config.data
|
if (notionIntegration?.config.data) {
|
||||||
: []
|
integrationArray = notionIntegration.config.data;
|
||||||
: [];
|
}
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
setisDeleting(true);
|
setisDeleting(true);
|
||||||
@@ -121,9 +121,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={index}
|
key={`${index}-${data.databaseId}`}
|
||||||
className="m-2 grid h-16 cursor-pointer grid-cols-6 content-center rounded-lg hover:bg-slate-100"
|
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -132,7 +132,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export const TYPE_MAPPING = {
|
|||||||
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
[TSurveyQuestionTypeEnum.Address]: ["rich_text"],
|
||||||
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
[TSurveyQuestionTypeEnum.Matrix]: ["rich_text"],
|
||||||
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
[TSurveyQuestionTypeEnum.Cal]: ["checkbox"],
|
||||||
|
[TSurveyQuestionTypeEnum.ContactInfo]: ["rich_text"],
|
||||||
|
[TSurveyQuestionTypeEnum.Ranking]: ["rich_text"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
export const UNSUPPORTED_TYPES_BY_NOTION = [
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
|
||||||
import { getTranslate } from "@/tolgee/server";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import {
|
import {
|
||||||
NOTION_AUTH_URL,
|
NOTION_AUTH_URL,
|
||||||
NOTION_OAUTH_CLIENT_ID,
|
NOTION_OAUTH_CLIENT_ID,
|
||||||
NOTION_OAUTH_CLIENT_SECRET,
|
NOTION_OAUTH_CLIENT_SECRET,
|
||||||
NOTION_REDIRECT_URI,
|
NOTION_REDIRECT_URI,
|
||||||
WEBAPP_URL,
|
WEBAPP_URL,
|
||||||
} from "@formbricks/lib/constants";
|
} from "@/lib/constants";
|
||||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
import { getNotionDatabases } from "@/lib/notion/service";
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
|
import { getTranslate } from "@/tolgee/server";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import notionLogo from "@/images/notion.png";
|
|||||||
import SlackLogo from "@/images/slacklogo.png";
|
import SlackLogo from "@/images/slacklogo.png";
|
||||||
import WebhookLogo from "@/images/webhook.png";
|
import WebhookLogo from "@/images/webhook.png";
|
||||||
import ZapierLogo from "@/images/zapier-small.png";
|
import ZapierLogo from "@/images/zapier-small.png";
|
||||||
|
import { getIntegrations } from "@/lib/integration/service";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { Card } from "@/modules/ui/components/integration-card";
|
import { Card } from "@/modules/ui/components/integration-card";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
@@ -16,7 +17,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
|
||||||
import { TIntegrationType } from "@formbricks/types/integration";
|
import { TIntegrationType } from "@formbricks/types/integration";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { getSlackChannels } from "@/lib/slack/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId, getProjectIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSlackChannels } from "@formbricks/lib/slack/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
|
|
||||||
const ZGetSlackChannelsAction = z.object({
|
const ZGetSlackChannelsAction = z.object({
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
import SlackLogo from "@/images/slacklogo.png";
|
import SlackLogo from "@/images/slacklogo.png";
|
||||||
|
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -15,8 +17,6 @@ import Link from "next/link";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationSlack,
|
TIntegrationSlack,
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||||
|
import { ManageIntegration } from "./ManageIntegration";
|
||||||
|
|
||||||
|
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
||||||
|
deleteIntegrationAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
|
||||||
|
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
||||||
|
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
|
||||||
|
open ? (
|
||||||
|
<div data-testid="delete-dialog">
|
||||||
|
<button onClick={onDelete}>confirm</button>
|
||||||
|
<button onClick={() => setOpen(false)}>cancel</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
|
||||||
|
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
environment: { id: "env1" } as TEnvironment,
|
||||||
|
setOpenAddIntegrationModal: vi.fn(),
|
||||||
|
setIsConnected: vi.fn(),
|
||||||
|
setSelectedIntegration: vi.fn(),
|
||||||
|
refreshChannels: vi.fn(),
|
||||||
|
handleSlackAuthorization: vi.fn(),
|
||||||
|
showReconnectButton: false,
|
||||||
|
locale: "en-US" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("ManageIntegration (Slack)", () => {
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
|
||||||
|
test("empty state", () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [], key: { team: { name: "team name" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/link_channel/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("link channel triggers handlers", async () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [], key: { team: { name: "team name" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/link_channel/));
|
||||||
|
expect(baseProps.refreshChannels).toHaveBeenCalled();
|
||||||
|
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
|
||||||
|
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("show reconnect button and triggers authorization", async () => {
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
showReconnectButton={true}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [], key: { team: { name: "Team" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button"));
|
||||||
|
expect(baseProps.handleSlackAuthorization).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list integrations and open edit", async () => {
|
||||||
|
const item = {
|
||||||
|
surveyName: "S",
|
||||||
|
channelName: "C",
|
||||||
|
questions: "Q",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
surveyId: "s",
|
||||||
|
channelId: "c",
|
||||||
|
} as unknown as TIntegrationSlackConfigData;
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [item], key: { team: { name: "team name" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText("S")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("S"));
|
||||||
|
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 });
|
||||||
|
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration success", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [], key: { team: { name: "team name" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
||||||
|
const { default: toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
||||||
|
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delete integration error", async () => {
|
||||||
|
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
||||||
|
render(
|
||||||
|
<ManageIntegration
|
||||||
|
{...baseProps}
|
||||||
|
slackIntegration={
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
config: { data: [], key: { team: { name: "team name" } } },
|
||||||
|
} as unknown as TIntegrationSlack
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await userEvent.click(screen.getByText(/delete_integration/));
|
||||||
|
await userEvent.click(screen.getByText("confirm"));
|
||||||
|
const { default: toast } = await import("react-hot-toast");
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
|
import { timeSince } from "@/lib/time";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
import { EmptySpaceFiller } from "@/modules/ui/components/empty-space-filler";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { T, useTranslate } from "@tolgee/react";
|
||||||
import { T } from "@tolgee/react";
|
|
||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { timeSince } from "@formbricks/lib/time";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
@@ -43,11 +42,10 @@ export const ManageIntegration = ({
|
|||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||||
const [isDeleting, setisDeleting] = useState(false);
|
const [isDeleting, setisDeleting] = useState(false);
|
||||||
const integrationArray = slackIntegration
|
let integrationArray: TIntegrationSlackConfigData[] = [];
|
||||||
? slackIntegration.config.data
|
if (slackIntegration?.config.data) {
|
||||||
? slackIntegration.config.data
|
integrationArray = slackIntegration.config.data;
|
||||||
: []
|
}
|
||||||
: [];
|
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
setisDeleting(true);
|
setisDeleting(true);
|
||||||
@@ -129,9 +127,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={index}
|
key={`${index}-${data.surveyName}-${data.channelName}`}
|
||||||
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg text-slate-700 hover:cursor-pointer hover:bg-slate-100"
|
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 text-slate-700 hover:cursor-pointer hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -141,7 +139,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper";
|
||||||
|
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||||
|
import { getIntegrationByType } from "@/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@/lib/utils/locale";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
|
||||||
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
|
||||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
import { TMembership } from "@formbricks/types/memberships";
|
import { TMembership } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
@@ -41,10 +41,10 @@ vi.mock("./components/EnvironmentStorageHandler", () => ({
|
|||||||
vi.mock("@/modules/environments/lib/utils", () => ({
|
vi.mock("@/modules/environments/lib/utils", () => ({
|
||||||
environmentIdLayoutChecks: vi.fn(),
|
environmentIdLayoutChecks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/project/service", () => ({
|
vi.mock("@/lib/project/service", () => ({
|
||||||
getProjectByEnvironmentId: vi.fn(),
|
getProjectByEnvironmentId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@formbricks/lib/membership/service", () => ({
|
vi.mock("@/lib/membership/service", () => ({
|
||||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ describe("EnvLayout", () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders successfully when all dependencies return valid data", async () => {
|
test("renders successfully when all dependencies return valid data", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any, // Mock translation function, we don't need to implement it for the test
|
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,
|
session: { user: { id: "user1" } } as Session,
|
||||||
@@ -77,7 +77,7 @@ describe("EnvLayout", () => {
|
|||||||
expect(screen.getByTestId("child")).toHaveTextContent("Content");
|
expect(screen.getByTestId("child")).toHaveTextContent("Content");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if project is not found", async () => {
|
test("throws error if project is not found", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: { user: { id: "user1" } } as Session,
|
session: { user: { id: "user1" } } as Session,
|
||||||
@@ -97,7 +97,7 @@ describe("EnvLayout", () => {
|
|||||||
).rejects.toThrow("common.project_not_found");
|
).rejects.toThrow("common.project_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if membership is not found", async () => {
|
test("throws error if membership is not found", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: { user: { id: "user1" } } as Session,
|
session: { user: { id: "user1" } } as Session,
|
||||||
@@ -115,7 +115,7 @@ describe("EnvLayout", () => {
|
|||||||
).rejects.toThrow("common.membership_not_found");
|
).rejects.toThrow("common.membership_not_found");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls redirect when session is null", async () => {
|
test("calls redirect when session is null", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: undefined as unknown as Session,
|
session: undefined as unknown as Session,
|
||||||
@@ -134,7 +134,7 @@ describe("EnvLayout", () => {
|
|||||||
).rejects.toThrow("Redirect called");
|
).rejects.toThrow("Redirect called");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error if user is null", async () => {
|
test("throws error if user is null", async () => {
|
||||||
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
vi.mocked(environmentIdLayoutChecks).mockResolvedValueOnce({
|
||||||
t: ((key: string) => key) as any,
|
t: ((key: string) => key) as any,
|
||||||
session: { user: { id: "user1" } } as Session,
|
session: { user: { id: "user1" } } as Session,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
import { environmentIdLayoutChecks } from "@/modules/environments/lib/utils";
|
||||||
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
import { EnvironmentIdBaseLayout } from "@/modules/ui/components/environmentId-base-layout";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
||||||
|
|
||||||
const EnvLayout = async (props: {
|
const EnvLayout = async (props: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
|
|
||||||
const EnvironmentPage = async (props) => {
|
const EnvironmentPage = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const AccountSettingsLayout = async (props) => {
|
const AccountSettingsLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
import { ZUserNotificationSettings } from "@formbricks/types/user";
|
||||||
|
|
||||||
const ZUpdateNotificationSettingsAction = z.object({
|
const ZUpdateNotificationSettingsAction = z.object({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const EditAlerts = ({
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<div className="col-span-1 flex cursor-default items-center justify-center space-x-2">
|
<div className="col-span-1 flex cursor-default items-center justify-center space-x-2">
|
||||||
<span>{t("environments.settings.notifications.every_response")}</span>
|
<span>{t("environments.settings.notifications.every_response")}</span>
|
||||||
<HelpCircleIcon className="h-4 w-4 shrink-0 text-slate-500" />
|
<HelpCircleIcon className="h-4 w-4 flex-shrink-0 text-slate-500" />
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -99,7 +99,7 @@ export const EditAlerts = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="m-2 flex h-16 items-center justify-center rounded-sm bg-slate-50 text-sm text-slate-500">
|
<div className="m-2 flex h-16 items-center justify-center rounded bg-slate-50 text-sm text-slate-500">
|
||||||
<p>{t("common.no_surveys_found")}</p>
|
<p>{t("common.no_surveys_found")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const IntegrationsTip = ({ environmentId }: IntegrationsTipProps) => {
|
|||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-xs md:space-y-0 md:text-base">
|
<div className="flex max-w-4xl items-center space-y-3 rounded-lg border border-blue-100 bg-blue-50 p-4 text-sm text-blue-900 shadow-sm md:space-y-0 md:text-base">
|
||||||
<SlackIcon className="mr-3 h-4 w-4 text-blue-400" />
|
<SlackIcon className="mr-3 h-4 w-4 text-blue-400" />
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
|
{t("environments.settings.notifications.need_slack_or_discord_notifications")}?
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||||
import { EditAlerts } from "./components/EditAlerts";
|
import { EditAlerts } from "./components/EditAlerts";
|
||||||
import { EditWeeklySummary } from "./components/EditWeeklySummary";
|
import { EditWeeklySummary } from "./components/EditWeeklySummary";
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteFile } from "@/lib/storage/service";
|
||||||
|
import { getFileNameWithIdFromUrl } from "@/lib/storage/utils";
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteFile } from "@formbricks/lib/storage/service";
|
|
||||||
import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils";
|
|
||||||
import { updateUser } from "@formbricks/lib/user/service";
|
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZUserUpdateInput } from "@formbricks/types/user";
|
import { ZUserUpdateInput } from "@formbricks/types/user";
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit
|
|||||||
<div>
|
<div>
|
||||||
<div className="relative h-10 w-10 overflow-hidden rounded-full">
|
<div className="relative h-10 w-10 overflow-hidden rounded-full">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-30">
|
||||||
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
|
<svg className="h-7 w-7 animate-spin text-slate-200" viewBox="0 0 24 24">
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
<path
|
<path
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { appLanguages } from "@/lib/i18n/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -23,7 +24,6 @@ import { ChevronDownIcon } from "lucide-react";
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { appLanguages } from "@formbricks/lib/i18n/utils";
|
|
||||||
import { TUser, ZUser } from "@formbricks/types/user";
|
import { TUser, ZUser } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { updateUserAction } from "../actions";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
|
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||||
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
@@ -7,9 +10,6 @@ import { PageHeader } from "@/modules/ui/components/page-header";
|
|||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||||
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
import { UpgradePrompt } from "@/modules/ui/components/upgrade-prompt";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getOrganizationsWhereUserIsSingleOwner } from "@formbricks/lib/organization/service";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteAccount } from "./components/DeleteAccount";
|
import { DeleteAccount } from "./components/DeleteAccount";
|
||||||
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
|
import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import Loading from "@/modules/organization/settings/api-keys/loading";
|
import Loading from "@/modules/organization/settings/api-keys/loading";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
export default function LoadingPage() {
|
export default function LoadingPage() {
|
||||||
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
return <Loading isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
import { SecondaryNavigation } from "@/modules/ui/components/secondary-navigation";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
interface OrganizationSettingsNavbarProps {
|
interface OrganizationSettingsNavbarProps {
|
||||||
@@ -22,7 +22,7 @@ export const OrganizationSettingsNavbar = ({
|
|||||||
loading,
|
loading,
|
||||||
}: OrganizationSettingsNavbarProps) => {
|
}: OrganizationSettingsNavbarProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { isMember } = getAccessFlags(membershipRole);
|
const { isMember, isOwner } = getAccessFlags(membershipRole);
|
||||||
const isPricingDisabled = isMember;
|
const isPricingDisabled = isMember;
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ export const OrganizationSettingsNavbar = ({
|
|||||||
label: t("common.api_keys"),
|
label: t("common.api_keys"),
|
||||||
href: `/environments/${environmentId}/settings/api-keys`,
|
href: `/environments/${environmentId}/settings/api-keys`,
|
||||||
current: pathname?.includes("/api-keys"),
|
current: pathname?.includes("/api-keys"),
|
||||||
|
hidden: !isOwner,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -8,7 +9,6 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -58,11 +58,6 @@ const Page = async (props) => {
|
|||||||
comingSoon: false,
|
comingSoon: false,
|
||||||
onRequest: false,
|
onRequest: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("environments.settings.enterprise.ai"),
|
|
||||||
comingSoon: false,
|
|
||||||
onRequest: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("environments.settings.enterprise.audit_logs"),
|
title: t("environments.settings.enterprise.audit_logs"),
|
||||||
comingSoon: false,
|
comingSoon: false,
|
||||||
@@ -97,7 +92,7 @@ const Page = async (props) => {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
{isEnterpriseEdition ? (
|
{isEnterpriseEdition ? (
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
|
<div className="mt-8 max-w-4xl rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
|
||||||
<div className="space-y-4 p-8">
|
<div className="space-y-4 p-8">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
|
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
|
||||||
@@ -152,7 +147,7 @@ const Page = async (props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-xs">
|
<div className="mt-8 rounded-lg border border-slate-300 bg-slate-100 shadow-sm">
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<h2 className="mr-2 inline-flex text-2xl font-bold text-slate-700">
|
<h2 className="mr-2 inline-flex text-2xl font-bold text-slate-700">
|
||||||
{t("environments.settings.enterprise.enterprise_features")}
|
{t("environments.settings.enterprise.enterprise_features")}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { deleteOrganization, updateOrganization } from "@/lib/organization/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 { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { deleteOrganization, updateOrganization } from "@formbricks/lib/organization/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 { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
import { ZOrganizationUpdateInput } from "@formbricks/types/organizations";
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
|
||||||
import { updateOrganizationAIEnabledAction } from "@/modules/ee/insights/actions";
|
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
|
||||||
import { Label } from "@/modules/ui/components/label";
|
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
|
||||||
import { useTranslate } from "@tolgee/react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
|
||||||
|
|
||||||
interface AIToggleProps {
|
|
||||||
environmentId: string;
|
|
||||||
organization: TOrganization;
|
|
||||||
isOwnerOrManager: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AIToggle = ({ organization, isOwnerOrManager }: AIToggleProps) => {
|
|
||||||
const { t } = useTranslate();
|
|
||||||
const [isAIEnabled, setIsAIEnabled] = useState(organization.isAIEnabled);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const handleUpdateOrganization = async (data) => {
|
|
||||||
try {
|
|
||||||
setIsAIEnabled(data.enabled);
|
|
||||||
setIsSubmitting(true);
|
|
||||||
const updatedOrganizationResponse = await updateOrganizationAIEnabledAction({
|
|
||||||
organizationId: organization.id,
|
|
||||||
data: {
|
|
||||||
isAIEnabled: data.enabled,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (updatedOrganizationResponse?.data) {
|
|
||||||
if (data.enabled) {
|
|
||||||
toast.success(t("environments.settings.general.formbricks_ai_enable_success_message"));
|
|
||||||
} else {
|
|
||||||
toast.success(t("environments.settings.general.formbricks_ai_disable_success_message"));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorMessage = getFormattedErrorMessage(updatedOrganizationResponse);
|
|
||||||
toast.error(errorMessage);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
toast.error(`Error: ${err.message}`);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Label htmlFor="formbricks-ai-toggle" className="cursor-pointer">
|
|
||||||
{t("environments.settings.general.enable_formbricks_ai")}
|
|
||||||
</Label>
|
|
||||||
<Switch
|
|
||||||
id="formbricks-ai-toggle"
|
|
||||||
disabled={!isOwnerOrManager || isSubmitting}
|
|
||||||
checked={isAIEnabled}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleUpdateOrganization({ enabled: !organization.isAIEnabled });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-xs text-slate-600">
|
|
||||||
{t("environments.settings.general.formbricks_ai_privacy_policy_text")}{" "}
|
|
||||||
<Link
|
|
||||||
className="underline"
|
|
||||||
href={"https://formbricks.com/privacy-policy"}
|
|
||||||
rel="noreferrer"
|
|
||||||
target="_blank">
|
|
||||||
{t("common.privacy_policy")}
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isOwnerOrManager && (
|
|
||||||
<Alert variant="warning" className="mt-4">
|
|
||||||
<AlertDescription>
|
|
||||||
{t("environments.settings.general.only_org_owner_can_perform_action")}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
import { deleteOrganizationAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -9,7 +10,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
|
|
||||||
type DeleteOrganizationProps = {
|
type DeleteOrganizationProps = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
import { updateOrganizationNameAction } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/actions";
|
||||||
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -18,7 +19,6 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
|
import { TOrganization, ZOrganization } from "@formbricks/types/organizations";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
import { LoadingCard } from "@/app/(app)/components/LoadingCard";
|
||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
|
import { IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
|
|
||||||
const Loading = async () => {
|
const Loading = async () => {
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import {
|
import { getUser } from "@/lib/user/service";
|
||||||
getIsMultiOrgEnabled,
|
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
getIsOrganizationAIReady,
|
|
||||||
getWhiteLabelPermission,
|
|
||||||
} from "@/modules/ee/license-check/lib/utils";
|
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
import { TEnvironmentAuth } from "@/modules/environments/types/environment-auth";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import Page from "./page";
|
import Page from "./page";
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
IS_PRODUCTION: false,
|
IS_PRODUCTION: false,
|
||||||
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
FB_LOGO_URL: "https://example.com/mock-logo.png",
|
||||||
@@ -33,12 +29,6 @@ vi.mock("@formbricks/lib/constants", () => ({
|
|||||||
WEBAPP_URL: "mock-webapp-url",
|
WEBAPP_URL: "mock-webapp-url",
|
||||||
SMTP_HOST: "mock-smtp-host",
|
SMTP_HOST: "mock-smtp-host",
|
||||||
SMTP_PORT: "mock-smtp-port",
|
SMTP_PORT: "mock-smtp-port",
|
||||||
AI_AZURE_LLM_RESSOURCE_NAME: "mock-ai-azure-llm-ressource-name",
|
|
||||||
AI_AZURE_LLM_API_KEY: "mock-ai",
|
|
||||||
AI_AZURE_LLM_DEPLOYMENT_ID: "mock-ai-azure-llm-deployment-id",
|
|
||||||
AI_AZURE_EMBEDDINGS_RESSOURCE_NAME: "mock-ai-azure-embeddings-ressource-name",
|
|
||||||
AI_AZURE_EMBEDDINGS_API_KEY: "mock-ai-azure-embeddings-api-key",
|
|
||||||
AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID: "mock-ai-azure-embeddings-deployment-id",
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("next-auth", () => ({
|
vi.mock("next-auth", () => ({
|
||||||
@@ -49,7 +39,7 @@ vi.mock("@/tolgee/server", () => ({
|
|||||||
getTranslate: vi.fn(),
|
getTranslate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@formbricks/lib/user/service", () => ({
|
vi.mock("@/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -59,7 +49,6 @@ vi.mock("@/modules/environments/lib/utils", () => ({
|
|||||||
|
|
||||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||||
getIsMultiOrgEnabled: vi.fn(),
|
getIsMultiOrgEnabled: vi.fn(),
|
||||||
getIsOrganizationAIReady: vi.fn(),
|
|
||||||
getWhiteLabelPermission: vi.fn(),
|
getWhiteLabelPermission: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -80,11 +69,10 @@ describe("Page", () => {
|
|||||||
vi.mocked(getUser).mockResolvedValue(mockUser);
|
vi.mocked(getUser).mockResolvedValue(mockUser);
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||||
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
vi.mocked(getIsMultiOrgEnabled).mockResolvedValue(true);
|
||||||
vi.mocked(getIsOrganizationAIReady).mockResolvedValue(true);
|
|
||||||
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
vi.mocked(getWhiteLabelPermission).mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the page with organization settings", async () => {
|
test("renders the page with organization settings", async () => {
|
||||||
const props = {
|
const props = {
|
||||||
params: Promise.resolve({ environmentId: "env-123" }),
|
params: Promise.resolve({ environmentId: "env-123" }),
|
||||||
};
|
};
|
||||||
@@ -94,7 +82,7 @@ describe("Page", () => {
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders if session user id empty", async () => {
|
test("renders if session user id empty", async () => {
|
||||||
mockEnvironmentAuth.session.user.id = "";
|
mockEnvironmentAuth.session.user.id = "";
|
||||||
|
|
||||||
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
vi.mocked(getEnvironmentAuth).mockResolvedValue(mockEnvironmentAuth);
|
||||||
@@ -108,7 +96,7 @@ describe("Page", () => {
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles getEnvironmentAuth error", async () => {
|
test("handles getEnvironmentAuth error", async () => {
|
||||||
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
vi.mocked(getEnvironmentAuth).mockRejectedValue(new Error("Authentication error"));
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||||
import {
|
import { getUser } from "@/lib/user/service";
|
||||||
getIsMultiOrgEnabled,
|
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
getIsOrganizationAIReady,
|
|
||||||
getWhiteLabelPermission,
|
|
||||||
} from "@/modules/ee/license-check/lib/utils";
|
|
||||||
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
import { EmailCustomizationSettings } from "@/modules/ee/whitelabel/email-customization/components/email-customization-settings";
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { SettingsId } from "@/modules/ui/components/settings-id";
|
import { SettingsId } from "@/modules/ui/components/settings-id";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
|
||||||
import { getUser } from "@formbricks/lib/user/service";
|
|
||||||
import { SettingsCard } from "../../components/SettingsCard";
|
import { SettingsCard } from "../../components/SettingsCard";
|
||||||
import { DeleteOrganization } from "./components/DeleteOrganization";
|
import { DeleteOrganization } from "./components/DeleteOrganization";
|
||||||
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
import { EditOrganizationNameForm } from "./components/EditOrganizationNameForm";
|
||||||
@@ -35,8 +30,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
|
|
||||||
const isOwnerOrManager = isManager || isOwner;
|
const isOwnerOrManager = isManager || isOwner;
|
||||||
|
|
||||||
const isOrganizationAIReady = await getIsOrganizationAIReady(organization.billing.plan);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
|
<PageHeader pageTitle={t("environments.settings.general.organization_settings")}>
|
||||||
@@ -56,17 +49,6 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
membershipRole={currentUserMembership?.role}
|
membershipRole={currentUserMembership?.role}
|
||||||
/>
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
{isOrganizationAIReady && (
|
|
||||||
<SettingsCard
|
|
||||||
title={t("environments.settings.general.formbricks_ai")}
|
|
||||||
description={t("environments.settings.general.formbricks_ai_description")}>
|
|
||||||
<AIToggle
|
|
||||||
environmentId={params.environmentId}
|
|
||||||
organization={organization}
|
|
||||||
isOwnerOrManager={isOwnerOrManager}
|
|
||||||
/>
|
|
||||||
</SettingsCard>
|
|
||||||
)}
|
|
||||||
<EmailCustomizationSettings
|
<EmailCustomizationSettings
|
||||||
organization={organization}
|
organization={organization}
|
||||||
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
hasWhiteLabelPermission={hasWhiteLabelPermission}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
|
||||||
|
|
||||||
const Layout = async (props) => {
|
const Layout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user