mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-05 16:19:55 -06:00
Merge branch 'main' of https://github.com/formbricks/formbricks
This commit is contained in:
@@ -104,8 +104,8 @@ NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
*/
|
||||
|
||||
# Encryption key
|
||||
# You can use: `openssl rand -base64 16` to generate one
|
||||
FORMBRICKS_ENCRYPTION_KEY=
|
||||
FORMBRICKS_ENCRYPTION_KEY=
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.23",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ export const meta = {
|
||||
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark surveys as displayed as well as responded for individual persons, ensuring seamless client-side interactions without compromising data security.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
#### Client API
|
||||
|
||||
# Displays API
|
||||
|
||||
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
This set of API can be used to
|
||||
- [Mark Survey as Displayed](#mark-survey-as-displayed-for-person)
|
||||
- [Mark Survey as Responded](#mark-survey-as-responded-for-person)
|
||||
|
||||
---
|
||||
|
||||
## Mark Survey as Displayed for Person {{ tag: 'POST', label: '/api/v1/client/diplays' }}
|
||||
@@ -10,10 +10,15 @@ export const meta = {
|
||||
|
||||
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
|
||||
|
||||
Checkout the [API Key Setup](/docs/api/api-key-setup) - to generate, store, or delete API Keys.
|
||||
|
||||
## Public Client API
|
||||
|
||||
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
- [Displays API](/docs/api/client/displays) - Mark Survey as Displayed or Responded for a Person
|
||||
- [Responses API](/docs/api/client/responses) - Create & update responses for a survey
|
||||
|
||||
## Management API
|
||||
|
||||
The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.
|
||||
@@ -24,6 +29,14 @@ API requests made to the Management API are authorized using a personal API key.
|
||||
|
||||
To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/api-key-setup).
|
||||
|
||||
- [Action Class API](/docs/api/management/action-classes) - Create, Update, and Delete Action Classes
|
||||
- [Attribute Class API](/docs/api/management/attribute-classes) - Create, Update, and Delete Attribute Classes
|
||||
- [Me API](/docs/api/management/me) - Retrieve Account Information
|
||||
- [People API](/docs/api/management/people) - Create, Update, and Delete People
|
||||
- [Responses API](/docs/api/management/responses) - Create, Update, and Delete Responses
|
||||
- [Surveys API](/docs/api/management/surveys) - Create, Update, and Delete Surveys
|
||||
- [Webhook API](/docs/api/management/webhooks) - Create, Update, and Delete Webhooks
|
||||
|
||||
<Note>
|
||||
By understanding the differences between these two APIs, you can choose the appropriate one for your needs,
|
||||
ensuring a secure and efficient integration with the Formbricks platform.
|
||||
@@ -14,104 +14,6 @@ The Public Client API is designed for the JavaScript SDK and does not require au
|
||||
|
||||
---
|
||||
|
||||
## List all responses {{ tag: 'GET', label: '/api/v1/management/responses' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the responses you have received for all your surveys.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Optional Query Params
|
||||
<Properties>
|
||||
<Property name="surveyId" type="string">
|
||||
SurveyId to filter responses by.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/responses' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data":[
|
||||
{
|
||||
"id":"cllnfybxd051rpl0gieavthr9",
|
||||
"createdAt":"2023-08-23T07:56:33.121Z",
|
||||
"updatedAt":"2023-08-23T07:56:41.793Z",
|
||||
"surveyId":"cllnfy2780fromy0hy7uoxvtn",
|
||||
"finished":true,
|
||||
"data":{
|
||||
"ec7agikkr58j8uonhioinkyk":"Loved it!",
|
||||
"gml6mgy71efgtq8np3s9je5p":"clicked",
|
||||
"klvpwd4x08x8quesihvw5l92":"Product Manager",
|
||||
"kp62fbqe8cfzmvy8qwpr81b2":"Very disappointed",
|
||||
"lkjaxb73ulydzeumhd51sx9g":"Not interesed.",
|
||||
"ryo75306flyg72iaeditbv51":"Would love if it had dark theme."
|
||||
},
|
||||
"meta":{
|
||||
"url":"https://app.formbricks.com/s/cllnfy2780fromy0hy7uoxvtn",
|
||||
"userAgent":{
|
||||
"os":"Linux",
|
||||
"browser":"Chrome"
|
||||
}
|
||||
},
|
||||
"personAttributes":null,
|
||||
"person":null,
|
||||
"notes":[],
|
||||
"tags":[]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '404 Not Found' }}
|
||||
{
|
||||
"code": "not_found",
|
||||
"message": "cllnfy2780fromy0hy7uoxvt not found",
|
||||
"details": {
|
||||
"resource_id": "survey",
|
||||
"resource_type": "cllnfy2780fromy0hy7uoxvt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Create a response {{ tag: 'POST', label: '/api/v1/client/responses' }}
|
||||
|
||||
Add a new response to a survey.
|
||||
@@ -222,7 +124,7 @@ Add a new response to a survey.
|
||||
|
||||
---
|
||||
|
||||
## Update a response {{ tag: 'POST', label: '/api/v1/client/responses/[responseId]' }}
|
||||
## Update a response {{ tag: 'POST', label: '/api/v1/client/responses/<response-id>' }}
|
||||
|
||||
Update an existing response in a survey.
|
||||
|
||||
@@ -247,10 +149,10 @@ Update an existing response in a survey.
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/client/responses/[responseId]">
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/client/responses/<response-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses/[responseId]' \
|
||||
curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses/<response-id>' \
|
||||
--data-raw '{
|
||||
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||
@@ -0,0 +1,301 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks People API: Fetch or Create Person Overview",
|
||||
description:
|
||||
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Action Classes API
|
||||
|
||||
This set of API can be used to
|
||||
- [List Actions](#get-all-action-classes)
|
||||
- [Get Action](#get-action-class-by-id)
|
||||
- [Create Actions](#create-action-class)
|
||||
- [Delete Actions](#delete-action-class)
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## Get all Action Classes {{ tag: 'GET', label: '/api/v1/management/action-classes' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get all the existing action classes in your environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/action-classes' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "cln8k0t47000gz87nw4ibwv35",
|
||||
"createdAt": "2023-10-02T07:13:19.207Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.207Z",
|
||||
"name": "New Session",
|
||||
"description": "Gets fired when a new session is created",
|
||||
"type": "automatic",
|
||||
"noCodeConfig": null,
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
},
|
||||
{
|
||||
"id": "cln8k0t55000uz87noerwdooj",
|
||||
"createdAt": "2023-10-02T07:13:19.241Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.241Z",
|
||||
"name": "Invited Team Member",
|
||||
"description": "Person invited a team member",
|
||||
"type": "noCode",
|
||||
"noCodeConfig": {
|
||||
"type": "innerHtml",
|
||||
"innerHtml": {
|
||||
"value": "Add Team Member"
|
||||
}
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Action Class by ID {{ tag: 'GET', label: '/api/v1/management/action-classes/<action-class-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Fetch an action class by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes/<action-class-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/action-classes/<action-class-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln8k0t55000uz87noerwdooj",
|
||||
"createdAt": "2023-10-02T07:13:19.241Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.241Z",
|
||||
"name": "Invited Team Member",
|
||||
"description": "Person invited a team member",
|
||||
"type": "noCode",
|
||||
"noCodeConfig": {
|
||||
"type": "innerHtml",
|
||||
"innerHtml": {
|
||||
"value": "Add Team Member"
|
||||
}
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Create Action Class {{ tag: 'POST', label: '/api/v1/management/action-classes/' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Create an action class.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Body
|
||||
|
||||
<CodeGroup title="Request Body">
|
||||
```json {{ title: 'cURL' }}
|
||||
{
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"name": "My Action from API",
|
||||
"type": "code"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/management/action-classes/">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X POST https://app.formbricks.com/api/v1/management/action-classes/ \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'x-api-key: <your-api-key>' \
|
||||
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Action from API", "type": "code"}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln9w1cno0008z8zu79nk5w0c",
|
||||
"createdAt": "2023-10-03T05:37:26.100Z",
|
||||
"updatedAt": "2023-10-03T05:37:26.100Z",
|
||||
"name": "My Action from API",
|
||||
"description": null,
|
||||
"type": "code",
|
||||
"noCodeConfig": null,
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Delete Action Class {{ tag: 'DELETE', label: '/api/v1/management/action-classes/<action-class-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Delete an action class by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/action-classes/<action-class-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE https://app.formbricks.com/api/v1/management/action-classes/<action-class-id> \
|
||||
--header 'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln9w1cno0008z8zu79nk5w0c",
|
||||
"createdAt": "2023-10-03T05:37:26.100Z",
|
||||
"updatedAt": "2023-10-03T05:37:26.100Z",
|
||||
"name": "My Action from API",
|
||||
"description": null,
|
||||
"type": "code",
|
||||
"noCodeConfig": null,
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,292 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks People API: Fetch or Create Person Overview",
|
||||
description:
|
||||
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interAttributes while maintaining data privacy.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Attribute Classes API
|
||||
|
||||
This set of API can be used to
|
||||
- [List Attributes](#get-all-attribute-classes)
|
||||
- [Get Attributes](#get-attribute-class-by-id)
|
||||
- [Create Attributes](#create-attribute-class)
|
||||
- [Delete Attributes](#delete-attribute-class)
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## Get all Attribute Classes {{ tag: 'GET', label: '/api/v1/management/attribute-classes' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get all the existing attribute classes in your environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/attribute-classes' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "cln8k0t47000kz87n3lh23zf0",
|
||||
"createdAt": "2023-10-02T07:13:19.207Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.207Z",
|
||||
"name": "email",
|
||||
"description": "The email of the person",
|
||||
"archived": false,
|
||||
"type": "automatic",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
},
|
||||
{
|
||||
"id": "cln8k0t55000xz87nrtwbo7sf",
|
||||
"createdAt": "2023-10-02T07:13:19.241Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.241Z",
|
||||
"name": "Name",
|
||||
"description": "Full Name of the Person",
|
||||
"archived": false,
|
||||
"type": "code",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Attribute Class by ID {{ tag: 'GET', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Fetch an Attribute class by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes/<attribute-class-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln8k0t47000jz87nfwcey6mh",
|
||||
"createdAt": "2023-10-02T07:13:19.207Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.207Z",
|
||||
"name": "userId",
|
||||
"description": "The internal ID of the person",
|
||||
"archived": false,
|
||||
"type": "automatic",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Create Attribute Class {{ tag: 'POST', label: '/api/v1/management/attribute-classes/' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Create an Attribute class.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Body
|
||||
|
||||
<CodeGroup title="Request Body">
|
||||
```json {{ title: 'cURL' }}
|
||||
{
|
||||
"environmentId": "clmlmwdqq0003196ufewo6ibg",
|
||||
"name": "My Attribute from API",
|
||||
"type": "code",
|
||||
"description": "My description"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/management/attribute-classes/">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X POST https://app.formbricks.com/api/v1/management/attribute-classes/ \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'x-api-key: <your-api-key>' \
|
||||
-d '{"environmentId": "clmlmwdqq0003196ufewo6ibg", "name": "My Attribute from API", "type": "code", "description":"My description"}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "clna0hd7z0009z8zue2z3a7wy",
|
||||
"createdAt": "2023-10-03T07:41:51.792Z",
|
||||
"updatedAt": "2023-10-03T07:41:51.792Z",
|
||||
"name": "My Attribute from API",
|
||||
"description": null,
|
||||
"archived": false,
|
||||
"type": "code",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Delete Attribute Class {{ tag: 'DELETE', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Delete an Attribute class by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/attribute-classes/<attribute-class-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id> \
|
||||
--header 'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "clna0hd7z0009z8zue2z3a7wy",
|
||||
"createdAt": "2023-10-03T07:41:51.792Z",
|
||||
"updatedAt": "2023-10-03T07:41:51.792Z",
|
||||
"name": "My Attribute from API",
|
||||
"description": null,
|
||||
"archived": false,
|
||||
"type": "code",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
78
apps/formbricks-com/app/docs/api/management/me/page.mdx
Normal file
78
apps/formbricks-com/app/docs/api/management/me/page.mdx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks People API: Fetch or Create Person Overview",
|
||||
description:
|
||||
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Me API
|
||||
|
||||
This API can be used to get your own current environment details.
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## Get Environment {{ tag: 'GET', label: '/api/v1/management/me' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get your current environment details.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/me">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/me' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"id": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.207Z",
|
||||
"updatedAt": "2023-10-02T07:14:14.162Z",
|
||||
"type": "production",
|
||||
"product": {
|
||||
"id": "cln8k0t47000ez87n57aqywvz",
|
||||
"name": "Demo Product"
|
||||
},
|
||||
"widgetSetupCompleted": true
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Unauthorized' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
234
apps/formbricks-com/app/docs/api/management/people/page.mdx
Normal file
234
apps/formbricks-com/app/docs/api/management/people/page.mdx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks People API: Fetch or Create Person Overview",
|
||||
description:
|
||||
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# People API
|
||||
|
||||
This set of API can be used to
|
||||
- [List People](#list-people)
|
||||
- [Get Person](#get-person)
|
||||
- [Delete Person](#delete-person)
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## List People {{ tag: 'GET', label: '/api/v1/management/people' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
List People
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/people' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "b4wgrzl363dn3zb6yy5gf265",
|
||||
"attributes": {
|
||||
"userId": "CYO618",
|
||||
"email": "sophia@amazon.com",
|
||||
"Name": "Sophia Johnson",
|
||||
"Role": "Designer",
|
||||
"Company": "Amazon",
|
||||
"Experience": "7 years",
|
||||
"Usage Frequency": "Yearly",
|
||||
"Company Size": "1628 employees",
|
||||
"Product Satisfaction Score": "62",
|
||||
"Recommendation Likelihood": "9"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
{
|
||||
"id": "jrb5iyzqvnkg9322ckhde3j4",
|
||||
"attributes": {
|
||||
"userId": "CYO511",
|
||||
"email": "antonio@ibm.com",
|
||||
"Name": "Antonio García",
|
||||
"Role": "Designer",
|
||||
"Company": "IBM",
|
||||
"Experience": "1 years",
|
||||
"Usage Frequency": "Weekly",
|
||||
"Company Size": "4023 employees",
|
||||
"Product Satisfaction Score": "77",
|
||||
"Recommendation Likelihood": "4"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '400 Bad Request' }}
|
||||
{
|
||||
"code": "bad_request",
|
||||
"message": "Fields are missing or incorrectly formatted",
|
||||
"details": {
|
||||
"userId": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Person {{ tag: 'GET', label: '/api/v1/management/people/<person-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get Person by ID
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people/<person-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/people/<person-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "jrb5iyzqvnkg9322ckhde3j4",
|
||||
"attributes": {
|
||||
"userId": "CYO511",
|
||||
"email": "antonio@ibm.com",
|
||||
"Name": "Antonio García",
|
||||
"Role": "Designer",
|
||||
"Company": "IBM",
|
||||
"Experience": "1 years",
|
||||
"Usage Frequency": "Weekly",
|
||||
"Company Size": "4023 employees",
|
||||
"Product Satisfaction Score": "77",
|
||||
"Recommendation Likelihood": "4"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '404 Not Found' }}
|
||||
{
|
||||
"code": "not_found",
|
||||
"message": "Person not found",
|
||||
"details": {
|
||||
"resource_id": "clmlmykc2000019vz5o3jglsa",
|
||||
"resource_type": "Person"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Delete Person {{ tag: 'DELETE', label: '/api/v1/management/people/<person-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Delete Person by ID
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/people/<person-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE https://app.formbricks.com/api/v1/management/people/<person-id> \
|
||||
--header 'x-api-key: <your-api-key>'
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"success": "Person deleted successfully"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '404 Not Found' }}
|
||||
{
|
||||
"code": "not_found",
|
||||
"message": "Person not found",
|
||||
"details": {
|
||||
"resource_id": "clmlmykc2000019vz5o3jglsa",
|
||||
"resource_type": "Person"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
289
apps/formbricks-com/app/docs/api/management/responses/page.mdx
Normal file
289
apps/formbricks-com/app/docs/api/management/responses/page.mdx
Normal file
@@ -0,0 +1,289 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly",
|
||||
description:
|
||||
"Unlock the full potential of Formbricks' Responses API. From fetching to updating survey responses, our comprehensive guide helps you integrate and manage survey data efficiently without compromising security. Ideal for client-side interactions.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Responses API
|
||||
|
||||
This set of API can be used to
|
||||
- [List Responses](#list-all-responses)
|
||||
- [Get Response](#get-response-by-id)
|
||||
- [Delete Response](#delete-a-response)
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## List all Responses {{ tag: 'GET', label: '/api/v1/management/responses' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the responses you have received in your environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/responses' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data":[
|
||||
{
|
||||
"id": "cln8k0tqv00pcz87no4qrw333",
|
||||
"createdAt": "2023-10-02T07:13:20.023Z",
|
||||
"updatedAt": "2023-10-02T07:13:20.023Z",
|
||||
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
|
||||
"finished": true,
|
||||
"data": {
|
||||
"interview-prompt": "clicked"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "MacOS",
|
||||
"browser": "Chrome"
|
||||
}
|
||||
},
|
||||
"personAttributes": null,
|
||||
"person": {
|
||||
"id": "e0x4i5tvsp8puxfztyrwykvn",
|
||||
"attributes": {
|
||||
"userId": "CYO675",
|
||||
"email": "ravi@netflix.com",
|
||||
"Name": "Ravi Kumar",
|
||||
"Role": "Manager",
|
||||
"Company": "Netflix",
|
||||
"Experience": "6 years",
|
||||
"Usage Frequency": "Monthly",
|
||||
"Company Size": "4610 employees",
|
||||
"Product Satisfaction Score": "43",
|
||||
"Recommendation Likelihood": "4"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/<response-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve a response by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses/<response-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/responses/<response-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data":
|
||||
{
|
||||
"id": "cln8k0tqv00pbz87nwo5lr72b",
|
||||
"createdAt": "2023-10-02T07:13:20.023Z",
|
||||
"updatedAt": "2023-10-02T07:13:20.023Z",
|
||||
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
|
||||
"finished": true,
|
||||
"data": {
|
||||
"interview-prompt": "clicked"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "Windows",
|
||||
"browser": "Edge"
|
||||
}
|
||||
},
|
||||
"personAttributes": null,
|
||||
"person": {
|
||||
"id": "hsx38f15v50ua8383uadagq5",
|
||||
"attributes": {
|
||||
"userId": "CYO278",
|
||||
"email": "jorge@facebook.com",
|
||||
"Name": "Jorge Sanchez",
|
||||
"Role": "Product Manager",
|
||||
"Company": "Facebook",
|
||||
"Experience": "10 years",
|
||||
"Usage Frequency": "Daily",
|
||||
"Company Size": "1685 employees",
|
||||
"Product Satisfaction Score": "84",
|
||||
"Recommendation Likelihood": "6"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Delete a response {{ tag: 'DELETE', label: '/api/v1/client/responses/<response-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Delete Response by ID
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/client/responses/<response-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE https://app.formbricks.com/api/v1/management/resposnes/<response-id> \
|
||||
--header 'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{ title: '200 Success' }}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln8k0tqv00pbz87nwo5lr72b",
|
||||
"createdAt": "2023-10-02T07:13:20.023Z",
|
||||
"updatedAt": "2023-10-02T07:13:20.023Z",
|
||||
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
|
||||
"finished": true,
|
||||
"data": {
|
||||
"interview-prompt": "clicked"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "Windows",
|
||||
"browser": "Edge"
|
||||
}
|
||||
},
|
||||
"personAttributes": null,
|
||||
"person": {
|
||||
"id": "hsx38f15v50ua8383uadagq5",
|
||||
"attributes": {
|
||||
"userId": "CYO278",
|
||||
"email": "jorge@facebook.com",
|
||||
"Name": "Jorge Sanchez",
|
||||
"Role": "Product Manager",
|
||||
"Company": "Facebook",
|
||||
"Experience": "10 years",
|
||||
"Usage Frequency": "Daily",
|
||||
"Company Size": "1685 employees",
|
||||
"Product Satisfaction Score": "84",
|
||||
"Recommendation Likelihood": "6"
|
||||
},
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"createdAt": "2023-10-02T07:13:19.444Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.444Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '400 Bad Request' }}
|
||||
{
|
||||
"code": "bad_request",
|
||||
"message": "surveyId was not provided.",
|
||||
"details": {
|
||||
"surveyId": "This field is required."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
667
apps/formbricks-com/app/docs/api/management/surveys/page.mdx
Normal file
667
apps/formbricks-com/app/docs/api/management/surveys/page.mdx
Normal file
@@ -0,0 +1,667 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys",
|
||||
description:
|
||||
"Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Surveys API
|
||||
|
||||
This set of API can be used to
|
||||
- [List All Surveys](#list-all-surveys)
|
||||
- [Get Survey](#get-survey-by-id)
|
||||
- [Create Survey](#create-survey)
|
||||
- [Delete Survey](#delete-survey-by-id)
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the surveys you have for the environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "cllnfy2780fromy0hy7uoxvtn",
|
||||
"createdAt": "2023-08-23T07:56:20.516Z",
|
||||
"updatedAt": "2023-08-23T07:56:26.947Z",
|
||||
"name": "Product Market Fit (Superhuman)",
|
||||
"type": "link",
|
||||
"environmentId": "cll2m30r70004mx0huqkitgqv",
|
||||
"status": "inProgress",
|
||||
"attributeFilters": [],
|
||||
"displayOption": "displayOnce",
|
||||
"autoClose": null,
|
||||
"triggers": [],
|
||||
"redirectUrl": null,
|
||||
"recontactDays": null,
|
||||
"questions": [
|
||||
{
|
||||
"id": "gml6mgy71efgtq8np3s9je5p",
|
||||
"type": "cta",
|
||||
"headline": "You are one of our power users! Do you have 5 minutes?",
|
||||
"required": false,
|
||||
"buttonLabel": "Happy to help!",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "skipped",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot!</span></p>",
|
||||
"buttonExternal": false,
|
||||
"dismissButtonLabel": "No, thanks."
|
||||
},
|
||||
{
|
||||
"id": "kp62fbqe8cfzmvy8qwpr81b2",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "How disappointed would you be if you could no longer use My Product?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "bdgy1hnwd7uwmfxk1ljqp1n5",
|
||||
"label": "Not at all disappointed"
|
||||
},
|
||||
{
|
||||
"id": "poabnvgtwenp8rb2v70gj4hj",
|
||||
"label": "Somewhat disappointed"
|
||||
},
|
||||
{
|
||||
"id": "opfiqyqz8wrqn0i0f7t24d3n",
|
||||
"label": "Very disappointed"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "klvpwd4x08x8quesihvw5l92",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "What is your role?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "c8nerw6l9gpsxcmqkn10f9hy",
|
||||
"label": "Founder"
|
||||
},
|
||||
{
|
||||
"id": "ebjqezei6a2axtuq86cleetn",
|
||||
"label": "Executive"
|
||||
},
|
||||
{
|
||||
"id": "ctiijjblyhlp22snypfamqt1",
|
||||
"label": "Product Manager"
|
||||
},
|
||||
{
|
||||
"id": "ibalyr0mhemfkkr82vypmg40",
|
||||
"label": "Product Owner"
|
||||
},
|
||||
{
|
||||
"id": "fipk606aegslbd0e7yhc0xjx",
|
||||
"label": "Software Engineer"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "ryo75306flyg72iaeditbv51",
|
||||
"type": "openText",
|
||||
"headline": "What type of people do you think would most benefit from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "lkjaxb73ulydzeumhd51sx9g",
|
||||
"type": "openText",
|
||||
"headline": "What is the main benefit your receive from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "ec7agikkr58j8uonhioinkyk",
|
||||
"type": "openText",
|
||||
"headline": "How can we improve My Product for you?",
|
||||
"subheader": "Please be as specific as possible.",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"thankYouCard": {
|
||||
"enabled": true,
|
||||
"headline": "Thank you!",
|
||||
"subheader": "We appreciate your feedback."
|
||||
},
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Get Survey by ID {{ tag: 'GET', label: '/api/v1/management/surveys/<survey-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Get a specific survey by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys/<survey-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln8k0tjz00n5z87nwq527h3z",
|
||||
"createdAt": "2023-10-02T07:13:19.775Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.775Z",
|
||||
"name": "Churn Survey",
|
||||
"type": "link",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"status": "inProgress",
|
||||
"attributeFilters": [],
|
||||
"displayOption": "displayOnce",
|
||||
"autoClose": null,
|
||||
"triggers": [],
|
||||
"redirectUrl": null,
|
||||
"recontactDays": null,
|
||||
"questions": [
|
||||
{
|
||||
"id": "churn-reason",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "Why did you cancel your subscription?",
|
||||
"subheader": "We're sorry to see you leave. Help us do better:",
|
||||
"required": true,
|
||||
"logic": [
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "Difficult to use",
|
||||
"destination": "easier-to-use"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "It's too expensive",
|
||||
"destination": "30-off"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "I am missing features",
|
||||
"destination": "missing-features"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "Poor customer service",
|
||||
"destination": "poor-service"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "I just didn't need it anymore",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"choices": [
|
||||
{
|
||||
"id": "isud2xethsw63dlwl89kr4kj",
|
||||
"label": "Difficult to use"
|
||||
},
|
||||
{
|
||||
"id": "opuu4ba3dlele3n0gjkuh27c",
|
||||
"label": "It's too expensive"
|
||||
},
|
||||
{
|
||||
"id": "gnypapo0rhvkt8pwosrphvbl",
|
||||
"label": "I am missing features"
|
||||
},
|
||||
{
|
||||
"id": "wkgsrsrazd9kfunqhzjezx6t",
|
||||
"label": "Poor customer service"
|
||||
},
|
||||
{
|
||||
"id": "pykmgyyw74vg0gaeryj6bo4c",
|
||||
"label": "I just didn't need it anymore"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "easier-to-use",
|
||||
"type": "openText",
|
||||
"headline": "What would have made {{productName}} easier to use?",
|
||||
"subheader": "",
|
||||
"required": true,
|
||||
"buttonLabel": "Send",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "submitted",
|
||||
"destination": "end"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "30-off",
|
||||
"type": "cta",
|
||||
"headline": "Get 30% off for the next year!",
|
||||
"required": true,
|
||||
"buttonLabel": "Get 30% off",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "clicked",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
|
||||
"buttonUrl": "https://formbricks.com",
|
||||
"buttonExternal": true,
|
||||
"dismissButtonLabel": "Skip"
|
||||
},
|
||||
{
|
||||
"id": "missing-features",
|
||||
"type": "openText",
|
||||
"headline": "What features are you missing?",
|
||||
"subheader": "",
|
||||
"required": true,
|
||||
"logic": [
|
||||
{
|
||||
"condition": "submitted",
|
||||
"destination": "end"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "poor-service",
|
||||
"type": "cta",
|
||||
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
|
||||
"required": true,
|
||||
"buttonLabel": "Send email to CEO",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "clicked",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
|
||||
"buttonUrl": "mailto:ceo@company.com",
|
||||
"buttonExternal": true,
|
||||
"dismissButtonLabel": "Skip"
|
||||
}
|
||||
],
|
||||
"thankYouCard": {
|
||||
"enabled": false
|
||||
},
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null,
|
||||
"surveyClosedMessage": null,
|
||||
"verifyEmail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
## Create Survey {{ tag: 'POST', label: '/api/v1/management/surveys' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Create a survey
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Body
|
||||
|
||||
<CodeGroup title="Request Body">
|
||||
```json {{ title: 'cURL' }}
|
||||
{
|
||||
"environmentId": "clmlmwdqq0003196ufewo6ibg",
|
||||
"type": "link",
|
||||
"name": "My new Survey"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="POST" label="/api/v1/management/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE \
|
||||
'https://app.formbricks.com/api/v1/management/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X POST https://app.formbricks.com/api/v1/management/surveys/ \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'x-api-key: <your-api-key>' \
|
||||
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Survey from API", "type": "link"}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "clna6bqnz000az8zubq3e757t",
|
||||
"createdAt": "2023-10-03T10:25:26.975Z",
|
||||
"updatedAt": "2023-10-03T10:25:26.975Z",
|
||||
"name": "My new Survey",
|
||||
"redirectUrl": null,
|
||||
"type": "link",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"status": "draft",
|
||||
"questions": [],
|
||||
"thankYouCard": {
|
||||
"enabled": false
|
||||
},
|
||||
"displayOption": "displayOnce",
|
||||
"recontactDays": null,
|
||||
"autoClose": null,
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null,
|
||||
"surveyClosedMessage": null,
|
||||
"verifyEmail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Delete a survey by its ID.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/surveys/<survey-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X DELETE \
|
||||
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"id": "cln8k0tjz00n5z87nwq527h3z",
|
||||
"createdAt": "2023-10-02T07:13:19.775Z",
|
||||
"updatedAt": "2023-10-02T07:13:19.775Z",
|
||||
"name": "Churn Survey",
|
||||
"type": "link",
|
||||
"environmentId": "cln8k0t47000fz87njmmu2bck",
|
||||
"status": "inProgress",
|
||||
"attributeFilters": [],
|
||||
"displayOption": "displayOnce",
|
||||
"autoClose": null,
|
||||
"triggers": [],
|
||||
"redirectUrl": null,
|
||||
"recontactDays": null,
|
||||
"questions": [
|
||||
{
|
||||
"id": "churn-reason",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "Why did you cancel your subscription?",
|
||||
"subheader": "We're sorry to see you leave. Help us do better:",
|
||||
"required": true,
|
||||
"logic": [
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "Difficult to use",
|
||||
"destination": "easier-to-use"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "It's too expensive",
|
||||
"destination": "30-off"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "I am missing features",
|
||||
"destination": "missing-features"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "Poor customer service",
|
||||
"destination": "poor-service"
|
||||
},
|
||||
{
|
||||
"condition": "equals",
|
||||
"value": "I just didn't need it anymore",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"choices": [
|
||||
{
|
||||
"id": "isud2xethsw63dlwl89kr4kj",
|
||||
"label": "Difficult to use"
|
||||
},
|
||||
{
|
||||
"id": "opuu4ba3dlele3n0gjkuh27c",
|
||||
"label": "It's too expensive"
|
||||
},
|
||||
{
|
||||
"id": "gnypapo0rhvkt8pwosrphvbl",
|
||||
"label": "I am missing features"
|
||||
},
|
||||
{
|
||||
"id": "wkgsrsrazd9kfunqhzjezx6t",
|
||||
"label": "Poor customer service"
|
||||
},
|
||||
{
|
||||
"id": "pykmgyyw74vg0gaeryj6bo4c",
|
||||
"label": "I just didn't need it anymore"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "easier-to-use",
|
||||
"type": "openText",
|
||||
"headline": "What would have made {{productName}} easier to use?",
|
||||
"subheader": "",
|
||||
"required": true,
|
||||
"buttonLabel": "Send",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "submitted",
|
||||
"destination": "end"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "30-off",
|
||||
"type": "cta",
|
||||
"headline": "Get 30% off for the next year!",
|
||||
"required": true,
|
||||
"buttonLabel": "Get 30% off",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "clicked",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
|
||||
"buttonUrl": "https://formbricks.com",
|
||||
"buttonExternal": true,
|
||||
"dismissButtonLabel": "Skip"
|
||||
},
|
||||
{
|
||||
"id": "missing-features",
|
||||
"type": "openText",
|
||||
"headline": "What features are you missing?",
|
||||
"subheader": "",
|
||||
"required": true,
|
||||
"logic": [
|
||||
{
|
||||
"condition": "submitted",
|
||||
"destination": "end"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "poor-service",
|
||||
"type": "cta",
|
||||
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
|
||||
"required": true,
|
||||
"buttonLabel": "Send email to CEO",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "clicked",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
|
||||
"buttonUrl": "mailto:ceo@company.com",
|
||||
"buttonExternal": true,
|
||||
"dismissButtonLabel": "Skip"
|
||||
}
|
||||
],
|
||||
"thankYouCard": {
|
||||
"enabled": false
|
||||
},
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null,
|
||||
"surveyClosedMessage": null,
|
||||
"verifyEmail": null
|
||||
}
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
@@ -1,30 +1,31 @@
|
||||
export const meta = {
|
||||
title: "Formbricks Webhook API Documentation - List, Retrieve, Create, and Delete Webhooks",
|
||||
description: "Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice",
|
||||
description:
|
||||
"Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice",
|
||||
};
|
||||
|
||||
#### Webhook API
|
||||
#### Management API
|
||||
|
||||
# Webhook API
|
||||
|
||||
Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks in Formbricks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment.
|
||||
|
||||
Before you start managing webhooks, you need to create an API key. This will be used for authorization when making requests to the Webhook API. Please see the [API key setup page](/docs/api/api-key-setup) for more details on how to create and manage your API keys.
|
||||
Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment.
|
||||
|
||||
The behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include "responseCreated", "responseUpdated", and "responseFinished". This allows you to customize your webhooks to only send notifications for the events that are relevant to your application.
|
||||
|
||||
Webhooks are tied to a specific Formbricks environment. Once set, a webhook will receive updates from all surveys within this environment. This makes it easy to manage your data flow and ensure that all relevant updates are caught by the webhook.
|
||||
|
||||
Our API has several REST endpoints enabling you to manage these webhooks, providing a great deal of flexibility:
|
||||
This set of API can be used to
|
||||
- [List All Webhooks](#list-webhooks)
|
||||
- [Get Webhook](#retrieve-webhook-by-id)
|
||||
- [Create Webhook](#create-webhook)
|
||||
- [Delete Webhook](#delete-webhook-by-id)
|
||||
|
||||
1. **List Webhooks:** Retrieve a list of all existing webhooks.
|
||||
1. **Retrieve Webhook by ID:** Retrieve a specific webhook by it's ID.
|
||||
1. **Create a New Webhook:** Add a new webhook to your system.
|
||||
1. **Get a Specific Webhook:** Query the details of a specific webhook using its unique ID.
|
||||
1. **Delete a Webhook:** Remove an existing webhook.
|
||||
And the detailed Webhook Paylod is elaborated [here](#webhook-payload).
|
||||
|
||||
These APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment.
|
||||
|
||||
<Note>You will need an API Key to interact with these APIs.</Note>
|
||||
|
||||
---
|
||||
|
||||
## List Webhooks {{ tag: 'GET', label: '/api/v1/webhooks' }}
|
||||
@@ -92,7 +93,7 @@ These APIs are designed to facilitate seamless integration of Formbricks with th
|
||||
|
||||
---
|
||||
|
||||
## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/[webhookId]' }}
|
||||
## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/<webhook-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -108,11 +109,11 @@ These APIs are designed to facilitate seamless integration of Formbricks with th
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks/[webhookId]">
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks/<webhook-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/webhooks/[webhookId]' \
|
||||
'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
@@ -255,7 +256,7 @@ Add a webhook to your product.
|
||||
|
||||
---
|
||||
|
||||
## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/[webhookId]' }}
|
||||
## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/<webhook-id>' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -271,10 +272,10 @@ Add a webhook to your product.
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/webhooks/[webhookId]">
|
||||
<CodeGroup title="Request" tag="DELETE" label="/api/v1/webhooks/<webhook-id>">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/[webhookId]' \
|
||||
curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
|
||||
--header 'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks People API: Fetch or Create Person Overview",
|
||||
description:
|
||||
"Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# People API
|
||||
|
||||
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
---
|
||||
|
||||
## Get or Create Person {{ tag: 'GET', label: '/api/v1/client/people/getOrCreate' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Fetch a Person or create (if they don't exist) by their userId and the environmentId.
|
||||
|
||||
### Mandatory Query Parameters
|
||||
<Properties>
|
||||
<Property name="userId" type="string">
|
||||
User ID to fetch or create (if it does not exist)
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
<Properties>
|
||||
<Property name="enviornmentId" type="string">
|
||||
Formbricks Environment ID
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/client/people/getOrCreate">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/client/people/getOrCreate?userId=<user-id>&environmentId=<env-id>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": {
|
||||
"person": {
|
||||
"id": "clm4bcxms02hspj0ht05k6q5c",
|
||||
"environmentId": "cll2m30r70004mx0huqkitgqv"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json {{ title: '400 Bad Request' }}
|
||||
{
|
||||
"code": "bad_request",
|
||||
"message": "Fields are missing or incorrectly formatted",
|
||||
"details": {
|
||||
"userId": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
@@ -1,180 +0,0 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys",
|
||||
description:
|
||||
"Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.",
|
||||
};
|
||||
|
||||
#### Management API
|
||||
|
||||
# Surveys API
|
||||
|
||||
The Survey API currently has one endpoint that allows you to get all the surveys you have in your Formbricks environment. You will need the [API Key](/docs/api/api-key-setup) to access the same!
|
||||
|
||||
---
|
||||
|
||||
## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the surveys you have for the environment.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
<Properties>
|
||||
<Property name="x-Api-Key" type="string">
|
||||
Your Formbricks API key.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl --location \
|
||||
'https://app.formbricks.com/api/v1/management/surveys' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<CodeGroup title="Response">
|
||||
|
||||
```json {{title:'200 Success'}}
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "cllnfy2780fromy0hy7uoxvtn",
|
||||
"createdAt": "2023-08-23T07:56:20.516Z",
|
||||
"updatedAt": "2023-08-23T07:56:26.947Z",
|
||||
"name": "Product Market Fit (Superhuman)",
|
||||
"type": "link",
|
||||
"environmentId": "cll2m30r70004mx0huqkitgqv",
|
||||
"status": "inProgress",
|
||||
"attributeFilters": [],
|
||||
"displayOption": "displayOnce",
|
||||
"autoClose": null,
|
||||
"triggers": [],
|
||||
"redirectUrl": null,
|
||||
"recontactDays": null,
|
||||
"questions": [
|
||||
{
|
||||
"id": "gml6mgy71efgtq8np3s9je5p",
|
||||
"type": "cta",
|
||||
"headline": "You are one of our power users! Do you have 5 minutes?",
|
||||
"required": false,
|
||||
"buttonLabel": "Happy to help!",
|
||||
"logic": [
|
||||
{
|
||||
"condition": "skipped",
|
||||
"destination": "end"
|
||||
}
|
||||
],
|
||||
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot!</span></p>",
|
||||
"buttonExternal": false,
|
||||
"dismissButtonLabel": "No, thanks."
|
||||
},
|
||||
{
|
||||
"id": "kp62fbqe8cfzmvy8qwpr81b2",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "How disappointed would you be if you could no longer use My Product?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "bdgy1hnwd7uwmfxk1ljqp1n5",
|
||||
"label": "Not at all disappointed"
|
||||
},
|
||||
{
|
||||
"id": "poabnvgtwenp8rb2v70gj4hj",
|
||||
"label": "Somewhat disappointed"
|
||||
},
|
||||
{
|
||||
"id": "opfiqyqz8wrqn0i0f7t24d3n",
|
||||
"label": "Very disappointed"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "klvpwd4x08x8quesihvw5l92",
|
||||
"type": "multipleChoiceSingle",
|
||||
"headline": "What is your role?",
|
||||
"subheader": "Please select one of the following options:",
|
||||
"required": true,
|
||||
"choices": [
|
||||
{
|
||||
"id": "c8nerw6l9gpsxcmqkn10f9hy",
|
||||
"label": "Founder"
|
||||
},
|
||||
{
|
||||
"id": "ebjqezei6a2axtuq86cleetn",
|
||||
"label": "Executive"
|
||||
},
|
||||
{
|
||||
"id": "ctiijjblyhlp22snypfamqt1",
|
||||
"label": "Product Manager"
|
||||
},
|
||||
{
|
||||
"id": "ibalyr0mhemfkkr82vypmg40",
|
||||
"label": "Product Owner"
|
||||
},
|
||||
{
|
||||
"id": "fipk606aegslbd0e7yhc0xjx",
|
||||
"label": "Software Engineer"
|
||||
}
|
||||
],
|
||||
"shuffleOption": "none"
|
||||
},
|
||||
{
|
||||
"id": "ryo75306flyg72iaeditbv51",
|
||||
"type": "openText",
|
||||
"headline": "What type of people do you think would most benefit from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "lkjaxb73ulydzeumhd51sx9g",
|
||||
"type": "openText",
|
||||
"headline": "What is the main benefit your receive from My Product?",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "ec7agikkr58j8uonhioinkyk",
|
||||
"type": "openText",
|
||||
"headline": "How can we improve My Product for you?",
|
||||
"subheader": "Please be as specific as possible.",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"thankYouCard": {
|
||||
"enabled": true,
|
||||
"headline": "Thank you!",
|
||||
"subheader": "We appreciate your feedback."
|
||||
},
|
||||
"delay": 0,
|
||||
"autoComplete": null,
|
||||
"closeOnDate": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
"message": "Not authenticated",
|
||||
"details": {
|
||||
"x-Api-Key": "Header not provided or API Key invalid"
|
||||
}
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
@@ -49,7 +49,7 @@ Server actions are used to perform server actions in client components. For exam
|
||||
|
||||
## Use service abstraction instead of direct database calls
|
||||
|
||||
We utilize [prisma](https://www.prisma.io/) as our Object-Relational Mapping (ORM) tool to interact with the database. This implies that when you need to fetch or modify data in the database, you will be utilizing prisma. All prisma calls should be written in the services folder `packages/lib/services`, and before creating a new service, please ensure that one does not already exist.
|
||||
We utilize [prisma](https://www.prisma.io/) as our Object-Relational Mapping (ORM) tool to interact with the database. This implies that when you need to fetch or modify data in the database, you will be utilizing prisma. All prisma calls should be written in the services folder `packages/lib`, and before creating a new service, please ensure that one does not already exist.
|
||||
|
||||
## Handle authentication and CORS in management APIs
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const meta = {
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
Here you'll find help with Frequently Recurring Problems
|
||||
Here you'll find help with frequently recurring problems
|
||||
|
||||
## "The app doesn't work after doing a prisma migration"
|
||||
|
||||
@@ -24,7 +24,7 @@ This can happen but fear not, the fix is easy: Delete the application storage of
|
||||
src={ClearAppData}
|
||||
alt="Demo App Preview"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## "I ran 'pnpm i' but there seems to be an error with the packages"
|
||||
@@ -74,9 +74,9 @@ However, in our experience it's better to run `pnpm dev` than having two termina
|
||||
src={UncaughtPromise}
|
||||
alt="Uncaught promise"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
This happens when you're using the Demo App and delete the Person within the Formbricks app which the widget is currently connected with. We're fixing it, but you can also just logout your test person and reload the page to get rid of it.
|
||||
|
||||
<Image src={Logout} alt="Logout Person" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image src={Logout} alt="Logout Person" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
14
apps/formbricks-com/app/docs/faq/page.mdx
Normal file
14
apps/formbricks-com/app/docs/faq/page.mdx
Normal file
@@ -0,0 +1,14 @@
|
||||
import FAQ from "@/components/docs/docsFaq";
|
||||
|
||||
export const meta = {
|
||||
title: "FAQ",
|
||||
description: "Frequently Asked Questions about Formbricks and how to use it.",
|
||||
};
|
||||
|
||||
#### FAQ
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
Here you'll find help with frequently recurring problems. If you can't find an answer to your question, please join our [Discord server](https://formbricks.com/discord).
|
||||
|
||||
<FAQ />
|
||||
@@ -157,8 +157,8 @@ To remove the integration with Google Account,
|
||||
For the above, we ask for:
|
||||
|
||||
1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25))
|
||||
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L13))
|
||||
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L70))
|
||||
1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L13))
|
||||
1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/googleSheet/service.ts#L70))
|
||||
|
||||
<Note>
|
||||
We do not store any other information of yours! We value Privacy more than you and rest assured you're safe
|
||||
|
||||
@@ -45,5 +45,3 @@ Natively embed qualitative user research into your B2B SaaS. Leverage Best Pract
|
||||
</div>
|
||||
|
||||
<GettingStarted />
|
||||
|
||||
<BestPractices />
|
||||
|
||||
@@ -42,16 +42,6 @@ The `userId` we are refering to is the `userId` of your own system. For example,
|
||||
|
||||
This allows you to connect the response to the user profile of this specific in the Formbricks database. You can then use the response data to create segments for further surveying or invite them to an interview, etc.
|
||||
|
||||
## getOrCreateUser - how it works exactly
|
||||
|
||||
By default, respondents of link surveys are **not** recorded and displayed in the People view. This would lead to your People view to be spammend with unidentified people.
|
||||
|
||||
If you add the `userId` URL parameter a Person will be created, if there is no existing person in your database with a matching `userId`.
|
||||
|
||||
**Case 1:** User with userId `ABC111` exists, the response from the link survey will be added to this users profile.
|
||||
|
||||
**Case 2:** User with userId `ABC222` does not yet exists, so it is created with the response from the link survey connected to this user.
|
||||
|
||||
<Image
|
||||
src={PeopleView}
|
||||
alt="If users are identified by email, it will show. If not, the internal Id will be set."
|
||||
|
||||
@@ -258,18 +258,27 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Gitpod", href: "/docs/contributing/gitpod" },
|
||||
{ title: "Demo App", href: "/docs/contributing/demo" },
|
||||
{ title: "Troubleshooting", href: "/docs/contributing/troubleshooting" },
|
||||
{ title: "FAQ", href: "/docs/faq" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "API",
|
||||
title: "Client API",
|
||||
links: [
|
||||
{ title: "Overview", href: "/docs/api/overview" },
|
||||
{ title: "API Key Setup", href: "/docs/api/api-key-setup" },
|
||||
{ title: "Displays", href: "/docs/api/display" },
|
||||
{ title: "People", href: "/docs/api/people" },
|
||||
{ title: "Responses", href: "/docs/api/responses" },
|
||||
{ title: "Surveys", href: "/docs/api/surveys" },
|
||||
{ title: "Webhook", href: "/docs/api/webhooks" },
|
||||
{ title: "Overview", href: "/docs/api/client/overview" },
|
||||
{ title: "Displays", href: "/docs/api/client/displays" },
|
||||
{ title: "Responses", href: "/docs/api/client/responses" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Management API",
|
||||
links: [
|
||||
{ title: "API Key Setup", href: "/docs/api/management/api-key-setup" },
|
||||
{ title: "Action Classes", href: "/docs/api/management/action-classes" },
|
||||
{ title: "Attribute Classes", href: "/docs/api/management/attribute-classes" },
|
||||
{ title: "Me", href: "/docs/api/management/me" },
|
||||
{ title: "People", href: "/docs/api/management/people" },
|
||||
{ title: "Surveys", href: "/docs/api/management/surveys" },
|
||||
{ title: "Webhooks", href: "/docs/api/management/webhooks" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
69
apps/formbricks-com/components/docs/docsFaq.tsx
Normal file
69
apps/formbricks-com/components/docs/docsFaq.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui";
|
||||
import FaqJsonLdComponent from "./faQJsonLD";
|
||||
|
||||
const FAQ_DATA = [
|
||||
{
|
||||
question: "What is an environment ID?",
|
||||
answer: () => (
|
||||
<>
|
||||
The environment ID is a unique identifier associated with each Environment in Formbricks,
|
||||
distinguishing between different setups like production, development, etc.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How can I implement authentication for the Formbricks API?",
|
||||
answer: () => (
|
||||
<>
|
||||
Formbricks provides 2 types of API keys for each environment ie development and production. You can
|
||||
generate, view, and manage these keys in the Settings section on the Admin dashboard. Include the API
|
||||
key in your requests to authenticate and gain access to Formbricks functionalities.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Can I run the deployment shell script on any server?",
|
||||
answer: () => (
|
||||
<>
|
||||
You can run it on any machine you own as long as its running a <b> Linux Ubuntu </b> distribution. And
|
||||
to forward the requests, make sure you have an <b>A record</b> setup for your domain pointing to the
|
||||
server.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Can I self-host Formbricks?",
|
||||
answer: () => (
|
||||
<>
|
||||
Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more
|
||||
control over data and compliance. And the best part? Self-hosting is available for free, always. For
|
||||
documentation on self hosting, click{" "}
|
||||
<a href="/docs/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export const faqJsonLdData = FAQ_DATA.map((faq) => ({
|
||||
questionName: faq.question,
|
||||
acceptedAnswerText: faq.answer(),
|
||||
}));
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<>
|
||||
<FaqJsonLdComponent data={faqJsonLdData} />
|
||||
<Accordion type="single" collapsible>
|
||||
{FAQ_DATA.map((faq, index) => (
|
||||
<AccordionItem key={`item-${index}`} value={`item-${index + 1}`} className="not-prose mb-0 mt-0">
|
||||
<AccordionTrigger>{faq.question}</AccordionTrigger>
|
||||
<AccordionContent>{faq.answer()}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
12
apps/formbricks-com/components/docs/faQJsonLD.tsx
Normal file
12
apps/formbricks-com/components/docs/faQJsonLD.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FAQPageJsonLd } from "next-seo";
|
||||
|
||||
export default function FaqJsonLdComponent({ data }) {
|
||||
const faqEntities = data.map(({ question, answer }) => ({
|
||||
questionName: question,
|
||||
acceptedAnswerText: answer,
|
||||
}));
|
||||
|
||||
return <FAQPageJsonLd mainEntity={faqEntities} />;
|
||||
}
|
||||
87
apps/formbricks-com/components/home/Faq.tsx
Normal file
87
apps/formbricks-com/components/home/Faq.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FAQPageJsonLd } from "next-seo";
|
||||
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui";
|
||||
|
||||
const FAQ_DATA = [
|
||||
{
|
||||
question: "What is Formbricks?",
|
||||
answer: () => (
|
||||
<>
|
||||
Formbricks is an open-source Experience Management tool that helps businesses understand what
|
||||
customers think and feel about their products. It integrates natively into your platform to conduct
|
||||
user research with a focus on data privacy and minimal development intervention.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How do I integrate Formbricks into my application?",
|
||||
answer: () => (
|
||||
<>
|
||||
Integrating Formbricks is a breeze. Simply copy a script tag to your HTML head, or use NPM to install
|
||||
Formbricks for platforms like React, Vue, Svelte, etc. Once installed, initialize Formbricks with your
|
||||
environment details. Learn more with our framework guides{" "}
|
||||
<a href="/docs/getting-started/framework-guides" className="text-brand-dark dark:text-brand-light">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Is Formbricks GDPR compliant?",
|
||||
answer: () => (
|
||||
<>
|
||||
Yes, Formbricks is fully GDPR compliant. Whether you use our cloud solution or decide to self-host, we
|
||||
ensure compliance with all data privacy regulations.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Can I self-host Formbricks?",
|
||||
answer: () => (
|
||||
<>
|
||||
Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more
|
||||
control over data and compliance. And the best part? Self-hosting is available for free, always. For
|
||||
documentation on self hosting, click{" "}
|
||||
<a href="/docs/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "How does Formbricks pricing work?",
|
||||
answer: () => (
|
||||
<>
|
||||
Formbricks offers a Free forever plan on the cloud that includes unlimited surveys, in-product
|
||||
surveys, and more. We also provide a self-hosting option which includes all free features and more,
|
||||
available at no cost. If you require additional features or responses, check out our pricing section
|
||||
above for more details.
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const faqJsonLdData = FAQ_DATA.map((faq) => ({
|
||||
questionName: faq.question,
|
||||
acceptedAnswerText: faq.answer(),
|
||||
}));
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<div className="max-w-7xl py-4 sm:px-6 sm:pb-6 lg:px-8" id="faq">
|
||||
<FAQPageJsonLd mainEntity={faqJsonLdData} />
|
||||
<HeadingCentered heading="Frequently Asked Questions" teaser="FAQ" closer />
|
||||
<Accordion type="single" collapsible className="px-4 sm:px-0">
|
||||
{FAQ_DATA.map((faq, index) => (
|
||||
<AccordionItem key={`item-${index}`} value={`item-${index + 1}`}>
|
||||
<AccordionTrigger>{faq.question}</AccordionTrigger>
|
||||
<AccordionContent>{faq.answer()}</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,8 @@
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "13.4.19",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "20.6.0",
|
||||
@@ -47,8 +49,6 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"prism-react-renderer": "^2.0.6",
|
||||
"prismjs": "^1.29.0",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-highlight-words": "^0.20.0",
|
||||
@@ -68,6 +68,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
"@types/react": "18.2.23",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Layout from "@/components/shared/Layout";
|
||||
import Hero from "@/components/home/Hero";
|
||||
import Faq from "@/components/home/Faq";
|
||||
import Features from "@/components/home/Features";
|
||||
import Highlights from "@/components/home/Highlights";
|
||||
import BreakerCTA from "@/components/shared/BreakerCTA";
|
||||
import Steps from "@/components/home/Steps";
|
||||
import GitHubSponsorship from "@/components/home/GitHubSponsorship";
|
||||
import Hero from "@/components/home/Hero";
|
||||
import Highlights from "@/components/home/Highlights";
|
||||
import Steps from "@/components/home/Steps";
|
||||
import BestPractices from "@/components/shared/BestPractices";
|
||||
import BreakerCTA from "@/components/shared/BreakerCTA";
|
||||
import Layout from "@/components/shared/Layout";
|
||||
|
||||
const IndexPage = () => (
|
||||
<Layout
|
||||
@@ -41,6 +42,8 @@ const IndexPage = () => (
|
||||
href="https://app.formbricks.com/auth/signup"
|
||||
inverted
|
||||
/>
|
||||
|
||||
<Faq />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
getActionCountInLast24Hours,
|
||||
getActionCountInLast7Days,
|
||||
getActionCountInLastHour,
|
||||
} from "@formbricks/lib/services/actions";
|
||||
} from "@formbricks/lib/action/service";
|
||||
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { AttributeClass } from "@prisma/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updatetAttributeClass } from "@formbricks/lib/services/attributeClass";
|
||||
import { updatetAttributeClass } from "@formbricks/lib/attributeClass/service";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AttributeSettingsTabProps {
|
||||
|
||||
@@ -5,7 +5,7 @@ import AttributeClassDataRow from "@/app/(app)/environments/[environmentId]/(act
|
||||
import AttributeTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading";
|
||||
import HowToAddAttributesButton from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -2,9 +2,9 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import Navigation from "@/app/(app)/environments/[environmentId]/Navigation";
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProducts } from "@formbricks/lib/services/product";
|
||||
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/team/service";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createMembership } from "@formbricks/lib/services/membership";
|
||||
import { createProduct } from "@formbricks/lib/services/product";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
@@ -79,6 +79,9 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
|
||||
singleUse: existingSurvey.singleUse
|
||||
? JSON.parse(JSON.stringify(existingSurvey.singleUse))
|
||||
: prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites
|
||||
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
||||
: prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail
|
||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||
: prismaClient.JsonNull,
|
||||
@@ -228,6 +231,7 @@ export async function copyToOtherEnvironmentAction(
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
|
||||
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
|
||||
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getSpreadSheets } from "@formbricks/lib/services/googleSheet";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/services/integrations";
|
||||
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
|
||||
import { createOrUpdateIntegration, deleteIntegration } from "@formbricks/lib/integration/service";
|
||||
import { TGoogleSheetIntegration } from "@formbricks/types/v1/integrations";
|
||||
|
||||
export async function upsertIntegrationAction(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import GoogleSheetWrapper from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/GoogleSheetWrapper";
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { getSpreadSheets } from "@formbricks/lib/services/googleSheet";
|
||||
import { getIntegrations } from "@formbricks/lib/services/integrations";
|
||||
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TGoogleSheetIntegration, TGoogleSpreadsheet } from "@formbricks/types/v1/integrations";
|
||||
import {
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
GOOGLE_SHEETS_CLIENT_SECRET,
|
||||
GOOGLE_SHEETS_REDIRECT_URL,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function GoogleSheet({ params }) {
|
||||
const enabled = !!(GOOGLE_SHEETS_CLIENT_ID && GOOGLE_SHEETS_CLIENT_SECRET && GOOGLE_SHEETS_REDIRECT_URL);
|
||||
|
||||
@@ -6,9 +6,9 @@ import n8nLogo from "@/images/n8n.png";
|
||||
import MakeLogo from "@/images/make-small.png";
|
||||
import { Card } from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
import { getCountOfWebhooksBasedOnSource } from "@formbricks/lib/services/webhook";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getIntegrations } from "@formbricks/lib/services/integrations";
|
||||
import { getCountOfWebhooksBasedOnSource } from "@formbricks/lib/webhook/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
|
||||
export default async function IntegrationsPage({ params }) {
|
||||
const environmentId = params.environmentId;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/services/webhook";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/v1/webhooks";
|
||||
|
||||
export const createWebhookAction = async (
|
||||
|
||||
@@ -5,9 +5,9 @@ import WebhookTable from "@/app/(app)/environments/[environmentId]/integrations/
|
||||
import WebhookTableHeading from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTableHeading";
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getWebhooks } from "@formbricks/lib/services/webhook";
|
||||
import { getWebhooks } from "@formbricks/lib/webhook/service";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function CustomWebhookPage({ params }) {
|
||||
const [webhooksUnsorted, surveys, environment] = await Promise.all([
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(activitySection)/ActivityTimeline";
|
||||
import { getActivityTimeline } from "@formbricks/lib/services/activity";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActivityTimeline } from "@formbricks/lib/activity/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function ActivitySection({
|
||||
environmentId,
|
||||
|
||||
@@ -3,9 +3,9 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
|
||||
import { capitalizeFirstLetter } from "@/lib/utils";
|
||||
import { getPerson } from "@formbricks/lib/services/person";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
|
||||
import { getSessionCount } from "@formbricks/lib/services/session";
|
||||
import { getSessionCount } from "@formbricks/lib/session/service";
|
||||
|
||||
export default async function AttributesSection({ personId }: { personId: string }) {
|
||||
const person = await getPerson(personId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import GoBackButton from "@/components/shared/GoBackButton";
|
||||
import { DeletePersonButton } from "./DeletePersonButton";
|
||||
import { getPerson } from "@formbricks/lib/services/person";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
|
||||
interface HeadingSectionProps {
|
||||
environmentId: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { deletePerson } from "@formbricks/lib/services/person";
|
||||
import { deletePerson } from "@formbricks/lib/person/service";
|
||||
|
||||
export const deletePersonAction = async (personId: string) => {
|
||||
await deletePerson(personId);
|
||||
|
||||
@@ -5,7 +5,7 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[
|
||||
import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection";
|
||||
import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function PersonPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
@@ -3,8 +3,8 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getPeople, getPeopleCount } from "@formbricks/lib/services/person";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { Pagination, PersonAvatar } from "@formbricks/ui";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import EditApiKeys from "./EditApiKeys";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getApiKeys } from "@formbricks/lib/apiKey/service";
|
||||
import { getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function ApiKeyList({
|
||||
environmentId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import ApiKeyList from "./ApiKeyList";
|
||||
import EnvironmentNotice from "@/components/shared/EnvironmentNotice";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Metadata } from "next";
|
||||
import SettingsNavbar from "./SettingsNavbar";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Settings",
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Button, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getPlacementStyle } from "@/lib/preview";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { updateProductAction } from "./actions";
|
||||
|
||||
const placements = [
|
||||
@@ -35,7 +35,9 @@ export function EditPlacement({ product }: EditPlacementProps) {
|
||||
darkOverlay: overlay === "darkOverlay",
|
||||
clickOutsideClose: clickOutside === "allow",
|
||||
};
|
||||
|
||||
await updateProductAction(product.id, inputProduct);
|
||||
|
||||
toast.success("Placement updated successfully.");
|
||||
} catch (error) {
|
||||
toast.error(`Error: ${error.message}`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { updateProduct } from "@formbricks/lib/services/product";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
|
||||
export async function updateProductAction(productId: string, inputProduct: Partial<TProductUpdateInput>) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
@@ -15,6 +15,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsTitle title="Look & Feel" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TTeam } from "@formbricks/types/v1/teams";
|
||||
import React from "react";
|
||||
import MembersInfo from "@/app/(app)/environments/[environmentId]/settings/members/EditMemberships/MembersInfo";
|
||||
import { getMembersByTeamId } from "@formbricks/lib/services/membership";
|
||||
import { getInvitesByTeamId } from "@formbricks/lib/services/invite";
|
||||
import { getMembersByTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getInvitesByTeamId } from "@formbricks/lib/invite/service";
|
||||
import { TMembership } from "@formbricks/types/v1/memberships";
|
||||
|
||||
type EditMembershipsProps = {
|
||||
|
||||
@@ -9,15 +9,15 @@ import {
|
||||
inviteUser,
|
||||
resendInvite,
|
||||
updateInvite,
|
||||
} from "@formbricks/lib/services/invite";
|
||||
} from "@formbricks/lib/invite/service";
|
||||
import {
|
||||
deleteMembership,
|
||||
getMembershipsByUserId,
|
||||
getMembershipByUserIdTeamId,
|
||||
transferOwnership,
|
||||
updateMembership,
|
||||
} from "@formbricks/lib/services/membership";
|
||||
import { deleteTeam, updateTeam } from "@formbricks/lib/services/team";
|
||||
} from "@formbricks/lib/membership/service";
|
||||
import { deleteTeam, updateTeam } from "@formbricks/lib/team/service";
|
||||
import { TInviteUpdateInput } from "@formbricks/types/v1/invites";
|
||||
import { TMembershipRole, TMembershipUpdateInput } from "@formbricks/types/v1/memberships";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import TeamActions from "@/app/(app)/environments/[environmentId]/settings/members/EditMemberships/TeamActions";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getMembershipsByUserId, getMembershipByUserIdTeamId } from "@formbricks/lib/services/membership";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getMembershipsByUserId, getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { Skeleton } from "@formbricks/ui";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { Suspense } from "react";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getProducts } from "@formbricks/lib/services/product";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import DeleteProductRender from "@/app/(app)/environments/[environmentId]/settings/product/DeleteProductRender";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/services/membership";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
|
||||
type DeleteProductProps = {
|
||||
environmentId: string;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use server";
|
||||
|
||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/services/product";
|
||||
import { deleteProduct, getProducts, updateProduct } from "@formbricks/lib/product/service";
|
||||
import { TProduct, TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/services/membership";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
|
||||
export const updateProductAction = async (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
@@ -6,7 +6,7 @@ import SettingsTitle from "../SettingsTitle";
|
||||
import EditProductName from "./EditProductName";
|
||||
import EditWaitingTime from "./EditWaitingTime";
|
||||
import DeleteProduct from "./DeleteProduct";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const [, product] = await Promise.all([
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { updateProfile, deleteProfile } from "@formbricks/lib/services/profile";
|
||||
import { updateProfile, deleteProfile } from "@formbricks/lib/profile/service";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
@@ -8,7 +8,7 @@ import SettingsTitle from "../SettingsTitle";
|
||||
import { DeleteAccount } from "./DeleteAccount";
|
||||
import { EditName } from "./EditName";
|
||||
import { EditAvatar } from "./EditAvatar";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
import { getProfile } from "@formbricks/lib/profile/service";
|
||||
|
||||
export default async function ProfileSettingsPage() {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { updateEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { TEnvironment, TEnvironmentUpdateInput } from "@formbricks/types/v1/environment";
|
||||
|
||||
export async function updateEnvironmentAction(
|
||||
|
||||
@@ -4,8 +4,8 @@ import { updateEnvironmentAction } from "@/app/(app)/environments/[environmentId
|
||||
import EnvironmentNotice from "@/components/shared/EnvironmentNotice";
|
||||
import WidgetStatusIndicator from "@/components/shared/WidgetStatusIndicator";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getActionsByEnvironmentId } from "@formbricks/lib/services/actions";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActionsByEnvironmentId } from "@formbricks/lib/action/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
import SettingsCard from "../SettingsCard";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import EditTagsWrapper from "./EditTagsWrapper";
|
||||
import SettingsTitle from "../SettingsTitle";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTagsOnResponsesCount } from "@formbricks/lib/services/tagOnResponse";
|
||||
import { getTagsOnResponsesCount } from "@formbricks/lib/tagOnResponse/service";
|
||||
|
||||
export default async function MembersSettingsPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
@@ -11,20 +11,23 @@ import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { ComputerDesktopIcon, DevicePhoneMobileIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
type TPreviewType = "modal" | "fullwidth" | "email";
|
||||
|
||||
interface PreviewSurveyProps {
|
||||
survey: TSurvey | Survey;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId?: string | null;
|
||||
previewType?: "modal" | "fullwidth" | "email";
|
||||
previewType?: TPreviewType;
|
||||
product: TProduct;
|
||||
environment: TEnvironment;
|
||||
}
|
||||
|
||||
let surveyNameTemp;
|
||||
|
||||
export default function PreviewSurvey({
|
||||
survey,
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
survey,
|
||||
previewType,
|
||||
product,
|
||||
environment,
|
||||
@@ -34,6 +37,18 @@ export default function PreviewSurvey({
|
||||
const [previewMode, setPreviewMode] = useState("desktop");
|
||||
const ContentRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { productOverwrites } = survey || {};
|
||||
|
||||
const {
|
||||
brandColor: surveyBrandColor,
|
||||
highlightBorderColor: surveyHighlightBorderColor,
|
||||
placement: surveyPlacement,
|
||||
} = productOverwrites || {};
|
||||
|
||||
const brandColor = surveyBrandColor || product.brandColor;
|
||||
const placement = surveyPlacement || product.placement;
|
||||
const highlightBorderColor = surveyHighlightBorderColor || product.highlightBorderColor;
|
||||
|
||||
useEffect(() => {
|
||||
// close modal if there are no questions left
|
||||
if (survey.type === "web" && !survey.thankYouCard.enabled) {
|
||||
@@ -52,6 +67,8 @@ export default function PreviewSurvey({
|
||||
resetQuestionProgress();
|
||||
surveyNameTemp = survey.name;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [survey]);
|
||||
|
||||
function resetQuestionProgress() {
|
||||
@@ -94,12 +111,12 @@ export default function PreviewSurvey({
|
||||
{previewType === "modal" ? (
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={product.placement}
|
||||
highlightBorderColor={product.highlightBorderColor}
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
previewMode="mobile">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -114,7 +131,7 @@ export default function PreviewSurvey({
|
||||
<div className="w-full max-w-md px-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -143,12 +160,12 @@ export default function PreviewSurvey({
|
||||
{previewType === "modal" ? (
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
placement={product.placement}
|
||||
highlightBorderColor={product.highlightBorderColor}
|
||||
placement={placement}
|
||||
highlightBorderColor={highlightBorderColor}
|
||||
previewMode="desktop">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
@@ -161,7 +178,7 @@ export default function PreviewSurvey({
|
||||
<div className="w-full max-w-md">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={product.brandColor}
|
||||
brandColor={brandColor}
|
||||
activeQuestionId={activeQuestionId || undefined}
|
||||
formbricksSignature={product.formbricksSignature}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
|
||||
@@ -3,8 +3,8 @@ import SurveyDropDownMenu from "@/app/(app)/environments/[environmentId]/surveys
|
||||
import SurveyStarter from "@/app/(app)/environments/[environmentId]/surveys/SurveyStarter";
|
||||
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
|
||||
import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import type { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { Badge } from "@formbricks/ui";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RESPONSES_LIMIT_FREE } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service";
|
||||
import { getSurveyResponses } from "@formbricks/lib/response/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/services/team";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
|
||||
export const getAnalysisData = async (surveyId: string, environmentId: string) => {
|
||||
const [survey, team, allResponses] = await Promise.all([
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { deleteResponse } from "@formbricks/lib/response/service";
|
||||
import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/services/responseNote";
|
||||
import { updateResponseNote, resolveResponseNote } from "@formbricks/lib/responseNote/service";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/services/tagOnResponse";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface ResponsePageProps {
|
||||
environment: TEnvironment;
|
||||
@@ -21,6 +22,7 @@ interface ResponsePageProps {
|
||||
responses: TResponse[];
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ const ResponsePage = ({
|
||||
responses,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: ResponsePageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
@@ -55,6 +58,7 @@ const ResponsePage = ({
|
||||
surveyId={surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -6,9 +6,10 @@ import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/survey
|
||||
import { getServerSession } from "next-auth";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getProfile } from "@formbricks/lib/profile/service";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -26,6 +27,11 @@ export default async function Page({ params }) {
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const profile = await getProfile(session.user.id);
|
||||
if (!profile) {
|
||||
throw new Error("Profile not found");
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
@@ -39,6 +45,7 @@ export default async function Page({ params }) {
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
profile={profile}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
type TSendEmailActionArgs = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export const sendEmailAction = async ({ html, subject, to }: TSendEmailActionArgs) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
return await sendEmbedSurveyPreviewEmail(to, subject, html);
|
||||
};
|
||||
@@ -1,17 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import LinkSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSurveyModal";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { ShareIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import ShareEmbedSurvey from "./ShareEmbedSurvey";
|
||||
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface LinkSurveyShareButtonProps {
|
||||
survey: TSurvey;
|
||||
className?: string;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
@@ -19,6 +23,8 @@ export default function LinkSurveyShareButton({
|
||||
survey,
|
||||
className,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: LinkSurveyShareButtonProps) {
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
@@ -43,11 +49,13 @@ export default function LinkSurveyShareButton({
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
) : (
|
||||
<LinkSurveyModal
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
product={product}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function LinkSingleUseSurveyModal({
|
||||
title="Preview survey"
|
||||
aria-label="Preview survey"
|
||||
className="flex justify-center"
|
||||
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}preview=true`}
|
||||
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}&preview=true`}
|
||||
target="_blank"
|
||||
EndIcon={EyeIcon}>
|
||||
Preview
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button, Dialog, DialogContent } from "@formbricks/ui";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
import { CodeBracketIcon, DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface LinkSurveyModalProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
export default function LinkSurveyModal({ survey, open, setOpen, surveyBaseUrl }: LinkSurveyModalProps) {
|
||||
const linkTextRef = useRef(null);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey.id, surveyBaseUrl]);
|
||||
|
||||
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh;
|
||||
overflow:auto;">
|
||||
<iframe
|
||||
src="${surveyUrl}"
|
||||
frameborder="0" style="position: absolute; left:0;
|
||||
top:0; width:100%; height:100%; border:0;">
|
||||
</iframe></div>`;
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="bottom-0 max-w-sm bg-white p-4 sm:bottom-auto sm:max-w-xl sm:p-6">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-teal-100">
|
||||
<CheckIcon className="h-6 w-6 text-teal-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900">Your survey is ready!</h3>
|
||||
{showEmbed ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">Embed survey on your website:</p>
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html">
|
||||
{iframeCode}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">Share this link to let people answer your survey:</p>
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="relative mt-3 max-w-full overflow-auto rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
<span
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
}}>
|
||||
{surveyUrl}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex flex-col justify-center gap-2 sm:flex-row sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
className="flex justify-center"
|
||||
onClick={() => {
|
||||
setShowEmbed(true);
|
||||
navigator.clipboard.writeText(iframeCode);
|
||||
toast.success("iframe code copied to clipboard!");
|
||||
}}
|
||||
EndIcon={CodeBracketIcon}>
|
||||
Embed
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowEmbed(false);
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
className="flex justify-center"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy URL
|
||||
</Button>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Preview survey"
|
||||
aria-label="Preview survey"
|
||||
className="flex justify-center"
|
||||
href={`${surveyUrl}?preview=true`}
|
||||
target="_blank"
|
||||
EndIcon={EyeIcon}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import LinkTab from "./shareEmbedTabs/LinkTab";
|
||||
import EmailTab from "./shareEmbedTabs/EmailTab";
|
||||
import WebpageTab from "./shareEmbedTabs/WebpageTab";
|
||||
import { useMemo, useState } from "react";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { DialogContent, Button, Dialog } from "@formbricks/ui";
|
||||
import { LinkIcon, EnvelopeIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface ShareEmbedSurveyProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
}
|
||||
|
||||
export default function ShareEmbedSurvey({
|
||||
survey,
|
||||
open,
|
||||
setOpen,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
}: ShareEmbedSurveyProps) {
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
|
||||
const { email } = profile;
|
||||
const { brandColor } = product;
|
||||
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
|
||||
const componentMap = {
|
||||
link: <LinkTab surveyUrl={surveyUrl} survey={survey} brandColor={brandColor} />,
|
||||
email: <EmailTab survey={survey} surveyUrl={surveyUrl} email={email} brandColor={brandColor} />,
|
||||
webpage: <WebpageTab surveyUrl={surveyUrl} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setActiveId(tabs[0].id);
|
||||
setOpen(open);
|
||||
}}>
|
||||
<DialogContent className="bottom-0 flex h-[95%] w-full flex-col gap-0 overflow-hidden rounded-2xl bg-white p-0 sm:max-w-none lg:bottom-auto lg:h-auto lg:w-[960px]">
|
||||
<div className="border-b border-gray-200 px-4 py-3 lg:px-6 lg:py-4 ">Share or embed your survey</div>
|
||||
<div className="flex grow overflow-x-hidden overflow-y-scroll">
|
||||
<div className="hidden basis-[326px] border-r border-gray-200 px-6 py-8 lg:block lg:shrink-0">
|
||||
<div className="flex w-max flex-col gap-3">
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
StartIcon={tab.icon}
|
||||
startIconClassName={cn("h-4 w-4")}
|
||||
variant="minimal"
|
||||
key={tab.id}
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"rounded-[4px] px-4 py-[6px] text-slate-600",
|
||||
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons
|
||||
tab.id === activeId
|
||||
? " border border-gray-200 bg-slate-100 font-semibold text-slate-900"
|
||||
: "border-transparent text-slate-500 hover:text-slate-700"
|
||||
)}
|
||||
aria-current={tab.id === activeId ? "page" : undefined}>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full grow flex-col gap-6 bg-gray-50 px-4 py-6 lg:p-6">
|
||||
<div className="flex h-full overflow-y-scroll lg:h-[590px] lg:overflow-y-visible">
|
||||
{componentMap[activeId]}
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-max rounded-md bg-slate-100 p-1 lg:hidden">
|
||||
{tabs.slice(0, 2).map((tab) => (
|
||||
<Button
|
||||
variant="minimal"
|
||||
key={tab.id}
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"rounded-sm px-3 py-[6px]",
|
||||
tab.id === activeId
|
||||
? "bg-white text-slate-900"
|
||||
: "border-transparent text-slate-700 hover:text-slate-900"
|
||||
)}>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: "link", label: "Share the Link", icon: LinkIcon },
|
||||
{ id: "email", label: "Embed in an Email", icon: EnvelopeIcon },
|
||||
{ id: "webpage", label: "Embed in a Web Page", icon: CodeBracketIcon },
|
||||
];
|
||||
@@ -5,14 +5,18 @@ import { Confetti } from "@formbricks/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkSurveyModal from "./LinkSurveyModal";
|
||||
import ShareEmbedSurvey from "./ShareEmbedSurvey";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
@@ -20,9 +24,12 @@ export default function SuccessMessage({
|
||||
environment,
|
||||
survey,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryMetadataProps) {
|
||||
const isSingleUse = survey.singleUse?.enabled ?? false;
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
@@ -61,11 +68,13 @@ export default function SuccessMessage({
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
) : (
|
||||
<LinkSurveyModal
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{confetti && <Confetti />}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryPageProps {
|
||||
environment: TEnvironment;
|
||||
@@ -24,6 +25,7 @@ interface SummaryPageProps {
|
||||
surveyBaseUrl: string;
|
||||
singleUseIds?: string[];
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
@@ -35,6 +37,7 @@ const SummaryPage = ({
|
||||
surveyBaseUrl,
|
||||
singleUseIds,
|
||||
product,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: SummaryPageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
@@ -60,6 +63,7 @@ const SummaryPage = ({
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Button, Input } from "@formbricks/ui";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
import { sendEmailAction } from "../../actions";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { CodeBracketIcon, DocumentDuplicateIcon, EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Column,
|
||||
Container,
|
||||
Button as EmailButton,
|
||||
Link,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from "@react-email/components";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface EmailTabProps {
|
||||
survey: TSurvey;
|
||||
surveyUrl: string;
|
||||
email: string;
|
||||
brandColor: string;
|
||||
}
|
||||
|
||||
export default function EmailTab({ survey, surveyUrl, email, brandColor }: EmailTabProps) {
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
const subject = "Formbricks Email Survey Preview";
|
||||
|
||||
const emailValues = useMemo(() => {
|
||||
return getEmailValues({ brandColor, survey, surveyUrl, preview: false });
|
||||
}, []);
|
||||
|
||||
const previewEmailValues = useMemo(() => {
|
||||
return getEmailValues({ brandColor, survey, surveyUrl, preview: true });
|
||||
}, []);
|
||||
|
||||
const sendPreviewEmail = async () => {
|
||||
try {
|
||||
await sendEmailAction({ html: previewEmailValues.html, subject, to: email });
|
||||
toast.success("Email sent!");
|
||||
} catch (err) {
|
||||
if (err instanceof AuthenticationError) {
|
||||
toast.error("You are not authenticated to perform this action.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong. Please try again later.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input type="email" placeholder="user@mail.com" className="h-11 grow bg-white" value={email} />
|
||||
{showEmbed ? (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
onClick={() => {
|
||||
toast.success("Embed code copied to clipboard!");
|
||||
navigator.clipboard.writeText(emailValues.html);
|
||||
}}
|
||||
className="shrink-0"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy code
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="send preview email"
|
||||
aria-label="send preview email"
|
||||
onClick={sendPreviewEmail}
|
||||
EndIcon={EnvelopeIcon}
|
||||
className="shrink-0">
|
||||
Send Preview
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="view embed code for email"
|
||||
aria-label="view embed code for email"
|
||||
onClick={() => setShowEmbed(!showEmbed)}
|
||||
EndIcon={CodeBracketIcon}
|
||||
className="shrink-0">
|
||||
{showEmbed ? "Hide Embed Code" : "View Embed Code"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
{showEmbed ? (
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html"
|
||||
showCopyToClipboard={false}>
|
||||
{emailValues.html}
|
||||
</CodeBlock>
|
||||
) : (
|
||||
<div className="">
|
||||
<div className="mb-6 flex gap-2">
|
||||
<div className="h-3 w-3 rounded-full bg-red-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
|
||||
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">
|
||||
To : {email || "user@mail.com"}
|
||||
</div>
|
||||
<div className="border-b border-slate-200 pb-2 text-sm">Subject : {subject}</div>
|
||||
<div className="p-4">{previewEmailValues.Component}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getEmailValues = ({ survey, surveyUrl, brandColor, preview }) => {
|
||||
const doctype =
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
||||
|
||||
const Template = getEmailTemplate(survey, surveyUrl, brandColor, preview);
|
||||
const html = render(Template, { pretty: true });
|
||||
const htmlWithoutDoctype = html.replace(doctype, "");
|
||||
|
||||
return { Component: Template, html: htmlWithoutDoctype };
|
||||
};
|
||||
|
||||
const getEmailTemplate = (survey: TSurvey, surveyUrl: string, brandColor: string, preview: boolean) => {
|
||||
const url = preview ? `${surveyUrl}?preview=true` : surveyUrl;
|
||||
const urlWithPrefilling = preview ? `${surveyUrl}?preview=true&` : `${surveyUrl}?`;
|
||||
|
||||
const firstQuestion = survey.questions[0];
|
||||
switch (firstQuestion.type) {
|
||||
case QuestionType.OpenText:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-gray-200 bg-slate-50" />
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.Consent:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 font-medium text-slate-800">
|
||||
<Text className="m-0 inline-block">{firstQuestion.label}</Text>
|
||||
</Container>
|
||||
<Container className="mx-0 mt-4 flex max-w-none justify-end">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
Reject
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=accepted`}
|
||||
className="bg-brand-color ml-2 inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-white">
|
||||
Accept
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.NPS:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex w-max flex-col">
|
||||
<Section className="block overflow-hidden rounded-md border border-gray-200">
|
||||
{Array.from({ length: 11 }, (_, i) => (
|
||||
<EmailButton
|
||||
key={i}
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${i}`}
|
||||
className="m-0 inline-flex h-10 w-10 items-center justify-center border-gray-200 p-0 text-slate-800">
|
||||
{i}
|
||||
</EmailButton>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="mt-2 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block w-max p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
{/* {!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="mt-4 cursor-pointer appearance-none rounded-md bg-brand-color px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)} */}
|
||||
|
||||
<EmailFooter />
|
||||
</Section>
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.CTA:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="mt-2 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mx-0 mt-4 max-w-none">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
{firstQuestion.dismissButtonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=clicked`}
|
||||
className="bg-brand-color inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel}
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.Rating:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex">
|
||||
<Section
|
||||
className={cn("inline-block w-max overflow-hidden rounded-md", {
|
||||
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
|
||||
})}>
|
||||
{Array.from({ length: firstQuestion.range }, (_, i) => (
|
||||
<EmailButton
|
||||
key={i}
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${i + 1}`}
|
||||
className={cn(
|
||||
"m-0 inline-flex h-16 w-16 items-center justify-center p-0 text-slate-800",
|
||||
{
|
||||
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
|
||||
}
|
||||
)}>
|
||||
{firstQuestion.scale === "smiley" && <Text className="text-3xl">😃</Text>}
|
||||
{firstQuestion.scale === "number" && i + 1}
|
||||
{firstQuestion.scale === "star" && <Text className="text-3xl">⭐</Text>}
|
||||
</EmailButton>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="m-0 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
{/* {!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="mt-4 cursor-pointer appearance-none rounded-md bg-brand-color px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)} */}
|
||||
<EmailFooter />
|
||||
</Section>
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.MultipleChoiceMulti:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices.map((choice) => (
|
||||
<Section
|
||||
className="mt-2 block w-full rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800"
|
||||
key={choice.id}>
|
||||
{choice.label}
|
||||
</Section>
|
||||
))}
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.MultipleChoiceSingle:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices
|
||||
.filter((choice) => choice.id !== "other")
|
||||
.map((choice) => (
|
||||
<Link
|
||||
key={choice.id}
|
||||
className="mt-2 block rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.label}`}>
|
||||
{choice.label}
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const EmailTemplateWrapper = ({ children, surveyUrl, brandColor }) => {
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"brand-color": brandColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}>
|
||||
<Link
|
||||
href={surveyUrl}
|
||||
target="_blank"
|
||||
className="mx-0 my-2 block rounded-lg border border-solid border-slate-300 bg-white p-8 font-sans text-inherit">
|
||||
{children}
|
||||
</Link>
|
||||
</Tailwind>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailFooter = () => {
|
||||
return (
|
||||
<Container className="m-auto mt-8 text-center ">
|
||||
<Link href="https://formbricks.com/" target="_blank" className="text-xs text-slate-400">
|
||||
Powered by Formbricks
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
import { SurveyInline } from "@/components/shared/Survey";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface EmailTabProps {
|
||||
surveyUrl: string;
|
||||
survey: TSurvey;
|
||||
brandColor: string;
|
||||
}
|
||||
|
||||
export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps) {
|
||||
const linkTextRef = useRef(null);
|
||||
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col gap-5">
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="relative grow overflow-auto rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
<span style={{ wordBreak: "break-all" }}>{surveyUrl}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy URL
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
<SurveyInline
|
||||
brandColor={brandColor}
|
||||
survey={survey}
|
||||
formbricksSignature={false}
|
||||
autoFocus={false}
|
||||
isRedirectDisabled={false}
|
||||
key={survey.id}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
className={cn(
|
||||
"absolute bottom-8 left-1/2 -translate-x-1/2 transform rounded-lg border border-slate-200 bg-white"
|
||||
)}
|
||||
EndIcon={ArrowUpRightIcon}
|
||||
title="Open survey in new tab"
|
||||
aria-label="Open survey in new tab"
|
||||
endIconClassName="h-4 w-4 "
|
||||
href={`${surveyUrl}?preview=true`}
|
||||
target="_blank">
|
||||
Open in new tab
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function WebpageTab({ surveyUrl }) {
|
||||
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh; overflow:auto;">
|
||||
<iframe
|
||||
src="${surveyUrl}"
|
||||
frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
|
||||
</iframe>
|
||||
</div>`;
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col gap-5">
|
||||
<div className="flex justify-between">
|
||||
<div className=""></div>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(iframeCode);
|
||||
toast.success("Embed code copied to clipboard!");
|
||||
}}
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy code
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html"
|
||||
showCopyToClipboard={false}>
|
||||
{iframeCode}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,11 +5,12 @@ import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/survey
|
||||
import SummaryPage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { generateSurveySingleUseId } from "@/lib/singleUseSurveys";
|
||||
import { getProfile } from "@formbricks/lib/profile/service";
|
||||
|
||||
const generateSingleUseIds = (isEncrypted: boolean) => {
|
||||
return Array(5)
|
||||
@@ -30,7 +31,12 @@ export default async function Page({ params }) {
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
const isSingleUseSurvey = survey.singleUse?.enabled ?? false;
|
||||
const singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
|
||||
|
||||
let singleUseIds: string[] | undefined = undefined;
|
||||
if (isSingleUseSurvey) {
|
||||
singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
@@ -39,6 +45,12 @@ export default async function Page({ params }) {
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const profile = await getProfile(session.user.id);
|
||||
if (!profile) {
|
||||
throw new Error("Profile not found");
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
@@ -52,6 +64,7 @@ export default async function Page({ params }) {
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
singleUseIds={isSingleUseSurvey ? singleUseIds : undefined}
|
||||
product={product}
|
||||
profile={profile}
|
||||
environmentTags={tags}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -24,6 +24,7 @@ import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryHeaderProps {
|
||||
surveyId: string;
|
||||
@@ -31,6 +32,7 @@ interface SummaryHeaderProps {
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
const SummaryHeader = ({
|
||||
@@ -39,6 +41,7 @@ const SummaryHeader = ({
|
||||
survey,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryHeaderProps) => {
|
||||
const router = useRouter();
|
||||
@@ -55,7 +58,13 @@ const SummaryHeader = ({
|
||||
</div>
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{survey.type === "link" && (
|
||||
<LinkSurveyShareButton survey={survey} surveyBaseUrl={surveyBaseUrl} singleUseIds={singleUseIds} />
|
||||
<LinkSurveyShareButton
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
@@ -82,6 +91,8 @@ const SummaryHeader = ({
|
||||
className="flex w-full justify-center p-1"
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -164,6 +175,8 @@ const SummaryHeader = ({
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
import { QuestionMarkCircleIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { ChevronDown, SplitIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs";
|
||||
|
||||
interface LogicEditorProps {
|
||||
@@ -141,6 +142,19 @@ export default function LogicEditor({
|
||||
};
|
||||
|
||||
const addLogic = () => {
|
||||
if (question.logic && question.logic?.length >= 0) {
|
||||
const hasUndefinedLogic = question.logic.some(
|
||||
(logic) =>
|
||||
logic.condition === undefined && logic.value === undefined && logic.destination === undefined
|
||||
);
|
||||
if (hasUndefinedLogic) {
|
||||
toast("Please fill current logic jumps first.", {
|
||||
icon: "🤓",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newLogic: TSurveyLogic[] = !question.logic ? [] : question.logic;
|
||||
newLogic.push({
|
||||
condition: undefined,
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function MultipleChoiceMultiForm({
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
const [isInvalidValue, setIsInvalidValue] = useState<string | null>(null);
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
none: {
|
||||
@@ -76,6 +77,24 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
|
||||
};
|
||||
|
||||
const findDuplicateLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
for (let j = i + 1; j < question.choices.length; j++) {
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx?: number) => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
@@ -112,6 +131,9 @@ export default function MultipleChoiceMultiForm({
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setIsInvalidValue(null);
|
||||
}
|
||||
let newLogic: any[] = [];
|
||||
question.logic?.forEach((logic) => {
|
||||
let newL: string | string[] | undefined = logic.value;
|
||||
@@ -198,7 +220,20 @@ export default function MultipleChoiceMultiForm({
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
isInvalid={isInValid && choice.label.trim() === ""}
|
||||
onBlur={() => {
|
||||
const duplicateLabel = findDuplicateLabel();
|
||||
if (duplicateLabel) {
|
||||
setIsInvalidValue(duplicateLabel);
|
||||
} else if (findEmptyLabel()) {
|
||||
setIsInvalidValue("");
|
||||
} else {
|
||||
setIsInvalidValue(null);
|
||||
}
|
||||
}}
|
||||
isInvalid={
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
}
|
||||
/>
|
||||
{question.choices && question.choices.length > 2 && (
|
||||
<TrashIcon
|
||||
|
||||
@@ -32,6 +32,7 @@ export default function MultipleChoiceSingleForm({
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [isInvalidValue, setIsInvalidValue] = useState<string | null>(null);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
@@ -52,6 +53,24 @@ export default function MultipleChoiceSingleForm({
|
||||
},
|
||||
};
|
||||
|
||||
const findDuplicateLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
for (let j = i + 1; j < question.choices.length; j++) {
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => {
|
||||
const newLabel = updatedAttributes.label;
|
||||
const oldLabel = question.choices[choiceIdx].label;
|
||||
@@ -112,6 +131,9 @@ export default function MultipleChoiceSingleForm({
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setIsInvalidValue(null);
|
||||
}
|
||||
let newLogic: any[] = [];
|
||||
question.logic?.forEach((logic) => {
|
||||
let newL: string | string[] | undefined = logic.value;
|
||||
@@ -197,8 +219,21 @@ export default function MultipleChoiceSingleForm({
|
||||
value={choice.label}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onBlur={() => {
|
||||
const duplicateLabel = findDuplicateLabel();
|
||||
if (duplicateLabel) {
|
||||
setIsInvalidValue(duplicateLabel);
|
||||
} else if (findEmptyLabel()) {
|
||||
setIsInvalidValue("");
|
||||
} else {
|
||||
setIsInvalidValue(null);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
isInvalid={isInValid && choice.label.trim() === ""}
|
||||
isInvalid={
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
}
|
||||
/>
|
||||
{question.choices && question.choices.length > 2 && (
|
||||
<TrashIcon
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
|
||||
import { getPlacementStyle } from "@/lib/preview";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
{ name: "Top Right", value: "topRight", disabled: false },
|
||||
{ name: "Top Left", value: "topLeft", disabled: false },
|
||||
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
|
||||
{ name: "Centered Modal", value: "center", disabled: false },
|
||||
];
|
||||
|
||||
type TPlacementProps = {
|
||||
currentPlacement: PlacementType;
|
||||
setCurrentPlacement: (placement: PlacementType) => void;
|
||||
setOverlay: (overlay: string) => void;
|
||||
overlay: string;
|
||||
setClickOutside: (clickOutside: boolean) => void;
|
||||
clickOutside: boolean;
|
||||
};
|
||||
|
||||
export default function Placement({
|
||||
setCurrentPlacement,
|
||||
currentPlacement,
|
||||
setOverlay,
|
||||
overlay,
|
||||
setClickOutside,
|
||||
clickOutside,
|
||||
}: TPlacementProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex">
|
||||
<RadioGroup onValueChange={(e) => setCurrentPlacement(e as PlacementType)} value={currentPlacement}>
|
||||
{placements.map((placement) => (
|
||||
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
|
||||
<Label
|
||||
htmlFor={placement.value}
|
||||
className={cn(placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900")}>
|
||||
{placement.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className="relative ml-8 h-40 w-full rounded bg-slate-200">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-16 w-16 rounded bg-slate-700",
|
||||
getPlacementStyle(currentPlacement)
|
||||
)}></div>
|
||||
</div>
|
||||
</div>
|
||||
{currentPlacement === "center" && (
|
||||
<>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">Centered modal overlay color</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(overlay) => setOverlay(overlay)}
|
||||
value={overlay}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="lightOverlay" value="light" />
|
||||
<Label htmlFor="lightOverlay" className="text-slate-900">
|
||||
Light Overlay
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="darkOverlay" value="dark" />
|
||||
<Label htmlFor="darkOverlay" className="text-slate-900">
|
||||
Dark Overlay
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setClickOutside(value === "allow")}
|
||||
value={clickOutside ? "allow" : "disallow"}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" />
|
||||
<Label htmlFor="disallow" className="text-slate-900">
|
||||
Don't Allow
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="allow" value="allow" />
|
||||
<Label htmlFor="allow" className="text-slate-900">
|
||||
Allow
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,15 @@ interface QuestionCardProps {
|
||||
isInValid: boolean;
|
||||
}
|
||||
|
||||
export function BackButtonInput({ value, onChange }) {
|
||||
export function BackButtonInput({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
value: string | undefined;
|
||||
onChange: (e: any) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Label htmlFor="backButtonLabel">"Back" Button Label</Label>
|
||||
@@ -53,6 +61,7 @@ export function BackButtonInput({ value, onChange }) {
|
||||
value={value}
|
||||
placeholder="Back"
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -235,9 +244,24 @@ export default function QuestionCard({
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
onChange={(e) => {
|
||||
const trimmedValue = e.target.value.trim(); // Remove spaces from the start and end
|
||||
const hasInternalSpaces = /\S\s\S/.test(trimmedValue); // Test if there are spaces between words
|
||||
|
||||
if (
|
||||
!trimmedValue.includes(" ") &&
|
||||
(trimmedValue === "" || hasInternalSpaces || !/\s/.test(trimmedValue))
|
||||
) {
|
||||
updateQuestion(questionIdx, { backButtonLabel: trimmedValue });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,6 +269,11 @@ export default function QuestionCard({
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -255,6 +284,11 @@ export default function QuestionCard({
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import RecontactOptionsCard from "./RecontactOptionsCard";
|
||||
import ResponseOptionsCard from "./ResponseOptionsCard";
|
||||
import WhenToSendCard from "./WhenToSendCard";
|
||||
import WhoToSendCard from "./WhoToSendCard";
|
||||
import StylingCard from "./StylingCard";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
@@ -54,6 +55,8 @@ export default function SettingsView({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
|
||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
"use client";
|
||||
|
||||
import Placement from "./Placement";
|
||||
import { PlacementType } from "@formbricks/types/js";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
import { ColorPicker, Label, Switch } from "@formbricks/ui";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
|
||||
interface StylingCardProps {
|
||||
localSurvey: TSurveyWithAnalytics;
|
||||
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurveyWithAnalytics>>;
|
||||
}
|
||||
|
||||
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { type, productOverwrites } = localSurvey;
|
||||
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
|
||||
|
||||
const togglePlacement = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
placement: !!placement ? null : "bottomRight",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleBrandColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
brandColor: !!brandColor ? null : "#64748b",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleHighlightBorderColor = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
highlightBorderColor: !!highlightBorderColor ? null : "#64748b",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleColorChange = (color: string) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
brandColor: color,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBorderColorChange = (color: string) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
highlightBorderColor: color,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handlePlacementChange = (placement: PlacementType) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
placement,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOverlay = (overlayType: string) => {
|
||||
const darkOverlay = overlayType === "dark";
|
||||
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
darkOverlay,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (clickOutside: boolean) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
clickOutside,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className="w-full rounded-lg border border-slate-300 bg-white">
|
||||
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
|
||||
<div className="inline-flex px-4 py-4">
|
||||
<div className="flex items-center pl-2 pr-5">
|
||||
<CheckCircleIcon className="h-8 w-8 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-slate-800">Styling</p>
|
||||
<p className="mt-1 truncate text-sm text-slate-500">Overwrite global styling settings</p>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
{/* Brand Color */}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="autoComplete" checked={!!brandColor} onCheckedChange={toggleBrandColor} />
|
||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite Brand Color</h3>
|
||||
<p className="text-xs font-normal text-slate-500">Change the main color for this survey.</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{brandColor && (
|
||||
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
|
||||
<div className="w-full max-w-xs">
|
||||
<Label htmlFor="brandcolor">Color (HEX)</Label>
|
||||
<ColorPicker color={brandColor} onChange={handleColorChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* positioning */}
|
||||
{type !== "link" && (
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="surveyDeadline" checked={!!placement} onCheckedChange={togglePlacement} />
|
||||
<Label htmlFor="surveyDeadline" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite Placement</h3>
|
||||
<p className="text-xs font-normal text-slate-500">Change the placement of this survey.</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{placement && (
|
||||
<div className="ml-2 mt-4 flex items-center space-x-1 pb-4">
|
||||
<div className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
|
||||
<div className="w-full items-center">
|
||||
<Placement
|
||||
currentPlacement={placement}
|
||||
setCurrentPlacement={handlePlacementChange}
|
||||
setOverlay={handleOverlay}
|
||||
overlay={darkOverlay ? "dark" : "light"}
|
||||
setClickOutside={handleClickOutside}
|
||||
clickOutside={!!clickOutside}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Highlight border */}
|
||||
{type !== "link" && (
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="autoComplete"
|
||||
checked={!!highlightBorderColor}
|
||||
onCheckedChange={toggleHighlightBorderColor}
|
||||
/>
|
||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Overwrite border highlight</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Change the border highlight for this survey.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{!!highlightBorderColor && (
|
||||
<div className="ml-2 mt-4 rounded-lg border bg-slate-50 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="highlightBorder"
|
||||
checked={!!highlightBorderColor}
|
||||
onCheckedChange={toggleHighlightBorderColor}
|
||||
/>
|
||||
<h2 className="text-sm font-medium text-slate-800">Show highlight border</h2>
|
||||
</div>
|
||||
{!!highlightBorderColor && (
|
||||
<div className="mt-6 w-full max-w-xs">
|
||||
<Label htmlFor="brandcolor">Color (HEX)</Label>
|
||||
<ColorPicker color={highlightBorderColor || ""} onChange={handleBorderColorChange} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
@@ -51,6 +51,8 @@ export default function SurveyEditor({
|
||||
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
|
||||
setActiveQuestionId(localSurvey.questions[0].id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey?.type]);
|
||||
|
||||
if (!localSurvey) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import AlertDialog from "@/components/shared/AlertDialog";
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
@@ -97,6 +98,14 @@ export default function SurveyMenuBar({
|
||||
};
|
||||
|
||||
const validateSurvey = (survey) => {
|
||||
const existingLogicConditions = new Set();
|
||||
const existingQuestionIds = new Set();
|
||||
|
||||
if (survey.questions.length === 0) {
|
||||
toast.error("Please add at least one question");
|
||||
return;
|
||||
}
|
||||
|
||||
faultyQuestions = [];
|
||||
for (let index = 0; index < survey.questions.length; index++) {
|
||||
const question = survey.questions[index];
|
||||
@@ -109,7 +118,67 @@ export default function SurveyMenuBar({
|
||||
// if there are any faulty questions, the user won't be allowed to save the survey
|
||||
if (faultyQuestions.length > 0) {
|
||||
setInvalidQuestions(faultyQuestions);
|
||||
toast.error("Please fill required fields");
|
||||
toast.error("Please fill all required fields.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const question of survey.questions) {
|
||||
if (existingQuestionIds.has(question.id)) {
|
||||
toast.error("There are 2 identical question IDs. Please update one.");
|
||||
return false;
|
||||
}
|
||||
existingQuestionIds.add(question.id);
|
||||
|
||||
if (
|
||||
question.type === QuestionType.MultipleChoiceSingle ||
|
||||
question.type === QuestionType.MultipleChoiceMulti
|
||||
) {
|
||||
const haveSameChoices =
|
||||
question.choices.some((element) => element.label.trim() === "") ||
|
||||
question.choices.some((element, index) =>
|
||||
question.choices
|
||||
.slice(index + 1)
|
||||
.some((nextElement) => nextElement.label.trim() === element.label.trim())
|
||||
);
|
||||
|
||||
if (haveSameChoices) {
|
||||
toast.error("You have two identical choices.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (const logic of question.logic || []) {
|
||||
const validFields = ["condition", "destination", "value"].filter(
|
||||
(field) => logic[field] !== undefined
|
||||
).length;
|
||||
|
||||
if (validFields < 2) {
|
||||
setInvalidQuestions([question.id]);
|
||||
toast.error("Incomplete logic jumps detected: Please fill or delete them.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (question.required && logic.condition === "skipped") {
|
||||
toast.error("You have a missing logic condition. Please update or delete it.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const thisLogic = `${logic.condition}-${logic.value}`;
|
||||
if (existingLogicConditions.has(thisLogic)) {
|
||||
setInvalidQuestions([question.id]);
|
||||
toast.error("You have 2 competing logic conditons. Please update or delete one.");
|
||||
return false;
|
||||
}
|
||||
existingLogicConditions.add(thisLogic);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
survey.redirectUrl &&
|
||||
!survey.redirectUrl.includes("https://") &&
|
||||
!survey.redirectUrl.includes("http://")
|
||||
) {
|
||||
toast.error("Please enter a valid URL for redirecting respondents.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,6 +197,10 @@ export default function SurveyMenuBar({
|
||||
};
|
||||
|
||||
const saveSurveyAction = async (shouldNavigateBack = false) => {
|
||||
if (localSurvey.questions.length === 0) {
|
||||
toast.error("Please add at least one question.");
|
||||
return;
|
||||
}
|
||||
setIsMutatingSurvey(true);
|
||||
// Create a copy of localSurvey with isDraft removed from every question
|
||||
const strippedSurvey: TSurvey = {
|
||||
@@ -139,6 +212,7 @@ export default function SurveyMenuBar({
|
||||
};
|
||||
|
||||
if (!validateSurvey(localSurvey)) {
|
||||
setIsMutatingSurvey(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,6 +314,7 @@ export default function SurveyMenuBar({
|
||||
onClick={async () => {
|
||||
setIsMutatingSurvey(true);
|
||||
if (!validateSurvey(localSurvey)) {
|
||||
setIsMutatingSurvey(false);
|
||||
return;
|
||||
}
|
||||
await updateSurveyAction({ ...localSurvey, status: "inProgress" });
|
||||
|
||||
@@ -7,6 +7,9 @@ import toast from "react-hot-toast";
|
||||
export default function UpdateQuestionId({ localSurvey, question, questionIdx, updateQuestion }) {
|
||||
const [currentValue, setCurrentValue] = useState(question.id);
|
||||
const [prevValue, setPrevValue] = useState(question.id);
|
||||
const [isInputInvalid, setIsInputInvalid] = useState(
|
||||
currentValue.trim() === "" || currentValue.includes(" ")
|
||||
);
|
||||
|
||||
const saveAction = () => {
|
||||
// return early if the input value was not changed
|
||||
@@ -14,28 +17,22 @@ export default function UpdateQuestionId({ localSurvey, question, questionIdx, u
|
||||
return;
|
||||
}
|
||||
|
||||
// check if id is unique
|
||||
const questionIds = localSurvey.questions.map((q) => q.id);
|
||||
if (questionIds.includes(currentValue)) {
|
||||
setIsInputInvalid(true);
|
||||
toast.error("IDs have to be unique per survey.");
|
||||
setCurrentValue(question.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if id contains any spaces
|
||||
if (currentValue.trim() === "" || currentValue.includes(" ")) {
|
||||
toast.error("ID should not contain space.");
|
||||
setCurrentValue(question.id);
|
||||
return;
|
||||
} else if (currentValue.trim() === "" || currentValue.includes(" ")) {
|
||||
setIsInputInvalid(true);
|
||||
toast.error("ID should not be empty.");
|
||||
} else {
|
||||
setIsInputInvalid(false);
|
||||
toast.success("Question ID updated.");
|
||||
}
|
||||
|
||||
updateQuestion(questionIdx, { id: currentValue });
|
||||
toast.success("Question ID updated.");
|
||||
setPrevValue(currentValue); // after successful update, set current value as previous value
|
||||
};
|
||||
|
||||
const isInputInvalid = currentValue.trim() === "" || currentValue.includes(" ");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label htmlFor="questionId">Question ID</Label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// extend this object in order to add more validation rules
|
||||
|
||||
import {
|
||||
TSurveyConsentQuestion,
|
||||
TSurveyMultipleChoiceMultiQuestion,
|
||||
TSurveyMultipleChoiceSingleQuestion,
|
||||
TSurveyQuestion,
|
||||
@@ -13,8 +14,16 @@ const validationRules = {
|
||||
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion) => {
|
||||
return !question.choices.some((element) => element.label.trim() === "");
|
||||
},
|
||||
consent: (question: TSurveyConsentQuestion) => {
|
||||
return question.label.trim() !== "";
|
||||
},
|
||||
defaultValidation: (question: TSurveyQuestion) => {
|
||||
return question.headline.trim() !== "";
|
||||
console.log(question);
|
||||
return (
|
||||
question.headline.trim() !== "" &&
|
||||
question.buttonLabel?.trim() !== "" &&
|
||||
question.backButtonLabel?.trim() !== ""
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import React from "react";
|
||||
import { FORMBRICKS_ENCRYPTION_KEY, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import SurveyEditor from "./SurveyEditor";
|
||||
import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { ErrorComponent } from "@formbricks/ui";
|
||||
|
||||
export default async function SurveysEditPage({ params }) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { updateEnvironmentAction } from "@/app/(app)/environments/[environmentId
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import WidgetStatusIndicator from "@/components/shared/WidgetStatusIndicator";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getActionsByEnvironmentId } from "@formbricks/lib/services/actions";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getActionsByEnvironmentId } from "@formbricks/lib/action/service";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { Metadata } from "next";
|
||||
import SurveysList from "./SurveyList";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import TemplateContainerWithPreview from "./TemplateContainer";
|
||||
import { getEnvironment } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
|
||||
export default async function SurveyTemplatesPage({ params }) {
|
||||
const environmentId = params.environmentId;
|
||||
|
||||
@@ -2123,5 +2123,6 @@ export const minimalSurvey: TSurvey = {
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
},
|
||||
productOverwrites: null,
|
||||
singleUse: null,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { updateProduct } from "@formbricks/lib/services/product";
|
||||
import { updateProfile } from "@formbricks/lib/services/profile";
|
||||
import { updateProduct } from "@formbricks/lib/product/service";
|
||||
import { updateProfile } from "@formbricks/lib/profile/service";
|
||||
import { TProductUpdateInput } from "@formbricks/types/v1/product";
|
||||
import { TProfileUpdateInput } from "@formbricks/types/v1/profile";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -2,9 +2,9 @@ export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/services/environment";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
import { getFirstEnvironmentByUserId } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getProfile } from "@formbricks/lib/profile/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import Onboarding from "./components/Onboarding";
|
||||
|
||||
@@ -13,8 +13,14 @@ export default async function OnboardingPage() {
|
||||
if (!session) {
|
||||
throw new Error("No session found");
|
||||
}
|
||||
const environment = await getFirstEnvironmentByUserId(session?.user.id);
|
||||
const profile = await getProfile(session?.user.id!);
|
||||
const userId = session?.user.id;
|
||||
const environment = await getFirstEnvironmentByUserId(userId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("No environment found for user");
|
||||
}
|
||||
|
||||
const profile = await getProfile(userId);
|
||||
const product = await getProductByEnvironmentId(environment?.id!);
|
||||
|
||||
if (!environment || !profile || !product) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { hasTeamAccess } from "@/lib/api/apiHelper";
|
||||
import { getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProduct } from "@formbricks/lib/services/product";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProduct } from "@formbricks/lib/product/service";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { hasTeamAccess } from "@/lib/api/apiHelper";
|
||||
import { getEnvironments } from "@formbricks/lib/services/environment";
|
||||
import { getProducts } from "@formbricks/lib/services/product";
|
||||
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { AuthenticationError, AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user