mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-02 10:30:23 -06:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1d697a83f | ||
|
|
69a7a57f41 | ||
|
|
24de1559a5 | ||
|
|
ec29abfcaf | ||
|
|
eac97db665 | ||
|
|
d8386328e7 | ||
|
|
d28f321aa2 | ||
|
|
e691c076a1 | ||
|
|
ad842e0e80 | ||
|
|
dcf4109c5b | ||
|
|
05287c135e | ||
|
|
6ff8ec21cf | ||
|
|
7b6e22aa04 | ||
|
|
ee56914285 | ||
|
|
a2e9cd3c43 | ||
|
|
359f29a264 | ||
|
|
576b15fec0 | ||
|
|
42434290da | ||
|
|
62c6189dfd | ||
|
|
21c9ebbca3 | ||
|
|
658d4687f9 | ||
|
|
3775453db8 | ||
|
|
edcaf8e639 | ||
|
|
3aa658a64e | ||
|
|
58fc66ad1c | ||
|
|
f68f87645f | ||
|
|
25f99da172 | ||
|
|
5da6faa972 | ||
|
|
02b25138ef |
4
.github/workflows/build-docs.yml
vendored
4
.github/workflows/build-docs.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: Build Docs
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docs
|
||||
|
||||
4
.github/workflows/build-web.yml
vendored
4
.github/workflows/build-web.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: Build Web
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Formbricks-web
|
||||
|
||||
@@ -7,6 +7,10 @@ on:
|
||||
schedule:
|
||||
# Runs "At 00:00." (see https://crontab.guru)
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cron-weeklySummary:
|
||||
env:
|
||||
|
||||
2
.github/workflows/cron-weeklySummary.yml
vendored
2
.github/workflows/cron-weeklySummary.yml
vendored
@@ -9,6 +9,8 @@ on:
|
||||
- cron: "0 8 * * 1"
|
||||
jobs:
|
||||
cron-weeklySummary:
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
|
||||
45
.github/workflows/e2e.yml
vendored
45
.github/workflows/e2e.yml
vendored
@@ -1,9 +1,28 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
AZURE_CLIENT_ID:
|
||||
required: false
|
||||
AZURE_TENANT_ID:
|
||||
required: false
|
||||
AZURE_SUBSCRIPTION_ID:
|
||||
required: false
|
||||
PLAYWRIGHT_SERVICE_URL:
|
||||
required: false
|
||||
# Add other secrets if necessary
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TELEMETRY_DISABLED: 1
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
actions: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Run E2E Tests
|
||||
@@ -83,7 +102,31 @@ jobs:
|
||||
- name: Install Playwright
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run E2E Tests
|
||||
- name: Set Azure Secret Variables
|
||||
run: |
|
||||
if [[ -n "${{ secrets.AZURE_CLIENT_ID }}" && -n "${{ secrets.AZURE_TENANT_ID }}" && -n "${{ secrets.AZURE_SUBSCRIPTION_ID }}" ]]; then
|
||||
echo "AZURE_ENABLED=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "AZURE_ENABLED=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Azure login
|
||||
if: env.AZURE_ENABLED == 'true'
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Run E2E Tests (Azure)
|
||||
if: env.AZURE_ENABLED == 'true'
|
||||
env:
|
||||
PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }}
|
||||
run: |
|
||||
pnpm test-e2e:azure
|
||||
|
||||
- name: Run E2E Tests (Local)
|
||||
if: env.AZURE_ENABLED == 'false'
|
||||
run: |
|
||||
pnpm test:e2e
|
||||
|
||||
|
||||
10
.github/workflows/lint.yml
vendored
10
.github/workflows/lint.yml
vendored
@@ -1,6 +1,10 @@
|
||||
name: Lint
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Linters
|
||||
@@ -8,16 +12,16 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
39
.github/workflows/pr.yml
vendored
39
.github/workflows/pr.yml
vendored
@@ -1,5 +1,13 @@
|
||||
name: PR Update
|
||||
|
||||
# Update permissions to include all necessary ones
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
actions: read
|
||||
checks: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -12,55 +20,28 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
has-files-requiring-all-checks: ${{ steps.filter.outputs.has-files-requiring-all-checks }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
has-files-requiring-all-checks:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
|
||||
test:
|
||||
name: Run Unit Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
|
||||
lint:
|
||||
name: Run Linters
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/lint.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Build Formbricks-web
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/build-web.yml
|
||||
secrets: inherit
|
||||
|
||||
docs:
|
||||
name: Build Docs
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/build-docs.yml
|
||||
secrets: inherit
|
||||
|
||||
e2e-test:
|
||||
name: Run E2E Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/e2e.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -69,6 +50,10 @@ jobs:
|
||||
needs: [lint, test, build, e2e-test, docs]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
statuses: write
|
||||
steps:
|
||||
- name: fail if conditional jobs failed
|
||||
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'skipped') || contains(needs.*.result, 'cancelled')
|
||||
|
||||
62
.github/workflows/prepare-release.yml
vendored
Normal file
62
.github/workflows/prepare-release.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: Prepare release
|
||||
run-name: Prepare release ${{ inputs.next_version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
next_version:
|
||||
required: true
|
||||
type: string
|
||||
description: "Version name"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --local user.email "github-actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
cd apps/web
|
||||
pnpm version ${{ inputs.next_version }} --no-workspaces-update
|
||||
|
||||
- name: Commit changes and create a branch
|
||||
run: |
|
||||
branch_name="release-v${{ inputs.next_version }}"
|
||||
git checkout -b "$branch_name"
|
||||
git add .
|
||||
git commit -m "chore: release v${{ inputs.next_version }}"
|
||||
git push origin "$branch_name"
|
||||
|
||||
- name: Create pull request
|
||||
run: |
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "release-v${{ inputs.next_version }}" \
|
||||
--title "chore: bump version to v${{ inputs.next_version }}" \
|
||||
--body "This PR contains the changes for the v${{ inputs.next_version }} release."
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
5
.github/workflows/release-changesets.yml
vendored
5
.github/workflows/release-changesets.yml
vendored
@@ -6,6 +6,11 @@ on:
|
||||
# branches:
|
||||
# - main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
packages: write
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
env:
|
||||
|
||||
2
.github/workflows/release-docker.yml
vendored
2
.github/workflows/release-docker.yml
vendored
@@ -8,6 +8,8 @@ on:
|
||||
jobs:
|
||||
release-image-on-dockerhub:
|
||||
name: Release on Dockerhub
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -6,6 +6,8 @@ jobs:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -3,7 +3,7 @@ name: "Welcome new contributors"
|
||||
on:
|
||||
issues:
|
||||
types: opened
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -14,7 +14,6 @@ module.exports = {
|
||||
typescript: {
|
||||
project: "tsconfig.json",
|
||||
},
|
||||
caseSensitive: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
37
apps/docs/app/self-hosting/rate-limiting/page.mdx
Normal file
37
apps/docs/app/self-hosting/rate-limiting/page.mdx
Normal file
@@ -0,0 +1,37 @@
|
||||
# Rate Limiting
|
||||
|
||||
To protect the platform from abuse and ensure fair usage, rate limiting is enforced by default on an IP-address basis. If a client exceeds the allowed number of requests within the specified time window, the API will return a `429 Too Many Requests` status code.
|
||||
|
||||
## Default Rate Limits
|
||||
|
||||
The following rate limits apply to various endpoints:
|
||||
|
||||
| **Endpoint** | **Rate Limit** | **Time Window** |
|
||||
| ----------------------- | -------------- | --------------- |
|
||||
| `POST /login` | 30 requests | 15 minutes |
|
||||
| `POST /signup` | 30 requests | 60 minutes |
|
||||
| `POST /verify-email` | 10 requests | 60 minutes |
|
||||
| `POST /forgot-password` | 5 requests | 60 minutes |
|
||||
| `GET /client-side-api` | 100 requests | 1 minute |
|
||||
| `POST /share` | 100 requests | 60 minutes |
|
||||
|
||||
If a request exceeds the defined rate limit, the server will respond with:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 429,
|
||||
"error": "Too many requests, Please try after a while!"
|
||||
}
|
||||
```
|
||||
|
||||
## Disabling Rate Limiting
|
||||
|
||||
For self-hosters, rate limiting can be disabled if necessary. However, we **strongly recommend keeping rate limiting enabled in production environments** to prevent abuse.
|
||||
|
||||
To disable rate limiting, set the following environment variable:
|
||||
|
||||
```bash
|
||||
RATE_LIMITING_DISABLED=1
|
||||
```
|
||||
|
||||
After making this change, restart your server to apply the new setting.
|
||||
@@ -144,6 +144,7 @@ export const navigation: NavGroup[] = [
|
||||
{ title: "Integrations", href: "/self-hosting/integrations" },
|
||||
{ title: "License", href: "/self-hosting/license" },
|
||||
{ title: "Cluster Setup", href: "/self-hosting/cluster-setup" },
|
||||
{ title: "Rate Limiting", href: "/self-hosting/rate-limiting" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,6 +24,9 @@ info:
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://{{baseurl}}
|
||||
variables:
|
||||
baseurl:
|
||||
default: "localhost:3000"
|
||||
tags:
|
||||
- name: Client API
|
||||
description: >-
|
||||
@@ -151,7 +154,7 @@ tags:
|
||||
|
||||
|
||||
Methods allowed: Get All, Get ,Create, and Delete Webhooks
|
||||
|
||||
|
||||
paths:
|
||||
/api/v1/client/{environmentId}/displays:
|
||||
post:
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
"prop-types": "15.8.1",
|
||||
"storybook": "8.4.7",
|
||||
"tsup": "8.3.5",
|
||||
"vite": "6.0.3"
|
||||
"vite": "6.0.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface AddressQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -33,7 +31,6 @@ export const AddressQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: AddressQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
@@ -107,7 +104,6 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -125,7 +121,6 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { UpdateQuestionId } from "./UpdateQuestionId";
|
||||
|
||||
@@ -8,7 +7,6 @@ interface AdvancedSettingsProps {
|
||||
questionIdx: number;
|
||||
localSurvey: TSurvey;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
@@ -16,7 +14,6 @@ export const AdvancedSettings = ({
|
||||
questionIdx,
|
||||
localSurvey,
|
||||
updateQuestion,
|
||||
contactAttributeKeys,
|
||||
}: AdvancedSettingsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -25,7 +22,6 @@ export const AdvancedSettings = ({
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
|
||||
<UpdateQuestionId
|
||||
|
||||
@@ -8,7 +8,6 @@ import { OptionsSwitch } from "@/modules/ui/components/options-switch";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface CTAQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,6 @@ export const CTAQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -59,7 +56,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -103,7 +99,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -120,7 +115,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
@@ -155,7 +149,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useEffect, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -20,7 +19,6 @@ interface CalQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -32,7 +30,6 @@ export const CalQuestionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: CalQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -60,7 +57,6 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div>
|
||||
@@ -77,7 +73,6 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ConditionalLogicProps {
|
||||
@@ -34,11 +33,9 @@ interface ConditionalLogicProps {
|
||||
questionIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export function ConditionalLogic({
|
||||
contactAttributeKeys,
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
@@ -46,11 +43,11 @@ export function ConditionalLogic({
|
||||
}: ConditionalLogicProps) {
|
||||
const t = useTranslations();
|
||||
const transformedSurvey = useMemo(() => {
|
||||
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", contactAttributeKeys);
|
||||
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", contactAttributeKeys);
|
||||
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default");
|
||||
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default");
|
||||
|
||||
return modifiedSurvey;
|
||||
}, [localSurvey, contactAttributeKeys]);
|
||||
}, [localSurvey]);
|
||||
|
||||
const addLogic = () => {
|
||||
const operator = getDefaultOperatorForQuestion(question, t);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -17,7 +16,6 @@ interface ConsentQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -29,7 +27,6 @@ export const ConsentQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ConsentQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
@@ -46,7 +43,6 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -80,7 +76,6 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface ContactInfoQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -33,7 +31,6 @@ export const ContactInfoQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ContactInfoQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -98,7 +95,6 @@ export const ContactInfoQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -116,7 +112,6 @@ export const ContactInfoQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { JSX } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -20,7 +19,6 @@ interface IDateQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -47,7 +45,6 @@ export const DateQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -65,7 +62,6 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -82,7 +78,6 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -39,7 +38,6 @@ interface EditEndingCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
plan: TOrganizationBillingPlan;
|
||||
addEndingCard: (index: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -55,7 +53,6 @@ export const EditEndingCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
plan,
|
||||
addEndingCard,
|
||||
isFormbricksCloud,
|
||||
@@ -200,21 +197,13 @@ export const EditEndingCard = ({
|
||||
<p className="text-sm font-semibold">
|
||||
{endingCard.type === "endScreen" &&
|
||||
(endingCard.headline &&
|
||||
recallToHeadline(
|
||||
endingCard.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(
|
||||
endingCard.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
)
|
||||
: t("environments.surveys.edit.ending_card"))}
|
||||
{endingCard.type === "redirectToUrl" &&
|
||||
@@ -274,19 +263,13 @@ export const EditEndingCard = ({
|
||||
isInvalid={isInvalid}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
updateSurvey={updateSurvey}
|
||||
endingCard={endingCard}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
{endingCard.type === "redirectToUrl" && (
|
||||
<RedirectUrlForm
|
||||
localSurvey={localSurvey}
|
||||
endingCard={endingCard}
|
||||
updateSurvey={updateSurvey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
<RedirectUrlForm localSurvey={localSurvey} endingCard={endingCard} updateSurvey={updateSurvey} />
|
||||
)}
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useTranslations } from "next-intl";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -23,7 +22,6 @@ interface EditWelcomeCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -35,7 +33,6 @@ export const EditWelcomeCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: EditWelcomeCardProps) => {
|
||||
const t = useTranslations();
|
||||
@@ -136,7 +133,6 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -173,7 +169,6 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
label={t("environments.surveys.edit.next_button_label")}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -90,7 +90,14 @@ export const EditorCardMenu = ({
|
||||
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
|
||||
card.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) ||
|
||||
(type === TSurveyQuestionTypeEnum.MultipleChoiceMulti &&
|
||||
card.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle)
|
||||
card.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle) ||
|
||||
(type === TSurveyQuestionTypeEnum.MultipleChoiceMulti &&
|
||||
card.type === TSurveyQuestionTypeEnum.Ranking) ||
|
||||
(type === TSurveyQuestionTypeEnum.Ranking &&
|
||||
card.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) ||
|
||||
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
|
||||
card.type === TSurveyQuestionTypeEnum.Ranking) ||
|
||||
(type === TSurveyQuestionTypeEnum.Ranking && card.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle)
|
||||
) {
|
||||
updateCard(cardIdx, {
|
||||
choices: card.choices,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
|
||||
import { RecallWrapper } from "@/modules/surveys/components/QuestionFormInput/components/RecallWrapper";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Switch } from "@/modules/ui/components/switch";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { headlineToRecall, recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -17,7 +19,6 @@ interface EndScreenFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
updateSurvey: (input: Partial<TSurveyEndScreenCard>) => void;
|
||||
endingCard: TSurveyEndScreenCard;
|
||||
locale: TUserLocale;
|
||||
@@ -29,12 +30,12 @@ export const EndScreenForm = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
updateSurvey,
|
||||
endingCard,
|
||||
locale,
|
||||
}: EndScreenFormProps) => {
|
||||
const t = useTranslations();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
||||
endingCard.type === "endScreen" &&
|
||||
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
|
||||
@@ -51,7 +52,6 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -65,7 +65,6 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
@@ -103,7 +102,7 @@ export const EndScreenForm = ({
|
||||
id="buttonLabel"
|
||||
label={t("environments.surveys.edit.button_label")}
|
||||
placeholder={t("environments.surveys.edit.create_your_own_survey")}
|
||||
className="bg-white"
|
||||
className="rounded-md"
|
||||
value={endingCard.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length + endingCardIndex}
|
||||
@@ -111,20 +110,64 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("environments.surveys.edit.button_url")}</Label>
|
||||
<Input
|
||||
id="buttonLink"
|
||||
name="buttonLink"
|
||||
className="bg-white"
|
||||
placeholder="https://formbricks.com"
|
||||
value={endingCard.buttonLink}
|
||||
onChange={(e) => updateSurvey({ buttonLink: e.target.value })}
|
||||
/>
|
||||
<div className="rounded-md bg-white">
|
||||
<RecallWrapper
|
||||
value={endingCard.buttonLink ?? ""}
|
||||
questionId={endingCard.id}
|
||||
onChange={(val, recallItems, fallbacks) => {
|
||||
const updatedValue = {
|
||||
...endingCard,
|
||||
buttonLink:
|
||||
recallItems && fallbacks ? headlineToRecall(val, recallItems, fallbacks) : val,
|
||||
};
|
||||
|
||||
updateSurvey(updatedValue);
|
||||
}}
|
||||
onAddFallback={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
isRecallAllowed
|
||||
localSurvey={localSurvey}
|
||||
usedLanguageCode={"default"}
|
||||
render={({ value, onChange, highlightedJSX, children }) => {
|
||||
return (
|
||||
<div className="group relative">
|
||||
{/* The highlight container is absolutely positioned behind the input */}
|
||||
<div
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
|
||||
dir="auto"
|
||||
key={highlightedJSX.toString()}>
|
||||
{highlightedJSX}
|
||||
</div>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
id="buttonLink"
|
||||
name="buttonLink"
|
||||
className="relative text-black caret-black"
|
||||
placeholder="https://formbricks.com"
|
||||
value={
|
||||
recallToHeadline(
|
||||
{
|
||||
[selectedLanguageCode]: value,
|
||||
},
|
||||
localSurvey,
|
||||
false,
|
||||
"default"
|
||||
)[selectedLanguageCode]
|
||||
}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { toast } from "react-hot-toast";
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -29,7 +28,6 @@ interface FileUploadFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isFormbricksCloud: boolean;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
@@ -43,7 +41,6 @@ export const FileUploadQuestionForm = ({
|
||||
project,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
isFormbricksCloud,
|
||||
locale,
|
||||
}: FileUploadFormProps): JSX.Element => {
|
||||
@@ -141,7 +138,6 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -158,7 +154,6 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { JSX } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { isLabelValidForAllLanguages } from "../lib/validation";
|
||||
@@ -24,7 +23,6 @@ interface MatrixQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -36,7 +34,6 @@ export const MatrixQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: MatrixQuestionFormProps): JSX.Element => {
|
||||
const languageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
@@ -118,7 +115,6 @@ export const MatrixQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -135,7 +131,6 @@ export const MatrixQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -166,7 +161,7 @@ export const MatrixQuestionForm = ({
|
||||
<div
|
||||
className="flex items-center"
|
||||
onKeyDown={(e) => handleKeyDown(e, "row")}
|
||||
key={`row-${index}-${question.rows.length}`}>
|
||||
key={`row-${index}`}>
|
||||
<QuestionFormInput
|
||||
id={`row-${index}`}
|
||||
label={""}
|
||||
@@ -179,7 +174,6 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
|
||||
}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
{question.rows.length > 2 && (
|
||||
@@ -219,7 +213,7 @@ export const MatrixQuestionForm = ({
|
||||
<div
|
||||
className="flex items-center"
|
||||
onKeyDown={(e) => handleKeyDown(e, "column")}
|
||||
key={`column-${index}-${question.columns.length}`}>
|
||||
key={`column-${index}`}>
|
||||
<QuestionFormInput
|
||||
id={`column-${index}`}
|
||||
label={""}
|
||||
@@ -232,7 +226,6 @@ export const MatrixQuestionForm = ({
|
||||
isInvalid={
|
||||
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
|
||||
}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
{question.columns.length > 2 && (
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useTranslations } from "next-intl";
|
||||
import { type JSX, useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TShuffleOption,
|
||||
@@ -34,7 +33,6 @@ interface MultipleChoiceQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -46,7 +44,6 @@ export const MultipleChoiceQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: MultipleChoiceQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -180,7 +177,6 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -198,7 +194,6 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -267,7 +262,6 @@ export const MultipleChoiceQuestionForm = ({
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { JSX } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface NPSQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,6 @@ export const NPSQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: NPSQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -54,7 +51,6 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -72,7 +68,6 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -108,7 +103,6 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -123,7 +117,6 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -143,7 +136,6 @@ export const NPSQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIco
|
||||
import { useTranslations } from "next-intl";
|
||||
import { JSX, useEffect, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
@@ -28,7 +27,6 @@ interface OpenQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -40,7 +38,6 @@ export const OpenQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: OpenQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -93,7 +90,6 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
label={t("environments.surveys.edit.question") + "*"}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -111,7 +107,6 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
label={t("common.description")}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -148,7 +143,6 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
label={t("common.placeholder")}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useTranslations } from "next-intl";
|
||||
import type { JSX } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -23,7 +22,6 @@ interface PictureSelectionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -35,7 +33,6 @@ export const PictureSelectionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: PictureSelectionFormProps): JSX.Element => {
|
||||
const environmentId = localSurvey.environmentId;
|
||||
@@ -84,7 +81,6 @@ export const PictureSelectionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -101,7 +97,6 @@ export const PictureSelectionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ import { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import {
|
||||
TI18nString,
|
||||
@@ -56,7 +55,6 @@ interface QuestionCardProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
addQuestion: (question: any, index?: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
isCxMode: boolean;
|
||||
@@ -78,7 +76,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
contactAttributeKeys,
|
||||
addQuestion,
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
@@ -220,21 +217,13 @@ export const QuestionCard = ({
|
||||
</div> */}
|
||||
<div className="flex grow flex-col justify-center" dir="auto">
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode]
|
||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(
|
||||
question.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode] ?? ""
|
||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
] ?? ""
|
||||
)
|
||||
: getTSurveyQuestionTypeEnumName(question.type, locale)}
|
||||
</p>
|
||||
@@ -278,7 +267,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
|
||||
@@ -291,7 +279,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
|
||||
@@ -304,7 +291,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.NPS ? (
|
||||
@@ -317,7 +303,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.CTA ? (
|
||||
@@ -330,7 +315,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Rating ? (
|
||||
@@ -343,7 +327,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Consent ? (
|
||||
@@ -355,7 +338,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Date ? (
|
||||
@@ -368,7 +350,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? (
|
||||
@@ -381,7 +362,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
|
||||
@@ -395,7 +375,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -409,7 +388,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Matrix ? (
|
||||
@@ -422,7 +400,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Address ? (
|
||||
@@ -435,7 +412,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
|
||||
@@ -448,7 +424,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? (
|
||||
@@ -461,7 +436,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : null}
|
||||
@@ -510,7 +484,6 @@ export const QuestionCard = ({
|
||||
localSurvey.questions.length - 1
|
||||
);
|
||||
}}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -527,7 +500,6 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
onBlur={(e) => {
|
||||
if (!question.backButtonLabel) return;
|
||||
@@ -557,7 +529,6 @@ export const QuestionCard = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -568,7 +539,6 @@ export const QuestionCard = ({
|
||||
questionIdx={questionIdx}
|
||||
localSurvey={localSurvey}
|
||||
updateQuestion={updateQuestion}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { GripVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -37,7 +36,6 @@ interface ChoiceProps {
|
||||
updatedAttributes: Partial<TSurveyMultipleChoiceQuestion> | Partial<TSurveyRankingQuestion>
|
||||
) => void;
|
||||
surveyLanguageCodes: string[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -56,7 +54,6 @@ export const QuestionOptionChoice = ({
|
||||
question,
|
||||
surveyLanguageCodes,
|
||||
updateQuestion,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ChoiceProps) => {
|
||||
const t = useTranslations();
|
||||
@@ -98,7 +95,6 @@ export const QuestionOptionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
@@ -120,7 +116,6 @@ export const QuestionOptionChoice = ({
|
||||
isInvalid && !isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className="border border-dashed"
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -18,7 +17,6 @@ interface QuestionsDraggableProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidQuestions: string[] | null;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
addQuestion: (question: any, index?: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
isCxMode: boolean;
|
||||
@@ -37,7 +35,6 @@ export const QuestionsDroppable = ({
|
||||
setActiveQuestionId,
|
||||
setSelectedLanguageCode,
|
||||
updateQuestion,
|
||||
contactAttributeKeys,
|
||||
addQuestion,
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
@@ -65,7 +62,6 @@ export const QuestionsDroppable = ({
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
lastQuestion={questionIdx === localSurvey.questions.length - 1}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(question.id) : false}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
addQuestion={addQuestion}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isCxMode={isCxMode}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import {
|
||||
@@ -60,7 +59,6 @@ interface QuestionsViewProps {
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
plan: TOrganizationBillingPlan;
|
||||
isCxMode: boolean;
|
||||
locale: TUserLocale;
|
||||
@@ -78,7 +76,6 @@ export const QuestionsView = ({
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
contactAttributeKeys,
|
||||
plan,
|
||||
isCxMode,
|
||||
locale,
|
||||
@@ -435,7 +432,6 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -458,7 +454,6 @@ export const QuestionsView = ({
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
invalidQuestions={invalidQuestions}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
addQuestion={addQuestion}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
isCxMode={isCxMode}
|
||||
@@ -487,7 +482,6 @@ export const QuestionsView = ({
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes(ending.id) : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
plan={plan}
|
||||
addEndingCard={addEndingCard}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { PlusIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { type JSX, useEffect, useRef, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionOptionChoice } from "./QuestionOptionChoice";
|
||||
@@ -26,7 +25,6 @@ interface RankingQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -38,7 +36,6 @@ export const RankingQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: RankingQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
@@ -131,7 +128,6 @@ export const RankingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -149,7 +145,6 @@ export const RankingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -214,7 +209,6 @@ export const RankingQuestionForm = ({
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
surveyLanguageCodes={surveyLanguageCodes}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { Dropdown } from "./RatingTypeDropdown";
|
||||
@@ -20,7 +19,6 @@ interface RatingQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -32,7 +30,6 @@ export const RatingQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const t = useTranslations();
|
||||
@@ -50,7 +47,6 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -68,7 +64,6 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -144,7 +139,6 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +154,6 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -180,7 +173,6 @@ export const RatingQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,15 @@ import { Label } from "@/modules/ui/components/label";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRef } from "react";
|
||||
import { headlineToRecall, recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface RedirectUrlFormProps {
|
||||
localSurvey: TSurvey;
|
||||
endingCard: TSurveyRedirectUrlCard;
|
||||
updateSurvey: (input: Partial<TSurveyRedirectUrlCard>) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const RedirectUrlForm = ({
|
||||
localSurvey,
|
||||
contactAttributeKeys,
|
||||
endingCard,
|
||||
updateSurvey,
|
||||
}: RedirectUrlFormProps) => {
|
||||
export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: RedirectUrlFormProps) => {
|
||||
const selectedLanguageCode = "default";
|
||||
const t = useTranslations();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -42,7 +35,6 @@ export const RedirectUrlForm = ({
|
||||
onAddFallback={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isRecallAllowed
|
||||
localSurvey={localSurvey}
|
||||
usedLanguageCode={"default"}
|
||||
@@ -69,8 +61,7 @@ export const RedirectUrlForm = ({
|
||||
},
|
||||
localSurvey,
|
||||
false,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
"default"
|
||||
)[selectedLanguageCode]
|
||||
}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -184,7 +184,6 @@ export const SurveyEditor = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
plan={plan}
|
||||
isCxMode={isCxMode}
|
||||
locale={locale}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { translate } from "@formbricks/lib/templates";
|
||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TConditionGroup,
|
||||
TLeftOperand,
|
||||
@@ -124,21 +123,11 @@ export const getConditionValueOptions = (
|
||||
return groupedOptions;
|
||||
};
|
||||
|
||||
export const replaceEndingCardHeadlineRecall = (
|
||||
survey: TSurvey,
|
||||
language: string,
|
||||
contactAttributeKeys: TContactAttributeKey[]
|
||||
) => {
|
||||
export const replaceEndingCardHeadlineRecall = (survey: TSurvey, language: string) => {
|
||||
const modifiedSurvey = structuredClone(survey);
|
||||
modifiedSurvey.endings.forEach((ending) => {
|
||||
if (ending.type === "endScreen") {
|
||||
ending.headline = recallToHeadline(
|
||||
ending.headline || {},
|
||||
modifiedSurvey,
|
||||
false,
|
||||
language,
|
||||
contactAttributeKeys
|
||||
);
|
||||
ending.headline = recallToHeadline(ending.headline || {}, modifiedSurvey, false, language);
|
||||
}
|
||||
});
|
||||
return modifiedSurvey;
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationAirtable,
|
||||
@@ -46,7 +45,6 @@ type AddIntegrationModalProps = {
|
||||
airtableArray: TIntegrationItem[];
|
||||
surveys: TSurvey[];
|
||||
airtableIntegration: TIntegrationAirtable;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
} & EditModeProps;
|
||||
|
||||
export type IntegrationModalInputs = {
|
||||
@@ -79,7 +77,6 @@ export const AddIntegrationModal = ({
|
||||
airtableIntegration,
|
||||
isEditMode,
|
||||
defaultData,
|
||||
contactAttributeKeys,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
@@ -323,38 +320,34 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(selectedSurvey, "default", contactAttributeKeys)?.questions.map(
|
||||
(question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(
|
||||
field.value?.filter((value) => value !== question.id)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
name={"questions"}
|
||||
render={({ field }) => (
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={question.id}
|
||||
value={question.id}
|
||||
className="bg-white"
|
||||
checked={field.value?.includes(question.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, question.id])
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import airtableLogo from "@/images/airtableLogo.svg";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -20,7 +19,6 @@ interface AirtableWrapperProps {
|
||||
environment: TEnvironment;
|
||||
isEnabled: boolean;
|
||||
webAppUrl: string;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -32,7 +30,6 @@ export const AirtableWrapper = ({
|
||||
environment,
|
||||
isEnabled,
|
||||
webAppUrl,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: AirtableWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
@@ -55,7 +52,6 @@ export const AirtableWrapper = ({
|
||||
airtableIntegration={airtableIntegration}
|
||||
setIsConnected={setIsConnected}
|
||||
surveys={surveys}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -28,7 +27,6 @@ interface ManageIntegrationProps {
|
||||
setIsConnected: (data: boolean) => void;
|
||||
surveys: TSurvey[];
|
||||
airtableArray: TIntegrationItem[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -40,15 +38,7 @@ const tableHeaders = [
|
||||
];
|
||||
|
||||
export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
const {
|
||||
airtableIntegration,
|
||||
environment,
|
||||
environmentId,
|
||||
setIsConnected,
|
||||
surveys,
|
||||
airtableArray,
|
||||
contactAttributeKeys,
|
||||
} = props;
|
||||
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
|
||||
const t = useTranslations();
|
||||
const [isDeleting, setisDeleting] = useState(false);
|
||||
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
|
||||
@@ -175,7 +165,6 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
|
||||
environmentId={environmentId}
|
||||
surveys={surveys}
|
||||
airtableIntegration={airtableIntegration}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
{...data}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper";
|
||||
import { getContactAttributeKeys } from "@/app/(app)/environments/[environmentId]/integrations/lib/contact-attribute-key";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
@@ -25,12 +24,11 @@ const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const isEnabled = !!AIRTABLE_CLIENT_ID;
|
||||
const [session, surveys, integrations, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [session, surveys, integrations, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
@@ -85,7 +83,6 @@ const Page = async (props) => {
|
||||
surveys={surveys}
|
||||
environment={environment}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
@@ -35,7 +34,6 @@ interface AddIntegrationModalProps {
|
||||
setOpen: (v: boolean) => void;
|
||||
googleSheetIntegration: TIntegrationGoogleSheets;
|
||||
selectedIntegration?: (TIntegrationGoogleSheetsConfigData & { index: number }) | null;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const AddIntegrationModal = ({
|
||||
@@ -45,7 +43,6 @@ export const AddIntegrationModal = ({
|
||||
setOpen,
|
||||
googleSheetIntegration,
|
||||
selectedIntegration,
|
||||
contactAttributeKeys,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const t = useTranslations();
|
||||
const integrationData: TIntegrationGoogleSheetsConfigData = {
|
||||
@@ -250,11 +247,7 @@ export const AddIntegrationModal = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto overflow-x-hidden rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(
|
||||
selectedSurvey,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
)?.questions.map((question) => (
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -5,7 +5,6 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import googleSheetLogo from "@/images/googleSheetsLogo.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationGoogleSheets,
|
||||
@@ -21,7 +20,6 @@ interface GoogleSheetWrapperProps {
|
||||
surveys: TSurvey[];
|
||||
googleSheetIntegration?: TIntegrationGoogleSheets;
|
||||
webAppUrl: string;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -31,7 +29,6 @@ export const GoogleSheetWrapper = ({
|
||||
surveys,
|
||||
googleSheetIntegration,
|
||||
webAppUrl,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: GoogleSheetWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(
|
||||
@@ -61,7 +58,6 @@ export const GoogleSheetWrapper = ({
|
||||
setOpen={setModalOpen}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -22,18 +22,16 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const isEnabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
const [session, surveys, integrations, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [session, surveys, integrations, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrations(params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
@@ -81,7 +79,6 @@ const Page = async (props) => {
|
||||
surveys={surveys}
|
||||
googleSheetIntegration={googleSheetIntegration}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
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 { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
export const getWebhookCountBySource = (environmentId: string, source?: Webhook["source"]): Promise<number> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [source, z.string().optional()]);
|
||||
|
||||
try {
|
||||
const count = await prisma.webhook.count({
|
||||
where: {
|
||||
environmentId,
|
||||
source,
|
||||
},
|
||||
});
|
||||
return count;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhookCountBySource-${environmentId}-${source}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentIdAndSource(environmentId, source)],
|
||||
}
|
||||
)();
|
||||
@@ -19,7 +19,6 @@ import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
|
||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TIntegrationInput } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -37,7 +36,6 @@ interface AddIntegrationModalProps {
|
||||
notionIntegration: TIntegrationNotion;
|
||||
databases: TIntegrationNotionDatabase[];
|
||||
selectedIntegration: (TIntegrationNotionConfigData & { index: number }) | null;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -49,7 +47,6 @@ export const AddIntegrationModal = ({
|
||||
notionIntegration,
|
||||
databases,
|
||||
selectedIntegration,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: AddIntegrationModalProps) => {
|
||||
const t = useTranslations();
|
||||
@@ -116,7 +113,7 @@ export const AddIntegrationModal = ({
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? replaceHeadlineRecall(selectedSurvey, "default", contactAttributeKeys)?.questions.map((q) => ({
|
||||
? replaceHeadlineRecall(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: getLocalizedValue(q.headline, "default"),
|
||||
type: q.type,
|
||||
@@ -148,7 +145,7 @@ export const AddIntegrationModal = ({
|
||||
{
|
||||
id: "createdAt",
|
||||
name: t("common.created_at"),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
type: TSurveyQuestionTypeEnum.Date,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ManageIntegration } from "@/app/(app)/environments/[environmentId]/inte
|
||||
import notionLogo from "@/images/notion.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import {
|
||||
TIntegrationNotion,
|
||||
@@ -23,7 +22,6 @@ interface NotionWrapperProps {
|
||||
webAppUrl: string;
|
||||
surveys: TSurvey[];
|
||||
databasesArray: TIntegrationNotionDatabase[];
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,6 @@ export const NotionWrapper = ({
|
||||
webAppUrl,
|
||||
surveys,
|
||||
databasesArray,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: NotionWrapperProps) => {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
@@ -65,7 +62,6 @@ export const NotionWrapper = ({
|
||||
notionIntegration={notionIntegration}
|
||||
databases={databasesArray}
|
||||
selectedIntegration={selectedIntegration}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<ManageIntegration
|
||||
|
||||
@@ -7,7 +7,6 @@ export const TYPE_MAPPING = {
|
||||
[TSurveyQuestionTypeEnum.OpenText]: [
|
||||
"created_by",
|
||||
"created_time",
|
||||
"date",
|
||||
"email",
|
||||
"last_edited_by",
|
||||
"last_edited_time",
|
||||
|
||||
@@ -24,7 +24,6 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationNotion, TIntegrationNotionDatabase } from "@formbricks/types/integration/notion";
|
||||
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
@@ -35,12 +34,11 @@ const Page = async (props) => {
|
||||
NOTION_AUTH_URL &&
|
||||
NOTION_REDIRECT_URI
|
||||
);
|
||||
const [session, surveys, notionIntegration, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [session, surveys, notionIntegration, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "notion"),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
@@ -89,7 +87,6 @@ const Page = async (props) => {
|
||||
notionIntegration={notionIntegration as TIntegrationNotion}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
databasesArray={databasesArray}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</PageContentWrapper>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getWebhookCountBySource } from "@/app/(app)/environments/[environmentId]/integrations/lib/webhook";
|
||||
import AirtableLogo from "@/images/airtableLogo.svg";
|
||||
import GoogleSheetsLogo from "@/images/googleSheetsLogo.png";
|
||||
import JsLogo from "@/images/jslogo.png";
|
||||
@@ -22,7 +23,6 @@ import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getWebhookCountBySource } from "@formbricks/lib/webhook/service";
|
||||
import { TIntegrationType } from "@formbricks/types/integration";
|
||||
|
||||
const Page = async (props) => {
|
||||
|
||||
@@ -6,14 +6,15 @@ import { Checkbox } from "@/modules/ui/components/checkbox";
|
||||
import { DropdownSelector } from "@/modules/ui/components/dropdown-selector";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { Modal } from "@/modules/ui/components/modal";
|
||||
import { CircleHelpIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
TIntegrationSlack,
|
||||
@@ -30,7 +31,6 @@ interface AddChannelMappingModalProps {
|
||||
slackIntegration: TIntegrationSlack;
|
||||
channels: TIntegrationItem[];
|
||||
selectedIntegration?: (TIntegrationSlackConfigData & { index: number }) | null;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const AddChannelMappingModal = ({
|
||||
@@ -41,7 +41,6 @@ export const AddChannelMappingModal = ({
|
||||
channels,
|
||||
slackIntegration,
|
||||
selectedIntegration,
|
||||
contactAttributeKeys,
|
||||
}: AddChannelMappingModalProps) => {
|
||||
const { handleSubmit } = useForm();
|
||||
const t = useTranslations();
|
||||
@@ -216,9 +215,18 @@ export const AddChannelMappingModal = ({
|
||||
setSelectedItem={setSelectedChannel}
|
||||
disabled={channels.length === 0}
|
||||
/>
|
||||
<Link
|
||||
href="https://formbricks.com/docs/developer-docs/integrations/slack"
|
||||
target="_blank"
|
||||
className="text-xs">
|
||||
<Button variant="ghost" size="sm" className="my-2" type="button">
|
||||
<CircleHelpIcon className="h-4 w-4" />
|
||||
{t("environments.integrations.slack.dont_see_your_channel")}
|
||||
</Button>
|
||||
</Link>
|
||||
{selectedChannel && hasMatchingId && (
|
||||
<p className="text-xs text-amber-700">
|
||||
<strong>{t("environments.integrations.slack.note")}:</strong>{" "}
|
||||
<strong>{t("common.note")}:</strong>{" "}
|
||||
{t("environments.integrations.slack.already_connected_another_survey")}
|
||||
</p>
|
||||
)}
|
||||
@@ -246,11 +254,7 @@ export const AddChannelMappingModal = ({
|
||||
<Label htmlFor="Surveys">{t("common.questions")}</Label>
|
||||
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{replaceHeadlineRecall(
|
||||
selectedSurvey,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
)?.questions?.map((question) => (
|
||||
{replaceHeadlineRecall(selectedSurvey, "default")?.questions?.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
|
||||
@@ -7,7 +7,6 @@ import { authorize } from "@/app/(app)/environments/[environmentId]/integrations
|
||||
import slackLogo from "@/images/slacklogo.png";
|
||||
import { ConnectIntegration } from "@/modules/ui/components/connect-integration";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationSlack, TIntegrationSlackConfigData } from "@formbricks/types/integration/slack";
|
||||
@@ -20,7 +19,6 @@ interface SlackWrapperProps {
|
||||
surveys: TSurvey[];
|
||||
slackIntegration?: TIntegrationSlack;
|
||||
webAppUrl: string;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -30,7 +28,6 @@ export const SlackWrapper = ({
|
||||
surveys,
|
||||
slackIntegration,
|
||||
webAppUrl,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: SlackWrapperProps) => {
|
||||
const [isConnected, setIsConnected] = useState(slackIntegration ? slackIntegration.config?.key : false);
|
||||
@@ -79,7 +76,6 @@ export const SlackWrapper = ({
|
||||
channels={slackChannels}
|
||||
slackIntegration={slackIntegration}
|
||||
selectedIntegration={selectedIntegration}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
<ManageIntegration
|
||||
environment={environment}
|
||||
|
||||
@@ -17,19 +17,17 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
import { getContactAttributeKeys } from "../lib/contact-attribute-key";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const isEnabled = !!(SLACK_CLIENT_ID && SLACK_CLIENT_SECRET);
|
||||
|
||||
const t = await getTranslations();
|
||||
const [session, surveys, slackIntegration, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [session, surveys, slackIntegration, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getSurveys(params.environmentId),
|
||||
getIntegrationByType(params.environmentId, "slack"),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
@@ -74,7 +72,6 @@ const Page = async (props) => {
|
||||
surveys={surveys}
|
||||
slackIntegration={slackIntegration as TIntegrationSlack}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,76 +1,3 @@
|
||||
import { AddWebhookButton } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/AddWebhookButton";
|
||||
import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData";
|
||||
import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable";
|
||||
import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
import { GoBackButton } from "@/modules/ui/components/go-back-button";
|
||||
import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper";
|
||||
import { PageHeader } from "@/modules/ui/components/page-header";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
import { getWebhooks } from "@formbricks/lib/webhook/service";
|
||||
import { WebhooksPage } from "@/modules/integrations/webhooks/page";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const [session, organization, webhooksUnsorted, surveys, environment] = await Promise.all([
|
||||
getServerSession(authOptions),
|
||||
getOrganizationByEnvironmentId(params.environmentId),
|
||||
getWebhooks(params.environmentId),
|
||||
getSurveys(params.environmentId, 200), // HOTFIX: not getting all surveys for now since it's maxing out the prisma accelerate limit
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
}
|
||||
|
||||
if (!organization) {
|
||||
throw new Error(t("common.organization_not_found"));
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||
const { isMember } = getAccessFlags(currentUserMembership?.role);
|
||||
|
||||
const projectPermission = await getProjectPermissionByUserId(session?.user.id, environment?.projectId);
|
||||
|
||||
const { hasReadAccess } = getTeamPermissionFlags(projectPermission);
|
||||
|
||||
const isReadOnly = isMember && hasReadAccess;
|
||||
|
||||
const webhooks = webhooksUnsorted.sort((a, b) => {
|
||||
if (a.createdAt > b.createdAt) return -1;
|
||||
if (a.createdAt < b.createdAt) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const renderAddWebhookButton = () => <AddWebhookButton environment={environment} surveys={surveys} />;
|
||||
const locale = await findMatchingLocale();
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<GoBackButton />
|
||||
<PageHeader pageTitle={t("common.webhooks")} cta={!isReadOnly ? renderAddWebhookButton() : <></>} />
|
||||
<WebhookTable environment={environment} webhooks={webhooks} surveys={surveys} isReadOnly={isReadOnly}>
|
||||
<WebhookTableHeading />
|
||||
{webhooks.map((webhook) => (
|
||||
<WebhookRowData key={webhook.id} webhook={webhook} surveys={surveys} locale={locale} />
|
||||
))}
|
||||
</WebhookTable>
|
||||
</PageContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
export default WebhooksPage;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
@@ -29,7 +28,6 @@ interface ResponsePageProps {
|
||||
surveyId: string;
|
||||
webAppUrl: string;
|
||||
user?: TUser;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
environmentTags: TTag[];
|
||||
responsesPerPage: number;
|
||||
locale: TUserLocale;
|
||||
@@ -42,7 +40,6 @@ export const ResponsePage = ({
|
||||
surveyId,
|
||||
webAppUrl,
|
||||
user,
|
||||
contactAttributeKeys,
|
||||
environmentTags,
|
||||
responsesPerPage,
|
||||
locale,
|
||||
@@ -112,8 +109,8 @@ export const ResponsePage = ({
|
||||
};
|
||||
|
||||
const surveyMemoized = useMemo(() => {
|
||||
return replaceHeadlineRecall(survey, "default", contactAttributeKeys);
|
||||
}, [contactAttributeKeys, survey]);
|
||||
return replaceHeadlineRecall(survey, "default");
|
||||
}, [survey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams?.get("referer")) {
|
||||
|
||||
@@ -141,7 +141,7 @@ const getQuestionColumnsData = (
|
||||
<span className="h-4 w-4">{QUESTIONS_ICON_MAP[question.type]}</span>
|
||||
<span className="truncate">
|
||||
{getLocalizedValue(
|
||||
recallToHeadline(question.headline, survey, false, "default", []),
|
||||
recallToHeadline(question.headline, survey, false, "default"),
|
||||
"default"
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/s
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
@@ -35,10 +34,9 @@ const Page = async (props) => {
|
||||
if (!session) {
|
||||
throw new Error(t("common.session_not_found"));
|
||||
}
|
||||
const [survey, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [survey, environment] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
@@ -112,7 +110,6 @@ const Page = async (props) => {
|
||||
webAppUrl={WEBAPP_URL}
|
||||
environmentTags={tags}
|
||||
user={user}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
responsesPerPage={RESPONSES_PER_PAGE}
|
||||
locale={locale}
|
||||
isReadOnly={isReadOnly}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryAddress } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -13,26 +12,14 @@ interface AddressSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryAddress;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const AddressSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: AddressSummaryProps) => {
|
||||
export const AddressSummary = ({ questionSummary, environmentId, survey, locale }: AddressSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div>
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryCta } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
@@ -10,11 +9,10 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface CTASummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryCta;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const CTASummary = ({ questionSummary, survey, contactAttributeKeys, locale }: CTASummaryProps) => {
|
||||
export const CTASummary = ({ questionSummary, survey, locale }: CTASummaryProps) => {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -23,7 +21,6 @@ export const CTASummary = ({ questionSummary, survey, contactAttributeKeys, loca
|
||||
survey={survey}
|
||||
questionSummary={questionSummary}
|
||||
showResponses={false}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
additionalInfo={
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { convertFloatToNDecimal } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryCal } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -10,21 +9,15 @@ interface CalSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryCal;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const CalSummary = ({ questionSummary, survey, contactAttributeKeys, locale }: CalSummaryProps) => {
|
||||
export const CalSummary = ({ questionSummary, survey, locale }: CalSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
<div>
|
||||
<div className="text flex justify-between px-2 pb-2">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -15,7 +14,6 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface ConsentSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryConsent;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
label: TI18nString,
|
||||
@@ -26,13 +24,7 @@ interface ConsentSummaryProps {
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const ConsentSummary = ({
|
||||
questionSummary,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: ConsentSummaryProps) => {
|
||||
export const ConsentSummary = ({ questionSummary, survey, setFilter, locale }: ConsentSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
const summaryItems = [
|
||||
{
|
||||
@@ -48,12 +40,7 @@ export const ConsentSummary = ({
|
||||
];
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{summaryItems.map((summaryItem) => {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryContactInfo } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -13,7 +12,6 @@ interface ContactInfoSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryContactInfo;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -21,18 +19,12 @@ export const ContactInfoSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ContactInfoSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div>
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useState } from "react";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryDate } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -15,7 +14,6 @@ interface DateQuestionSummary {
|
||||
questionSummary: TSurveyQuestionSummaryDate;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -23,7 +21,6 @@ export const DateQuestionSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: DateQuestionSummary) => {
|
||||
const t = useTranslations();
|
||||
@@ -38,12 +35,7 @@ export const DateQuestionSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="">
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useState } from "react";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryFileUpload } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -16,7 +15,6 @@ interface FileUploadSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryFileUpload;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -24,7 +22,6 @@ export const FileUploadSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: FileUploadSummaryProps) => {
|
||||
const [visibleResponses, setVisibleResponses] = useState(10);
|
||||
@@ -38,12 +35,7 @@ export const FileUploadSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="">
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">{t("common.user")}</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TooltipRenderer } from "@/modules/ui/components/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -14,7 +13,6 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface MatrixQuestionSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryMatrix;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
label: TI18nString,
|
||||
@@ -28,7 +26,6 @@ interface MatrixQuestionSummaryProps {
|
||||
export const MatrixQuestionSummary = ({
|
||||
questionSummary,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: MatrixQuestionSummaryProps) => {
|
||||
@@ -48,16 +45,13 @@ export const MatrixQuestionSummary = ({
|
||||
return "";
|
||||
};
|
||||
|
||||
const columns = questionSummary.data[0] ? Object.keys(questionSummary.data[0].columnPercentages) : [];
|
||||
const columns = questionSummary.data[0]
|
||||
? questionSummary.data[0].columnPercentages.map((c) => c.column)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="overflow-x-auto p-6">
|
||||
{/* Summary Table */}
|
||||
<table className="mx-auto border-collapse cursor-default text-left">
|
||||
@@ -81,7 +75,7 @@ export const MatrixQuestionSummary = ({
|
||||
<p className="max-w-40 overflow-hidden text-ellipsis whitespace-nowrap">{rowLabel}</p>
|
||||
</TooltipRenderer>
|
||||
</td>
|
||||
{Object.entries(columnPercentages).map(([column, percentage]) => (
|
||||
{columnPercentages.map(({ column, percentage }) => (
|
||||
<td
|
||||
key={column}
|
||||
className="text-center text-slate-500 dark:border-slate-700 dark:text-slate-400">
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -24,7 +23,6 @@ interface MultipleChoiceSummaryProps {
|
||||
environmentId: string;
|
||||
surveyType: TSurveyType;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
label: TI18nString,
|
||||
@@ -40,7 +38,6 @@ export const MultipleChoiceSummary = ({
|
||||
environmentId,
|
||||
surveyType,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: MultipleChoiceSummaryProps) => {
|
||||
@@ -73,7 +70,6 @@ export const MultipleChoiceSummary = ({
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
additionalInfo={
|
||||
questionSummary.type === "multipleChoiceMulti" ? (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { HalfCircle, ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -15,7 +14,6 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface NPSSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryNps;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
@@ -26,13 +24,7 @@ interface NPSSummaryProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const NPSSummary = ({
|
||||
questionSummary,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: NPSSummaryProps) => {
|
||||
export const NPSSummary = ({ questionSummary, survey, setFilter, locale }: NPSSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
const applyFilter = (group: string) => {
|
||||
const filters = {
|
||||
@@ -69,12 +61,7 @@ export const NPSSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{["promoters", "passives", "detractors", "dismissed"].map((group) => (
|
||||
<div className="cursor-pointer hover:opacity-80" key={group} onClick={() => applyFilter(group)}>
|
||||
|
||||
@@ -8,7 +8,6 @@ import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { getContactIdentifier } from "@formbricks/lib/utils/contact";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryOpenText } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
@@ -17,7 +16,6 @@ interface OpenTextSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryOpenText;
|
||||
environmentId: string;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isAIEnabled: boolean;
|
||||
documentsPerPage?: number;
|
||||
locale: TUserLocale;
|
||||
@@ -27,7 +25,6 @@ export const OpenTextSummary = ({
|
||||
questionSummary,
|
||||
environmentId,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
isAIEnabled,
|
||||
documentsPerPage,
|
||||
locale,
|
||||
@@ -64,7 +61,6 @@ export const OpenTextSummary = ({
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
additionalInfo={
|
||||
isAIEnabled && questionSummary.insightsEnabled === false ? (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ProgressBar } from "@/modules/ui/components/progress-bar";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -17,7 +16,6 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface PictureChoiceSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryPictureSelection;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
label: TI18nString,
|
||||
@@ -31,7 +29,6 @@ interface PictureChoiceSummaryProps {
|
||||
export const PictureChoiceSummary = ({
|
||||
questionSummary,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: PictureChoiceSummaryProps) => {
|
||||
@@ -42,7 +39,6 @@ export const PictureChoiceSummary = ({
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
additionalInfo={
|
||||
questionSummary.question.allowMulti ? (
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslations } from "next-intl";
|
||||
import type { JSX } from "react";
|
||||
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummary } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -12,7 +11,6 @@ interface HeadProps {
|
||||
showResponses?: boolean;
|
||||
additionalInfo?: JSX.Element;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -21,7 +19,6 @@ export const QuestionSummaryHeader = ({
|
||||
additionalInfo,
|
||||
showResponses = true,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: HeadProps) => {
|
||||
const questionType = getQuestionTypes(locale).find((type) => type.id === questionSummary.question.type);
|
||||
@@ -50,13 +47,7 @@ export const QuestionSummaryHeader = ({
|
||||
<div className={"align-center flex justify-between gap-4"}>
|
||||
<h3 className="pb-1 text-lg font-semibold text-slate-900 md:text-xl">
|
||||
{formatTextWithSlashes(
|
||||
recallToHeadline(
|
||||
questionSummary.question.headline,
|
||||
survey,
|
||||
true,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
)["default"]
|
||||
recallToHeadline(questionSummary.question.headline, survey, true, "default")["default"]
|
||||
)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionSummaryRanking, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
import { convertFloatToNDecimal } from "../lib/utils";
|
||||
@@ -9,17 +8,10 @@ interface RankingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRanking;
|
||||
surveyType: TSurveyType;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const RankingSummary = ({
|
||||
questionSummary,
|
||||
surveyType,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: RankingSummaryProps) => {
|
||||
export const RankingSummary = ({ questionSummary, surveyType, survey, locale }: RankingSummaryProps) => {
|
||||
// sort by count and transform to array
|
||||
const t = useTranslations();
|
||||
const results = Object.values(questionSummary.choices).sort((a, b) => {
|
||||
@@ -28,12 +20,7 @@ export const RankingSummary = ({
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<QuestionSummaryHeader questionSummary={questionSummary} survey={survey} locale={locale} />
|
||||
<div className="space-y-5 px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
|
||||
{results.map((result, resultsIdx) => (
|
||||
<div key={result.value} className="group cursor-pointer">
|
||||
|
||||
@@ -4,7 +4,6 @@ import { RatingResponse } from "@/modules/ui/components/rating-response";
|
||||
import { CircleSlash2, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
@@ -18,7 +17,6 @@ import { QuestionSummaryHeader } from "./QuestionSummaryHeader";
|
||||
interface RatingSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummaryRating;
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
setFilter: (
|
||||
questionId: TSurveyQuestionId,
|
||||
label: TI18nString,
|
||||
@@ -29,13 +27,7 @@ interface RatingSummaryProps {
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
export const RatingSummary = ({
|
||||
questionSummary,
|
||||
survey,
|
||||
contactAttributeKeys,
|
||||
setFilter,
|
||||
locale,
|
||||
}: RatingSummaryProps) => {
|
||||
export const RatingSummary = ({ questionSummary, survey, setFilter, locale }: RatingSummaryProps) => {
|
||||
const t = useTranslations();
|
||||
const getIconBasedOnScale = useMemo(() => {
|
||||
const scale = questionSummary.question.scale;
|
||||
@@ -49,7 +41,6 @@ export const RatingSummary = ({
|
||||
<QuestionSummaryHeader
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
additionalInfo={
|
||||
<div className="flex items-center space-x-2 rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -3,16 +3,14 @@ import { TimerIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getQuestionIcon } from "@formbricks/lib/utils/questions";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface SummaryDropOffsProps {
|
||||
dropOff: TSurveySummary["dropOff"];
|
||||
survey: TSurvey;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const SummaryDropOffs = ({ dropOff, survey, contactAttributeKeys }: SummaryDropOffsProps) => {
|
||||
export const SummaryDropOffs = ({ dropOff, survey }: SummaryDropOffsProps) => {
|
||||
const t = useTranslations();
|
||||
const getIcon = (questionType: TSurveyQuestionType) => {
|
||||
const Icon = getQuestionIcon(questionType);
|
||||
@@ -71,8 +69,7 @@ export const SummaryDropOffs = ({ dropOff, survey, contactAttributeKeys }: Summa
|
||||
},
|
||||
survey,
|
||||
true,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
"default"
|
||||
)["default"]
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -26,7 +26,6 @@ import { SkeletonLoader } from "@/modules/ui/components/skeleton-loader";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TI18nString, TSurveyQuestionId, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
@@ -40,7 +39,6 @@ interface SummaryListProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
totalResponseCount: number;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isAIEnabled: boolean;
|
||||
documentsPerPage?: number;
|
||||
locale: TUserLocale;
|
||||
@@ -52,7 +50,6 @@ export const SummaryList = ({
|
||||
responseCount,
|
||||
survey,
|
||||
totalResponseCount,
|
||||
contactAttributeKeys,
|
||||
isAIEnabled,
|
||||
documentsPerPage,
|
||||
locale,
|
||||
@@ -137,7 +134,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isAIEnabled={isAIEnabled}
|
||||
documentsPerPage={documentsPerPage}
|
||||
locale={locale}
|
||||
@@ -155,7 +151,6 @@ export const SummaryList = ({
|
||||
environmentId={environment.id}
|
||||
surveyType={survey.type}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -167,7 +162,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -179,7 +173,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -190,7 +183,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -202,7 +194,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -214,7 +205,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -227,7 +217,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -239,7 +228,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -251,7 +239,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -262,7 +249,6 @@ export const SummaryList = ({
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
setFilter={setFilter}
|
||||
locale={locale}
|
||||
/>
|
||||
@@ -275,7 +261,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -287,7 +272,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
surveyType={survey.type}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
@@ -309,7 +293,6 @@ export const SummaryList = ({
|
||||
questionSummary={questionSummary}
|
||||
environmentId={environment.id}
|
||||
survey={survey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,6 @@ import { useParams, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useIntervalWhenFocused } from "@formbricks/lib/utils/hooks/useIntervalWhenFocused";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys/types";
|
||||
import { TUser, TUserLocale } from "@formbricks/types/user";
|
||||
@@ -47,7 +46,6 @@ interface SummaryPageProps {
|
||||
webAppUrl: string;
|
||||
user?: TUser;
|
||||
totalResponseCount: number;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isAIEnabled: boolean;
|
||||
documentsPerPage?: number;
|
||||
locale: TUserLocale;
|
||||
@@ -60,7 +58,6 @@ export const SummaryPage = ({
|
||||
surveyId,
|
||||
webAppUrl,
|
||||
totalResponseCount,
|
||||
contactAttributeKeys,
|
||||
isAIEnabled,
|
||||
documentsPerPage,
|
||||
locale,
|
||||
@@ -156,8 +153,8 @@ export const SummaryPage = ({
|
||||
);
|
||||
|
||||
const surveyMemoized = useMemo(() => {
|
||||
return replaceHeadlineRecall(survey, "default", contactAttributeKeys);
|
||||
}, [survey, contactAttributeKeys]);
|
||||
return replaceHeadlineRecall(survey, "default");
|
||||
}, [survey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchParams?.get("referer")) {
|
||||
@@ -173,13 +170,7 @@ export const SummaryPage = ({
|
||||
setShowDropOffs={setShowDropOffs}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
{showDropOffs && (
|
||||
<SummaryDropOffs
|
||||
dropOff={surveySummary.dropOff}
|
||||
survey={surveyMemoized}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
)}
|
||||
{showDropOffs && <SummaryDropOffs dropOff={surveySummary.dropOff} survey={surveyMemoized} />}
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter survey={surveyMemoized} />
|
||||
{!isReadOnly && !isSharingPage && (
|
||||
@@ -193,7 +184,6 @@ export const SummaryPage = ({
|
||||
survey={surveyMemoized}
|
||||
environment={environment}
|
||||
totalResponseCount={totalResponseCount}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isAIEnabled={isAIEnabled}
|
||||
documentsPerPage={documentsPerPage}
|
||||
locale={locale}
|
||||
|
||||
@@ -778,13 +778,15 @@ export const getQuestionSummary = async (
|
||||
totalResponsesForRow += countMap[row][col];
|
||||
});
|
||||
|
||||
const columnPercentages = columns.reduce((acc, col) => {
|
||||
const columnPercentages = columns.map((col) => {
|
||||
const count = countMap[row][col];
|
||||
const percentage =
|
||||
totalResponsesForRow > 0 ? ((count / totalResponsesForRow) * 100).toFixed(2) : "0.00";
|
||||
acc[col] = percentage;
|
||||
return acc;
|
||||
}, {});
|
||||
return {
|
||||
column: col,
|
||||
percentage: Number(percentage),
|
||||
};
|
||||
});
|
||||
|
||||
return { rowLabel: row, columnPercentages, totalResponsesForRow };
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[s
|
||||
import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA";
|
||||
import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getContactAttributeKeys } from "@/modules/ee/contacts/lib/contacts";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getProjectPermissionByUserId } from "@/modules/ee/teams/lib/roles";
|
||||
import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams";
|
||||
@@ -42,10 +41,9 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const [survey, environment, contactAttributeKeys] = await Promise.all([
|
||||
const [survey, environment] = await Promise.all([
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
getContactAttributeKeys(params.environmentId),
|
||||
]);
|
||||
if (!environment) {
|
||||
throw new Error(t("common.environment_not_found"));
|
||||
@@ -118,7 +116,6 @@ const SurveyPage = async (props: { params: Promise<{ environmentId: string; surv
|
||||
webAppUrl={WEBAPP_URL}
|
||||
user={user}
|
||||
totalResponseCount={totalResponseCount}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isAIEnabled={isAIEnabled}
|
||||
documentsPerPage={DOCUMENTS_PER_PAGE}
|
||||
isReadOnly={isReadOnly}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
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 { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
export const getContactAttributes = reactCache((contactId: string) =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([contactId, ZId]);
|
||||
|
||||
try {
|
||||
const prismaAttributes = await prisma.contactAttribute.findMany({
|
||||
where: {
|
||||
contactId,
|
||||
},
|
||||
select: {
|
||||
attributeKey: {
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
value: true,
|
||||
},
|
||||
});
|
||||
|
||||
return prismaAttributes.reduce((acc, attr) => {
|
||||
acc[attr.attributeKey.key] = attr.value;
|
||||
return acc;
|
||||
}, {}) as TContactAttributes;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getContactAttributes-insights-${contactId}`],
|
||||
{
|
||||
tags: [contactAttributeCache.tag.byContactId(contactId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
TSurveyQuestionTypeEnum,
|
||||
ZSurveyQuestions,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { getContactAttributes } from "./contact-attribute";
|
||||
import { TInsightCreateInput, TNearestInsights, ZInsightCreateInput } from "./types";
|
||||
|
||||
export const generateInsightsForSurveyResponsesConcept = async (
|
||||
@@ -99,9 +98,6 @@ export const generateInsightsForSurveyResponsesConcept = async (
|
||||
|
||||
const answersForDocumentCreationPromises = await Promise.all(
|
||||
responsesWithOpenTextAnswers.map(async (response) => {
|
||||
const contactAttributes = response.contactId
|
||||
? await getContactAttributes(response.contactId)
|
||||
: {};
|
||||
const responseEntries = openTextQuestionsWithInsights.map((question) => {
|
||||
const responseText = response.data[question.id] as string;
|
||||
if (!responseText) {
|
||||
@@ -110,7 +106,6 @@ export const generateInsightsForSurveyResponsesConcept = async (
|
||||
|
||||
const headline = parseRecallInfo(
|
||||
question.headline[response.language ?? "default"],
|
||||
contactAttributes,
|
||||
response.data,
|
||||
response.variables
|
||||
);
|
||||
@@ -255,8 +250,6 @@ export const generateInsightsForSurveyResponses = async (
|
||||
const createDocumentPromises: Promise<TCreatedDocument | undefined>[] = [];
|
||||
|
||||
for (const response of responsesWithOpenTextAnswers) {
|
||||
const contactAttributes = response.contactId ? await getContactAttributes(response.contactId) : {};
|
||||
|
||||
for (const question of openTextQuestionsWithInsights) {
|
||||
const responseText = response.data[question.id] as string;
|
||||
if (!responseText) {
|
||||
@@ -265,7 +258,6 @@ export const generateInsightsForSurveyResponses = async (
|
||||
|
||||
const headline = parseRecallInfo(
|
||||
question.headline[response.language ?? "default"],
|
||||
contactAttributes,
|
||||
response.data,
|
||||
response.variables
|
||||
);
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { contactAttributeCache } from "@/lib/cache/contact-attribute";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
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 { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
|
||||
export const getContactAttributes = reactCache((contactId: string) =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([contactId, ZId]);
|
||||
|
||||
try {
|
||||
const prismaAttributes = await prisma.contactAttribute.findMany({
|
||||
where: {
|
||||
contactId,
|
||||
},
|
||||
select: {
|
||||
attributeKey: {
|
||||
select: {
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
value: true,
|
||||
},
|
||||
});
|
||||
|
||||
return prismaAttributes.reduce((acc, attr) => {
|
||||
acc[attr.attributeKey.key] = attr.value;
|
||||
return acc;
|
||||
}, {}) as TContactAttributes;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getContactAttributes-pipeline-${contactId}`],
|
||||
{
|
||||
tags: [contactAttributeCache.tag.byContactId(contactId)],
|
||||
}
|
||||
)()
|
||||
);
|
||||
@@ -1,4 +1,6 @@
|
||||
import { TPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { writeData as airtableWriteData } from "@formbricks/lib/airtable/service";
|
||||
import { NOTION_RICH_TEXT_LIMIT } from "@formbricks/lib/constants";
|
||||
import { writeData } from "@formbricks/lib/googleSheet/service";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { writeData as writeNotionData } from "@formbricks/lib/notion/service";
|
||||
@@ -6,15 +8,13 @@ import { processResponseData } from "@formbricks/lib/responses";
|
||||
import { writeDataToSlack } from "@formbricks/lib/slack/service";
|
||||
import { getFormattedDateTimeString } from "@formbricks/lib/utils/datetime";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributes } from "@formbricks/types/attributes";
|
||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||
import { truncateText } from "@formbricks/lib/utils/strings";
|
||||
import { Result } from "@formbricks/types/error-handlers";
|
||||
import { TIntegration, TIntegrationType } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/google-sheet";
|
||||
import { TIntegrationNotion, TIntegrationNotionConfigData } from "@formbricks/types/integration/notion";
|
||||
import { TIntegrationSlack } from "@formbricks/types/integration/slack";
|
||||
import { TPipelineInput } from "@formbricks/types/pipelines";
|
||||
import { TResponseMeta } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
|
||||
@@ -40,14 +40,13 @@ const processDataForIntegration = async (
|
||||
includeMetadata: boolean,
|
||||
includeHiddenFields: boolean,
|
||||
includeCreatedAt: boolean,
|
||||
questionIds: string[],
|
||||
contactAttributes?: TContactAttributes
|
||||
questionIds: string[]
|
||||
): Promise<string[][]> => {
|
||||
const ids =
|
||||
includeHiddenFields && survey.hiddenFields.fieldIds
|
||||
? [...questionIds, ...survey.hiddenFields.fieldIds]
|
||||
: questionIds;
|
||||
const values = await extractResponses(integrationType, data, ids, survey, contactAttributes);
|
||||
const values = await extractResponses(integrationType, data, ids, survey);
|
||||
if (includeMetadata) {
|
||||
values[0].push(convertMetaObjectToString(data.response.meta));
|
||||
values[1].push("Metadata");
|
||||
@@ -73,8 +72,7 @@ const processDataForIntegration = async (
|
||||
export const handleIntegrations = async (
|
||||
integrations: TIntegration[],
|
||||
data: TPipelineInput,
|
||||
survey: TSurvey,
|
||||
contactAttributes: TContactAttributes
|
||||
survey: TSurvey
|
||||
) => {
|
||||
for (const integration of integrations) {
|
||||
switch (integration.type) {
|
||||
@@ -89,12 +87,7 @@ export const handleIntegrations = async (
|
||||
}
|
||||
break;
|
||||
case "slack":
|
||||
const slackResult = await handleSlackIntegration(
|
||||
integration as TIntegrationSlack,
|
||||
data,
|
||||
survey,
|
||||
contactAttributes
|
||||
);
|
||||
const slackResult = await handleSlackIntegration(integration as TIntegrationSlack, data, survey);
|
||||
if (!slackResult.ok) {
|
||||
console.error("Error in slack integration: ", slackResult.error);
|
||||
}
|
||||
@@ -199,8 +192,7 @@ const handleGoogleSheetsIntegration = async (
|
||||
const handleSlackIntegration = async (
|
||||
integration: TIntegrationSlack,
|
||||
data: TPipelineInput,
|
||||
survey: TSurvey,
|
||||
contactAttributes: TContactAttributes
|
||||
survey: TSurvey
|
||||
): Promise<Result<void, Error>> => {
|
||||
try {
|
||||
if (integration.config.data.length > 0) {
|
||||
@@ -214,8 +206,7 @@ const handleSlackIntegration = async (
|
||||
!!element.includeMetadata,
|
||||
!!element.includeHiddenFields,
|
||||
!!element.includeCreatedAt,
|
||||
element.questionIds,
|
||||
contactAttributes
|
||||
element.questionIds
|
||||
);
|
||||
await writeDataToSlack(integration.config.key, element.channelId, values, survey?.name);
|
||||
}
|
||||
@@ -238,8 +229,7 @@ const extractResponses = async (
|
||||
integrationType: TIntegrationType,
|
||||
pipelineData: TPipelineInput,
|
||||
questionIds: string[],
|
||||
survey: TSurvey,
|
||||
attributes?: TAttributes
|
||||
survey: TSurvey
|
||||
): Promise<string[][]> => {
|
||||
const responses: string[] = [];
|
||||
const questions: string[] = [];
|
||||
@@ -285,7 +275,6 @@ const extractResponses = async (
|
||||
questions.push(
|
||||
parseRecallInfo(
|
||||
getLocalizedValue(question?.headline, "default"),
|
||||
integrationType === "slack" ? attributes : {},
|
||||
integrationType === "slack" ? pipelineData.response.data : emptyResponseObject,
|
||||
integrationType === "slack" ? pipelineData.response.variables : {}
|
||||
) || ""
|
||||
@@ -392,6 +381,16 @@ const getValue = (colType: string, value: string | string[] | Date | number | Re
|
||||
},
|
||||
];
|
||||
case "rich_text":
|
||||
if (typeof value === "string") {
|
||||
return [
|
||||
{
|
||||
text: {
|
||||
content:
|
||||
value.length > NOTION_RICH_TEXT_LIMIT ? truncateText(value, NOTION_RICH_TEXT_LIMIT) : value,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
text: {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { createDocumentAndAssignInsight } from "@/app/api/(internal)/pipeline/lib/documents";
|
||||
import { sendSurveyFollowUps } from "@/app/api/(internal)/pipeline/lib/survey-follow-up";
|
||||
import { ZPipelineInput } from "@/app/api/(internal)/pipeline/types/pipelines";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { sendResponseFinishedEmail } from "@/modules/email";
|
||||
import { getSurveyFollowUpsPermission } from "@/modules/survey-follow-ups/lib/utils";
|
||||
import { PipelineTriggers, Webhook } from "@prisma/client";
|
||||
import { headers } from "next/headers";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
@@ -16,10 +19,6 @@ import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
import { getPromptText } from "@formbricks/lib/utils/ai";
|
||||
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { webhookCache } from "@formbricks/lib/webhook/cache";
|
||||
import { TPipelineTrigger, ZPipelineInput } from "@formbricks/types/pipelines";
|
||||
import { TWebhook } from "@formbricks/types/webhooks";
|
||||
import { getContactAttributes } from "./lib/contact-attribute";
|
||||
import { handleIntegrations } from "./lib/handleIntegrations";
|
||||
|
||||
export const POST = async (request: Request) => {
|
||||
@@ -44,7 +43,6 @@ export const POST = async (request: Request) => {
|
||||
}
|
||||
|
||||
const { environmentId, surveyId, event, response } = inputValidation.data;
|
||||
const contactAttributes = response.contact?.id ? await getContactAttributes(response.contact?.id) : {};
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
if (!organization) {
|
||||
@@ -53,7 +51,7 @@ export const POST = async (request: Request) => {
|
||||
|
||||
// Fetch webhooks
|
||||
const getWebhooksForPipeline = cache(
|
||||
async (environmentId: string, event: TPipelineTrigger, surveyId: string) => {
|
||||
async (environmentId: string, event: PipelineTriggers, surveyId: string) => {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
@@ -68,7 +66,7 @@ export const POST = async (request: Request) => {
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
);
|
||||
const webhooks: TWebhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
|
||||
const webhooks: Webhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
|
||||
// Prepare webhook and email promises
|
||||
|
||||
// Fetch with timeout of 5 seconds to prevent hanging
|
||||
@@ -107,7 +105,7 @@ export const POST = async (request: Request) => {
|
||||
}
|
||||
|
||||
if (integrations.length > 0) {
|
||||
await handleIntegrations(integrations, inputValidation.data, survey, contactAttributes);
|
||||
await handleIntegrations(integrations, inputValidation.data, survey);
|
||||
}
|
||||
|
||||
// Fetch users with notifications in a single query
|
||||
@@ -219,7 +217,6 @@ export const POST = async (request: Request) => {
|
||||
|
||||
const headline = parseRecallInfo(
|
||||
question.headline[response.language ?? "default"],
|
||||
contactAttributes,
|
||||
response.data,
|
||||
response.variables
|
||||
);
|
||||
|
||||
12
apps/web/app/api/(internal)/pipeline/types/pipelines.ts
Normal file
12
apps/web/app/api/(internal)/pipeline/types/pipelines.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
import { ZResponse } from "@formbricks/types/responses";
|
||||
|
||||
export const ZPipelineInput = z.object({
|
||||
event: ZWebhook.shape.triggers.element,
|
||||
response: ZResponse,
|
||||
environmentId: z.string(),
|
||||
surveyId: z.string(),
|
||||
});
|
||||
|
||||
export type TPipelineInput = z.infer<typeof ZPipelineInput>;
|
||||
@@ -25,11 +25,9 @@ export const getNotificationResponse = (
|
||||
const surveys: TWeeklySummaryNotificationDataSurvey[] = [];
|
||||
// iterate through the surveys and calculate the overall insights
|
||||
for (const survey of environment.surveys) {
|
||||
const parsedSurvey = replaceHeadlineRecall(
|
||||
survey as unknown as TSurvey,
|
||||
"default",
|
||||
environment.attributeKeys
|
||||
) as TSurvey & { responses: TWeeklyEmailResponseData[] };
|
||||
const parsedSurvey = replaceHeadlineRecall(survey as unknown as TSurvey, "default") as TSurvey & {
|
||||
responses: TWeeklyEmailResponseData[];
|
||||
};
|
||||
const surveyData: TWeeklySummaryNotificationDataSurvey = {
|
||||
id: parsedSurvey.id,
|
||||
name: parsedSurvey.name,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service";
|
||||
import { TAuthenticationApiKey } from "@formbricks/types/auth";
|
||||
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironmentIdFromApiKey } from "./lib/api-key";
|
||||
|
||||
export const authenticateRequest = async (request: Request): Promise<TAuthenticationApiKey | null> => {
|
||||
const apiKey = request.headers.get("x-api-key");
|
||||
if (apiKey) {
|
||||
const apiKeyData = await getApiKeyFromKey(apiKey);
|
||||
if (apiKeyData) {
|
||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||
if (environmentId) {
|
||||
const authentication: TAuthenticationApiKey = {
|
||||
type: "apiKey",
|
||||
environmentId: apiKeyData.environmentId,
|
||||
environmentId,
|
||||
};
|
||||
return authentication;
|
||||
}
|
||||
|
||||
50
apps/web/app/api/v1/lib/api-key.ts
Normal file
50
apps/web/app/api/v1/lib/api-key.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { apiKeyCache } from "@/lib/cache/api-key";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { getHash } from "@formbricks/lib/crypto";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZString } from "@formbricks/types/common";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
|
||||
export const getEnvironmentIdFromApiKey = reactCache(async (apiKey: string): Promise<string | null> => {
|
||||
const hashedKey = getHash(apiKey);
|
||||
return cache(
|
||||
async () => {
|
||||
validateInputs([apiKey, ZString]);
|
||||
|
||||
if (!apiKey) {
|
||||
throw new InvalidInputError("API key cannot be null or undefined.");
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKeyData = await prisma.apiKey.findUnique({
|
||||
where: {
|
||||
hashedKey,
|
||||
},
|
||||
select: {
|
||||
environmentId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!apiKeyData) {
|
||||
throw new ResourceNotFoundError("apiKey", apiKey);
|
||||
}
|
||||
|
||||
return apiKeyData.environmentId;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getEnvironmentIdFromApiKey-${apiKey}`],
|
||||
{
|
||||
tags: [apiKeyCache.tag.byHashedKey(hashedKey)],
|
||||
}
|
||||
)();
|
||||
});
|
||||
59
apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts
Normal file
59
apps/web/app/api/v1/webhooks/[webhookId]/lib/webhook.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
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 { DatabaseError } from "@formbricks/types/errors";
|
||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const deleteWebhook = async (id: string): Promise<Webhook> => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
try {
|
||||
let deletedWebhook = await prisma.webhook.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: deletedWebhook.id,
|
||||
environmentId: deletedWebhook.environmentId,
|
||||
source: deletedWebhook.source,
|
||||
});
|
||||
|
||||
return deletedWebhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
|
||||
throw new ResourceNotFoundError("Webhook", id);
|
||||
}
|
||||
throw new DatabaseError(`Database error when deleting webhook with ID ${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhook = async (id: string): Promise<Webhook | null> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([id, ZId]);
|
||||
|
||||
try {
|
||||
const webhook = await prisma.webhook.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return webhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhook-${id}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byId(id)],
|
||||
}
|
||||
)();
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
|
||||
import { deleteWebhook, getWebhook } from "@/app/api/v1/webhooks/[webhookId]/lib/webhook";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { headers } from "next/headers";
|
||||
import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service";
|
||||
import { deleteWebhook, getWebhook } from "@formbricks/lib/webhook/service";
|
||||
|
||||
export const GET = async (_: Request, props: { params: Promise<{ webhookId: string }> }) => {
|
||||
const params = await props.params;
|
||||
@@ -10,8 +10,8 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri
|
||||
if (!apiKey) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
const apiKeyData = await getApiKeyFromKey(apiKey);
|
||||
if (!apiKeyData) {
|
||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||
if (!environmentId) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const GET = async (_: Request, props: { params: Promise<{ webhookId: stri
|
||||
if (!webhook) {
|
||||
return responses.notFoundResponse("Webhook", params.webhookId);
|
||||
}
|
||||
if (webhook.environmentId !== apiKeyData.environmentId) {
|
||||
if (webhook.environmentId !== environmentId) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
return responses.successResponse(webhook);
|
||||
@@ -33,8 +33,8 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s
|
||||
if (!apiKey) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
const apiKeyData = await getApiKeyFromKey(apiKey);
|
||||
if (!apiKeyData) {
|
||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||
if (!environmentId) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const DELETE = async (_: Request, props: { params: Promise<{ webhookId: s
|
||||
if (!webhook) {
|
||||
return responses.notFoundResponse("Webhook", params.webhookId);
|
||||
}
|
||||
if (webhook.environmentId !== apiKeyData.environmentId) {
|
||||
if (webhook.environmentId !== environmentId) {
|
||||
return responses.unauthorizedResponse();
|
||||
}
|
||||
|
||||
|
||||
73
apps/web/app/api/v1/webhooks/lib/webhook.ts
Normal file
73
apps/web/app/api/v1/webhooks/lib/webhook.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { TWebhookInput, ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { webhookCache } from "@/lib/cache/webhook";
|
||||
import { Prisma, Webhook } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
|
||||
import { validateInputs } from "@formbricks/lib/utils/validate";
|
||||
import { ZId, ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
|
||||
export const createWebhook = async (environmentId: string, webhookInput: TWebhookInput): Promise<Webhook> => {
|
||||
validateInputs([environmentId, ZId], [webhookInput, ZWebhookInput]);
|
||||
|
||||
try {
|
||||
const createdWebhook = await prisma.webhook.create({
|
||||
data: {
|
||||
...webhookInput,
|
||||
surveyIds: webhookInput.surveyIds || [],
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
webhookCache.revalidate({
|
||||
id: createdWebhook.id,
|
||||
environmentId: createdWebhook.environmentId,
|
||||
source: createdWebhook.source,
|
||||
});
|
||||
|
||||
return createdWebhook;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
if (!(error instanceof InvalidInputError)) {
|
||||
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getWebhooks = (environmentId: string, page?: number): Promise<Webhook[]> =>
|
||||
cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId], [page, ZOptionalNumber]);
|
||||
|
||||
try {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId: environmentId,
|
||||
},
|
||||
take: page ? ITEMS_PER_PAGE : undefined,
|
||||
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
|
||||
});
|
||||
return webhooks;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getWebhooks-${environmentId}-${page}`],
|
||||
{
|
||||
tags: [webhookCache.tag.byEnvironmentId(environmentId)],
|
||||
}
|
||||
)();
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getEnvironmentIdFromApiKey } from "@/app/api/v1/lib/api-key";
|
||||
import { createWebhook, getWebhooks } from "@/app/api/v1/webhooks/lib/webhook";
|
||||
import { ZWebhookInput } from "@/app/api/v1/webhooks/types/webhooks";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { headers } from "next/headers";
|
||||
import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service";
|
||||
import { createWebhook, getWebhooks } from "@formbricks/lib/webhook/service";
|
||||
import { DatabaseError, InvalidInputError } from "@formbricks/types/errors";
|
||||
import { ZWebhookInput } from "@formbricks/types/webhooks";
|
||||
|
||||
export const GET = async () => {
|
||||
const headersList = await headers();
|
||||
@@ -12,14 +12,14 @@ export const GET = async () => {
|
||||
if (!apiKey) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
const apiKeyData = await getApiKeyFromKey(apiKey);
|
||||
if (!apiKeyData) {
|
||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||
if (!environmentId) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
// get webhooks from database
|
||||
try {
|
||||
const webhooks = await getWebhooks(apiKeyData.environmentId);
|
||||
const webhooks = await getWebhooks(environmentId);
|
||||
return Response.json({ data: webhooks });
|
||||
} catch (error) {
|
||||
if (error instanceof DatabaseError) {
|
||||
@@ -35,8 +35,8 @@ export const POST = async (request: Request) => {
|
||||
if (!apiKey) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
const apiKeyData = await getApiKeyFromKey(apiKey);
|
||||
if (!apiKeyData) {
|
||||
const environmentId = await getEnvironmentIdFromApiKey(apiKey);
|
||||
if (!environmentId) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
const webhookInput = await request.json();
|
||||
@@ -52,7 +52,7 @@ export const POST = async (request: Request) => {
|
||||
|
||||
// add webhook to database
|
||||
try {
|
||||
const webhook = await createWebhook(apiKeyData.environmentId, inputValidation.data);
|
||||
const webhook = await createWebhook(environmentId, inputValidation.data);
|
||||
return responses.successResponse(webhook);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
|
||||
16
apps/web/app/api/v1/webhooks/types/webhooks.ts
Normal file
16
apps/web/app/api/v1/webhooks/types/webhooks.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { ZWebhook } from "@formbricks/database/zod/webhooks";
|
||||
|
||||
export const ZWebhookInput = ZWebhook.partial({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
}).pick({
|
||||
name: true,
|
||||
source: true,
|
||||
surveyIds: true,
|
||||
triggers: true,
|
||||
url: true,
|
||||
});
|
||||
|
||||
export type TWebhookInput = z.infer<typeof ZWebhookInput>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TPipelineInput } from "@/app/lib/types/pipelines";
|
||||
import { CRON_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { TPipelineInput } from "@formbricks/types/pipelines";
|
||||
|
||||
export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => {
|
||||
return fetch(`${WEBAPP_URL}/api/pipeline`, {
|
||||
|
||||
9
apps/web/app/lib/types/pipelines.ts
Normal file
9
apps/web/app/lib/types/pipelines.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { PipelineTriggers } from "@prisma/client";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
|
||||
export interface TPipelineInput {
|
||||
event: PipelineTriggers;
|
||||
response: TResponse;
|
||||
environmentId: string;
|
||||
surveyId: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user