Compare commits
41 Commits
feat/3214-
...
add-infras
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b55e0fc27e | ||
|
|
84993cf42b | ||
|
|
33543f59f8 | ||
|
|
47826a45aa | ||
|
|
6f043ec16e | ||
|
|
c2703788ae | ||
|
|
ef7df0fc77 | ||
|
|
cdb8199199 | ||
|
|
b0ded570ff | ||
|
|
5c0b29eed4 | ||
|
|
8e16d8daf6 | ||
|
|
deea760a17 | ||
|
|
f56f08e3c1 | ||
|
|
5daeab6554 | ||
|
|
0d11c08be7 | ||
|
|
e7fbdb4d00 | ||
|
|
9538c2e6e3 | ||
|
|
3b5f9adcd1 | ||
|
|
90480317af | ||
|
|
352e905529 | ||
|
|
588768c849 | ||
|
|
2bab855b05 | ||
|
|
7fdc2eec34 | ||
|
|
464455be2b | ||
|
|
0a11c6aed5 | ||
|
|
8566b4c3da | ||
|
|
9465bd15f2 | ||
|
|
37791fc78f | ||
|
|
98c16eb4b8 | ||
|
|
7803de6ee7 | ||
|
|
d9e2267a3a | ||
|
|
9872d17abe | ||
|
|
4dad1226ce | ||
|
|
5a2f8c586a | ||
|
|
eba60d4777 | ||
|
|
e633fc76be | ||
|
|
700068dc9f | ||
|
|
979ad9e78a | ||
|
|
0f2f3b1af8 | ||
|
|
35e4b1f965 | ||
|
|
047c9abe9c |
@@ -1,4 +1,4 @@
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
@@ -22,11 +22,11 @@ services:
|
||||
# Uncomment the next line to use a non-root user for all processes.
|
||||
# user: node
|
||||
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
|
||||
# (Adding the "ports" property to this file will not forward from a Codespace.)
|
||||
|
||||
db:
|
||||
image: postgres:latest
|
||||
image: pgvector/pgvector:pg17
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
@@ -41,12 +41,11 @@ services:
|
||||
image: mailhog/mailhog
|
||||
network_mode: service:app
|
||||
logging:
|
||||
driver: "none" # disable saving logs
|
||||
driver:
|
||||
"none" # disable saving logs
|
||||
# ports:
|
||||
# - 8025:8025 # web ui
|
||||
# 1025:1025 # smtp server
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
postgres-data: null
|
||||
|
||||
@@ -180,3 +180,9 @@ UNSPLASH_ACCESS_KEY=
|
||||
|
||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||
# CUSTOM_CACHE_DISABLED=1
|
||||
|
||||
# Azure AI settings
|
||||
# AI_AZURE_RESSOURCE_NAME=
|
||||
# AI_AZURE_API_KEY=
|
||||
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||
3
.github/workflows/e2e.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
image: pgvector/pgvector:pg17
|
||||
env:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
@@ -50,6 +50,7 @@ jobs:
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
|
||||
echo "" >> .env
|
||||
echo "E2E_TESTING=1" >> .env
|
||||
shell: bash
|
||||
|
||||
|
||||
2
LICENSE
@@ -2,7 +2,7 @@ Copyright (c) 2024 Formbricks GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "packages/ee/" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE".
|
||||
- All content that resides under the "packages/ee/", "apps/web/modules/ee" & "apps/web/app/(ee)" directories of this repository, if these directories exist, is licensed under the license defined in "packages/ee/LICENSE".
|
||||
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
@@ -67,7 +67,19 @@ git clone https://github.com/formbricks/formbricks && cd formbricks
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
|
||||
2. Setup Node.JS with nvm:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setup Node version with nvm">
|
||||
|
||||
```bash
|
||||
nvm install && nvm use
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Install dependencies via pnpm">
|
||||
@@ -79,7 +91,7 @@ pnpm install
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
|
||||
4. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Define environment variables">
|
||||
@@ -91,7 +103,7 @@ cp .env.example .env
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length:
|
||||
5. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length:
|
||||
|
||||
- For Linux
|
||||
|
||||
@@ -121,7 +133,7 @@ sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
|
||||
6. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Start Formbricks Dev Setup">
|
||||
|
||||
@@ -48,7 +48,7 @@ Initialize the Formbricks JS Client for surveys. When used in a web app, pass a
|
||||
<CodeGroup title="Initialize Formbricks">
|
||||
|
||||
```javascript
|
||||
import formbricks from "@formbricks/js/app";
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
formbricks.init({
|
||||
environmentId: "<your-environment-id>", // required
|
||||
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
45
apps/docs/app/global/add-image-or-video-question/page.mdx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import AddImageOrVideoToQuestionImage from "./images/add-image-or-video-to-question-image.webp";
|
||||
import AddImageOrVideoToQuestionVideo from "./images/add-image-or-video-to-question-video.webp";
|
||||
import AddImageOrVideoToQuestion from "./images/add-image-or-video-to-question.webp";
|
||||
|
||||
#### Add Image or Video to a Question
|
||||
|
||||
Enhance your questions by adding images or videos. This makes instructions clearer and the survey more engaging.
|
||||
|
||||
## How to Add Images
|
||||
|
||||
Click the icon on the right side of the question to add an image or video:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestion}
|
||||
alt="Overview of adding image or video to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Upload an image by clicking the upload icon or dragging the file:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestionImage}
|
||||
alt="Overview of adding image to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## How to Add Videos
|
||||
Toggle to add a video via link:
|
||||
|
||||
<MdxImage
|
||||
src={AddImageOrVideoToQuestionVideo}
|
||||
alt="Overview of adding video to question"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Supported Video Platforms
|
||||
|
||||
We support YouTube, Vimeo, and Loom URLs.
|
||||
|
||||
<Note>**YouTube Privacy Mode**: This option reduces tracking by converting YouTube URLs to no-cookie URLs. It only works with YouTube.</Note>
|
||||
BIN
apps/docs/app/global/question-type/address/images/address.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
36
apps/docs/app/global/question-type/address/page.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Address from "./images/address.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Address
|
||||
|
||||
The Address question type allows respondents to input their address details, including multiple fields such as address lines, city, state, and country. You can configure the question by adding a title, an optional description, and toggling specific fields to be required.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/m8w91e8wi52pdao8un1f4twu" />
|
||||
|
||||
## Elements
|
||||
|
||||
<MdxImage
|
||||
src={Address}
|
||||
alt="Overview of Address question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Question
|
||||
Provide a question to describe the address information you are requesting.
|
||||
|
||||
### Description
|
||||
Optionally, add a description to guide the user.
|
||||
|
||||
### Toggle Fields
|
||||
You can choose to show and require any or all of the following fields in the form:
|
||||
- Address Line 1
|
||||
- Address Line 2
|
||||
- City
|
||||
- State
|
||||
- Zip Code
|
||||
- Country
|
||||
|
||||
BIN
apps/docs/app/global/question-type/consent/images/consent.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
28
apps/docs/app/global/question-type/consent/page.mdx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Consent from "./images/consent.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Consent
|
||||
|
||||
The Consent card is used to obtain user agreement regarding a product, service, or policy. It features a bold statement or question as the title, followed by a brief description. At the end of the card, users can confirm their consent by checking a checkbox to indicate their agreement.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/orxp15pca6x2nfr3v8pttpwm" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Consent}
|
||||
alt="Overview of Consent question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
A bold statement or question asking for user consent, displayed prominently at the top of the card.
|
||||
|
||||
### Description
|
||||
A short explanation or additional context for the consent request, displayed below the title. The text can be formatted, and hyperlinks are allowed within the description.
|
||||
|
||||
### Checkbox
|
||||
At the bottom of the card, users can confirm their agreement by checking the box, indicating their consent to the question or statement above. The label for the checkbox is also editable.
|
||||
BIN
apps/docs/app/global/question-type/contact/images/contact.webp
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
36
apps/docs/app/global/question-type/contact/page.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Contact from "./images/contact.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Contact Info
|
||||
|
||||
The Contact Info question type allows respondents to provide their basic contact information such as name, email, and phone number. You can customize the form with a title, an optional description, and control which fields to display and require.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/z2zjoonfeythx5n6z5qijbsg" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Contact}
|
||||
alt="Overview of Contact Info question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
|
||||
Specify a title to describe the information you're collecting.
|
||||
|
||||
### Description
|
||||
|
||||
Optionally, add a description to give additional context.
|
||||
|
||||
### Toggle Fields
|
||||
|
||||
You can choose to show and require any or all of the following fields:
|
||||
- First Name
|
||||
- Last Name
|
||||
- Email
|
||||
- Phone Number
|
||||
- Company
|
||||
BIN
apps/docs/app/global/question-type/date/images/date.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
32
apps/docs/app/global/question-type/date/page.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Date from "./images/date.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Date
|
||||
|
||||
The Date question type allows respondents to provide a date, such as when they are available or when an event is scheduled. It features a title to guide the respondent on what date to enter, and an optional description to provide further details or context.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/rk844spc8ffls25vzkxzzhse" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Date}
|
||||
alt="Overview of Date question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what date you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Date Format
|
||||
Choose from multiple date formats for the input:
|
||||
- MM-DD-YYYY
|
||||
- DD-MM-YYYY
|
||||
- YYYY-MM-DD
|
||||
|
||||
|
After Width: | Height: | Size: 9.2 KiB |
34
apps/docs/app/global/question-type/file-upload/page.mdx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import FileUpload from "./images/file-upload.webp";
|
||||
|
||||
#### Questions Type
|
||||
|
||||
# File Upload
|
||||
|
||||
The File Upload question type allows respondents to upload files related to your survey, such as production documents or requirement specifications. It features a title to guide the user on what to upload and an optional description to provide additional context.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/oo4e6vva48w0trn01ht8krwo" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={FileUpload}
|
||||
alt="Overview of Fill Upload question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title that informs the respondent about the purpose of the file upload.
|
||||
|
||||
### Description
|
||||
Provide an optional description to give respondents more details or instructions about what files they need to upload.
|
||||
|
||||
### Allow Multiple Files
|
||||
Enable this option to allow respondents to upload multiple files at once.
|
||||
|
||||
### Max File Size
|
||||
You can set a maximum file size limit, and an input box will appear to specify the size in MB.
|
||||
|
||||
### File Type Restrictions
|
||||
You can restrict the allowed file types. An input box will appear where you can specify the file formats, such as `.pdf`, `.jpg`, `.docx`, etc.
|
||||
|
After Width: | Height: | Size: 11 KiB |
43
apps/docs/app/global/question-type/free-text/page.mdx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import FreeText from "./images/free-text.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Free Text",
|
||||
description: "Free text questions allow respondents to enter a custom answer.",
|
||||
};
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Free Text
|
||||
|
||||
Free text questions allow respondents to enter a custom answer. Displays a title and an input field for the respondent to type in.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/cm2b2eftv000012b0l3htbu0a" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={FreeText}
|
||||
alt="Overview of Free Text question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Placeholder
|
||||
Specify a placeholder text to display in the input field.
|
||||
|
||||
### Input Type
|
||||
|
||||
Choose the type of input field to display. Options include:
|
||||
|
||||
- **Text**: A text area input. This can be converted to a single line input field if needed, by toggling the _"Long answer"_ switch at the bottom of the question segment.
|
||||
- **Email**: A single-line text input that validates the input as an email address.
|
||||
- **URL**: A single-line text input that validates the input as a URL.
|
||||
- **Number**: A single-line text input that validates the input as a number and shows "increase" and "decrease" buttons.
|
||||
- **Phone**: A single-line text input that validates the input as a phone number.
|
||||
BIN
apps/docs/app/global/question-type/matrix/images/matrix.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
36
apps/docs/app/global/question-type/matrix/page.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Matrix from "./images/matrix.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Matrix
|
||||
|
||||
Matrix questions allow respondents to select a value for each option presented in rows. The values range from 0 to a user-defined maximum (e.g., 0 to X). The selection is made using radio buttons, and users can choose any value within the defined range, including 0.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/obqeey0574jig4lo2gqyv51e" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Matrix}
|
||||
alt="Overview of Matrix question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Rows
|
||||
Define the options shown on the left side of the matrix. These represent the items for which users will select a value.
|
||||
|
||||
### Columns
|
||||
Represent the range of values from 0 to X (right side of the screen). Users can choose any value, including 0, using radio buttons.
|
||||
|
||||
### Select ordering
|
||||
|
||||
- Keep current order: This will keep the order of options the same for all respondents.
|
||||
- Randomize all: This will randomize the options for each respondent.
|
||||
|
After Width: | Height: | Size: 16 KiB |
36
apps/docs/app/global/question-type/multi-select/page.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import MultiSelect from "./images/multi-select.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Multi Select",
|
||||
description: "Multi select questions allow respondents to select several answers from a list",
|
||||
};
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Multi Select
|
||||
|
||||
Multi select questions allow respondents to select several answers from a list. Displays a title and a list of checkboxes for the respondent to choose from.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/jhyo6lwzf6eh3fyplhlp7h5f" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={MultiSelect}
|
||||
alt="Overview of Multi Select question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Options
|
||||
Define the options shown in the list. These represent the items for which users will select.
|
||||
|
||||
Other than the fact that respondents can select multiple options, multi select questions are similar to [single select](/global/question-types/single-select) questions.
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,31 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import NetPromoterScore from "./images/net-promoter-score.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Net Promoter Score
|
||||
|
||||
Net Promoter Score questions allow respondents to rate a question on a scale from 0 to 10. Displays a title and a list of radio buttons for the respondent to choose from.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/vqmpasmnt5qcpsa4enheips0" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={NetPromoterScore}
|
||||
alt="Overview of Net Promoter Score question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Labels
|
||||
Add labels for the lower and upper bounds of the score. The default is "Not at all likely" and "Extremely likely".
|
||||
|
||||
### Add color coding
|
||||
Add color coding to the score. This will show a color bar above the score.
|
||||
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,36 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import PictureSelection from "./images/picture-selection.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Picture Selection",
|
||||
description: "Picture selection questions allow respondents to select one or more images from a list",
|
||||
};
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Picture Selection
|
||||
|
||||
Picture selection questions allow respondents to select one or more images from a list. Displays a title and a list of images for the respondent to choose from.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/xtgmwxlk7jxxr4oi6ym7odki" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={PictureSelection}
|
||||
alt="Overview of Picture Selection question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Images
|
||||
Images can be uploaded via click or drag and drop. At least two images are required.
|
||||
|
||||
### Allow Multi Select
|
||||
This option allows user to select more than one image.
|
||||
BIN
apps/docs/app/global/question-type/ranking/images/ranking.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
32
apps/docs/app/global/question-type/ranking/page.mdx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Ranking from "./images/ranking.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Ranking
|
||||
|
||||
Ranking questions let respondents select options in order from 1 to the total number of options. As they make their choices, the list is automatically rearranged in numerical order.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/z6s84x9wbyk0yqqtfaz238px" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Ranking}
|
||||
alt="Overview of Ranking question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Options
|
||||
You need to add at least two options so that users can rearrange them in numerical order based on their selection.
|
||||
|
||||
### Select ordering
|
||||
- Keep current order: This will keep the order of options the same for all respondents.
|
||||
- Randomize all: This will randomize the options for each respondent.
|
||||
BIN
apps/docs/app/global/question-type/rating/images/rating.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
39
apps/docs/app/global/question-type/rating/page.mdx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import Rating from "./images/rating.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Rating",
|
||||
description: "Rating questions allow respondents to rate questions on a scale",
|
||||
};
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Rating
|
||||
|
||||
Rating questions allow respondents to rate questions on a scale. Displays a title and a rating scale consisting of a number of images and labels for the lower and upper ends of the scale.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/cx7u4n6hwvc3nztuk4vdezl9" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={Rating}
|
||||
alt="Overview of Rating question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Scale
|
||||
Select the icon to be used for the rating scale. The options include: stars, numbers or smileys. The default is stars.
|
||||
|
||||
### Range
|
||||
Select the range of the rating scale. the options include: 3, 4, 5, 7 or 10. The default is 5.
|
||||
|
||||
### Labels
|
||||
Add labels for the lower and upper bounds of the rating scale. The default is "Not good" and "Very good".
|
||||
|
After Width: | Height: | Size: 10 KiB |
31
apps/docs/app/global/question-type/schedule/page.mdx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import ScheduleCall from "./images/schedule-call.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Schedule A Meeting
|
||||
|
||||
The Schedule A Meeting question type allows respondents to book a meeting by selecting a date and time. It includes a title to guide the respondent, along with an optional description to provide additional context for the meeting setup.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/hx08x27c2aghywh57rroe6fi" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={ScheduleCall}
|
||||
alt="Overview of Schedule question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Cal.com Username/Event
|
||||
Add an input box where users can enter their [`cal.com`](https://cal.com/) username and event URL (e.g., `username/event`).
|
||||
|
||||
### Custom Hostname (Optional)
|
||||
Enable an input box for adding a custom hostname, which is necessary if using a self-hosted instance of [`cal.com`](https://cal.com/docs/self-hosting/installation).
|
||||
|
After Width: | Height: | Size: 12 KiB |
44
apps/docs/app/global/question-type/single-select/page.mdx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import SingleSelect from "./images/single-select.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Single Select",
|
||||
description: "Single select questions allow respondents to select one answer from a list",
|
||||
};
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Single Select
|
||||
|
||||
Single select questions allow respondents to select one answer from a list. Displays a title and a list of radio buttons for the respondent to choose from.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/wybd3v3cxpdfve4472fu3lhi" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={SingleSelect}
|
||||
alt="Overview of Single Select question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
Add a clear title to inform the respondent what information you are asking for.
|
||||
|
||||
### Description
|
||||
Provide an optional description with further instructions.
|
||||
|
||||
### Options
|
||||
The list of answers the respondent can choose from.
|
||||
|
||||
### Additional Actions
|
||||
|
||||
- Add "Other": Adds an "Other" option to allow respondents to enter a custom answer. This will show two inputs, one for the label text and one for the placeholder.
|
||||
|
||||
- Convert to Multiple Select: Converts the question to a multiple select question. This will show checkboxes instead of radio buttons.
|
||||
|
||||
- Order dropdown: Allows you to choose the order in which the options are displayed.
|
||||
- Keep current order: Options will be displayed in the order you added them.
|
||||
- Randomize all: Options will be displayed in a random order.
|
||||
- Randomize all except last option: Options will be displayed in a random order, except for the last one.
|
||||
|
After Width: | Height: | Size: 13 KiB |
29
apps/docs/app/global/question-type/statement-cta/page.mdx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import SurveyEmbed from "@/components/SurveyEmbed";
|
||||
import StatementCTA from "./images/statement-CTA.webp";
|
||||
|
||||
#### Question Type
|
||||
|
||||
# Statement (Call to Action)
|
||||
|
||||
The Statement question type allows you to display descriptive information in your survey, such as a message or instruction. It consists of a title (can be Question or Short Note) and a description, which can be a brief note(realted to CTA) or guideline. Instead of collecting input, this type includes a call to action button for further steps, such as booking an interview call.
|
||||
|
||||
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/k3p7r7riyy504u4zziqat8zj" />
|
||||
|
||||
## Elements
|
||||
<MdxImage
|
||||
src={StatementCTA}
|
||||
alt="Overview of Statement question type"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### Title
|
||||
This is the main question or heading that appears at the top of the card.
|
||||
|
||||
### Description
|
||||
A brief note or instruction displayed under the title, typically used to provide context or instructions for the next step.
|
||||
|
||||
### Button Options
|
||||
- Button to continue in survey: This will continue respondent with the survey, form or fillups.
|
||||
- Button to link to external URL: Selecting this option will open-up URL input box below when us set URL the button will redirect to your setted link.
|
||||
@@ -48,7 +48,7 @@ We have step-by-step guides to configure our third-party integrations with a sel
|
||||
<Note>
|
||||
{" "}
|
||||
Once you’ve configured your integration, See our Integration sections to see how to use them within your Formbricks
|
||||
app [here](/how-to-formbricks/integrations)
|
||||
app [here](/developer-docs/integrations/airtable)
|
||||
</Note>
|
||||
|
||||
### Step by Step Guides
|
||||
@@ -102,12 +102,11 @@ Enabling the Airtable Integration in a self-hosted environment requires creating
|
||||
/>
|
||||
|
||||
5. Click on the "Save" button and you are done
|
||||
6. Now just copy **Client ID** and **Redirect URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||
6. Now just copy **Client ID** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||
|
||||
- `AIRTABLE_CLIENT_ID`
|
||||
- `AIRTABLE_REDIRECT_URL`
|
||||
|
||||
Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Airtable Integration with Formbricks](/integrations#airtable) section to link an Airtable with Formbricks.
|
||||
Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Airtable Integration with Formbricks](/developer-docs/integrations/airtable) section to link an Airtable with Formbricks.
|
||||
|
||||
## Google Sheets
|
||||
|
||||
@@ -152,7 +151,7 @@ Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and *
|
||||
- `GOOGLE_SHEETS_CLIENT_SECRET`
|
||||
- `GOOGLE_SHEETS_REDIRECT_URL`
|
||||
|
||||
Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Google Sheets Integration with Formbricks](/integrations#google-sheets) section to link a Google Sheet with Formbricks.
|
||||
Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Google Sheets Integration with Formbricks](/developer-docs/integrations/google-sheets) section to link a Google Sheet with Formbricks.
|
||||
|
||||
## Notion:
|
||||
|
||||
@@ -170,7 +169,7 @@ Enabling the Notion Integration in a self-hosted environment requires a setup us
|
||||
- `NOTION_OAUTH_CLIENT_ID` - OAuth Client ID
|
||||
- `NOTION_OAUTH_CLIENT_SECRET` - OAuth Client Secret
|
||||
|
||||
Voila! You have successfully enabled the Notion integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Notion Integration with Formbricks](/integrations#notion) section to link your Notion with Formbricks.
|
||||
Voila! You have successfully enabled the Notion integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Notion Integration with Formbricks](/developer-docs/integrations/notion) section to link your Notion with Formbricks.
|
||||
|
||||
## n8n
|
||||
|
||||
@@ -289,7 +288,7 @@ Once the execution is successful, you'll receive the content in the discord chan
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Voila! You have successfully enabled the n8n integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks](/integrations#n8n) Integrations section to know more about the capabilities with Formbricks with n8n.
|
||||
Voila! You have successfully enabled the n8n integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks](/developer-docs/integrations/n8n) Integrations section to know more about the capabilities with Formbricks with n8n.
|
||||
|
||||
## Slack
|
||||
|
||||
@@ -321,7 +320,7 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
|
||||
8. Now, you need to enable the public distribution of your app. Go to the **Basic Information** tab and click on the **Manage distribution** button and click on the "Distribute App".
|
||||
9. Scroll down to the **Share your app with other workspaces** section, complete the checklist and click on the **Activate public distribution** button.
|
||||
|
||||
Voila! You have successfully enabled the Slack integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Slack Integration](/integrations#slack) section to link a Slack workspace with Formbricks.
|
||||
Voila! You have successfully enabled the Slack integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Slack Integration](/developer-docs/integrations/slack) section to link a Slack workspace with Formbricks.
|
||||
|
||||
## Zapier
|
||||
|
||||
@@ -348,7 +347,7 @@ Then, choose the event you want to trigger the Zap on:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Now you need an API key. Please refer to the [API Key Setup](/developer-docs/rest-api#api-key) page to learn how to create one.
|
||||
Now you need an API key. Please refer to the [API Key Setup](/developer-docs/rest-api##how-to-generate-an-api-key) page to learn how to create one.
|
||||
|
||||
Once you copied it in the newly opened Zapier window, you will be connected:
|
||||
|
||||
@@ -359,6 +358,6 @@ Once you copied it in the newly opened Zapier window, you will be connected:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Voila! You have successfully configured Zapier to work with your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Zapier Integration](/integrations#zapier) section to connect it with your Formbricks app and see it live.
|
||||
Voila! You have successfully configured Zapier to work with your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Zapier Integration](/developer-docs/integrations/zapier) section to connect it with your Formbricks app and see it live.
|
||||
|
||||
---
|
||||
|
||||
@@ -160,6 +160,33 @@ const NavigationGroup = ({
|
||||
const pathname = usePathname();
|
||||
const [isActiveGroup, setIsActiveGroup] = useState<boolean>(false);
|
||||
|
||||
// We need to expand the group with the current link so we loop over all links
|
||||
// Until we find the one and then expand the groups
|
||||
useEffect(() => {
|
||||
const findMatchingGroup = () => {
|
||||
for (const group of navigation) {
|
||||
for (const link of group.links) {
|
||||
if (!link.children) continue;
|
||||
|
||||
const matchingChild = link.children.find((child) => pathname && child.href.startsWith(pathname));
|
||||
|
||||
if (matchingChild) {
|
||||
setOpenGroups([`${group.title}-${link.title}`]);
|
||||
setActiveGroup(group);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findMatchingGroup();
|
||||
|
||||
return () => {
|
||||
setOpenGroups([]);
|
||||
setActiveGroup(null);
|
||||
};
|
||||
}, [pathname, setActiveGroup, setOpenGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsActiveGroup(activeGroup?.title === group.title);
|
||||
}, [activeGroup?.title, group.title]);
|
||||
|
||||
25
apps/docs/components/SurveyEmbed.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
interface SurveyEmbedProps {
|
||||
surveyUrl: string;
|
||||
}
|
||||
|
||||
const SurveyEmbed: React.FC<SurveyEmbedProps> = ({ surveyUrl }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
maxHeight: "100vh",
|
||||
overflow: "auto",
|
||||
borderRadius: "12px",
|
||||
}}>
|
||||
<iframe
|
||||
src={surveyUrl}
|
||||
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", border: 0 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyEmbed;
|
||||
@@ -9,7 +9,7 @@ export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}>
|
||||
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
|
||||
<path d="M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,6 +49,10 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
|
||||
{ title: "PIN Protected Surveys", href: "/link-surveys/pin-protected-surveys" },
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" },
|
||||
{
|
||||
title: "Add Image/Video to Question",
|
||||
href: "/global/add-image-or-video-question",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -76,6 +80,10 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
|
||||
{
|
||||
title: "Add Image/Video to Question",
|
||||
href: "/global/add-image-or-video-question",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -83,6 +91,26 @@ export const navigation: Array<NavGroup> = [
|
||||
{
|
||||
title: "Core Features",
|
||||
links: [
|
||||
{
|
||||
title: "Question Types",
|
||||
children: [
|
||||
{ title: "Free Text", href: "/global/question-type/free-text" },
|
||||
{ title: "Select Single", href: "/global/question-type/single-select" },
|
||||
{ title: "Select Multiple", href: "/global/question-type/multi-select" },
|
||||
{ title: "Select Picture", href: "/global/question-type/picture-selection" },
|
||||
{ title: "Rating", href: "/global/question-type/rating" },
|
||||
{ title: "Net Promoter Score", href: "/global/question-type/net-promoter-score" },
|
||||
{ title: "Ranking", href: "/global/question-type/ranking" },
|
||||
{ title: "Matrix", href: "/global/question-type/matrix" },
|
||||
{ title: "Statement (Call to Action)", href: "/global/question-type/statement-cta" },
|
||||
{ title: "Consent", href: "/global/question-type/consent" },
|
||||
{ title: "File Upload", href: "/global/question-type/file-upload" },
|
||||
{ title: "Date", href: "/global/question-type/date" },
|
||||
{ title: "Schedule a Meeting", href: "/global/question-type/schedule" },
|
||||
{ title: "Address", href: "/global/question-type/address" },
|
||||
{ title: "Contact Info", href: "/global/question-type/contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Integrations",
|
||||
children: [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LucideProps } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { OptionCard } from "@formbricks/ui/components/OptionCard";
|
||||
|
||||
interface OnboardingOptionsContainerProps {
|
||||
@@ -40,11 +39,7 @@ export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContain
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"flex w-5/6 justify-center gap-8 text-center md:flex-row lg:w-2/3": options.length >= 3,
|
||||
"flex justify-center gap-8": options.length < 3,
|
||||
})}>
|
||||
<div className="flex w-full max-w-5xl flex-wrap justify-center gap-8 text-center">
|
||||
{options.map((option) =>
|
||||
option.href ? (
|
||||
<Link key={option.title} href={option.href}>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner";
|
||||
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
|
||||
|
||||
const EnvLayout = async ({ children, params }) => {
|
||||
const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
@@ -61,4 +61,4 @@ const EnvLayout = async ({ children, params }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvLayout;
|
||||
export default SurveyEditorEnvironmentLayout;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
@@ -22,6 +23,7 @@ interface AddQuestionButtonProps {
|
||||
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
|
||||
|
||||
@@ -44,7 +46,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col" ref={parent}>
|
||||
{/* <hr className="py-1 text-slate-600" /> */}
|
||||
{availableQuestionTypes.map((questionType) => (
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -91,6 +92,7 @@ export const AddressQuestionForm = ({
|
||||
question.country,
|
||||
]);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -106,7 +108,7 @@ export const AddressQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -40,6 +40,17 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB
|
||||
"/animated-bgs/Thumbnails/28_Thumb.mp4": "/animated-bgs/4K/28_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/29_Thumb.mp4": "/animated-bgs/4K/29_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/31_Thumb.mp4": "/animated-bgs/4K/31_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/32_Thumb.mp4": "/animated-bgs/4K/32_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/33_Thumb.mp4": "/animated-bgs/4K/33_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/34_Thumb.mp4": "/animated-bgs/4K/34_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/35_Thumb.mp4": "/animated-bgs/4K/35_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/36_Thumb.mp4": "/animated-bgs/4K/36_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/37_Thumb.mp4": "/animated-bgs/4K/37_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/38_Thumb.mp4": "/animated-bgs/4K/38_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/39_Thumb.mp4": "/animated-bgs/4K/39_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/40_Thumb.mp4": "/animated-bgs/4K/40_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/41_Thumb.mp4": "/animated-bgs/4K/41_4k.mp4",
|
||||
};
|
||||
|
||||
const togglePlayback = (index: number, type: "play" | "pause") => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
@@ -32,6 +33,8 @@ export const BackgroundStylingCard = ({
|
||||
isUnsplashConfigured,
|
||||
form,
|
||||
}: BackgroundStylingCardProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -72,7 +75,7 @@ export const BackgroundStylingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="pt-1 text-slate-600" />
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<FormField
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -41,9 +42,9 @@ export const CTAQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<form ref={parent}>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -42,6 +43,7 @@ export const CardStylingSettings = ({
|
||||
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "simple";
|
||||
const roundness = form.watch("roundness") ?? 8;
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -78,7 +80,7 @@ export const CardStylingSettings = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getDefaultOperatorForQuestion,
|
||||
replaceEndingCardHeadlineRecall,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
@@ -111,16 +112,17 @@ export function ConditionalLogic({
|
||||
logic: logicCopy,
|
||||
});
|
||||
};
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<div className="mt-4" ref={parent}>
|
||||
<Label className="flex gap-2">
|
||||
Conditional Logic
|
||||
<SplitIcon className="h-4 w-4 rotate-90" />
|
||||
</Label>
|
||||
|
||||
{question.logic && question.logic.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
<div className="mt-2 flex flex-col gap-4" ref={parent}>
|
||||
{question.logic.map((logicItem, logicItemIdx) => (
|
||||
<div
|
||||
key={logicItem.id}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -78,6 +79,9 @@ export const ContactInfoQuestionForm = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -93,7 +97,7 @@ export const ContactInfoQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -45,7 +46,7 @@ export const DateQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -60,7 +61,7 @@ export const DateQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -13,7 +13,12 @@ import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyEndScreenCard,
|
||||
TSurveyQuestionId,
|
||||
TSurveyRedirectUrlCard,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
|
||||
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
|
||||
|
||||
@@ -22,7 +27,7 @@ interface EditEndingCardProps {
|
||||
endingCardIndex: number;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId: string | null;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
@@ -198,7 +203,7 @@ export const EditEndingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="mt-3 px-4 pb-6">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "mt-3 pb-6"}`}>
|
||||
<TooltipRenderer
|
||||
shouldRender={endingCard.type === "endScreen" && isRedirectToUrlDisabled}
|
||||
tooltipContent={"Redirect To Url is not available on free plan"}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { FileInput } from "@formbricks/ui/components/FileInput";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
@@ -17,7 +17,7 @@ interface EditWelcomeCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId: string | null;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
@@ -72,7 +72,7 @@ export const EditWelcomeCard = ({
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out">
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-200 ease-in-out">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="flex cursor-pointer justify-between rounded-r-lg p-4 hover:bg-slate-50">
|
||||
@@ -102,7 +102,7 @@ export const EditWelcomeCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`}>
|
||||
<form>
|
||||
<div className="mt-2">
|
||||
<Label htmlFor="companyLogo">Company Logo</Label>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon, XCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -121,6 +122,8 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion(questionIdx, { maxSizeInMB: checked ? defaultMaxSizeInMB : undefined });
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -135,7 +138,7 @@ export const FileUploadQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon, SparklesIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -67,6 +68,8 @@ export const FormStylingSettings = ({
|
||||
}
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -103,7 +106,7 @@ export const FormStylingSettings = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyHiddenFields, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { validateId } from "@formbricks/types/surveys/validation";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
@@ -18,8 +19,8 @@ import { Tag } from "@formbricks/ui/components/Tag";
|
||||
interface HiddenFieldsCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
|
||||
}
|
||||
|
||||
export const HiddenFieldsCard = ({
|
||||
@@ -85,6 +86,9 @@ export const HiddenFieldsCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Auto Animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
@@ -124,8 +128,8 @@ export const HiddenFieldsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex gap-2">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-wrap gap-2" ref={parent}>
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -89,6 +90,8 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
},
|
||||
];
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -114,7 +117,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
<RadioGroup
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getDefaultOperatorForQuestion,
|
||||
getMatchValueProps,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -52,6 +53,8 @@ export function LogicEditorConditions({
|
||||
updateQuestion,
|
||||
depth = 0,
|
||||
}: LogicEditorConditionsProps) {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const handleAddConditionBelow = (resourceId: string) => {
|
||||
const operator = getDefaultOperatorForQuestion(question);
|
||||
|
||||
@@ -334,7 +337,7 @@ export function LogicEditorConditions({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<div ref={parent} className="flex flex-col gap-y-2">
|
||||
{conditions?.conditions.map((condition, index) => renderCondition(condition, index, conditions))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -96,7 +97,8 @@ export const MatrixQuestionForm = ({
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
|
||||
/// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -111,7 +113,7 @@ export const MatrixQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -150,7 +152,7 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Rows section */}
|
||||
<Label htmlFor="rows">Rows</Label>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.rows.map((_, index) => (
|
||||
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "row")}>
|
||||
<QuestionFormInput
|
||||
@@ -192,7 +194,7 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Columns section */}
|
||||
<Label htmlFor="columns">Columns</Label>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.columns.map((_, index) => (
|
||||
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "column")}>
|
||||
<QuestionFormInput
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -158,6 +159,8 @@ export const MultipleChoiceQuestionForm = ({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -173,7 +176,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -236,7 +239,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col" ref={parent}>
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -32,6 +33,9 @@ export const NPSQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: NPSQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -47,7 +51,7 @@ export const NPSQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -54,6 +55,8 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion(questionIdx, updatedAttributes);
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -69,7 +72,7 @@ export const OpenQuestionForm = ({
|
||||
label={"Question*"}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -64,7 +65,8 @@ export const PictureSelectionForm = ({
|
||||
choices: updatedChoices,
|
||||
});
|
||||
};
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -79,7 +81,7 @@ export const PictureSelectionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[e
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
TI18nString,
|
||||
TSurvey,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
@@ -46,8 +48,8 @@ interface QuestionCardProps {
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
deleteQuestion: (questionIdx: number) => void;
|
||||
duplicateQuestion: (questionIdx: number) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
@@ -84,12 +86,18 @@ export const QuestionCard = ({
|
||||
|
||||
const open = activeQuestionId === question.id;
|
||||
const [openAdvanced, setOpenAdvanced] = useState(question.logic && question.logic.length > 0);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const updateEmptyNextButtonLabels = (labelValue: TI18nString) => {
|
||||
const updateEmptyButtonLabels = (
|
||||
labelKey: "buttonLabel" | "backButtonLabel",
|
||||
labelValue: TI18nString,
|
||||
skipIndex: number
|
||||
) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel[selectedLanguageCode]?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
if (index === skipIndex) return;
|
||||
const currentLabel = q[labelKey];
|
||||
if (!currentLabel || currentLabel[selectedLanguageCode]?.trim() === "") {
|
||||
updateQuestion(index, { [labelKey]: labelValue });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -223,7 +231,7 @@ export const QuestionCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-4">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-4"}`}>
|
||||
{question.type === TSurveyQuestionTypeEnum.OpenText ? (
|
||||
<OpenQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
@@ -417,7 +425,7 @@ export const QuestionCard = ({
|
||||
{openAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings"}
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="space-y-4">
|
||||
<Collapsible.CollapsibleContent className="flex flex-col gap-4" ref={parent}>
|
||||
{question.type !== TSurveyQuestionTypeEnum.NPS &&
|
||||
question.type !== TSurveyQuestionTypeEnum.Rating &&
|
||||
question.type !== TSurveyQuestionTypeEnum.CTA ? (
|
||||
@@ -443,7 +451,11 @@ export const QuestionCard = ({
|
||||
};
|
||||
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||
updateEmptyButtonLabels(
|
||||
"buttonLabel",
|
||||
translatedNextButtonLabel,
|
||||
localSurvey.questions.length - 1
|
||||
);
|
||||
}}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
@@ -462,6 +474,14 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
onBlur={(e) => {
|
||||
if (!question.backButtonLabel) return;
|
||||
let translatedBackButtonLabel = {
|
||||
...question.backButtonLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { QuestionCard } from "./QuestionCard";
|
||||
|
||||
interface QuestionsDraggableProps {
|
||||
@@ -11,8 +12,8 @@ interface QuestionsDraggableProps {
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
deleteQuestion: (questionIdx: number) => void;
|
||||
duplicateQuestion: (questionIdx: number) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
invalidQuestions: string[] | null;
|
||||
@@ -41,8 +42,10 @@ export const QuestionsDroppable = ({
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
}: QuestionsDraggableProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="group mb-5 flex w-full flex-col gap-5">
|
||||
<div className="group mb-5 flex w-full flex-col gap-5" ref={parent}>
|
||||
<SortableContext items={localSurvey.questions} strategy={verticalListSortingStrategy}>
|
||||
{localSurvey.questions.map((question, questionIdx) => (
|
||||
<QuestionCard
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import React, { SetStateAction, useEffect, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
TSingleCondition,
|
||||
TSurveyLogic,
|
||||
TSurveyLogicAction,
|
||||
TSurveyQuestionId,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation";
|
||||
@@ -47,8 +49,8 @@ import { QuestionsDroppable } from "./QuestionsDroppable";
|
||||
interface QuestionsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: React.Dispatch<SetStateAction<TSurvey>>;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (questionId: TSurveyQuestionId | null) => void;
|
||||
product: TProduct;
|
||||
invalidQuestions: string[] | null;
|
||||
setInvalidQuestions: React.Dispatch<SetStateAction<string[] | null>>;
|
||||
@@ -85,7 +87,6 @@ export const QuestionsView = ({
|
||||
}, [localSurvey.questions]);
|
||||
|
||||
const surveyLanguages = localSurvey.languages;
|
||||
const [backButtonLabel, setbackButtonLabel] = useState(null);
|
||||
|
||||
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
|
||||
const updateConditions = (conditions: TConditionGroup): TConditionGroup => {
|
||||
@@ -237,22 +238,6 @@ export const QuestionsView = ({
|
||||
...updatedAttributes,
|
||||
};
|
||||
|
||||
if ("backButtonLabel" in updatedAttributes) {
|
||||
const backButtonLabel = updatedSurvey.questions[questionIdx].backButtonLabel;
|
||||
// If the value of backbuttonLabel is equal to {default:""}, then delete backButtonLabel key
|
||||
if (
|
||||
backButtonLabel &&
|
||||
Object.keys(backButtonLabel).length === 1 &&
|
||||
backButtonLabel["default"].trim() === ""
|
||||
) {
|
||||
delete updatedSurvey.questions[questionIdx].backButtonLabel;
|
||||
} else {
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
question.backButtonLabel = updatedAttributes.backButtonLabel;
|
||||
});
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
}
|
||||
const attributesToCheck = ["buttonLabel", "upperLabel", "lowerLabel"];
|
||||
|
||||
// If the value of buttonLabel, lowerLabel or upperLabel is equal to {default:""}, then delete buttonLabel key
|
||||
@@ -331,9 +316,6 @@ export const QuestionsView = ({
|
||||
|
||||
const addQuestion = (question: TSurveyQuestion, index?: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
|
||||
@@ -429,6 +411,9 @@ export const QuestionsView = ({
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="mt-12 w-full px-5 py-4">
|
||||
{!isCxMode && (
|
||||
@@ -472,7 +457,7 @@ export const QuestionsView = ({
|
||||
</DndContext>
|
||||
|
||||
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} />
|
||||
<div className="mt-5 flex flex-col gap-5">
|
||||
<div className="mt-5 flex flex-col gap-5" ref={parent}>
|
||||
<hr className="border-t border-dashed" />
|
||||
<DndContext
|
||||
id="endings"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -107,6 +108,8 @@ export const RankingQuestionForm = ({
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -122,7 +125,7 @@ export const RankingQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -181,7 +184,7 @@ export const RankingQuestionForm = ({
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col" ref={parent}>
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -31,7 +32,7 @@ export const RatingQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -47,7 +48,7 @@ export const RatingQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div ref={parent}>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -57,6 +58,9 @@ export const RecontactOptionsCard = ({
|
||||
);
|
||||
const [displayLimit, setDisplayLimit] = useState(localSurvey.displayLimit ?? 1);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const handleCheckMark = () => {
|
||||
if (ignoreWaiting) {
|
||||
const updatedSurvey = { ...localSurvey, recontactDays: null };
|
||||
@@ -119,7 +123,7 @@ export const RecontactOptionsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="pb-3">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col ${open && "pb-3"}`} ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
<RadioGroup
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ArrowUpRight, CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -272,6 +273,7 @@ export const ResponseOptionsCard = ({
|
||||
return;
|
||||
}
|
||||
};
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -295,7 +297,7 @@ export const ResponseOptionsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
{/* Close Survey on Limit */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TabBar } from "@formbricks/ui/components/TabBar";
|
||||
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
|
||||
@@ -30,7 +31,7 @@ export const SurveyBgSelectorTab = ({
|
||||
isUnsplashConfigured,
|
||||
}: SurveyBgSelectorTabProps) => {
|
||||
const [activeTab, setActiveTab] = useState(bgType || "color");
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
const [colorBackground, setColorBackground] = useState(bg);
|
||||
const [animationBackground, setAnimationBackground] = useState(bg);
|
||||
const [uploadBackground, setUploadBackground] = useState(bg);
|
||||
@@ -93,7 +94,7 @@ export const SurveyBgSelectorTab = ({
|
||||
tabStyle="button"
|
||||
className="bg-slate-100"
|
||||
/>
|
||||
<div className="w-full rounded-b-lg border-x border-b border-slate-200 px-4 pb-4 pt-2">
|
||||
<div className="w-full rounded-b-lg border-x border-b border-slate-200 px-4 pb-4 pt-2" ref={parent}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ export const SurveyMenuBar = ({
|
||||
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
|
||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||
const cautionText = "This survey received responses.";
|
||||
const cautionText = "Changes will lead to inconsistencies.";
|
||||
|
||||
useEffect(() => {
|
||||
if (audiencePrompt && activeId === "settings") {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -70,6 +71,8 @@ export const SurveyPlacementCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -95,7 +98,7 @@ export const SurveyPlacementCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="pb-3">
|
||||
<Collapsible.CollapsibleContent className={`flex ${open && "pb-3"}`} ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { FileDigitIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem";
|
||||
|
||||
interface SurveyVariablesCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId: TSurveyQuestionId | null;
|
||||
setActiveQuestionId: (id: TSurveyQuestionId | null) => void;
|
||||
}
|
||||
|
||||
const variablesCardId = `fb-variables-${Date.now()}`;
|
||||
@@ -22,6 +23,7 @@ export const SurveyVariablesCard = ({
|
||||
setActiveQuestionId,
|
||||
}: SurveyVariablesCardProps) => {
|
||||
const open = activeQuestionId === variablesCardId;
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const setOpenState = (state: boolean) => {
|
||||
if (state) {
|
||||
@@ -57,8 +59,8 @@ export const SurveyVariablesCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { AlertCircle, CheckIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -154,6 +155,7 @@ export const TargetingCard = ({
|
||||
() => (localSurvey?.segment ? localSurvey.segment?.surveys?.length > 1 : false),
|
||||
[localSurvey.segment]
|
||||
);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -176,7 +178,7 @@ export const TargetingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="min-w-full overflow-auto">
|
||||
<Collapsible.CollapsibleContent className="flex min-w-full flex-col overflow-auto" ref={parent}>
|
||||
<hr className="text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-5 p-6">
|
||||
|
||||
@@ -110,6 +110,13 @@ const defaultImages = [
|
||||
regularWithAttribution: "/image-backgrounds/kittens.webp",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
alt_description: "Windows",
|
||||
urls: {
|
||||
regularWithAttribution: "/image-backgrounds/windows.webp",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -126,6 +127,9 @@ export const WhenToSendCard = ({
|
||||
}
|
||||
}, [localSurvey.type]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const containsEmptyTriggers = useMemo(() => {
|
||||
return !localSurvey.triggers || !localSurvey.triggers.length || !localSurvey.triggers[0];
|
||||
}, [localSurvey]);
|
||||
@@ -167,7 +171,7 @@ export const WhenToSendCard = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="px-3 pb-3 pt-1">
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
TSurveyLogicAction,
|
||||
TSurveyLogicConditionsOperator,
|
||||
TSurveyQuestion,
|
||||
TSurveyQuestionId,
|
||||
TSurveyQuestionTypeEnum,
|
||||
TSurveyVariable,
|
||||
} from "@formbricks/types/surveys/types";
|
||||
@@ -1023,7 +1024,7 @@ const isUsedInRightOperand = (
|
||||
}
|
||||
};
|
||||
|
||||
export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): number => {
|
||||
export const findQuestionUsedInLogic = (survey: TSurvey, questionId: TSurveyQuestionId): number => {
|
||||
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
|
||||
if (isConditionGroup(condition)) {
|
||||
// It's a TConditionGroup
|
||||
@@ -1053,7 +1054,11 @@ export const findQuestionUsedInLogic = (survey: TSurvey, questionId: string): nu
|
||||
);
|
||||
};
|
||||
|
||||
export const findOptionUsedInLogic = (survey: TSurvey, questionId: string, optionId: string): number => {
|
||||
export const findOptionUsedInLogic = (
|
||||
survey: TSurvey,
|
||||
questionId: TSurveyQuestionId,
|
||||
optionId: string
|
||||
): number => {
|
||||
const isUsedInCondition = (condition: TSingleCondition | TConditionGroup): boolean => {
|
||||
if (isConditionGroup(condition)) {
|
||||
// It's a TConditionGroup
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"use server";
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { generateObject } from "ai";
|
||||
import { z } from "zod";
|
||||
import { authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { llmModel } from "@formbricks/lib/aiModels";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { createSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getIsAIEnabled } from "@formbricks/lib/utils/ai";
|
||||
import { ZId, ZString } from "@formbricks/types/common";
|
||||
import { ZSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
const ZCreateAISurveyAction = z.object({
|
||||
environmentId: ZId,
|
||||
prompt: ZString,
|
||||
});
|
||||
|
||||
export const createAISurveyAction = authenticatedActionClient
|
||||
.schema(ZCreateAISurveyAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(parsedInput.environmentId);
|
||||
|
||||
await checkAuthorization({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
rules: ["survey", "create"],
|
||||
});
|
||||
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const isAIEnabled = await getIsAIEnabled(organization.billing.plan);
|
||||
|
||||
if (!isAIEnabled) {
|
||||
throw new Error("AI is not enabled for this organization");
|
||||
}
|
||||
|
||||
const { object } = await generateObject({
|
||||
model: llmModel,
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
questions: z.array(
|
||||
z.object({
|
||||
headline: z.string(),
|
||||
subheader: z.string(),
|
||||
type: z.enum(["openText", "multipleChoiceSingle", "multipleChoiceMulti"]),
|
||||
choices: z
|
||||
.array(z.string())
|
||||
.min(2, { message: "Multiple Choice Question must have at least two choices" })
|
||||
.optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
system: `You are a survey AI. Create a survey with 3 questions max that fits the schema and user input.`,
|
||||
prompt: parsedInput.prompt,
|
||||
experimental_telemetry: { isEnabled: true },
|
||||
});
|
||||
|
||||
const parsedQuestions = object.questions.map((question) => {
|
||||
return ZSurveyQuestion.parse({
|
||||
id: createId(),
|
||||
headline: { default: question.headline },
|
||||
subheader: { default: question.subheader },
|
||||
type: question.type,
|
||||
choices: question.choices
|
||||
? question.choices.map((choice) => ({ id: createId(), label: { default: choice } }))
|
||||
: undefined,
|
||||
required: true,
|
||||
});
|
||||
});
|
||||
|
||||
return await createSurvey(parsedInput.environmentId, { name: object.name, questions: parsedQuestions });
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { createAISurveyAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions";
|
||||
import { Sparkles } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@formbricks/ui/components/Card";
|
||||
import { Textarea } from "@formbricks/ui/components/Textarea";
|
||||
|
||||
interface FormbricksAICardProps {
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const FormbricksAICard = ({ environmentId }: FormbricksAICardProps) => {
|
||||
const router = useRouter();
|
||||
const [aiPrompt, setAiPrompt] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
// Here you would typically send the data to your backend
|
||||
const createSurveyResponse = await createAISurveyAction({
|
||||
environmentId,
|
||||
prompt: aiPrompt,
|
||||
});
|
||||
|
||||
if (createSurveyResponse?.data) {
|
||||
router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit`);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
// Reset form field after submission
|
||||
setAiPrompt("");
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mx-auto w-full bg-gradient-to-tr from-slate-100 to-slate-200">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold">Formbricks AI</CardTitle>
|
||||
<CardDescription>
|
||||
Describe your survey and let Formbricks AI create the survey for you
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<Textarea
|
||||
className="bg-slate-50"
|
||||
id="ai-prompt"
|
||||
placeholder="Enter survey information (e.g. key topics to cover)"
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAiPrompt(e.target.value)}
|
||||
required
|
||||
aria-label="AI Prompt"
|
||||
/>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full shadow-sm"
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
variant="secondary"
|
||||
loading={isLoading}>
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Generate
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FormbricksAICard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard";
|
||||
import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar";
|
||||
import { useState } from "react";
|
||||
import { customSurvey } from "@formbricks/lib/templates";
|
||||
@@ -9,6 +10,7 @@ import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { PreviewSurvey } from "@formbricks/ui/components/PreviewSurvey";
|
||||
import { SearchBar } from "@formbricks/ui/components/SearchBar";
|
||||
import { Separator } from "@formbricks/ui/components/Separator";
|
||||
import { TemplateList } from "@formbricks/ui/components/TemplateList";
|
||||
import { minimalSurvey } from "../../lib/minimalSurvey";
|
||||
|
||||
@@ -18,6 +20,7 @@ type TemplateContainerWithPreviewProps = {
|
||||
environment: TEnvironment;
|
||||
user: TUser;
|
||||
prefilledFilters: (TProductConfigChannel | TProductConfigIndustry | TTemplateRole | null)[];
|
||||
isAIEnabled: boolean;
|
||||
};
|
||||
|
||||
export const TemplateContainerWithPreview = ({
|
||||
@@ -25,6 +28,7 @@ export const TemplateContainerWithPreview = ({
|
||||
environment,
|
||||
user,
|
||||
prefilledFilters,
|
||||
isAIEnabled,
|
||||
}: TemplateContainerWithPreviewProps) => {
|
||||
const initialTemplate = customSurvey;
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate>(initialTemplate);
|
||||
@@ -48,6 +52,15 @@ export const TemplateContainerWithPreview = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAIEnabled && (
|
||||
<>
|
||||
<div className="px-6">
|
||||
<FormbricksAICard environmentId={environment.id} />
|
||||
</div>
|
||||
<Separator className="mt-4" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<TemplateList
|
||||
environment={environment}
|
||||
product={product}
|
||||
|
||||
@@ -65,6 +65,8 @@ const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
|
||||
environment={environment}
|
||||
product={product}
|
||||
prefilledFilters={prefilledFilters}
|
||||
// AI Survey Creation -- Need improvement
|
||||
isAIEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -54,6 +55,8 @@ export const PersonTable = ({
|
||||
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const router = useRouter();
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
// Generate columns
|
||||
const columns = useMemo(
|
||||
() => generatePersonTableColumns(isExpanded ?? false, searchValue),
|
||||
@@ -189,7 +192,7 @@ export const PersonTable = ({
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
<TableBody ref={parent}>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
||||
@@ -43,7 +43,6 @@ export const BasicCreateSegmentModal = ({
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
const [segment, setSegment] = useState<TSegment>(initialSegmentState);
|
||||
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
|
||||
|
||||
const handleResetState = () => {
|
||||
setSegment(initialSegmentState);
|
||||
setOpen(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
|
||||
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
|
||||
import { getIsAIEnabled } from "@/app/lib/utils";
|
||||
import type { Session } from "next-auth";
|
||||
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
@@ -63,6 +64,8 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
]);
|
||||
}
|
||||
|
||||
const isAIEnabled = await getIsAIEnabled(organization);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
|
||||
<DevEnvironmentBanner environment={environment} />
|
||||
@@ -93,6 +96,7 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
isMultiOrgEnabled={isMultiOrgEnabled}
|
||||
isAIEnabled={isAIEnabled}
|
||||
/>
|
||||
<div id="mainContent" className="flex-1 overflow-y-auto bg-slate-50">
|
||||
<TopControlBar
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
Cog,
|
||||
CreditCardIcon,
|
||||
GaugeIcon,
|
||||
GlobeIcon,
|
||||
GlobeLockIcon,
|
||||
KeyIcon,
|
||||
@@ -67,6 +68,7 @@ interface NavigationProps {
|
||||
isMultiOrgEnabled: boolean;
|
||||
isFormbricksCloud?: boolean;
|
||||
membershipRole?: TMembershipRole;
|
||||
isAIEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const MainNavigation = ({
|
||||
@@ -78,6 +80,7 @@ export const MainNavigation = ({
|
||||
isMultiOrgEnabled,
|
||||
isFormbricksCloud = true,
|
||||
membershipRole,
|
||||
isAIEnabled = false,
|
||||
}: NavigationProps) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -158,6 +161,13 @@ export const MainNavigation = ({
|
||||
|
||||
const mainNavigation = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: "Experience",
|
||||
href: `/environments/${environment.id}/experience`,
|
||||
icon: GaugeIcon,
|
||||
isActive: pathname?.includes("/experience"),
|
||||
isHidden: !isAIEnabled,
|
||||
},
|
||||
{
|
||||
name: "Surveys",
|
||||
href: `/environments/${environment.id}/surveys`,
|
||||
@@ -206,7 +216,7 @@ export const MainNavigation = ({
|
||||
},
|
||||
{
|
||||
label: "Organization",
|
||||
href: `/environments/${environment.id}/settings/members`,
|
||||
href: `/environments/${environment.id}/settings/general`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
@@ -322,7 +332,6 @@ export const MainNavigation = ({
|
||||
</p>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ExperiencePage } from "@/modules/ee/insights/experience/page";
|
||||
|
||||
export default ExperiencePage;
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
TIntegrationGoogleSheetsConfigData,
|
||||
TIntegrationGoogleSheetsInput,
|
||||
} from "@formbricks/types/integration/google-sheet";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { AdditionalIntegrationSettings } from "@formbricks/ui/components/AdditionalIntegrationSettings";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Checkbox } from "@formbricks/ui/components/Checkbox";
|
||||
@@ -150,7 +150,7 @@ export const AddIntegrationModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (questionId: string) => {
|
||||
const handleCheckboxChange = (questionId: TSurveyQuestionId) => {
|
||||
setSelectedQuestions((prevValues) =>
|
||||
prevValues.includes(questionId)
|
||||
? prevValues.filter((value) => value !== questionId)
|
||||
|
||||
@@ -17,8 +17,8 @@ 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";
|
||||
import { Card } from "@formbricks/ui/components/Card";
|
||||
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
|
||||
import { Card } from "@formbricks/ui/components/IntegrationCard";
|
||||
import { PageContentWrapper } from "@formbricks/ui/components/PageContentWrapper";
|
||||
import { PageHeader } from "@formbricks/ui/components/PageHeader";
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
TIntegrationSlackConfigData,
|
||||
TIntegrationSlackInput,
|
||||
} from "@formbricks/types/integration/slack";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
import { AdditionalIntegrationSettings } from "@formbricks/ui/components/AdditionalIntegrationSettings";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Checkbox } from "@formbricks/ui/components/Checkbox";
|
||||
@@ -136,7 +136,7 @@ export const AddChannelMappingModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (questionId: string) => {
|
||||
const handleCheckboxChange = (questionId: TSurveyQuestionId) => {
|
||||
setSelectedQuestions((prevValues) =>
|
||||
prevValues.includes(questionId)
|
||||
? prevValues.filter((value) => value !== questionId)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/HardcodedTriggers";
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -42,6 +43,11 @@ export const AddWebhookModal = ({ environmentId, surveys, open, setOpen }: AddWe
|
||||
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
const { valid, error } = validWebHookURL(testEndpointInput);
|
||||
if (!valid) {
|
||||
toast.error(error ?? "Something went wrong please try again!");
|
||||
return;
|
||||
}
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/HardcodedTriggers";
|
||||
import { SurveyCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/SurveyCheckboxGroup";
|
||||
import { TriggerCheckboxGroup } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/TriggerCheckboxGroup";
|
||||
import { validWebHookURL } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/lib/utils";
|
||||
import clsx from "clsx";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -46,6 +47,11 @@ export const WebhookSettingsTab = ({ webhook, surveys, setOpen }: ActionSettings
|
||||
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
const { valid, error } = validWebHookURL(testEndpointInput);
|
||||
if (!valid) {
|
||||
toast.error(error ?? "Something went wrong please try again!");
|
||||
return;
|
||||
}
|
||||
setHittingEndpoint(true);
|
||||
await testEndpointAction({ url: testEndpointInput });
|
||||
setHittingEndpoint(false);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export const validWebHookURL = (urlInput: string) => {
|
||||
const trimmedInput = urlInput.trim();
|
||||
if (!trimmedInput) {
|
||||
return { valid: false, error: "Please enter a URL" };
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(trimmedInput);
|
||||
|
||||
if (url.protocol !== "https:") {
|
||||
return { valid: false, error: "URL must start with https://" };
|
||||
}
|
||||
|
||||
const domainError: string =
|
||||
"Please enter a complete URL with a valid domain (e.g., https://formbricks.com)";
|
||||
|
||||
const multipleSlashesPattern = /(?<!:)\/\/+/;
|
||||
if (multipleSlashesPattern.test(trimmedInput)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: domainError,
|
||||
};
|
||||
}
|
||||
|
||||
const validDomainPattern = /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
if (!validDomainPattern.test(url.hostname)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: domainError,
|
||||
};
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
} catch (error) {
|
||||
return { valid: false, error: "Invalid URL format. Please enter a complete URL including https://" };
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,7 @@ import { FormbricksClient } from "../../components/FormbricksClient";
|
||||
import EnvironmentStorageHandler from "./components/EnvironmentStorageHandler";
|
||||
import { PosthogIdentify } from "./components/PosthogIdentify";
|
||||
|
||||
const EnvLayout = async ({ children, params }) => {
|
||||
export const EnvLayout = async ({ children, params }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session || !session.user) {
|
||||
return redirect(`/auth/login`);
|
||||
|
||||