Merge branch 'gitpod-doc' of https://github.com/anjy7/formbricks into gitpod-doc

This commit is contained in:
Anjy Gupta
2023-10-04 17:42:04 +00:00
203 changed files with 4759 additions and 1964 deletions

View File

@@ -101,3 +101,7 @@ GOOGLE_CLIENT_SECRET=
# Cron Secret
CRON_SECRET=
# Encryption key
# You can use: `openssl rand -base64 16` to generate one
FORMBRICKS_ENCRYPTION_KEY=

View File

@@ -104,4 +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=
*/

View File

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

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -45,5 +45,3 @@ Natively embed qualitative user research into your B2B SaaS. Leverage Best Pract
</div>
<GettingStarted />
<BestPractices />

View File

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

View File

@@ -57,7 +57,7 @@ These variables must also be provided at runtime.
| Variable | Description | Required | Default |
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- |
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` |
| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` |
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |

View File

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

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

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

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

View File

@@ -299,8 +299,8 @@ const Leaderboard = [
points: "200",
},
{
name: "Arjun",
points: "100",
name: "Naitik Kapadia (Arjun)",
points: "200",
},
{
name: "Yashhhh",
@@ -332,7 +332,7 @@ const Leaderboard = [
},
{
name: "Eldemarkki",
points: "100",
points: "500",
},
{
name: "Suyash",
@@ -360,17 +360,57 @@ const Leaderboard = [
},
{
name: "Aditya Deshlahre",
points: "450",
points: "550",
link: "https://github.com/adityadeshlahre",
},
{
name: "Rutam",
points: "250",
points: "350",
},
{
name: "Sagnik Sahoo",
points: "100",
},
{
name: "Prasoon Mahawar",
points: "100",
},
{
name: "Dushmanta",
points: "100",
},
{
name: "Arjavv",
points: "100",
},
{
name: "Ashish Khare",
points: "100",
},
{
name: "Rohit Mondal",
points: "100",
},
{
name: "noobcoder",
points: "100",
},
{
name: "Rayyan Alam (Rayy)",
points: "100",
},
{
name: "Ayush",
points: "100",
},
{
name: "Zechariah",
points: "100",
},
{
name: "Rajarshi Misra",
points: "100",
},
];
export default function FormTribeHackathon() {

View File

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

View File

@@ -11,8 +11,8 @@ import {
getActionCountInLast24Hours,
getActionCountInLast7Days,
getActionCountInLastHour,
} from "@formbricks/lib/services/actions";
import { getSurveysByActionClassId } from "@formbricks/lib/services/survey";
} from "@formbricks/lib/action/service";
import { getSurveysByActionClassId } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/v1/errors";
export async function deleteActionClassAction(environmentId, actionClassId: string) {

View File

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

View File

@@ -1,6 +1,6 @@
"use server";
import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey";
import { getSurveysByAttributeClassId } from "@formbricks/lib/survey/service";
export const GetActiveInactiveSurveysAction = async (
attributeClassId: string

View File

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

View File

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

View File

@@ -1,133 +1,45 @@
"use server";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { prisma } from "@formbricks/database";
import { ResourceNotFoundError } from "@formbricks/types/v1/errors";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
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";
import { Team } from "@prisma/client";
import { Prisma as prismaClient } from "@prisma/client/";
import { createProduct } from "@formbricks/lib/services/product";
import { getServerSession } from "next-auth";
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
const newTeam = await prisma.team.create({
data: {
name: teamName,
memberships: {
create: {
user: { connect: { id: ownerUserId } },
role: "owner",
accepted: true,
},
},
products: {
create: [
{
name: "My Product",
environments: {
create: [
{
type: "production",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
{
type: "development",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
],
},
},
],
},
},
include: {
memberships: true,
products: {
include: {
environments: true,
},
},
},
export async function createTeamAction(teamName: string): Promise<Team> {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const newTeam = await createTeam({
name: teamName,
});
const teamId = newTeam?.id;
await createMembership(newTeam.id, session.user.id, {
role: "owner",
accepted: true,
});
if (teamId) {
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
method: "POST",
headers: {
"x-api-key": INTERNAL_SECRET,
},
});
}
await createProduct(newTeam.id, {
name: "My Product",
});
return newTeam;
}
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const existingSurvey = await getSurvey(surveyId);
if (!existingSurvey) {
@@ -164,7 +76,12 @@ export async function duplicateSurveyAction(environmentId: string, surveyId: str
surveyClosedMessage: existingSurvey.surveyClosedMessage
? JSON.parse(JSON.stringify(existingSurvey.surveyClosedMessage))
: prismaClient.JsonNull,
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,
@@ -178,6 +95,24 @@ export async function copyToOtherEnvironmentAction(
surveyId: string,
targetEnvironmentId: string
) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorizedToAccessSourceEnvironment = await hasUserEnvironmentAccess(
session.user.id,
environmentId
);
if (!isAuthorizedToAccessSourceEnvironment) throw new AuthorizationError("Not authorized");
const isAuthorizedToAccessTargetEnvironment = await hasUserEnvironmentAccess(
session.user.id,
targetEnvironmentId
);
if (!isAuthorizedToAccessTargetEnvironment) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const existingSurvey = await prisma.survey.findFirst({
where: {
id: surveyId,
@@ -295,6 +230,8 @@ export async function copyToOtherEnvironmentAction(
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
singleUse: existingSurvey.singleUse ?? prismaClient.JsonNull,
productOverwrites: existingSurvey.productOverwrites ?? prismaClient.JsonNull,
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
},
});
@@ -302,12 +239,32 @@ export async function copyToOtherEnvironmentAction(
}
export const deleteSurveyAction = async (surveyId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
await deleteSurvey(surveyId);
};
export const createProductAction = async (environmentId: string, productName: string) => {
const productCreated = await createProduct(environmentId, productName);
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const newEnvironment = productCreated.environments[0];
return newEnvironment;
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
const team = await getTeamByEnvironmentId(environmentId);
if (!team) throw new ResourceNotFoundError("Team from environment", environmentId);
const product = await createProduct(team.id, {
name: productName,
});
// get production environment
const productionEnvironment = product.environments.find((environment) => environment.type === "production");
if (!productionEnvironment) throw new ResourceNotFoundError("Production environment", environmentId);
return productionEnvironment;
};

View File

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

View File

@@ -1,8 +1,8 @@
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 { getSurveys } from "@formbricks/lib/services/survey";
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 {
GOOGLE_SHEETS_CLIENT_ID,
@@ -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);

View File

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

View File

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

View File

@@ -4,10 +4,10 @@ import WebhookRowData from "@/app/(app)/environments/[environmentId]/integration
import WebhookTable from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTable";
import WebhookTableHeading from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookTableHeading";
import GoBackButton from "@/components/shared/GoBackButton";
import { getSurveys } from "@formbricks/lib/services/survey";
import { getWebhooks } from "@formbricks/lib/services/webhook";
import { getSurveys } from "@formbricks/lib/survey/service";
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([

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseTimeline";
import { getSurveys } from "@formbricks/lib/survey/service";
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
import { getSurveys } from "@formbricks/lib/services/survey";
import { TEnvironment } from "@formbricks/types/v1/environment";
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
import { TSurvey } from "@formbricks/types/v1/surveys";

View File

@@ -3,6 +3,7 @@ import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
import { TResponseWithSurvey } from "@formbricks/types/v1/responses";
import Link from "next/link";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui";
import { TEnvironment } from "@formbricks/types/v1/environment";
export default function ResponseFeed({
@@ -63,6 +64,7 @@ export default function ResponseFeed({
/>
</div>
</div>
<div className="mt-3 space-y-3">
{response.survey.questions.map((question) => (
<div key={question.id}>
@@ -75,6 +77,18 @@ export default function ResponseFeed({
</div>
))}
</div>
<div className="flex w-full justify-end">
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<p className="text-sm text-slate-500">{response.singleUseId}</p>
</TooltipTrigger>
<TooltipContent>
<p className="text-sm text-slate-500">Single Use Id</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
"use client";
import { updateTeamAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions";
import { updateTeamNameAction } from "@/app/(app)/environments/[environmentId]/settings/members/actions";
import { TTeam } from "@formbricks/types/v1/teams";
import { Button, Input, Label } from "@formbricks/ui";
import { useRouter } from "next/navigation";
@@ -43,7 +43,7 @@ export default function EditTeamName({ team }: TEditTeamNameProps) {
const handleUpdateTeamName: SubmitHandler<TEditTeamNameForm> = async (data) => {
try {
setIsUpdatingTeam(true);
await updateTeamAction(team.id, data);
await updateTeamNameAction(team.id, data.name);
setIsUpdatingTeam(false);
toast.success("Team name updated successfully.");

View File

@@ -9,24 +9,33 @@ 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 { TTeamUpdateInput } from "@formbricks/types/v1/teams";
import { getServerSession } from "next-auth";
import { hasTeamAccess, hasTeamAuthority, hasTeamOwnership, isOwner } from "@formbricks/lib/auth";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
export const updateTeamAction = async (teamId: string, data: TTeamUpdateInput) => {
return await updateTeam(teamId, data);
export const updateTeamNameAction = async (teamId: string, teamName: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
return await updateTeam(teamId, { name: teamName });
};
export const updateMembershipAction = async (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ interface SurveyDropDownMenuProps {
environment: TEnvironment;
otherEnvironment: TEnvironment;
surveyBaseUrl: string;
singleUseId?: string;
}
export default function SurveyDropDownMenu({
@@ -44,6 +45,7 @@ export default function SurveyDropDownMenu({
environment,
otherEnvironment,
surveyBaseUrl,
singleUseId,
}: SurveyDropDownMenuProps) {
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [loading, setLoading] = useState(false);
@@ -155,7 +157,11 @@ export default function SurveyDropDownMenu({
<DropdownMenuItem>
<Link
className="flex w-full items-center"
href={`/s/${survey.id}?preview=true`}
href={
singleUseId
? `/s/${survey.id}?suId=${singleUseId}&preview=true`
: `/s/${survey.id}?preview=true`
}
target="_blank">
<EyeIcon className="mr-2 h-4 w-4" />
Preview Survey
@@ -165,8 +171,11 @@ export default function SurveyDropDownMenu({
<button
className="flex w-full items-center"
onClick={() => {
navigator.clipboard.writeText(surveyUrl);
navigator.clipboard.writeText(
singleUseId ? `${surveyUrl}?suId=${singleUseId}` : surveyUrl
);
toast.success("Copied link to clipboard");
router.refresh();
}}>
<LinkIcon className="mr-2 h-4 w-4" />
Copy Link

View File

@@ -3,13 +3,14 @@ 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 { getSurveys } from "@formbricks/lib/services/survey";
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";
import { ComputerDesktopIcon, LinkIcon, PlusIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { generateSurveySingleUseId } from "@/lib/singleUseSurveys";
export default async function SurveysList({ environmentId }: { environmentId: string }) {
const product = await getProductByEnvironmentId(environmentId);
@@ -45,57 +46,63 @@ export default async function SurveysList({ environmentId }: { environmentId: st
</Link>
{surveys
.sort((a, b) => b.updatedAt?.getTime() - a.updatedAt?.getTime())
.map((survey) => (
<li key={survey.id} className="relative col-span-1 h-56">
<div className="delay-50 flex h-full flex-col justify-between rounded-md bg-white shadow transition ease-in-out hover:scale-105">
<div className="px-6 py-4">
<Badge
StartIcon={survey.type === "link" ? LinkIcon : ComputerDesktopIcon}
startIconClassName="mr-2"
text={
survey.type === "link"
? "Link Survey"
: survey.type === "web"
? "In-Product Survey"
: ""
.map((survey) => {
const isSingleUse = survey.singleUse?.enabled ?? false;
const isEncrypted = survey.singleUse?.isEncrypted ?? false;
const singleUseId = isSingleUse ? generateSurveySingleUseId(isEncrypted) : undefined;
return (
<li key={survey.id} className="relative col-span-1 h-56">
<div className="delay-50 flex h-full flex-col justify-between rounded-md bg-white shadow transition ease-in-out hover:scale-105">
<div className="px-6 py-4">
<Badge
StartIcon={survey.type === "link" ? LinkIcon : ComputerDesktopIcon}
startIconClassName="mr-2"
text={
survey.type === "link"
? "Link Survey"
: survey.type === "web"
? "In-Product Survey"
: ""
}
type="gray"
size={"tiny"}
className="font-base"></Badge>
<p className="my-2 line-clamp-3 text-lg">{survey.name}</p>
</div>
<Link
href={
survey.status === "draft"
? `/environments/${environmentId}/surveys/${survey.id}/edit`
: `/environments/${environmentId}/surveys/${survey.id}/summary`
}
type="gray"
size={"tiny"}
className="font-base"></Badge>
<p className="my-2 line-clamp-3 text-lg">{survey.name}</p>
</div>
<Link
href={
survey.status === "draft"
? `/environments/${environmentId}/surveys/${survey.id}/edit`
: `/environments/${environmentId}/surveys/${survey.id}/summary`
}
className="absolute h-full w-full"></Link>
<div className="divide-y divide-slate-100">
<div className="flex justify-between px-4 py-2 text-right sm:px-6">
<div className="flex items-center">
{survey.status !== "draft" && (
<>
<SurveyStatusIndicator status={survey.status} tooltip environment={environment} />
</>
)}
{survey.status === "draft" && (
<span className="text-xs italic text-slate-400">Draft</span>
)}
className="absolute h-full w-full"></Link>
<div className="divide-y divide-slate-100">
<div className="flex justify-between px-4 py-2 text-right sm:px-6">
<div className="flex items-center">
{survey.status !== "draft" && (
<>
<SurveyStatusIndicator status={survey.status} tooltip environment={environment} />
</>
)}
{survey.status === "draft" && (
<span className="text-xs italic text-slate-400">Draft</span>
)}
</div>
<SurveyDropDownMenu
survey={survey}
key={`surveys-${survey.id}`}
environmentId={environmentId}
environment={environment}
otherEnvironment={otherEnvironment!}
surveyBaseUrl={SURVEY_BASE_URL}
singleUseId={singleUseId}
/>
</div>
<SurveyDropDownMenu
survey={survey}
key={`surveys-${survey.id}`}
environmentId={environmentId}
environment={environment}
otherEnvironment={otherEnvironment!}
surveyBaseUrl={SURVEY_BASE_URL}
/>
</div>
</div>
</div>
</li>
))}
</li>
);
})}
</ul>
<UsageAttributesUpdater numSurveys={surveys.length} />
</>

View File

@@ -4,6 +4,7 @@ import TemplateList from "@/app/(app)/environments/[environmentId]/surveys/templ
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import type { TEnvironment } from "@formbricks/types/v1/environment";
import type { TProduct } from "@formbricks/types/v1/product";
import { TSurveyInput } from "@formbricks/types/v1/surveys";
import { TTemplate } from "@formbricks/types/v1/templates";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -28,7 +29,7 @@ export default function SurveyStarter({
...template.preset,
type: surveyType,
autoComplete,
};
} as Partial<TSurveyInput>;
try {
const survey = await createSurveyAction(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);

View File

@@ -1,8 +1,8 @@
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 { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
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([

View File

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

View File

@@ -147,6 +147,11 @@ export default function SingleResponse({
)}
<div className="flex space-x-4 text-sm">
{data.singleUseId && (
<span className="flex items-center rounded-full bg-slate-100 px-3 text-slate-600">
{data.singleUseId}
</span>
)}
{data.finished && (
<span className="flex items-center rounded-full bg-slate-100 px-3 text-slate-600">
Completed <CheckCircleIcon className="ml-1 h-5 w-5 text-green-400" />

View File

@@ -6,8 +6,8 @@ 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";
export default async function Page({ params }) {

View File

@@ -6,19 +6,23 @@ import { Button } from "@formbricks/ui";
import { ShareIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import clsx from "clsx";
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
interface LinkSurveyShareButtonProps {
survey: TSurvey;
className?: string;
surveyBaseUrl: string;
singleUseIds?: string[];
}
export default function LinkSurveyShareButton({
survey,
className,
surveyBaseUrl,
singleUseIds,
}: LinkSurveyShareButtonProps) {
const [showLinkModal, setShowLinkModal] = useState(false);
const isSingleUse = survey.singleUse?.enabled ?? false;
return (
<>
@@ -31,7 +35,14 @@ export default function LinkSurveyShareButton({
onClick={() => setShowLinkModal(true)}>
<ShareIcon className="h-5 w-5" />
</Button>
{showLinkModal && (
{showLinkModal && isSingleUse && singleUseIds ? (
<LinkSingleUseSurveyModal
survey={survey}
open={showLinkModal}
setOpen={setShowLinkModal}
singleUseIds={singleUseIds}
/>
) : (
<LinkSurveyModal
survey={survey}
open={showLinkModal}

View File

@@ -0,0 +1,122 @@
"use client";
import { Button, Dialog, DialogContent } from "@formbricks/ui";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/outline";
import { CheckCircleIcon, DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
import { useRef, useState } from "react";
import toast from "react-hot-toast";
import { truncateMiddle } from "@/lib/utils";
import { cn } from "@formbricks/lib/cn";
import { useRouter } from "next/navigation";
interface LinkSingleUseSurveyModalProps {
survey: TSurvey;
open: boolean;
setOpen: (open: boolean) => void;
singleUseIds: string[];
}
export default function LinkSingleUseSurveyModal({
survey,
open,
setOpen,
singleUseIds,
}: LinkSingleUseSurveyModalProps) {
const defaultSurveyUrl = `${window.location.protocol}//${window.location.host}/s/${survey.id}`;
const [selectedSingleUseIds, setSelectedSingleIds] = useState<number[]>([]);
const linkTextRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const handleLinkOnClick = (index: number) => {
setSelectedSingleIds([...selectedSingleUseIds, index]);
const surveyUrl = `${defaultSurveyUrl}?suId=${singleUseIds[index]}`;
navigator.clipboard.writeText(surveyUrl);
toast.success("URL copied to clipboard!");
};
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>
<div className="mt-4">
<p className="text-sm text-gray-500">
Here are 5 single use links to let people answer your survey:
</p>
<div ref={linkTextRef}>
{singleUseIds.map((singleUseId, index) => {
const isSelected = selectedSingleUseIds.includes(index);
return (
<div
key={singleUseId}
className={cn(
"row relative mt-3 flex max-w-full cursor-pointer items-center justify-between overflow-auto rounded-lg border border-slate-300 bg-slate-50 px-8 py-4 text-left text-slate-800 transition-all duration-200 ease-in-out hover:border-slate-500",
isSelected && "border-slate-200 text-slate-400 hover:border-slate-200"
)}
onClick={() => {
if (!isSelected) {
handleLinkOnClick(index);
}
}}>
<span>{truncateMiddle(`${defaultSurveyUrl}?suId=${singleUseId}`, 48)}</span>
{isSelected ? (
<CheckCircleIcon className="ml-4 h-4 w-4" />
) : (
<DocumentDuplicateIcon className="ml-4 h-4 w-4" />
)}
</div>
);
})}
</div>
</div>
<div className="mt-4 flex flex-col justify-center gap-2 sm:flex-row sm:justify-end">
<Button
variant="secondary"
title="Generate new single-use survey link"
aria-label="Generate new single-use survey link"
className="flex justify-center"
onClick={() => {
router.refresh();
setSelectedSingleIds([]);
toast.success("New survey links generated!");
}}
EndIcon={ArrowPathIcon}>
Regenerate
</Button>
<Button
variant="secondary"
onClick={() => {
setSelectedSingleIds(Array.from(singleUseIds.keys()));
const allSurveyUrls = singleUseIds
.map((singleUseId) => `${defaultSurveyUrl}?suId=${singleUseId}`)
.join("\n");
navigator.clipboard.writeText(allSurveyUrls);
toast.success("All URLs copied to clipboard!");
}}
title="Copy all survey links to clipboard"
aria-label="Copy all survey links to clipboard"
className="flex justify-center"
EndIcon={DocumentDuplicateIcon}>
Copy 5 URLs
</Button>
<Button
variant="darkCTA"
title="Preview survey"
aria-label="Preview survey"
className="flex justify-center"
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}&preview=true`}
target="_blank"
EndIcon={EyeIcon}>
Preview
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -6,15 +6,23 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import LinkSurveyModal from "./LinkSurveyModal";
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
import { TEnvironment } from "@formbricks/types/v1/environment";
interface SummaryMetadataProps {
environment: TEnvironment;
survey: TSurvey;
surveyBaseUrl: string;
singleUseIds?: string[];
}
export default function SuccessMessage({ environment, survey, surveyBaseUrl }: SummaryMetadataProps) {
export default function SuccessMessage({
environment,
survey,
surveyBaseUrl,
singleUseIds,
}: SummaryMetadataProps) {
const isSingleUse = survey.singleUse?.enabled ?? false;
const searchParams = useSearchParams();
const [showLinkModal, setShowLinkModal] = useState(false);
const [confetti, setConfetti] = useState(false);
@@ -45,7 +53,14 @@ export default function SuccessMessage({ environment, survey, surveyBaseUrl }: S
return (
<>
{showLinkModal && (
{showLinkModal && isSingleUse && singleUseIds ? (
<LinkSingleUseSurveyModal
survey={survey}
open={showLinkModal}
setOpen={setShowLinkModal}
singleUseIds={singleUseIds}
/>
) : (
<LinkSurveyModal
survey={survey}
open={showLinkModal}

View File

@@ -22,6 +22,7 @@ interface SummaryPageProps {
surveyId: string;
responses: TResponse[];
surveyBaseUrl: string;
singleUseIds?: string[];
product: TProduct;
environmentTags: TTag[];
}
@@ -32,6 +33,7 @@ const SummaryPage = ({
surveyId,
responses,
surveyBaseUrl,
singleUseIds,
product,
environmentTags,
}: SummaryPageProps) => {
@@ -56,6 +58,7 @@ const SummaryPage = ({
survey={survey}
surveyId={surveyId}
surveyBaseUrl={surveyBaseUrl}
singleUseIds={singleUseIds}
product={product}
/>
<CustomFilter

View File

@@ -5,20 +5,37 @@ 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";
const generateSingleUseIds = (isEncrypted: boolean) => {
return Array(5)
.fill(null)
.map(() => {
return generateSurveySingleUseId(isEncrypted);
});
};
export default async function Page({ params }) {
const session = await getServerSession(authOptions);
if (!session) {
throw new Error("Unauthorized");
}
const [{ responses, survey }, environment] = await Promise.all([
getAnalysisData(params.surveyId, params.environmentId),
getEnvironment(params.environmentId),
]);
const isSingleUseSurvey = survey.singleUse?.enabled ?? false;
let singleUseIds: string[] | undefined = undefined;
if (isSingleUseSurvey) {
singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
}
if (!environment) {
throw new Error("Environment not found");
}
@@ -38,6 +55,7 @@ export default async function Page({ params }) {
survey={survey}
surveyId={params.surveyId}
surveyBaseUrl={SURVEY_BASE_URL}
singleUseIds={isSingleUseSurvey ? singleUseIds : undefined}
product={product}
environmentTags={tags}
/>

View File

@@ -23,7 +23,7 @@ import LinkSurveyShareButton from "@/app/(app)/environments/[environmentId]/surv
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
import { TEnvironment } from "@formbricks/types/v1/environment";
import { TProduct } from "@formbricks/types/v1/product";
import { surveyMutateAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
interface SummaryHeaderProps {
surveyId: string;
@@ -31,8 +31,16 @@ interface SummaryHeaderProps {
survey: TSurvey;
surveyBaseUrl: string;
product: TProduct;
singleUseIds?: string[];
}
const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }: SummaryHeaderProps) => {
const SummaryHeader = ({
surveyId,
environment,
survey,
surveyBaseUrl,
product,
singleUseIds,
}: SummaryHeaderProps) => {
const router = useRouter();
const isCloseOnDateEnabled = survey.closeOnDate !== null;
@@ -46,7 +54,9 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
<span className="text-base font-extralight text-slate-600">{product.name}</span>
</div>
<div className="hidden justify-end gap-x-1.5 sm:flex">
{survey.type === "link" && <LinkSurveyShareButton survey={survey} surveyBaseUrl={surveyBaseUrl} />}
{survey.type === "link" && (
<LinkSurveyShareButton survey={survey} surveyBaseUrl={surveyBaseUrl} singleUseIds={singleUseIds} />
)}
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
<SurveyStatusDropdown environment={environment} survey={survey} />
) : null}
@@ -72,6 +82,7 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
className="flex w-full justify-center p-1"
survey={survey}
surveyBaseUrl={surveyBaseUrl}
singleUseIds={singleUseIds}
/>
<DropdownMenuSeparator />
</>
@@ -97,7 +108,7 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
value={survey.status}
onValueChange={(value) => {
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
surveyMutateAction({ ...survey, status: castedValue })
updateSurveyAction({ ...survey, status: castedValue })
.then(() => {
toast.success(
value === "inProgress"
@@ -149,7 +160,12 @@ const SummaryHeader = ({ surveyId, environment, survey, surveyBaseUrl, product }
</DropdownMenuContent>
</DropdownMenu>
</div>
<SuccessMessage environment={environment} survey={survey} surveyBaseUrl={surveyBaseUrl} />
<SuccessMessage
environment={environment}
survey={survey}
surveyBaseUrl={surveyBaseUrl}
singleUseIds={singleUseIds}
/>
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -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">&quot;Back&quot; 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>
)}

View File

@@ -1,7 +1,17 @@
"use client";
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
import { AdvancedOptionToggle, DatePicker, Input, Label } from "@formbricks/ui";
import {
AdvancedOptionToggle,
DatePicker,
Input,
Label,
Switch,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useEffect, useState } from "react";
@@ -10,9 +20,14 @@ import toast from "react-hot-toast";
interface ResponseOptionsCardProps {
localSurvey: TSurveyWithAnalytics;
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
isEncryptionKeySet: boolean;
}
export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: ResponseOptionsCardProps) {
export default function ResponseOptionsCard({
localSurvey,
setLocalSurvey,
isEncryptionKeySet,
}: ResponseOptionsCardProps) {
const [open, setOpen] = useState(false);
const autoComplete = localSurvey.autoComplete !== null;
const [redirectToggle, setRedirectToggle] = useState(false);
@@ -27,6 +42,12 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
subheading: "This free & open-source survey has been closed",
});
const [singleUseMessage, setSingleUseMessage] = useState({
heading: "The survey has already been answered.",
subheading: "You can only use this link once.",
});
const [singleUseEncryption, setSingleUseEncryption] = useState(isEncryptionKeySet);
const [verifyEmailSurveyDetails, setVerifyEmailSurveyDetails] = useState({
name: "",
subheading: "",
@@ -104,6 +125,53 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
};
const handleSingleUseSurveyToggle = () => {
if (!localSurvey.singleUse?.enabled) {
setLocalSurvey({
...localSurvey,
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: singleUseEncryption },
});
} else {
setLocalSurvey({ ...localSurvey, singleUse: { enabled: false, isEncrypted: false } });
}
};
const handleSingleUseSurveyMessageChange = ({
heading,
subheading,
}: {
heading?: string;
subheading?: string;
}) => {
const message = {
heading: heading ?? singleUseMessage.heading,
subheading: subheading ?? singleUseMessage.subheading,
};
const localSurveySingleUseEnabled = localSurvey.singleUse?.enabled ?? false;
setSingleUseMessage(message);
setLocalSurvey({
...localSurvey,
singleUse: { enabled: localSurveySingleUseEnabled, ...message, isEncrypted: singleUseEncryption },
});
};
const hangleSingleUseEncryptionToggle = () => {
if (!singleUseEncryption) {
setSingleUseEncryption(true);
setLocalSurvey({
...localSurvey,
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: true },
});
} else {
setSingleUseEncryption(false);
setLocalSurvey({
...localSurvey,
singleUse: { enabled: true, ...singleUseMessage, isEncrypted: false },
});
}
};
const handleVerifyEmailSurveyDetailsChange = ({
name,
subheading,
@@ -134,6 +202,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
setSurveyClosedMessageToggle(true);
}
if (localSurvey.singleUse?.enabled) {
setSingleUseMessage({
heading: localSurvey.singleUse.heading ?? singleUseMessage.heading,
subheading: localSurvey.singleUse.subheading ?? singleUseMessage.subheading,
});
setSingleUseEncryption(localSurvey.singleUse.isEncrypted);
}
if (localSurvey.verifyEmail) {
setVerifyEmailSurveyDetails({
name: localSurvey.verifyEmail.name!,
@@ -302,6 +378,81 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
</div>
</AdvancedOptionToggle>
{/* Single User Survey Options */}
<AdvancedOptionToggle
htmlId="singleUserSurveyOptions"
isChecked={!!localSurvey.singleUse?.enabled}
onToggle={handleSingleUseSurveyToggle}
title="Single-Use Survey Links"
description="Allow only 1 response per survey link."
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<div className="row mb-2 flex cursor-default items-center space-x-2">
<Label htmlFor="howItWorks">How it works</Label>
</div>
<ul className="mb-3 ml-4 cursor-default list-inside list-disc space-y-1">
<li className="text-sm text-slate-600">
Blocks survey if the survey URL has no Single Use Id (suId).
</li>
<li className="text-sm text-slate-600">
Blocks survey if a submission with the Single Use Id (suId) in the URL exists already.
</li>
</ul>
<Label htmlFor="headline">&lsquo;Link Used&rsquo; Message</Label>
<Input
autoFocus
id="heading"
className="mb-4 mt-2 bg-white"
name="heading"
defaultValue={singleUseMessage.heading}
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
/>
<Label htmlFor="headline">Subheading</Label>
<Input
className="mb-4 mt-2 bg-white"
id="subheading"
name="subheading"
defaultValue={singleUseMessage.subheading}
onChange={(e) => handleSingleUseSurveyMessageChange({ subheading: e.target.value })}
/>
<Label htmlFor="headline">URL Encryption</Label>
<div>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger>
<div className="mt-2 flex items-center space-x-1 ">
<Switch
id="encryption-switch"
checked={singleUseEncryption}
onCheckedChange={hangleSingleUseEncryptionToggle}
disabled={!isEncryptionKeySet}
/>
<Label htmlFor="encryption-label">
<div className="ml-2">
<p className="text-sm font-normal text-slate-600">
Enable encryption of Single Use Id (suId) in survey URL.
</p>
</div>
</Label>
</div>
</TooltipTrigger>
{!isEncryptionKeySet && (
<TooltipContent side={"top"}>
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">
FORMBRICKS_ENCRYPTION_KEY needs to be set to enable this feature.
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
</AdvancedOptionToggle>
{/* Verify Email Section */}
<AdvancedOptionToggle
htmlId="verifyEmailBeforeSubmission"
isChecked={verifyEmailToggle}

View File

@@ -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";
@@ -14,6 +15,7 @@ interface SettingsViewProps {
setLocalSurvey: (survey: TSurveyWithAnalytics) => void;
actionClasses: TActionClass[];
attributeClasses: TAttributeClass[];
isEncryptionKeySet: boolean;
}
export default function SettingsView({
@@ -22,6 +24,7 @@ export default function SettingsView({
setLocalSurvey,
actionClasses,
attributeClasses,
isEncryptionKeySet,
}: SettingsViewProps) {
return (
<div className="mt-12 space-y-3 p-5">
@@ -41,13 +44,19 @@ export default function SettingsView({
actionClasses={actionClasses}
/>
<ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
<ResponseOptionsCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
isEncryptionKeySet={isEncryptionKeySet}
/>
<RecontactOptionsCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
/>
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
</div>
);
}

View File

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

View File

@@ -20,6 +20,7 @@ interface SurveyEditorProps {
environment: TEnvironment;
actionClasses: TActionClass[];
attributeClasses: TAttributeClass[];
isEncryptionKeySet: boolean;
}
export default function SurveyEditor({
@@ -28,6 +29,7 @@ export default function SurveyEditor({
environment,
actionClasses,
attributeClasses,
isEncryptionKeySet,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -49,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) {
@@ -88,6 +92,7 @@ export default function SurveyEditor({
setLocalSurvey={setLocalSurvey}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
isEncryptionKeySet={isEncryptionKeySet}
/>
)}
</main>

View File

@@ -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";
@@ -14,7 +15,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { validateQuestion } from "./Validation";
import { deleteSurveyAction, surveyMutateAction } from "./actions";
import { deleteSurveyAction, updateSurveyAction } from "./actions";
interface SurveyMenuBarProps {
localSurvey: TSurveyWithAnalytics;
@@ -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,11 +212,12 @@ export default function SurveyMenuBar({
};
if (!validateSurvey(localSurvey)) {
setIsMutatingSurvey(false);
return;
}
try {
await surveyMutateAction({ ...strippedSurvey });
await updateSurveyAction({ ...strippedSurvey });
router.refresh();
setIsMutatingSurvey(false);
toast.success("Changes saved.");
@@ -155,6 +229,7 @@ export default function SurveyMenuBar({
} else {
router.push(`/environments/${environment.id}/surveys`);
}
router.refresh();
}
} catch (e) {
console.error(e);
@@ -239,9 +314,10 @@ export default function SurveyMenuBar({
onClick={async () => {
setIsMutatingSurvey(true);
if (!validateSurvey(localSurvey)) {
setIsMutatingSurvey(false);
return;
}
await surveyMutateAction({ ...localSurvey, status: "inProgress" });
await updateSurveyAction({ ...localSurvey, status: "inProgress" });
router.refresh();
setIsMutatingSurvey(false);
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);

View File

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

View File

@@ -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() !== ""
);
},
};

View File

@@ -1,12 +1,28 @@
"use server";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { deleteSurvey, updateSurvey } from "@formbricks/lib/services/survey";
import { deleteSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import { AuthorizationError } from "@formbricks/types/v1/errors";
export async function updateSurveyAction(survey: TSurvey): Promise<TSurvey> {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, survey.id);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
export async function surveyMutateAction(survey: TSurvey): Promise<TSurvey> {
return await updateSurvey(survey);
}
export async function deleteSurveyAction(surveyId: string) {
export const deleteSurveyAction = async (surveyId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
await deleteSurvey(surveyId);
}
};

View File

@@ -1,12 +1,12 @@
export const revalidate = REVALIDATION_INTERVAL;
import React from "react";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { FORMBRICKS_ENCRYPTION_KEY, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import SurveyEditor from "./SurveyEditor";
import { getSurveyWithAnalytics } from "@formbricks/lib/services/survey";
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
import { getEnvironment } from "@formbricks/lib/services/environment";
import { getSurveyWithAnalytics } from "@formbricks/lib/survey/service";
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 }) {
@@ -17,6 +17,7 @@ export default async function SurveysEditPage({ params }) {
getActionClasses(params.environmentId),
getAttributeClasses(params.environmentId),
]);
const isEncryptionKeySet = !!FORMBRICKS_ENCRYPTION_KEY;
if (!survey || !environment || !actionClasses || !attributeClasses || !product) {
return <ErrorComponent />;
}
@@ -29,6 +30,7 @@ export default async function SurveysEditPage({ params }) {
environment={environment}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
isEncryptionKeySet={isEncryptionKeySet}
/>
</>
);

View File

@@ -1,7 +1,18 @@
"use server";
import { createSurvey } from "@formbricks/lib/services/survey";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { createSurvey } from "@formbricks/lib/survey/service";
import { AuthorizationError } from "@formbricks/types/v1/errors";
import { TSurvey } from "@formbricks/types/v1/surveys";
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
export async function createSurveyAction(environmentId: string, surveyBody: any) {
return await createSurvey(environmentId, surveyBody);
}

View File

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

View File

@@ -22,6 +22,7 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { createSurveyAction } from "./actions";
import { customSurvey, templates } from "./templates";
import { TSurveyInput } from "@formbricks/types/v1/surveys";
type TemplateList = {
environmentId: string;
@@ -77,7 +78,7 @@ export default function TemplateList({
...activeTemplate.preset,
type: surveyType,
autoComplete,
};
} as Partial<TSurveyInput>;
const survey = await createSurveyAction(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
};

View File

@@ -1,7 +1,18 @@
"use server";
import { createSurvey } from "@formbricks/lib/services/survey";
import { createSurvey } from "@formbricks/lib/survey/service";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/v1/errors";
import { TSurvey } from "@formbricks/types/v1/surveys";
export async function createSurveyAction(environmentId: string, surveyBody: Partial<TSurvey>) {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
if (!isAuthorized) throw new AuthorizationError("Not authorized");
export async function createSurveyAction(environmentId: string, surveyBody: any) {
return await createSurvey(environmentId, surveyBody);
}

Some files were not shown because too many files have changed in this diff Show More