mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Compare commits
2 Commits
response-q
...
fix/bulk-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0306c27b | ||
|
|
62960686a1 |
11
.env.example
11
.env.example
@@ -206,6 +206,12 @@ UNKEY_ROOT_KEY=
|
|||||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||||
# CUSTOM_CACHE_DISABLED=1
|
# CUSTOM_CACHE_DISABLED=1
|
||||||
|
|
||||||
|
# Azure AI settings
|
||||||
|
# AI_AZURE_RESSOURCE_NAME=
|
||||||
|
# AI_AZURE_API_KEY=
|
||||||
|
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
||||||
|
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||||
|
|
||||||
# INTERCOM_APP_ID=
|
# INTERCOM_APP_ID=
|
||||||
# INTERCOM_SECRET_KEY=
|
# INTERCOM_SECRET_KEY=
|
||||||
|
|
||||||
@@ -213,8 +219,3 @@ UNKEY_ROOT_KEY=
|
|||||||
# PROMETHEUS_ENABLED=
|
# PROMETHEUS_ENABLED=
|
||||||
# PROMETHEUS_EXPORTER_PORT=
|
# PROMETHEUS_EXPORTER_PORT=
|
||||||
|
|
||||||
# The SENTRY_DSN is used for error tracking and performance monitoring with Sentry.
|
|
||||||
# SENTRY_DSN=
|
|
||||||
# The SENTRY_AUTH_TOKEN variable is picked up by the Sentry Build Plugin.
|
|
||||||
# It's used automatically by Sentry during the build for authentication when uploading source maps.
|
|
||||||
# SENTRY_AUTH_TOKEN=
|
|
||||||
|
|||||||
26
.github/copilot-instructions.md
vendored
26
.github/copilot-instructions.md
vendored
@@ -1,26 +0,0 @@
|
|||||||
# 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,13 +12,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
ENVIRONMENT:
|
|
||||||
description: 'The environment to deploy to'
|
|
||||||
required: true
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- stage
|
|
||||||
- prod
|
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
VERSION:
|
VERSION:
|
||||||
@@ -30,10 +23,6 @@ on:
|
|||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'ghcr.io/formbricks/formbricks'
|
default: 'ghcr.io/formbricks/formbricks'
|
||||||
ENVIRONMENT:
|
|
||||||
description: 'The environment to deploy to'
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -59,8 +48,6 @@ jobs:
|
|||||||
AWS_REGION: eu-central-1
|
AWS_REGION: eu-central-1
|
||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
- uses: helmfile/helmfile-action@v2
|
||||||
name: Deploy Formbricks Cloud Prod
|
|
||||||
if: (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && github.event.inputs.ENVIRONMENT == 'prod'
|
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ inputs.VERSION }}
|
VERSION: ${{ inputs.VERSION }}
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
REPOSITORY: ${{ inputs.REPOSITORY }}
|
||||||
@@ -71,23 +58,7 @@ jobs:
|
|||||||
helm-plugins: >
|
helm-plugins: >
|
||||||
https://github.com/databus23/helm-diff,
|
https://github.com/databus23/helm-diff,
|
||||||
https://github.com/jkroepke/helm-secrets
|
https://github.com/jkroepke/helm-secrets
|
||||||
helmfile-args: apply -l environment=prod
|
helmfile-args: apply
|
||||||
helmfile-auto-init: "false"
|
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
|
||||||
|
|
||||||
- uses: helmfile/helmfile-action@v2
|
|
||||||
name: Deploy Formbricks Cloud Stage
|
|
||||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ENVIRONMENT == 'stage'
|
|
||||||
env:
|
|
||||||
VERSION: ${{ inputs.VERSION }}
|
|
||||||
REPOSITORY: ${{ inputs.REPOSITORY }}
|
|
||||||
FORMBRICKS_INGRESS_CERT_ARN: ${{ secrets.STAGE_FORMBRICKS_INGRESS_CERT_ARN }}
|
|
||||||
FORMBRICKS_ROLE_ARN: ${{ secrets.STAGE_FORMBRICKS_ROLE_ARN }}
|
|
||||||
with:
|
|
||||||
helm-plugins: >
|
|
||||||
https://github.com/databus23/helm-diff,
|
|
||||||
https://github.com/jkroepke/helm-secrets
|
|
||||||
helmfile-args: apply -l environment=stage
|
|
||||||
helmfile-auto-init: "false"
|
helmfile-auto-init: "false"
|
||||||
helmfile-workdirectory: infra/formbricks-cloud-helm
|
helmfile-workdirectory: infra/formbricks-cloud-helm
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/formbricks-release.yml
vendored
1
.github/workflows/formbricks-release.yml
vendored
@@ -31,4 +31,3 @@ jobs:
|
|||||||
- helm-chart-release
|
- helm-chart-release
|
||||||
with:
|
with:
|
||||||
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
VERSION: ${{ needs.docker-build.outputs.VERSION }}
|
||||||
ENVIRONMENT: "prod"
|
|
||||||
|
|||||||
56
.github/workflows/release-changesets.yml
vendored
Normal file
56
.github/workflows/release-changesets.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Release Changesets
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
#push:
|
||||||
|
# branches:
|
||||||
|
# - main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
steps:
|
||||||
|
- name: Harden the runner (Audit all outbound calls)
|
||||||
|
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2.7.0
|
||||||
|
|
||||||
|
- name: Setup Node.js 18.x
|
||||||
|
uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 # v2.5.2
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd # v2.2.4
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||||
|
|
||||||
|
- name: Create Release Pull Request or Publish to npm
|
||||||
|
id: changesets
|
||||||
|
uses: changesets/action@c8bada60c408975afd1a20b3db81d6eee6789308 # v1.4.9
|
||||||
|
with:
|
||||||
|
# This expects you to have a script called release which does a build for your packages and calls changeset publish
|
||||||
|
publish: pnpm release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
@@ -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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -82,6 +82,8 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -102,6 +102,8 @@ jobs:
|
|||||||
secrets: |
|
secrets: |
|
||||||
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
database_url=${{ secrets.DUMMY_DATABASE_URL }}
|
||||||
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
encryption_key=${{ secrets.DUMMY_ENCRYPTION_KEY }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
# Sign the resulting Docker image digest except on PRs.
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
# This will only write to the public Rekor transparency log when the Docker
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,4 +72,3 @@ 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 apps/web/locales
|
git add packages/lib/messages
|
||||||
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": "./apps/web/locales"
|
"path": "./packages/lib/messages"
|
||||||
},
|
},
|
||||||
"push": {
|
"push": {
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"language": "en-US",
|
"language": "en-US",
|
||||||
"path": "./apps/web/locales/en-US.json"
|
"path": "./packages/lib/messages/en-US.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "de-DE",
|
"language": "de-DE",
|
||||||
"path": "./apps/web/locales/de-DE.json"
|
"path": "./packages/lib/messages/de-DE.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "fr-FR",
|
"language": "fr-FR",
|
||||||
"path": "./apps/web/locales/fr-FR.json"
|
"path": "./packages/lib/messages/fr-FR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-BR",
|
"language": "pt-BR",
|
||||||
"path": "./apps/web/locales/pt-BR.json"
|
"path": "./packages/lib/messages/pt-BR.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "zh-Hant-TW",
|
"language": "zh-Hant-TW",
|
||||||
"path": "./apps/web/locales/zh-Hant-TW.json"
|
"path": "./packages/lib/messages/zh-Hant-TW.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"language": "pt-PT",
|
"language": "pt-PT",
|
||||||
"path": "./apps/web/locales/pt-PT.json"
|
"path": "./packages/lib/messages/pt-PT.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"forceMode": "OVERRIDE"
|
"forceMode": "OVERRIDE"
|
||||||
|
|||||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -1,10 +1,13 @@
|
|||||||
{
|
{
|
||||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
"github.copilot.chat.codeGeneration.instructions": [
|
||||||
|
{
|
||||||
|
"text": "When generating tests, always use vitest and use the `test` function instead of `it`."
|
||||||
|
}
|
||||||
|
],
|
||||||
"sonarlint.connectedMode.project": {
|
"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,20 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
extends: ["@formbricks/eslint-config/legacy-next.js"],
|
||||||
ignorePatterns: ["**/package.json", "**/tsconfig.json"],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ["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.21 AS base
|
FROM node:22-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS base
|
||||||
|
|
||||||
#
|
#
|
||||||
## step 1: Prune monorepo
|
## step 1: Prune monorepo
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TProjectConfigChannel } from "@formbricks/types/project";
|
import { TProjectConfigChannel } from "@formbricks/types/project";
|
||||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||||
import { WEBAPP_URL } from "@/lib/constants";
|
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId } from "@/lib/project/service";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||||
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
interface ConnectPageProps {
|
interface ConnectPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -44,7 +44,7 @@ const Page = async (props: ConnectPageProps) => {
|
|||||||
channel={channel}
|
channel={channel}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}`}>
|
<Link href={`/environments/${environment.id}`}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { hasUserEnvironmentAccess } from "@/lib/environment/auth";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||||
import { AuthorizationError } from "@formbricks/types/errors";
|
import { AuthorizationError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
const OnboardingLayout = async (props) => {
|
const OnboardingLayout = async (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { replaceQuestionPresetPlaceholders } from "@/lib/utils/templates";
|
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
||||||
import { TProject } from "@formbricks/types/project";
|
import { TProject } from "@formbricks/types/project";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import {
|
import { getDefaultEndingCard } from "@/app/lib/templates";
|
||||||
buildCTAQuestion,
|
|
||||||
buildNPSQuestion,
|
|
||||||
buildOpenTextQuestion,
|
|
||||||
buildRatingQuestion,
|
|
||||||
getDefaultEndingCard,
|
|
||||||
} from "@/app/lib/survey-builder";
|
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import { TFnType } from "@tolgee/react";
|
import { TFnType } from "@tolgee/react";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||||
import { TXMTemplate } from "@formbricks/types/templates";
|
import { TXMTemplate } from "@formbricks/types/templates";
|
||||||
|
|
||||||
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
|
||||||
@@ -31,26 +26,35 @@ const npsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.nps_survey_name"),
|
name: t("templates.nps_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildNPSQuestion({
|
{
|
||||||
headline: t("templates.nps_survey_question_1_headline"),
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.NPS,
|
||||||
|
headline: { default: t("templates.nps_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.nps_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.nps_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
id: createId(),
|
||||||
headline: t("templates.nps_survey_question_2_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.nps_survey_question_2_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
headline: t("templates.nps_survey_question_3_headline"),
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.nps_survey_question_3_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -63,8 +67,9 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.star_rating_survey_name"),
|
name: t("templates.star_rating_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -97,15 +102,16 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: t("templates.star_rating_survey_question_1_headline"),
|
headline: { default: t("templates.star_rating_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.star_rating_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.star_rating_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildCTAQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
html: t("templates.star_rating_survey_question_2_html"),
|
html: { default: t("templates.star_rating_survey_question_2_html") },
|
||||||
|
type: TSurveyQuestionTypeEnum.CTA,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -132,23 +138,25 @@ const starRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: t("templates.star_rating_survey_question_2_headline"),
|
headline: { default: t("templates.star_rating_survey_question_2_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
buttonUrl: "https://formbricks.com/github",
|
||||||
buttonLabel: t("templates.star_rating_survey_question_2_button_label"),
|
buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
|
||||||
buttonExternal: true,
|
buttonExternal: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
headline: t("templates.star_rating_survey_question_3_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.star_rating_survey_question_3_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
subheader: t("templates.star_rating_survey_question_3_subheader"),
|
subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
|
||||||
buttonLabel: t("templates.star_rating_survey_question_3_button_label"),
|
buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
|
||||||
placeholder: t("templates.star_rating_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -161,8 +169,9 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.csat_survey_name"),
|
name: t("templates.csat_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -195,14 +204,15 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: t("templates.csat_survey_question_1_headline"),
|
headline: { default: t("templates.csat_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.csat_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.csat_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildOpenTextQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -229,20 +239,25 @@ const csatSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: t("templates.csat_survey_question_2_headline"),
|
headline: { default: t("templates.csat_survey_question_2_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: t("templates.csat_survey_question_2_placeholder"),
|
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
headline: t("templates.csat_survey_question_3_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.csat_survey_question_3_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
placeholder: t("templates.csat_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -252,22 +267,28 @@ const cessSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.cess_survey_name"),
|
name: t("templates.cess_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "number",
|
scale: "number",
|
||||||
headline: t("templates.cess_survey_question_1_headline"),
|
headline: { default: t("templates.cess_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.cess_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.cess_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildOpenTextQuestion({
|
{
|
||||||
headline: t("templates.cess_survey_question_2_headline"),
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.cess_survey_question_2_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: t("templates.cess_survey_question_2_placeholder"),
|
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -280,8 +301,9 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...defaultSurvey,
|
...defaultSurvey,
|
||||||
name: t("templates.smileys_survey_name"),
|
name: t("templates.smileys_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildRatingQuestion({
|
{
|
||||||
id: reusableQuestionIds[0],
|
id: reusableQuestionIds[0],
|
||||||
|
type: TSurveyQuestionTypeEnum.Rating,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -314,15 +336,16 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
range: 5,
|
range: 5,
|
||||||
scale: "smiley",
|
scale: "smiley",
|
||||||
headline: t("templates.smileys_survey_question_1_headline"),
|
headline: { default: t("templates.smileys_survey_question_1_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
lowerLabel: t("templates.smileys_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.smileys_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
|
||||||
t,
|
isColorCodingEnabled: false,
|
||||||
}),
|
},
|
||||||
buildCTAQuestion({
|
{
|
||||||
id: reusableQuestionIds[1],
|
id: reusableQuestionIds[1],
|
||||||
html: t("templates.smileys_survey_question_2_html"),
|
html: { default: t("templates.smileys_survey_question_2_html") },
|
||||||
|
type: TSurveyQuestionTypeEnum.CTA,
|
||||||
logic: [
|
logic: [
|
||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
@@ -349,23 +372,25 @@ const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
headline: t("templates.smileys_survey_question_2_headline"),
|
headline: { default: t("templates.smileys_survey_question_2_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
buttonUrl: "https://formbricks.com/github",
|
buttonUrl: "https://formbricks.com/github",
|
||||||
buttonLabel: t("templates.smileys_survey_question_2_button_label"),
|
buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
|
||||||
buttonExternal: true,
|
buttonExternal: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
|
||||||
id: reusableQuestionIds[2],
|
id: reusableQuestionIds[2],
|
||||||
headline: t("templates.smileys_survey_question_3_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.smileys_survey_question_3_headline") },
|
||||||
required: true,
|
required: true,
|
||||||
subheader: t("templates.smileys_survey_question_3_subheader"),
|
subheader: { default: t("templates.smileys_survey_question_3_subheader") },
|
||||||
buttonLabel: t("templates.smileys_survey_question_3_button_label"),
|
buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
|
||||||
placeholder: t("templates.smileys_survey_question_3_placeholder"),
|
placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -375,26 +400,37 @@ const enpsSurvey = (t: TFnType): TXMTemplate => {
|
|||||||
...getXMSurveyDefault(t),
|
...getXMSurveyDefault(t),
|
||||||
name: t("templates.enps_survey_name"),
|
name: t("templates.enps_survey_name"),
|
||||||
questions: [
|
questions: [
|
||||||
buildNPSQuestion({
|
{
|
||||||
headline: t("templates.enps_survey_question_1_headline"),
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.NPS,
|
||||||
|
headline: {
|
||||||
|
default: t("templates.enps_survey_question_1_headline"),
|
||||||
|
},
|
||||||
required: false,
|
required: false,
|
||||||
lowerLabel: t("templates.enps_survey_question_1_lower_label"),
|
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
|
||||||
upperLabel: t("templates.enps_survey_question_1_upper_label"),
|
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
|
||||||
isColorCodingEnabled: true,
|
isColorCodingEnabled: true,
|
||||||
t,
|
},
|
||||||
}),
|
{
|
||||||
buildOpenTextQuestion({
|
id: createId(),
|
||||||
headline: t("templates.enps_survey_question_2_headline"),
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.enps_survey_question_2_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
buildOpenTextQuestion({
|
},
|
||||||
headline: t("templates.enps_survey_question_3_headline"),
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: TSurveyQuestionTypeEnum.OpenText,
|
||||||
|
headline: { default: t("templates.enps_survey_question_3_headline") },
|
||||||
required: false,
|
required: false,
|
||||||
inputType: "text",
|
inputType: "text",
|
||||||
t,
|
charLimit: {
|
||||||
}),
|
enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||||
import { getEnvironment } from "@/lib/environment/service";
|
|
||||||
import { getProjectByEnvironmentId, getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getUser } from "@/lib/user/service";
|
|
||||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +7,9 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||||
|
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
|
||||||
interface XMTemplatePageProps {
|
interface XMTemplatePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
|
|||||||
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
<XMTemplateList project={project} user={user} environmentId={environment.id} />
|
||||||
{projects.length >= 2 && (
|
{projects.length >= 2 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={`/environments/${environment.id}/surveys`}>
|
<Link href={`/environments/${environment.id}/surveys`}>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
import { TOrganizationTeam } from "@/app/(app)/(onboarding)/types/onboarding";
|
||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { teamCache } from "@/lib/cache/team";
|
import { teamCache } from "@/lib/cache/team";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { cache } from "@formbricks/lib/cache";
|
||||||
|
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +24,8 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getEnvironments } from "@/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
const LandingLayout = async (props) => {
|
const LandingLayout = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar";
|
||||||
import { getOrganizationsByUserId } from "@/lib/organization/service";
|
|
||||||
import { getUser } from "@/lib/user/service";
|
|
||||||
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { notFound, redirect } from "next/navigation";
|
import { notFound, redirect } from "next/navigation";
|
||||||
|
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { canUserAccessOrganization } from "@/lib/organization/auth";
|
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
|
||||||
import { getUser } from "@/lib/user/service";
|
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
import { act, cleanup, render, screen } from "@testing-library/react";
|
import { act, cleanup, render, screen } from "@testing-library/react";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, 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("@/lib/constants", () => ({
|
vi.mock("@formbricks/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: false,
|
IS_FORMBRICKS_CLOUD: false,
|
||||||
POSTHOG_API_KEY: "mock-posthog-api-key",
|
POSTHOG_API_KEY: "mock-posthog-api-key",
|
||||||
POSTHOG_HOST: "mock-posthog-host",
|
POSTHOG_HOST: "mock-posthog-host",
|
||||||
@@ -42,13 +42,13 @@ vi.mock("next-auth", () => ({
|
|||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
redirect: vi.fn(),
|
redirect: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/auth", () => ({
|
vi.mock("@formbricks/lib/organization/auth", () => ({
|
||||||
canUserAccessOrganization: vi.fn(),
|
canUserAccessOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/service", () => ({
|
vi.mock("@formbricks/lib/organization/service", () => ({
|
||||||
getOrganization: vi.fn(),
|
getOrganization: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/user/service", () => ({
|
vi.mock("@formbricks/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/tolgee/server", () => ({
|
vi.mock("@/tolgee/server", () => ({
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
interface ChannelPageProps {
|
interface ChannelPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Header } from "@/modules/ui/components/header";
|
import { Header } from "@/modules/ui/components/header";
|
||||||
@@ -7,6 +6,7 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
|
||||||
interface ModePageProps {
|
interface ModePageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
|
|||||||
<OnboardingOptionsContainer options={channelOptions} />
|
<OnboardingOptionsContainer options={channelOptions} />
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||||
import { previewSurvey } from "@/app/lib/templates";
|
import { previewSurvey } from "@/app/lib/templates";
|
||||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@/lib/localStorage";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
|
||||||
@@ -27,6 +26,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||||
import {
|
import {
|
||||||
TProjectConfigChannel,
|
TProjectConfigChannel,
|
||||||
TProjectConfigIndustry,
|
TProjectConfigIndustry,
|
||||||
@@ -225,7 +225,7 @@ export const ProjectSettings = ({
|
|||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={256}
|
width={256}
|
||||||
height={56}
|
height={56}
|
||||||
className="absolute top-2 left-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
<p className="text-sm text-slate-400">{t("common.preview")}</p>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
|
||||||
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
|
||||||
import { DEFAULT_BRAND_COLOR } from "@/lib/constants";
|
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
import { getOrganizationAuth } from "@/modules/organization/lib/utils";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +8,8 @@ import { getTranslate } from "@/tolgee/server";
|
|||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
|
||||||
|
|
||||||
interface ProjectSettingsPageProps {
|
interface ProjectSettingsPageProps {
|
||||||
@@ -65,7 +65,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
|
|||||||
/>
|
/>
|
||||||
{projects.length >= 1 && (
|
{projects.length >= 1 && (
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
|
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
asChild>
|
asChild>
|
||||||
<Link href={"/"}>
|
<Link href={"/"}>
|
||||||
|
|||||||
@@ -1,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, test, 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("@/lib/environment/service", () => ({
|
vi.mock("@formbricks/lib/environment/service", () => ({
|
||||||
getEnvironment: vi.fn(),
|
getEnvironment: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
|
|||||||
@@ -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 "@/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
export const LoadingCard = ({
|
export const LoadingCard = ({
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getOrganization } from "@/lib/organization/service";
|
|
||||||
import { getOrganizationProjectsCount } from "@/lib/project/service";
|
|
||||||
import { updateUser } from "@/lib/user/service";
|
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +8,9 @@ import {
|
|||||||
} from "@/modules/ee/license-check/lib/utils";
|
} from "@/modules/ee/license-check/lib/utils";
|
||||||
import { createProject } from "@/modules/projects/settings/lib/project";
|
import { createProject } from "@/modules/projects/settings/lib/project";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||||
|
import { getOrganizationProjectsCount } from "@formbricks/lib/project/service";
|
||||||
|
import { updateUser } from "@formbricks/lib/user/service";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
import { ZProjectUpdateInput } from "@formbricks/types/project";
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { deleteActionClass, getActionClass, updateActionClass } from "@/lib/actionClass/service";
|
|
||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { getSurveysByActionClassId } from "@/lib/survey/service";
|
|
||||||
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
import { actionClient, authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromActionClassId, getProjectIdFromActionClassId } from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/actionClass/service";
|
||||||
|
import { cache } from "@formbricks/lib/cache";
|
||||||
|
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||||
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
import { ZActionClassInput } from "@formbricks/types/action-classes";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
import { convertDateTimeStringShort } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
|
||||||
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
import { createActionClassAction } from "@/modules/survey/editor/actions";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
import { ErrorComponent } from "@/modules/ui/components/error-component";
|
||||||
@@ -12,6 +10,8 @@ import { LoadingSpinner } from "@/modules/ui/components/loading-spinner";
|
|||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||||
|
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||||
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { getActiveInactiveSurveysAction } from "../actions";
|
import { getActiveInactiveSurveysAction } from "../actions";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
import { ACTION_TYPE_ICON_LOOKUP } from "@/app/(app)/environments/[environmentId]/actions/utils";
|
||||||
import { timeSince } from "@/lib/time";
|
import { timeSince } from "@formbricks/lib/time";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
import { TUserLocale } from "@formbricks/types/user";
|
import { TUserLocale } from "@formbricks/types/user";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const ActionClassDataRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto text-center text-sm whitespace-nowrap text-slate-500">
|
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||||
{timeSince(actionClass.createdAt.toString(), locale)}
|
{timeSince(actionClass.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></div>
|
<div className="text-center"></div>
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act
|
|||||||
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData";
|
||||||
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading";
|
||||||
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal";
|
||||||
import { getActionClasses } from "@/lib/actionClass/service";
|
|
||||||
import { getEnvironments } from "@/lib/environment/service";
|
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||||
|
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Actions",
|
title: "Actions",
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||||
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
|
||||||
import { getEnvironment, getEnvironments } from "@/lib/environment/service";
|
|
||||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
|
||||||
import {
|
|
||||||
getMonthlyActiveOrganizationPeopleCount,
|
|
||||||
getMonthlyOrganizationResponseCount,
|
|
||||||
getOrganizationByEnvironmentId,
|
|
||||||
getOrganizationsByUserId,
|
|
||||||
} from "@/lib/organization/service";
|
|
||||||
import { getUserProjects } from "@/lib/project/service";
|
|
||||||
import { getUser } from "@/lib/user/service";
|
|
||||||
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
import { getEnterpriseLicense, getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||||
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
|
||||||
@@ -19,6 +7,18 @@ import { LimitsReachedBanner } from "@/modules/ui/components/limits-reached-bann
|
|||||||
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
import { PendingDowngradeBanner } from "@/modules/ui/components/pending-downgrade-banner";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
|
import { IS_DEVELOPMENT, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||||
|
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
|
import {
|
||||||
|
getMonthlyActiveOrganizationPeopleCount,
|
||||||
|
getMonthlyOrganizationResponseCount,
|
||||||
|
getOrganizationByEnvironmentId,
|
||||||
|
getOrganizationsByUserId,
|
||||||
|
} from "@formbricks/lib/organization/service";
|
||||||
|
import { getUserProjects } from "@formbricks/lib/project/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
|
||||||
interface EnvironmentLayoutProps {
|
interface EnvironmentLayoutProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@formbricks/lib/localStorage";
|
||||||
|
|
||||||
interface EnvironmentStorageHandlerProps {
|
interface EnvironmentStorageHandlerProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Label } from "@/modules/ui/components/label";
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
import { Switch } from "@/modules/ui/components/switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface EnvironmentSwitchProps {
|
interface EnvironmentSwitchProps {
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import { formbricksLogout } from "@/app/lib/formbricks";
|
import { formbricksLogout } from "@/app/lib/formbricks";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
|
||||||
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
import { CreateOrganizationModal } from "@/modules/organization/components/CreateOrganizationModal";
|
||||||
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
import { ProjectSwitcher } from "@/modules/projects/components/project-switcher";
|
||||||
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
import { ProfileAvatar } from "@/modules/ui/components/avatars";
|
||||||
@@ -48,6 +45,9 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
|
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
import { EnvironmentSwitch } from "@/app/(app)/environments/[environmentId]/components/EnvironmentSwitch";
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
|
||||||
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
import { TTeamPermission } from "@/modules/ee/teams/project-teams/types/team";
|
||||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -10,6 +9,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
import { BugIcon, CircleUserIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TOrganizationRole } from "@formbricks/types/memberships";
|
import { TOrganizationRole } from "@formbricks/types/memberships";
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
import { AlertTriangleIcon, CheckIcon, RotateCcwIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
|
||||||
interface WidgetStatusIndicatorProps {
|
interface WidgetStatusIndicatorProps {
|
||||||
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
|
|||||||
<currentStatus.icon />
|
<currentStatus.icon />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
|
||||||
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
|
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
|
||||||
{status === "notImplemented" && (
|
{status === "notImplemented" && (
|
||||||
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
|
||||||
<RotateCcwIcon />
|
<RotateCcwIcon />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { createOrUpdateIntegration, deleteIntegration } from "@/lib/integration/service";
|
|
||||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||||
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
getProjectIdFromIntegrationId,
|
getProjectIdFromIntegrationId,
|
||||||
} from "@/lib/utils/helper";
|
} from "@/lib/utils/helper";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZIntegrationInput } from "@formbricks/types/integration";
|
import { ZIntegrationInput } from "@formbricks/types/integration";
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { createOrUpdateIntegrationAction } from "@/app/(app)/environments/[envir
|
|||||||
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
import { BaseSelectDropdown } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/BaseSelectDropdown";
|
||||||
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
import { fetchTables } from "@/app/(app)/environments/[environmentId]/integrations/airtable/lib/airtable";
|
||||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
@@ -25,6 +23,8 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import {
|
import {
|
||||||
TIntegrationAirtable,
|
TIntegrationAirtable,
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TIntegrationAirtable, TIntegrationAirtableConfig } from "@formbricks/types/integration/airtable";
|
|
||||||
import { ManageIntegration } from "./ManageIntegration";
|
|
||||||
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
|
||||||
deleteIntegrationAction: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock(
|
|
||||||
"@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal",
|
|
||||||
() => ({
|
|
||||||
AddIntegrationModal: ({ open, setOpenWithStates }) =>
|
|
||||||
open ? (
|
|
||||||
<div data-testid="add-modal">
|
|
||||||
<button onClick={() => setOpenWithStates(false)}>close</button>
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
|
||||||
DeleteDialog: ({ open, setOpen, onDelete }) =>
|
|
||||||
open ? (
|
|
||||||
<div data-testid="delete-dialog">
|
|
||||||
<button onClick={onDelete}>confirm</button>
|
|
||||||
<button onClick={() => setOpen(false)}>cancel</button>
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
}));
|
|
||||||
vi.mock("react-hot-toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
|
|
||||||
|
|
||||||
const baseProps = {
|
|
||||||
environment: { id: "env1" } as TEnvironment,
|
|
||||||
environmentId: "env1",
|
|
||||||
setIsConnected: vi.fn(),
|
|
||||||
surveys: [],
|
|
||||||
airtableArray: [],
|
|
||||||
locale: "en-US" as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("ManageIntegration", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("empty state", () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
airtableIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
|
||||||
} as TIntegrationAirtable
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/link_new_table/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("open add modal", async () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
airtableIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
|
||||||
} as TIntegrationAirtable
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/link_new_table/));
|
|
||||||
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("list integrations and open edit modal", async () => {
|
|
||||||
const item = {
|
|
||||||
baseId: "b",
|
|
||||||
tableId: "t",
|
|
||||||
surveyId: "s",
|
|
||||||
surveyName: "S",
|
|
||||||
tableName: "T",
|
|
||||||
questions: "Q",
|
|
||||||
questionIds: ["x"],
|
|
||||||
createdAt: new Date(),
|
|
||||||
includeVariables: false,
|
|
||||||
includeHiddenFields: false,
|
|
||||||
includeMetadata: false,
|
|
||||||
includeCreatedAt: false,
|
|
||||||
};
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
airtableIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [item] } as unknown as TIntegrationAirtableConfig,
|
|
||||||
} as TIntegrationAirtable
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("S")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("S"));
|
|
||||||
expect(screen.getByTestId("add-modal")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration success", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
airtableIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
|
||||||
} as TIntegrationAirtable
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
|
||||||
const { toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.success).toHaveBeenCalled();
|
|
||||||
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration error", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
airtableIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] } as unknown as TIntegrationAirtableConfig,
|
|
||||||
} as TIntegrationAirtable
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
const { toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.error).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
AddIntegrationModal,
|
AddIntegrationModal,
|
||||||
IntegrationModalInputs,
|
IntegrationModalInputs,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
} from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AddIntegrationModal";
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -14,6 +13,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
|
import { timeSince } from "@formbricks/lib/time";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
@@ -98,17 +98,17 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
{integrationData.length ? (
|
{integrationData.length ? (
|
||||||
<div className="mt-6 w-full rounded-lg border border-slate-200">
|
<div className="mt-6 w-full rounded-lg border border-slate-200">
|
||||||
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||||
{tableHeaders.map((header) => (
|
{tableHeaders.map((header, idx) => (
|
||||||
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
|
<div key={idx} className={`col-span-2 hidden text-center sm:block`}>
|
||||||
{t(header)}
|
{t(header)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{integrationData.map((data, index) => (
|
{integrationData.map((data, index) => (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.baseId}-${data.tableId}-${data.surveyId}`}
|
key={index}
|
||||||
className="grid h-16 w-full grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDefaultValues({
|
setDefaultValues({
|
||||||
base: data.baseId,
|
base: data.baseId,
|
||||||
@@ -129,7 +129,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), props.locale)}
|
{timeSince(data.createdAt.toString(), props.locale)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import { getAirtableTables } from "@/lib/airtable/service";
|
|
||||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@/lib/constants";
|
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||||
|
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||||
|
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||||
|
|
||||||
|
|||||||
@@ -1,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,9 +8,7 @@ import {
|
|||||||
isValidGoogleSheetsUrl,
|
isValidGoogleSheetsUrl,
|
||||||
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
} from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/lib/util";
|
||||||
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
import GoogleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
||||||
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
import { AdditionalIntegrationSettings } from "@/modules/ui/components/additional-integration-settings";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Checkbox } from "@/modules/ui/components/checkbox";
|
import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||||
@@ -23,6 +21,8 @@ import Image from "next/image";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
TIntegrationGoogleSheetsConfigData,
|
TIntegrationGoogleSheetsConfigData,
|
||||||
@@ -255,7 +255,7 @@ export const AddIntegrationModal = ({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||||
<div className="mt-1 max-h-[15vh] overflow-x-hidden overflow-y-auto rounded-lg border border-slate-200">
|
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
||||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
|
||||||
import { ManageIntegration } from "./ManageIntegration";
|
|
||||||
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
|
||||||
deleteIntegrationAction: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("react-hot-toast", () => ({
|
|
||||||
default: { success: vi.fn(), error: vi.fn() },
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
|
||||||
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
|
|
||||||
open ? (
|
|
||||||
<div data-testid="delete-dialog">
|
|
||||||
<button onClick={onDelete}>confirm</button>
|
|
||||||
<button onClick={() => setOpen(false)}>cancel</button>
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
|
|
||||||
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const baseProps = {
|
|
||||||
environment: { id: "env1" } as TEnvironment,
|
|
||||||
setOpenAddIntegrationModal: vi.fn(),
|
|
||||||
setIsConnected: vi.fn(),
|
|
||||||
setSelectedIntegration: vi.fn(),
|
|
||||||
locale: "en-US" as const,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
describe("ManageIntegration (Google Sheets)", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("empty state", () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
googleSheetIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] },
|
|
||||||
} as unknown as TIntegrationGoogleSheets
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText(/no_integrations_yet/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/link_new_sheet/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("click link new sheet", async () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
googleSheetIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] },
|
|
||||||
} as unknown as TIntegrationGoogleSheets
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(/link_new_sheet/));
|
|
||||||
|
|
||||||
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
|
|
||||||
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("list integrations and open edit", async () => {
|
|
||||||
const item = {
|
|
||||||
spreadsheetId: "sid",
|
|
||||||
spreadsheetName: "SheetName",
|
|
||||||
surveyId: "s1",
|
|
||||||
surveyName: "Survey1",
|
|
||||||
questionIds: ["q1"],
|
|
||||||
questions: "Q",
|
|
||||||
createdAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
googleSheetIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [item] },
|
|
||||||
} as unknown as TIntegrationGoogleSheets
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.getByText("Survey1")).toBeInTheDocument();
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByText("Survey1"));
|
|
||||||
|
|
||||||
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({
|
|
||||||
...item,
|
|
||||||
index: 0,
|
|
||||||
});
|
|
||||||
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration success", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
googleSheetIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] },
|
|
||||||
} as unknown as TIntegrationGoogleSheets
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
|
|
||||||
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
|
||||||
|
|
||||||
const { default: toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
|
||||||
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration error", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
googleSheetIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { email: "a@b.com", data: [] },
|
|
||||||
} as unknown as TIntegrationGoogleSheets
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
|
|
||||||
const { default: toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
||||||
import { timeSince } from "@/lib/time";
|
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
@@ -10,6 +9,7 @@ import { useTranslate } from "@tolgee/react";
|
|||||||
import { Trash2Icon } from "lucide-react";
|
import { Trash2Icon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { timeSince } from "@formbricks/lib/time";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import {
|
import {
|
||||||
TIntegrationGoogleSheets,
|
TIntegrationGoogleSheets,
|
||||||
@@ -36,10 +36,11 @@ export const ManageIntegration = ({
|
|||||||
}: ManageIntegrationProps) => {
|
}: ManageIntegrationProps) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||||
let integrationArray: TIntegrationGoogleSheetsConfigData[] = [];
|
const integrationArray = googleSheetIntegration
|
||||||
if (googleSheetIntegration?.config.data) {
|
? googleSheetIntegration.config.data
|
||||||
integrationArray = googleSheetIntegration.config.data;
|
? googleSheetIntegration.config.data
|
||||||
}
|
: []
|
||||||
|
: [];
|
||||||
const [isDeleting, setisDeleting] = useState(false);
|
const [isDeleting, setisDeleting] = useState(false);
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
@@ -111,9 +112,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.spreadsheetName}-${data.surveyName}`}
|
key={index}
|
||||||
className="grid h-16 w-full cursor-pointer grid-cols-8 content-center rounded-lg p-2 hover:bg-slate-100"
|
className="m-2 grid h-16 cursor-pointer grid-cols-8 content-center rounded-lg hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -123,7 +124,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper";
|
||||||
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
import { getSurveys } from "@/app/(app)/environments/[environmentId]/integrations/lib/surveys";
|
||||||
import {
|
|
||||||
GOOGLE_SHEETS_CLIENT_ID,
|
|
||||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
|
||||||
GOOGLE_SHEETS_REDIRECT_URL,
|
|
||||||
WEBAPP_URL,
|
|
||||||
} from "@/lib/constants";
|
|
||||||
import { getIntegrations } from "@/lib/integration/service";
|
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
||||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||||
import { getTranslate } from "@/tolgee/server";
|
import { getTranslate } from "@/tolgee/server";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import {
|
||||||
|
GOOGLE_SHEETS_CLIENT_ID,
|
||||||
|
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||||
|
GOOGLE_SHEETS_REDIRECT_URL,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@formbricks/lib/constants";
|
||||||
|
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||||
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { surveyCache } from "@/lib/survey/cache";
|
|
||||||
import { selectSurvey } from "@/lib/survey/service";
|
|
||||||
import { transformPrismaSurvey } from "@/lib/survey/utils";
|
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { cache } from "@formbricks/lib/cache";
|
||||||
|
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||||
|
import { selectSurvey } from "@formbricks/lib/survey/service";
|
||||||
|
import { transformPrismaSurvey } from "@formbricks/lib/survey/utils";
|
||||||
|
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { cache } from "@/lib/cache";
|
|
||||||
import { webhookCache } from "@/lib/cache/webhook";
|
import { webhookCache } from "@/lib/cache/webhook";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
|
||||||
import { Prisma, Webhook } from "@prisma/client";
|
import { Prisma, Webhook } from "@prisma/client";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { cache } from "@formbricks/lib/cache";
|
||||||
|
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ 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";
|
||||||
@@ -21,6 +18,9 @@ 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,
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import type {
|
|
||||||
TIntegrationNotion,
|
|
||||||
TIntegrationNotionConfig,
|
|
||||||
TIntegrationNotionConfigData,
|
|
||||||
TIntegrationNotionCredential,
|
|
||||||
} from "@formbricks/types/integration/notion";
|
|
||||||
import { ManageIntegration } from "./ManageIntegration";
|
|
||||||
|
|
||||||
vi.mock("react-hot-toast", () => ({ success: vi.fn(), error: vi.fn() }));
|
|
||||||
vi.mock("@/lib/time", () => ({ timeSince: () => "ago" }));
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
|
||||||
deleteIntegrationAction: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("ManageIntegration", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
environment: {} as any,
|
|
||||||
locale: "en-US" as const,
|
|
||||||
setOpenAddIntegrationModal: vi.fn(),
|
|
||||||
setIsConnected: vi.fn(),
|
|
||||||
setSelectedIntegration: vi.fn(),
|
|
||||||
handleNotionAuthorization: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
test("shows empty state when no databases", () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...defaultProps}
|
|
||||||
notionIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: {
|
|
||||||
data: [] as TIntegrationNotionConfigData[],
|
|
||||||
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
|
|
||||||
} as TIntegrationNotionConfig,
|
|
||||||
} as TIntegrationNotion
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("environments.integrations.notion.no_databases_found")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders list and handles clicks", async () => {
|
|
||||||
const data = [
|
|
||||||
{ surveyName: "S", databaseName: "D", createdAt: new Date().toISOString(), databaseId: "db" },
|
|
||||||
] as unknown as TIntegrationNotionConfigData[];
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...defaultProps}
|
|
||||||
notionIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data, key: { workspace_name: "ws" } as TIntegrationNotionCredential },
|
|
||||||
} as TIntegrationNotion
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("S")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("S"));
|
|
||||||
expect(defaultProps.setSelectedIntegration).toHaveBeenCalledWith({ ...data[0], index: 0 });
|
|
||||||
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("update and link new buttons invoke handlers", async () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...defaultProps}
|
|
||||||
notionIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: {
|
|
||||||
data: [],
|
|
||||||
key: { workspace_name: "ws" } as TIntegrationNotionCredential,
|
|
||||||
} as TIntegrationNotionConfig,
|
|
||||||
} as TIntegrationNotion
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText("environments.integrations.notion.update_connection"));
|
|
||||||
expect(defaultProps.handleNotionAuthorization).toHaveBeenCalled();
|
|
||||||
await userEvent.click(screen.getByText("environments.integrations.notion.link_new_database"));
|
|
||||||
expect(defaultProps.setOpenAddIntegrationModal).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"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";
|
||||||
@@ -11,6 +10,7 @@ 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
|
||||||
let integrationArray: TIntegrationNotionConfigData[] = [];
|
? notionIntegration.config.data
|
||||||
if (notionIntegration?.config.data) {
|
? 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 (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.databaseId}`}
|
key={index}
|
||||||
className="grid h-16 w-full cursor-pointer grid-cols-6 content-center rounded-lg p-2 hover:bg-slate-100"
|
className="m-2 grid h-16 cursor-pointer grid-cols-6 content-center rounded-lg 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>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
NOTION_AUTH_URL,
|
|
||||||
NOTION_OAUTH_CLIENT_ID,
|
|
||||||
NOTION_OAUTH_CLIENT_SECRET,
|
|
||||||
NOTION_REDIRECT_URI,
|
|
||||||
WEBAPP_URL,
|
|
||||||
} from "@/lib/constants";
|
|
||||||
import { getIntegrationByType } from "@/lib/integration/service";
|
|
||||||
import { getNotionDatabases } from "@/lib/notion/service";
|
|
||||||
import { findMatchingLocale } from "@/lib/utils/locale";
|
|
||||||
import { getEnvironmentAuth } from "@/modules/environments/lib/utils";
|
import { 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 {
|
||||||
|
NOTION_AUTH_URL,
|
||||||
|
NOTION_OAUTH_CLIENT_ID,
|
||||||
|
NOTION_OAUTH_CLIENT_SECRET,
|
||||||
|
NOTION_REDIRECT_URI,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@formbricks/lib/constants";
|
||||||
|
import { getIntegrationByType } from "@formbricks/lib/integration/service";
|
||||||
|
import { getNotionDatabases } from "@formbricks/lib/notion/service";
|
||||||
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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";
|
||||||
@@ -17,6 +16,7 @@ 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,8 +2,6 @@
|
|||||||
|
|
||||||
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";
|
||||||
@@ -17,6 +15,8 @@ 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,
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
import { deleteIntegrationAction } from "@/app/(app)/environments/[environmentId]/integrations/actions";
|
|
||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
|
||||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
|
||||||
import { ManageIntegration } from "./ManageIntegration";
|
|
||||||
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/integrations/actions", () => ({
|
|
||||||
deleteIntegrationAction: vi.fn(),
|
|
||||||
}));
|
|
||||||
vi.mock("react-hot-toast", () => ({ default: { success: vi.fn(), error: vi.fn() } }));
|
|
||||||
vi.mock("@/modules/ui/components/delete-dialog", () => ({
|
|
||||||
DeleteDialog: ({ open, setOpen, onDelete }: any) =>
|
|
||||||
open ? (
|
|
||||||
<div data-testid="delete-dialog">
|
|
||||||
<button onClick={onDelete}>confirm</button>
|
|
||||||
<button onClick={() => setOpen(false)}>cancel</button>
|
|
||||||
</div>
|
|
||||||
) : null,
|
|
||||||
}));
|
|
||||||
vi.mock("@/modules/ui/components/empty-space-filler", () => ({
|
|
||||||
EmptySpaceFiller: ({ emptyMessage }: any) => <div>{emptyMessage}</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const baseProps = {
|
|
||||||
environment: { id: "env1" } as TEnvironment,
|
|
||||||
setOpenAddIntegrationModal: vi.fn(),
|
|
||||||
setIsConnected: vi.fn(),
|
|
||||||
setSelectedIntegration: vi.fn(),
|
|
||||||
refreshChannels: vi.fn(),
|
|
||||||
handleSlackAuthorization: vi.fn(),
|
|
||||||
showReconnectButton: false,
|
|
||||||
locale: "en-US" as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("ManageIntegration (Slack)", () => {
|
|
||||||
afterEach(() => cleanup());
|
|
||||||
|
|
||||||
test("empty state", () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [], key: { team: { name: "team name" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText(/connect_your_first_slack_channel/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText(/link_channel/)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("link channel triggers handlers", async () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [], key: { team: { name: "team name" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/link_channel/));
|
|
||||||
expect(baseProps.refreshChannels).toHaveBeenCalled();
|
|
||||||
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith(null);
|
|
||||||
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("show reconnect button and triggers authorization", async () => {
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
showReconnectButton={true}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [], key: { team: { name: "Team" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("environments.integrations.slack.slack_reconnect_button")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("environments.integrations.slack.slack_reconnect_button"));
|
|
||||||
expect(baseProps.handleSlackAuthorization).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("list integrations and open edit", async () => {
|
|
||||||
const item = {
|
|
||||||
surveyName: "S",
|
|
||||||
channelName: "C",
|
|
||||||
questions: "Q",
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
surveyId: "s",
|
|
||||||
channelId: "c",
|
|
||||||
} as unknown as TIntegrationSlackConfigData;
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [item], key: { team: { name: "team name" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("S")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("S"));
|
|
||||||
expect(baseProps.setSelectedIntegration).toHaveBeenCalledWith({ ...item, index: 0 });
|
|
||||||
expect(baseProps.setOpenAddIntegrationModal).toHaveBeenCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration success", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ data: true } as any);
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [], key: { team: { name: "team name" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
expect(screen.getByTestId("delete-dialog")).toBeInTheDocument();
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
expect(deleteIntegrationAction).toHaveBeenCalledWith({ integrationId: "1" });
|
|
||||||
const { default: toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.success).toHaveBeenCalledWith("environments.integrations.integration_removed_successfully");
|
|
||||||
expect(baseProps.setIsConnected).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("delete integration error", async () => {
|
|
||||||
vi.mocked(deleteIntegrationAction).mockResolvedValue({ error: "fail" } as any);
|
|
||||||
render(
|
|
||||||
<ManageIntegration
|
|
||||||
{...baseProps}
|
|
||||||
slackIntegration={
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
config: { data: [], key: { team: { name: "team name" } } },
|
|
||||||
} as unknown as TIntegrationSlack
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText(/delete_integration/));
|
|
||||||
await userEvent.click(screen.getByText("confirm"));
|
|
||||||
const { default: toast } = await import("react-hot-toast");
|
|
||||||
expect(toast.error).toHaveBeenCalledWith(expect.any(String));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
"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 { T, useTranslate } from "@tolgee/react";
|
import { 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";
|
||||||
@@ -42,10 +43,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);
|
||||||
let integrationArray: TIntegrationSlackConfigData[] = [];
|
const integrationArray = slackIntegration
|
||||||
if (slackIntegration?.config.data) {
|
? slackIntegration.config.data
|
||||||
integrationArray = slackIntegration.config.data;
|
? slackIntegration.config.data
|
||||||
}
|
: []
|
||||||
|
: [];
|
||||||
|
|
||||||
const handleDeleteIntegration = async () => {
|
const handleDeleteIntegration = async () => {
|
||||||
setisDeleting(true);
|
setisDeleting(true);
|
||||||
@@ -127,9 +129,9 @@ export const ManageIntegration = ({
|
|||||||
{integrationArray &&
|
{integrationArray &&
|
||||||
integrationArray.map((data, index) => {
|
integrationArray.map((data, index) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={`${index}-${data.surveyName}-${data.channelName}`}
|
key={index}
|
||||||
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"
|
className="m-2 grid h-16 grid-cols-8 content-center rounded-lg text-slate-700 hover:cursor-pointer hover:bg-slate-100"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editIntegration(index);
|
editIntegration(index);
|
||||||
}}>
|
}}>
|
||||||
@@ -139,7 +141,7 @@ export const ManageIntegration = ({
|
|||||||
<div className="col-span-2 text-center">
|
<div className="col-span-2 text-center">
|
||||||
{timeSince(data.createdAt.toString(), locale)}
|
{timeSince(data.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,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, test, 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("@/lib/project/service", () => ({
|
vi.mock("@formbricks/lib/project/service", () => ({
|
||||||
getProjectByEnvironmentId: vi.fn(),
|
getProjectByEnvironmentId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/membership/service", () => ({
|
vi.mock("@formbricks/lib/membership/service", () => ({
|
||||||
getMembershipByUserIdOrganizationId: vi.fn(),
|
getMembershipByUserIdOrganizationId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"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,
|
||||||
@@ -24,6 +23,7 @@ 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,8 +1,5 @@
|
|||||||
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";
|
||||||
@@ -10,6 +7,9 @@ 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 {
|
||||||
|
|||||||
@@ -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,5 +1,4 @@
|
|||||||
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";
|
||||||
@@ -9,6 +8,7 @@ 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,6 +58,11 @@ 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,
|
||||||
@@ -118,7 +123,7 @@ const Page = async (props) => {
|
|||||||
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
<div className="relative isolate mt-8 overflow-hidden rounded-lg bg-slate-900 px-3 pt-8 shadow-2xl sm:px-8 md:pt-12 lg:flex lg:gap-x-10 lg:px-12 lg:pt-0">
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
viewBox="0 0 1024 1024"
|
||||||
className="absolute top-1/2 left-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
className="absolute left-1/2 top-1/2 -z-10 h-[64rem] w-[64rem] -translate-y-1/2 [mask-image:radial-gradient(closest-side,white,transparent)] sm:left-full sm:-ml-80 lg:left-1/2 lg:ml-0 lg:-translate-x-1/2 lg:translate-y-0"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<circle
|
<circle
|
||||||
cx={512}
|
cx={512}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"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,7 +1,6 @@
|
|||||||
"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";
|
||||||
@@ -10,6 +9,7 @@ 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,7 +1,6 @@
|
|||||||
"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";
|
||||||
@@ -19,6 +18,7 @@ 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,13 +1,17 @@
|
|||||||
import { getUser } from "@/lib/user/service";
|
import {
|
||||||
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
getIsMultiOrgEnabled,
|
||||||
|
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, test, 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("@/lib/constants", () => ({
|
vi.mock("@formbricks/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",
|
||||||
@@ -29,6 +33,12 @@ vi.mock("@/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", () => ({
|
||||||
@@ -39,7 +49,7 @@ vi.mock("@/tolgee/server", () => ({
|
|||||||
getTranslate: vi.fn(),
|
getTranslate: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/user/service", () => ({
|
vi.mock("@formbricks/lib/user/service", () => ({
|
||||||
getUser: vi.fn(),
|
getUser: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -49,6 +59,7 @@ 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(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -69,6 +80,7 @@ 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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar";
|
||||||
import { FB_LOGO_URL, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle";
|
||||||
import { getUser } from "@/lib/user/service";
|
import {
|
||||||
import { getIsMultiOrgEnabled, getWhiteLabelPermission } from "@/modules/ee/license-check/lib/utils";
|
getIsMultiOrgEnabled,
|
||||||
|
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";
|
||||||
@@ -30,6 +35,8 @@ 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")}>
|
||||||
@@ -49,6 +56,17 @@ 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;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { Badge } from "@/modules/ui/components/badge";
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
|
|
||||||
export const SettingsCard = ({
|
export const SettingsCard = ({
|
||||||
title,
|
title,
|
||||||
@@ -31,7 +31,7 @@ export const SettingsCard = ({
|
|||||||
id={title}>
|
id={title}>
|
||||||
<div className="border-b border-slate-200 px-4 pb-4">
|
<div className="border-b border-slate-200 px-4 pb-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h3 className="text-lg leading-6 font-medium text-slate-900 capitalize">{title}</h3>
|
<h3 className="text-lg font-medium capitalize leading-6 text-slate-900">{title}</h3>
|
||||||
<div className="ml-2">
|
<div className="ml-2">
|
||||||
{beta && <Badge size="normal" type="warning" text="Beta" />}
|
{beta && <Badge size="normal" type="warning" text="Beta" />}
|
||||||
{soon && (
|
{soon && (
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { getResponseCountBySurveyId, getResponses } from "@/lib/response/service";
|
import { generateInsightsForSurvey } from "@/app/api/(internal)/insights/lib/utils";
|
||||||
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 { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
import { getOrganizationIdFromSurveyId, getProjectIdFromSurveyId } from "@/lib/utils/helper";
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
import { ZResponseFilterCriteria } from "@formbricks/types/responses";
|
||||||
import { getSurveySummary } from "./summary/lib/surveySummary";
|
import { getSurveySummary } from "./summary/lib/surveySummary";
|
||||||
@@ -107,3 +108,31 @@ export const getResponseCountAction = authenticatedActionClient
|
|||||||
|
|
||||||
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
return getResponseCountBySurveyId(parsedInput.surveyId, parsedInput.filterCriteria);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ZGenerateInsightsForSurveyAction = z.object({
|
||||||
|
surveyId: ZId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateInsightsForSurveyAction = authenticatedActionClient
|
||||||
|
.schema(ZGenerateInsightsForSurveyAction)
|
||||||
|
.action(async ({ ctx, parsedInput }) => {
|
||||||
|
await checkAuthorizationUpdated({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
organizationId: await getOrganizationIdFromSurveyId(parsedInput.surveyId),
|
||||||
|
access: [
|
||||||
|
{
|
||||||
|
type: "organization",
|
||||||
|
schema: ZGenerateInsightsForSurveyAction,
|
||||||
|
data: parsedInput,
|
||||||
|
roles: ["owner", "manager"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "projectTeam",
|
||||||
|
projectId: await getProjectIdFromSurveyId(parsedInput.surveyId),
|
||||||
|
minPermission: "readWrite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
generateInsightsForSurvey(parsedInput.surveyId);
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||||
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
import { getFormattedFilters } from "@/app/lib/surveys/surveys";
|
||||||
import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
import { getResponseCountBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
||||||
import { useIntervalWhenFocused } from "@/lib/utils/hooks/useIntervalWhenFocused";
|
|
||||||
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 { InboxIcon, PresentationIcon } from "lucide-react";
|
import { InboxIcon, PresentationIcon } from "lucide-react";
|
||||||
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
import { useParams, usePathname, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useIntervalWhenFocused } from "@formbricks/lib/utils/hooks/useIntervalWhenFocused";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
|
||||||
interface SurveyAnalysisNavigationProps {
|
interface SurveyAnalysisNavigationProps {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
|
||||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||||
|
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{ surveyId: string; environmentId: string }>;
|
params: Promise<{ surveyId: string; environmentId: string }>;
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
getResponseCountBySurveySharingKeyAction,
|
getResponseCountBySurveySharingKeyAction,
|
||||||
getResponsesBySurveySharingKeyAction,
|
getResponsesBySurveySharingKeyAction,
|
||||||
} from "@/app/share/[sharingKey]/actions";
|
} from "@/app/share/[sharingKey]/actions";
|
||||||
import { replaceHeadlineRecall } from "@/lib/utils/recall";
|
|
||||||
import { useParams, useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
import { TResponse } from "@formbricks/types/responses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
import type { Cell, Row } from "@tanstack/react-table";
|
|
||||||
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 { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
|
||||||
import { ResponseTableCell } from "./ResponseTableCell";
|
|
||||||
|
|
||||||
const makeCell = (
|
|
||||||
id: string,
|
|
||||||
size = 100,
|
|
||||||
first = false,
|
|
||||||
last = false,
|
|
||||||
content = "CellContent"
|
|
||||||
): Cell<TResponseTableData, unknown> =>
|
|
||||||
({
|
|
||||||
column: {
|
|
||||||
id,
|
|
||||||
getSize: () => size,
|
|
||||||
getIsFirstColumn: () => first,
|
|
||||||
getIsLastColumn: () => last,
|
|
||||||
getStart: () => 0,
|
|
||||||
columnDef: { cell: () => content },
|
|
||||||
},
|
|
||||||
id,
|
|
||||||
getContext: () => ({}),
|
|
||||||
}) as unknown as Cell<TResponseTableData, unknown>;
|
|
||||||
|
|
||||||
const makeRow = (id: string, selected = false): Row<TResponseTableData> =>
|
|
||||||
({ id, getIsSelected: () => selected }) as unknown as Row<TResponseTableData>;
|
|
||||||
|
|
||||||
describe("ResponseTableCell", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders cell content", () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1");
|
|
||||||
render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={vi.fn()}
|
|
||||||
responses={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(screen.getByText("CellContent")).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls setSelectedResponseId on cell click when not select column", async () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1");
|
|
||||||
const setSel = vi.fn();
|
|
||||||
render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={setSel}
|
|
||||||
responses={[{ id: "r1" } as TResponse]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText("CellContent"));
|
|
||||||
expect(setSel).toHaveBeenCalledWith("r1");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not call setSelectedResponseId on select column click", async () => {
|
|
||||||
const cell = makeCell("select");
|
|
||||||
const row = makeRow("r1");
|
|
||||||
const setSel = vi.fn();
|
|
||||||
render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={setSel}
|
|
||||||
responses={[{ id: "r1" } as TResponse]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByText("CellContent"));
|
|
||||||
expect(setSel).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders maximize icon for createdAt column and handles click", async () => {
|
|
||||||
const cell = makeCell("createdAt", 120, false, false);
|
|
||||||
const row = makeRow("r2");
|
|
||||||
const setSel = vi.fn();
|
|
||||||
render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={setSel}
|
|
||||||
responses={[{ id: "r2" } as TResponse]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const btn = screen.getByRole("button", { name: /expand response/i });
|
|
||||||
expect(btn).toBeDefined();
|
|
||||||
await userEvent.click(btn);
|
|
||||||
expect(setSel).toHaveBeenCalledWith("r2");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("does not apply selected style when row.getIsSelected() is false", () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1", false);
|
|
||||||
const { container } = render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={vi.fn()}
|
|
||||||
responses={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(container.firstChild).not.toHaveClass("bg-slate-100");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("applies selected style when row.getIsSelected() is true", () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1", true);
|
|
||||||
const { container } = render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={vi.fn()}
|
|
||||||
responses={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
expect(container.firstChild).toHaveClass("bg-slate-100");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders collapsed height class when isExpanded is false", () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1");
|
|
||||||
const { container } = render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={false}
|
|
||||||
setSelectedResponseId={vi.fn()}
|
|
||||||
responses={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const inner = container.querySelector("div > div");
|
|
||||||
expect(inner).toHaveClass("h-10");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders expanded height class when isExpanded is true", () => {
|
|
||||||
const cell = makeCell("col1");
|
|
||||||
const row = makeRow("r1");
|
|
||||||
const { container } = render(
|
|
||||||
<ResponseTableCell
|
|
||||||
cell={cell}
|
|
||||||
row={row}
|
|
||||||
isExpanded={true}
|
|
||||||
setSelectedResponseId={vi.fn()}
|
|
||||||
responses={[]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const inner = container.querySelector("div > div");
|
|
||||||
expect(inner).toHaveClass("h-full");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { cn } from "@/lib/cn";
|
|
||||||
import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils";
|
import { getCommonPinningStyles } from "@/modules/ui/components/data-table/lib/utils";
|
||||||
import { TableCell } from "@/modules/ui/components/table";
|
import { TableCell } from "@/modules/ui/components/table";
|
||||||
import { Cell, Row, flexRender } from "@tanstack/react-table";
|
import { Cell, Row, flexRender } from "@tanstack/react-table";
|
||||||
import { Maximize2Icon } from "lucide-react";
|
import { Maximize2Icon } from "lucide-react";
|
||||||
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
import { TResponse, TResponseTableData } from "@formbricks/types/responses";
|
||||||
|
|
||||||
interface ResponseTableCellProps {
|
interface ResponseTableCellProps {
|
||||||
@@ -35,13 +35,11 @@ export const ResponseTableCell = ({
|
|||||||
|
|
||||||
// Conditional rendering of maximize icon
|
// Conditional rendering of maximize icon
|
||||||
const renderMaximizeIcon = cell.column.id === "createdAt" && (
|
const renderMaximizeIcon = cell.column.id === "createdAt" && (
|
||||||
<button
|
<div
|
||||||
type="button"
|
className="hidden flex-shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 hover:border-slate-300 group-hover:flex"
|
||||||
aria-label="Expand response"
|
|
||||||
className="hidden flex-shrink-0 cursor-pointer items-center rounded-md border border-slate-200 bg-white p-2 group-hover:flex hover:border-slate-300 focus:outline-none"
|
|
||||||
onClick={handleCellClick}>
|
onClick={handleCellClick}>
|
||||||
<Maximize2Icon className="h-4 w-4" />
|
<Maximize2Icon className="h-4 w-4" />
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
|
||||||
import { processResponseData } from "@/lib/responses";
|
|
||||||
import { getContactIdentifier } from "@/lib/utils/contact";
|
|
||||||
import { getFormattedDateTimeString } from "@/lib/utils/datetime";
|
|
||||||
import { recallToHeadline } from "@/lib/utils/recall";
|
|
||||||
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
import { RenderResponse } from "@/modules/analysis/components/SingleResponseCard/components/RenderResponse";
|
||||||
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
|
import { VARIABLES_ICON_MAP, getQuestionIconMap } from "@/modules/survey/lib/questions";
|
||||||
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
import { getSelectionColumn } from "@/modules/ui/components/data-table";
|
||||||
@@ -14,6 +9,11 @@ import { ColumnDef } from "@tanstack/react-table";
|
|||||||
import { TFnType } from "@tolgee/react";
|
import { TFnType } from "@tolgee/react";
|
||||||
import { CircleHelpIcon, EyeOffIcon, MailIcon, TagIcon } from "lucide-react";
|
import { CircleHelpIcon, EyeOffIcon, MailIcon, TagIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||||
|
import { processResponseData } from "@formbricks/lib/responses";
|
||||||
|
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||||
|
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
|
||||||
|
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||||
import { TResponseTableData } from "@formbricks/types/responses";
|
import { TResponseTableData } from "@formbricks/types/responses";
|
||||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
import { SurveyAnalysisNavigation } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyAnalysisNavigation";
|
||||||
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||||
|
import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner";
|
||||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||||
import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@/lib/constants";
|
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||||
import { getSurveyDomain } from "@/lib/getSurveyUrl";
|
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
|
||||||
import { getSurvey } from "@/lib/survey/service";
|
|
||||||
import { getTagsByEnvironmentId } from "@/lib/tag/service";
|
|
||||||
import { getUser } from "@/lib/user/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 {
|
||||||
|
MAX_RESPONSES_FOR_INSIGHT_GENERATION,
|
||||||
|
RESPONSES_PER_PAGE,
|
||||||
|
WEBAPP_URL,
|
||||||
|
} from "@formbricks/lib/constants";
|
||||||
|
import { getSurveyDomain } from "@formbricks/lib/getSurveyUrl";
|
||||||
|
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||||
|
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||||
|
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||||
|
import { getUser } from "@formbricks/lib/user/service";
|
||||||
|
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||||
|
|
||||||
const Page = async (props) => {
|
const Page = async (props) => {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslate();
|
const t = await getTranslate();
|
||||||
|
|
||||||
const { session, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
const { session, environment, organization, isReadOnly } = await getEnvironmentAuth(params.environmentId);
|
||||||
|
|
||||||
const survey = await getSurvey(params.surveyId);
|
const survey = await getSurvey(params.surveyId);
|
||||||
|
|
||||||
@@ -35,6 +42,11 @@ const Page = async (props) => {
|
|||||||
|
|
||||||
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
|
const totalResponseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||||
|
|
||||||
|
const isAIEnabled = await getIsAIEnabled({
|
||||||
|
isAIEnabled: organization.isAIEnabled,
|
||||||
|
billing: organization.billing,
|
||||||
|
});
|
||||||
|
const shouldGenerateInsights = needsInsightsGeneration(survey);
|
||||||
const locale = await findMatchingLocale();
|
const locale = await findMatchingLocale();
|
||||||
const surveyDomain = getSurveyDomain();
|
const surveyDomain = getSurveyDomain();
|
||||||
|
|
||||||
@@ -49,9 +61,16 @@ const Page = async (props) => {
|
|||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
user={user}
|
user={user}
|
||||||
surveyDomain={surveyDomain}
|
surveyDomain={surveyDomain}
|
||||||
responseCount={totalResponseCount}
|
|
||||||
/>
|
/>
|
||||||
}>
|
}>
|
||||||
|
{isAIEnabled && shouldGenerateInsights && (
|
||||||
|
<EnableInsightsBanner
|
||||||
|
surveyId={survey.id}
|
||||||
|
surveyResponseCount={totalResponseCount}
|
||||||
|
maxResponseCount={MAX_RESPONSES_FOR_INSIGHT_GENERATION}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<SurveyAnalysisNavigation
|
<SurveyAnalysisNavigation
|
||||||
environmentId={environment.id}
|
environmentId={environment.id}
|
||||||
survey={survey}
|
survey={survey}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user