Compare commits

..

29 Commits

Author SHA1 Message Date
Matti Nannt
f1d697a83f chore: prepare 3.1.3 release (#4682) 2025-01-28 14:09:45 +01:00
Dhruwang Jariwala
69a7a57f41 chore: removed contact attributes from recall (#4651)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-01-28 11:23:23 +00:00
Anshuman Pandey
24de1559a5 fix: fixes formbricks.logout (#4672) 2025-01-28 11:22:49 +00:00
Piyush Gupta
ec29abfcaf fix: new contribution message action (#4661) 2025-01-28 05:36:50 +00:00
Dhruwang Jariwala
eac97db665 test: unit test for auth module (#4612)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2025-01-27 13:13:40 +00:00
Dhruwang Jariwala
d8386328e7 fix: added link to notion doc (#4670) 2025-01-27 12:44:09 +00:00
Paribesh Nepal
d28f321aa2 fix: long answer (#4654)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-01-27 07:33:29 +00:00
Piyush Gupta
e691c076a1 chore: adds webhook types (#4606)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2025-01-24 12:23:07 +00:00
Matti Nannt
ad842e0e80 chore: fix openAPI specs throw error (#4662) 2025-01-24 10:55:34 +01:00
Dhruwang Jariwala
dcf4109c5b docs: rate limiting docs (#4652)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-01-24 06:25:42 +00:00
Chromico Rek
05287c135e fix: prevent duplicate value appending in docker-compose.yml (#4618) 2025-01-24 05:52:32 +00:00
Piyush Gupta
6ff8ec21cf fix: e2e test (#4660) 2025-01-24 04:57:23 +00:00
Matti Nannt
7b6e22aa04 chore: increase version number to 3.1.2 (#4658) 2025-01-23 19:00:04 +01:00
Piyush Gupta
ee56914285 fix: onboarding organization creation bug (#4641) 2025-01-23 16:36:45 +00:00
Paribesh Nepal
a2e9cd3c43 fix: Increase response limit (#4650)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2025-01-23 12:23:48 +00:00
Dhruwang Jariwala
359f29a264 chore: added playwright cloud to gh workflow (#4556)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
2025-01-23 09:22:56 +00:00
Dhruwang Jariwala
576b15fec0 feat: cmd + enter to jump to next question (#4626) 2025-01-23 06:55:01 +00:00
Dhruwang Jariwala
42434290da fix: created at mapping in notion integration (#4640) 2025-01-23 06:01:41 +00:00
dependabot[bot]
62c6189dfd chore(deps-dev): bump the npm_and_yarn group across 9 directories with 1 update (#4639)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 21:09:12 +00:00
Matti Nannt
21c9ebbca3 chore: increase version number to 3.1.1 (#4647) 2025-01-22 17:48:34 +01:00
Matti Nannt
658d4687f9 fix: draft release workflow (#4643)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-01-22 15:38:02 +01:00
Dhruwang Jariwala
3775453db8 chore: permissions to workflows (#4599)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-01-22 12:06:52 +00:00
Dhruwang Jariwala
edcaf8e639 fix: recall to ending card button url (#4627) 2025-01-22 12:05:50 +00:00
Matti Nannt
3aa658a64e chore: add release workflow (#4638) 2025-01-22 12:08:33 +01:00
Paribesh Nepal
58fc66ad1c fix: Change type Single / Multi select to Ranking should not remove o… (#4622) 2025-01-21 05:20:37 +00:00
Dhruwang Jariwala
f68f87645f fix: column ordering (#4621) 2025-01-21 05:06:43 +00:00
Paribesh Nepal
25f99da172 fix: colour picker (#4619) 2025-01-20 04:17:11 +00:00
Anshuman Pandey
5da6faa972 fix: overlapping UI and translations (#4615) 2025-01-17 13:58:15 +00:00
Piyush Gupta
02b25138ef chore: API key types (#4610)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-01-17 10:36:29 +00:00
196 changed files with 2864 additions and 1774 deletions

View File

@@ -1,6 +1,10 @@
name: Build Docs
on:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Build Docs

View File

@@ -1,6 +1,10 @@
name: Build Web
on:
workflow_call:
permissions:
contents: read
jobs:
build:
name: Build Formbricks-web

View File

@@ -7,6 +7,10 @@ on:
schedule:
# Runs "At 00:00." (see https://crontab.guru)
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
cron-weeklySummary:
env:

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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 }}

View File

@@ -6,6 +6,11 @@ on:
# branches:
# - main
permissions:
contents: write
pull-requests: write
packages: write
concurrency: ${{ github.workflow }}-${{ github.ref }}
env:

View File

@@ -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 }}

View File

@@ -6,6 +6,8 @@ jobs:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@v3

View File

@@ -3,7 +3,7 @@ name: "Welcome new contributors"
on:
issues:
types: opened
pull_request:
pull_request_target:
types: opened
permissions:

View File

@@ -14,7 +14,6 @@ module.exports = {
typescript: {
project: "tsconfig.json",
},
caseSensitive: false,
},
},
};

View 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.

View File

@@ -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" },
],
},
{

View File

@@ -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:

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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,

View File

@@ -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>
)}

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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}
/>
))}

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}
/>
)}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
/>
))}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -184,7 +184,6 @@ export const SurveyEditor = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
contactAttributeKeys={contactAttributeKeys}
plan={plan}
isCxMode={isCxMode}
locale={locale}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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}
/>
) : (

View File

@@ -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}
/>
)}

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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)],
}
)();

View File

@@ -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,
},
];

View File

@@ -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

View File

@@ -7,7 +7,6 @@ export const TYPE_MAPPING = {
[TSurveyQuestionTypeEnum.OpenText]: [
"created_by",
"created_time",
"date",
"email",
"last_edited_by",
"last_edited_time",

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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")) {

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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={
<>

View File

@@ -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">

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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" ? (

View File

@@ -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)}>

View File

@@ -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 ? (

View File

@@ -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 ? (

View File

@@ -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>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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}
/>
);

View File

@@ -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}

View File

@@ -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 };
});

View File

@@ -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}

View File

@@ -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)],
}
)()
);

View File

@@ -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
);

View File

@@ -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)],
}
)()
);

View File

@@ -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: {

View File

@@ -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
);

View 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>;

View File

@@ -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,

View File

@@ -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;
}

View 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)],
}
)();
});

View 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)],
}
)();

View File

@@ -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();
}

View 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)],
}
)();

View File

@@ -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) {

View 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>;

View File

@@ -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`, {

View 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