Compare commits

..

17 Commits

Author SHA1 Message Date
Piyush Gupta
435dbf2261 Merge branch 'main' of https://github.com/formbricks/formbricks into version-fix 2024-09-17 12:55:14 +05:30
Piyush Gupta
df74510756 fix: version check API calls 2024-09-17 12:54:20 +05:30
pandeymangg
57135d167b fix: github api call cache 2024-09-13 11:49:53 +05:30
Anshuman Pandey
916e47c55d Merge branch 'main' into main 2024-09-13 10:35:30 +05:30
RajuGangitla
5faccf1063 resolve merge conflicts 2024-09-11 10:22:45 +00:00
RajuGangitla
ebbc5abd3c IS_FORMBRICKS_CLOUD condtion added 2024-09-11 10:21:46 +00:00
Johannes
ad445d1915 tweaks 2024-09-11 11:35:52 +02:00
RajuGangitla
756d0c84af Merge branch 'formbricks:main' into main 2024-09-11 11:07:13 +05:30
RajuGangitla
8f6b356870 Merge branch 'main' of https://github.com/RajuGangitla/formbricks 2024-09-11 05:35:33 +00:00
RajuGangitla
e0ba9cb998 feat: info about new formbricks version 2024-09-11 05:34:36 +00:00
RajuGangitla
c6f1e9e7d5 Merge branch 'formbricks:main' into main 2024-09-11 09:58:58 +05:30
Anshuman Pandey
f025a70d20 Merge branch 'main' into main 2024-09-09 17:44:12 +05:30
pandeymangg
ba1e478d68 Merge remote-tracking branch 'origin/main' into raju-gangitla 2024-09-09 16:42:35 +05:30
Johannes
8e57082376 Merge branch 'main' into main 2024-09-08 15:25:29 +02:00
Johannes
2681d03e15 move loading to StatCard, parallelize API calls 2024-09-08 15:23:31 +02:00
RajuGangitla
72e023c530 Merge branch 'main' of https://github.com/RajuGangitla/formbricks 2024-09-06 07:40:34 +00:00
RajuGangitla
7088f9bd26 fix :Loading Skeleton for survey summary page #3085 2024-09-06 07:36:06 +00:00
781 changed files with 7717 additions and 19686 deletions

View File

@@ -1,7 +1,7 @@
name: oss.gg hack submission 🕹️
description: "Submit your contribution for the for the oss.gg hackathon"
title: "[🕹️]"
labels: 🕹️ oss.gg, player submission, hacktoberfest
title: "[oss.gg hackathon]"
labels: 🕹️ oss.gg, player submission
assignees: []
body:
- type: textarea

View File

@@ -20,20 +20,6 @@ jobs:
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
types: |
fix
feat
chore
docs
style
refactor
perf
test
build
ci
revert
ossgg
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this

View File

@@ -13,8 +13,8 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.446.0",
"next": "14.2.13",
"lucide-react": "^0.418.0",
"next": "14.2.5",
"react": "18.3.1",
"react-dom": "18.3.1"
},

View File

@@ -1,6 +1,6 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import "@formbricks/ui/globals.css";
import "../styles/globals.css";
const App = ({ Component, pageProps }: AppProps) => {
return (

View File

@@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Example on overriding packages/js colors */
.dark {
--fb-brand-color: red;
--fb-brand-text-color: white;
--fb-border-color: green;
--fb-border-color-highlight: var(--slate-500);
--fb-focus-color: red;
--fb-heading-color: yellow;
--fb-subheading-color: green;
--fb-info-text-color: orange;
--fb-signature-text-color: blue;
--fb-survey-background-color: black;
--fb-accent-background-color: rgb(13, 13, 12);
--fb-accent-background-color-selected: red;
--fb-placeholder-color: white;
--fb-shadow-color: yellow;
--fb-rating-fill: var(--yellow-300);
--fb-rating-hover: var(--yellow-500);
--fb-back-btn-border: currentColor;
--fb-submit-btn-border: transparent;
--fb-rating-selected: black;
}

View File

@@ -129,8 +129,10 @@ Locate that file. We are using the [Tailwind Template “Syntax”](https://tail
```tsx
import { useRouter } from "next/router";
import { useState } from "react";
import { Button } from "@formbricks/ui/components/Button";
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/components/Popover";
import { Button } from "@formbricks/ui/Button";
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
export const DocsFeedback = () => {

View File

@@ -79,6 +79,28 @@ Promise<{ id: string }, NetworkError | Error>
</CodeGroup>
</Col>
- Update Display
<Col>
<CodeGroup title="Update Display">
```javascript {{ title: 'Update Display Method Call'}}
await api.client.display.update(
displayId: "<your-display-id>",
{
userId: "<your-user-id>", // optional
responseId: "<your-response-id>", // optional
},
);
```
```javascript {{ title: 'Update Display Method Return Type' }}
Promise<{ }, NetworkError | Error]>
```
</CodeGroup>
</Col>
## Responses
- Create Response
@@ -151,6 +173,29 @@ Promise<{ }, NetworkError | Error]>
</CodeGroup>
</Col>
## Action
- Create Action:
<Note> An environment cannot have 2 actions with the same name. </Note>
<Col>
<CodeGroup title="Create Action">
```javascript {{ title: 'Create Action Method Call'}}
await api.client.action.create({
name: "<your-action-name>", // required
userId: "<your-user-id>", // required
});
```
```javascript {{ title: 'Create Action Method Return Type' }}
Promise<{ }, NetworkError | Error]>
```
</CodeGroup>
</Col>
## Attribute
- Update Attribute

View File

@@ -1,9 +1,4 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@formbricks/ui/components/Accordion";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
import { FaqJsonLdComponent } from "./FAQPageJsonLd";
const FAQ_DATA = [

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@@ -1,171 +0,0 @@
import { MdxImage } from "@/components/MdxImage";
import ActionCalculateOperators from "./images/action-calculate-operators.webp";
import ActionCalculateValue from "./images/action-calculate-value.webp";
import ActionCalculateVariables from "./images/action-calculate-variables.webp";
import ActionCalculate from "./images/action-calculate.webp";
import ActionJump from "./images/action-jump.webp";
import ActionOptions from "./images/action-options.webp";
import ActionRequire from "./images/action-require.webp";
import AddLogic from "./images/add-logic.webp";
import ConditionChaining from "./images/condition-chaining.webp";
import ConditionOperators from "./images/condition-operators.webp";
import ConditionOptions from "./images/condition-options.webp";
import ConditionValue from "./images/condition-value.webp";
import Conditions from "./images/conditions.webp";
import Editor from "./images/editor.webp";
import QuestionLogic from "./images/question-logic.webp";
export const metadata = {
title: "Logic Editor",
description:
"Create complex survey logic with the Logic Editor. Use conditions, actions, and variables to create a personalized survey experience.",
};
# Logic Editor
Create complex survey logic with the Logic Editor. Use conditions, actions, and variables to create a personalized survey experience.
<MdxImage src={Editor} alt="Logic Editor" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
## Terminology
- **Condition**: A rule that determines when an action should be executed.
- **Action**: A task that is executed when a condition is met.
## **Creating Logic**
1. **Add a Logic Block**: Click the `Add logic +` button to add a new logic block.
<MdxImage src={AddLogic} alt="Add Logic" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
<Note>
You can add multiple logic blocks to a survey. Logic blocks are executed in the order they are added. You
can rearrange the order of logic blocks.
</Note>
2. **Add Conditions**: Add conditions to the logic block. Conditions are rules that determine when an action should be executed.
<MdxImage
src={Conditions}
alt="Add Conditions"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Conditons can be based on:
- **Question**: The answer to a question.
- **Variable**: A variable value.
- **Hidden Field**: The value of a hidden field.
2.a **Condition Options**: Choose from a list of available conditions.
<MdxImage
src={ConditionOptions}
alt="Condition Options"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2.b **Condition Operators**: Choose an operator to compare the condition value.
<MdxImage
src={ConditionOperators}
alt="Condition Operators"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2.c **Condition Value**: Enter a value to compare the condition against.
Comparisons can be made against a fixed value or a dynamic value.
Dynamic values can be based on a question, variable, or hidden field.
<MdxImage
src={ConditionValue}
alt="Condition Value"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
- Conditions can be grouped. - Conditions can be combined using AND or OR operators. You can add multiple
conditions to a logic block. Conditions are evaluated in the order they are added.
</Note>
<MdxImage
src={ConditionChaining}
alt="Condition Chaining"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. **Add Actions**: Add actions to the logic block. Actions are tasks that are executed when a condition is met.
<Note>You can add multiple actions to a logic block. Actions are executed in the order they are added.</Note>
- 3.a **Action Options**: Choose from a list of available actions.
<MdxImage
src={ActionOptions}
alt="Add Actions"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Action is of the following types:
- **Calculate**: Perform a calculation. These variables are then available for use in other questions.
- Calculations can be performed on variables.
- Calculations can be based on fixed values or dynamic values.
<MdxImage
src={ActionCalculateVariables}
alt="Action Calculate Variables"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={ActionCalculateOperators}
alt="Action Calculate Variables"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={ActionCalculateValue}
alt="Action Calculate Variables"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<MdxImage
src={ActionCalculate}
alt="Action Calculate"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Require Answer**: Make a question required. Only the optional questions can be marked as required while filling the survey.
<MdxImage
src={ActionRequire}
alt="Action Require"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- **Jump to Question**: Skip to a specific question. The user will be redirected to the specified question based on the condition.
<MdxImage
src={ActionJump}
alt="Action Jump"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. **Save Logic**: Click the `Save` button to save the logic block.
# Question Logic
This logic is executed when the user answers the question. Logic can be as simple as showing a follow-up question based on the answer or as complex as calculating a score based on multiple answers.
<MdxImage
src={QuestionLogic}
alt="Question Logic"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,75 +0,0 @@
import { MdxImage } from "@/components/MdxImage";
import StepOne from "./images/1-publish-to-web.webp";
import StepTwo from "./images/2-warning-publish.webp";
import StepThree from "./images/3-share-link.webp";
export const metadata = {
title: "Shareable Dashboards",
description:
"Create shareable links to dashboards of specific surveys.",
};
# Shareable Dashboards
Formbricks allows you to create public, shareable versions of your survey results dashboards. This feature enables you to easily share survey results with stakeholders, team members, or the public without granting access to your Formbricks account.
## How To Publish Survey Results
1. **Go to survey summary**: Choose the survey for which you want to create a shareable dashboard and go to its summary page.
2. **Share results**: Click the "Share results" and then "Publish to web".
<MdxImage
src={StepOne}
alt="Go to survey summary"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. **Confirm**: Click "Publish to public web" (it's public).
<MdxImage
src={StepTwo}
alt="Go to survey summary"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
4. **Share link**: Formbricks has generated a unique URL for your public dashboard. Share it around.
<MdxImage
src={StepThree}
alt="Go to survey summary"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>Whoever has access to the link can access the survey results.</Note>
## How To Unpublish Survey Results
Unpublish is very simple: Go to "Share results" -> "Unpublish from web" -> "Unpublish".
<MdxImage
src={StepThree}
alt="Go to survey summary"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Key Features
- **Read-only access**: Viewers can see survey results but cannot modify data or settings.
- **Real-time updates**: The shared dashboard reflects current survey data in real-time.
- **Filters included**: Visitors can access all filters to dissect the data.
- **Revocable access**: You can disable the shared link at any time to restrict access.
## Use Cases
- Share results with clients or stakeholders
- Publish survey findings to your website or blog
- Collaborate with team members without sharing account credentials
- Create transparency by making certain survey results public
Shareable dashboards provide a simple yet powerful way to disseminate survey insights while maintaining control over your Formbricks account and data.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -1,15 +1,15 @@
import { MdxImage } from "@/components/MdxImage";
import EntraIDAppReg01 from "./images/entra_app_reg_01.webp";
import EntraIDAppReg02 from "./images/entra_app_reg_02.webp";
import EntraIDAppReg03 from "./images/entra_app_reg_03.webp";
import EntraIDAppReg04 from "./images/entra_app_reg_04.webp";
import EntraIDAppReg05 from "./images/entra_app_reg_05.webp";
import EntraIDAppReg06 from "./images/entra_app_reg_06.webp";
import EntraIDAppReg07 from "./images/entra_app_reg_07.webp";
import EntraIDAppReg08 from "./images/entra_app_reg_08.webp";
import EntraIDAppReg09 from "./images/entra_app_reg_09.webp";
import EntraIDAppReg10 from "./images/entra_app_reg_10.webp";
import EntraIDAppReg01 from "./images/entra_app_reg_01.png";
import EntraIDAppReg02 from "./images/entra_app_reg_02.png";
import EntraIDAppReg03 from "./images/entra_app_reg_03.png";
import EntraIDAppReg04 from "./images/entra_app_reg_04.png";
import EntraIDAppReg05 from "./images/entra_app_reg_05.png";
import EntraIDAppReg06 from "./images/entra_app_reg_06.png";
import EntraIDAppReg07 from "./images/entra_app_reg_07.png";
import EntraIDAppReg08 from "./images/entra_app_reg_08.png";
import EntraIDAppReg09 from "./images/entra_app_reg_09.png";
import EntraIDAppReg10 from "./images/entra_app_reg_10.png";
export const metadata = {
title: "Configure Formbricks with External auth providers",
@@ -38,7 +38,7 @@ These variables are present inside your machines docker-compose file. Restart
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | |
| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) |
| S3_ENDPOINT | Endpoint for S3. | optional | (resolved by the AWS SDK) |
| PRIVACY_URL | URL for privacy policy. | optional | |
| TERMS_URL | URL for terms of service. | optional | |
| IMPRINT_URL | URL for imprint. | optional | |

View File

@@ -1,17 +1,15 @@
// ResponsiveVideo.tsx
// ResponsiveVideo.js
export const ResponsiveVideo = ({ src, title }) => {
return (
<div className="max-w-[1280px]">
<div className="relative w-full overflow-hidden pt-[56.25%]">
<iframe
src={src}
title={title}
frameBorder="0"
className="absolute left-0 top-0 h-full w-full"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen></iframe>
</div>
<div className="relative w-full overflow-hidden pt-[56.25%]">
<iframe
src={src}
title={title}
frameBorder="0"
className="absolute left-0 top-0 h-full w-full"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen></iframe>
</div>
);
};

View File

@@ -44,7 +44,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
{ title: "Recall Functionality", href: "/global/recall" }, // global
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
],
},
],
@@ -69,7 +68,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
{ title: "Recall Functionality", href: "/global/recall" }, // global
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
],
},
],
@@ -93,7 +91,6 @@ export const navigation: Array<NavGroup> = [
{ title: "User Metadata", href: "/global/metadata" },
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
{ title: "Conditional Logic", href: "/global/conditional-logic" },
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" },
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
{ title: "Recall Functionality", href: "/global/recall" },
@@ -109,7 +106,6 @@ export const navigation: Array<NavGroup> = [
links: [
{ title: "Access Roles", href: "/global/access-roles" },
{ title: "Styling Theme", href: "/global/styling-theme" },
{ title: "Logic Editor", href: "/global/logic-editor" },
],
},
{

View File

@@ -13,39 +13,39 @@
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@algolia/autocomplete-core": "^1.17.4",
"@calcom/embed-react": "^1.5.1",
"@calcom/embed-react": "^1.5.0",
"@docsearch/css": "3",
"@docsearch/react": "^3.6.1",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^2.1.8",
"@headlessui/react": "^2.1.2",
"@headlessui/tailwindcss": "^0.2.1",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "14.2.13",
"@next/mdx": "14.2.5",
"@paralleldrive/cuid2": "^2.2.2",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/typography": "^0.5.13",
"acorn": "^8.12.1",
"autoprefixer": "^10.4.20",
"autoprefixer": "^10.4.19",
"clsx": "^2.1.1",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.43",
"framer-motion": "11.7.0",
"framer-motion": "11.3.20",
"lottie-web": "^5.12.2",
"lucide": "^0.446.0",
"lucide-react": "^0.446.0",
"lucide": "^0.418.0",
"lucide-react": "^0.418.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"next": "14.2.13",
"next-plausible": "^3.12.2",
"next-seo": "^6.6.0",
"next": "14.2.5",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-sitemap": "^4.2.3",
"next-themes": "^0.3.0",
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.4.0",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"react": "18.3.1",
"react-dom": "18.3.1",
@@ -56,13 +56,13 @@
"remark-gfm": "^4.0.0",
"remark-mdx": "^3.0.1",
"schema-dts": "^1.1.2",
"sharp": "^0.33.5",
"sharp": "^0.33.4",
"shiki": "^0.14.7",
"simple-functional-loader": "^1.2.1",
"tailwindcss": "^3.4.13",
"tailwindcss": "^3.4.7",
"unist-util-filter": "^5.0.1",
"unist-util-visit": "^5.0.0",
"zustand": "^4.5.5"
"zustand": "^4.5.4"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",

View File

@@ -12,30 +12,30 @@
},
"dependencies": {
"@formbricks/ui": "workspace:*",
"eslint-plugin-react-refresh": "^0.4.12",
"eslint-plugin-react-refresh": "^0.4.9",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@chromatic-com/storybook": "^2.0.2",
"@chromatic-com/storybook": "^1.6.1",
"@formbricks/config-typescript": "workspace:*",
"@storybook/addon-a11y": "^8.3.3",
"@storybook/addon-essentials": "^8.3.3",
"@storybook/addon-interactions": "^8.3.3",
"@storybook/addon-links": "^8.3.3",
"@storybook/addon-onboarding": "^8.3.3",
"@storybook/blocks": "^8.3.3",
"@storybook/react": "^8.3.3",
"@storybook/react-vite": "^8.3.3",
"@storybook/test": "^8.3.3",
"@typescript-eslint/eslint-plugin": "^8.7.0",
"@typescript-eslint/parser": "^8.7.0",
"@storybook/addon-a11y": "^8.2.9",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
"@storybook/addon-onboarding": "^8.2.9",
"@storybook/blocks": "^8.2.9",
"@storybook/react": "^8.2.9",
"@storybook/react-vite": "^8.2.9",
"@storybook/test": "^8.2.9",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitejs/plugin-react": "^4.3.1",
"esbuild": "^0.24.0",
"eslint-plugin-storybook": "^0.9.0",
"esbuild": "^0.23.0",
"eslint-plugin-storybook": "^0.8.0",
"prop-types": "^15.8.1",
"storybook": "^8.3.3",
"tsup": "^8.3.0",
"vite": "^5.4.8"
"storybook": "^8.2.9",
"tsup": "^8.2.4",
"vite": "^5.4.1"
}
}

View File

@@ -1,5 +1,5 @@
/** @type {import('tailwindcss').Config} */
import base from "../../packages/ui/tailwind.config";
import base from "../../packages/config-tailwind/tailwind.config";
export default {
...base,

View File

@@ -9,7 +9,7 @@ import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Button } from "@formbricks/ui/Button";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
interface ConnectWithFormbricksProps {

View File

@@ -7,9 +7,9 @@ import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { z } from "zod";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/components/Button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
interface InviteOrganizationMemberProps {
organization: TOrganization;

View File

@@ -4,10 +4,10 @@ import "prismjs/themes/prism.css";
import { useState } from "react";
import toast from "react-hot-toast";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { CodeBlock } from "@formbricks/ui/components/CodeBlock";
import { TabBar } from "@formbricks/ui/components/TabBar";
import { Html5Icon, NpmIcon } from "@formbricks/ui/components/icons";
import { Button } from "@formbricks/ui/Button";
import { CodeBlock } from "@formbricks/ui/CodeBlock";
import { TabBar } from "@formbricks/ui/TabBar";
import { Html5Icon, NpmIcon } from "@formbricks/ui/icons";
const tabs = [
{ id: "html", label: "HTML", icon: <Html5Icon /> },

View File

@@ -5,8 +5,8 @@ import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface InvitePageProps {
params: {

View File

@@ -1,10 +1,12 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ConnectPageProps {
params: {
@@ -24,12 +26,18 @@ const Page = async ({ params }: ConnectPageProps) => {
throw new Error("Product not found");
}
const channel = product.config.channel || null;
const channel = product.config.channel;
const industry = product.config.industry;
if (!channel || !industry) {
return notFound();
}
const customHeadline = getCustomHeadline(channel, industry);
return (
<div className="flex min-h-full flex-col items-center justify-center py-10">
<Header
title={`Let's connect your product with Formbricks`}
title={`Let's connect your ${customHeadline} with Formbricks`}
subtitle="It takes less than 4 minutes, pinky promise!"
/>
<div className="space-y-4 text-center">

View File

@@ -1,100 +0,0 @@
"use client";
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
import { XMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { TProduct } from "@formbricks/types/product";
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { createSurveyAction } from "@formbricks/ui/components/TemplateList/actions";
interface XMTemplateListProps {
product: TProduct;
user: TUser;
environmentId: string;
}
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const router = useRouter();
const createSurvey = async (activeTemplate: TXMTemplate) => {
const augmentedTemplate: TSurveyCreateInput = {
...activeTemplate,
type: "link",
createdBy: user.id,
};
const createSurveyResponse = await createSurveyAction({
environmentId: environmentId,
surveyBody: augmentedTemplate,
});
if (createSurveyResponse?.data) {
router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
} else {
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
toast.error(errorMessage);
}
};
const handleTemplateClick = (templateIdx) => {
setActiveTemplateId(templateIdx);
const template = XMTemplates[templateIdx];
const newTemplate = replacePresetPlaceholders(template, product);
createSurvey(newTemplate);
};
const XMTemplateOptions = [
{
title: "NPS",
description: "Implement proven best practices to understand WHY people buy.",
icon: ShoppingCartIcon,
onClick: () => handleTemplateClick(0),
isLoading: activeTemplateId === 0,
},
{
title: "5-Star Rating",
description: "Universal feedback solution to gauge overall satisfaction.",
icon: StarIcon,
onClick: () => handleTemplateClick(1),
isLoading: activeTemplateId === 1,
},
{
title: "CSAT",
description: "Implement best practices to measure customer satisfaction.",
icon: ThumbsUpIcon,
onClick: () => handleTemplateClick(2),
isLoading: activeTemplateId === 2,
},
{
title: "CES",
description: "Leverage every touchpoint to understand ease of customer interaction.",
icon: ActivityIcon,
onClick: () => handleTemplateClick(3),
isLoading: activeTemplateId === 3,
},
{
title: "Smileys",
description: "Use visual indicators to capture feedback across customer touchpoints.",
icon: SmileIcon,
onClick: () => handleTemplateClick(4),
isLoading: activeTemplateId === 4,
},
{
title: "eNPS",
description: "Universal feedback to understand employee engagement and satisfaction.",
icon: UsersIcon,
onClick: () => handleTemplateClick(5),
isLoading: activeTemplateId === 5,
},
];
return <OnboardingOptionsContainer options={XMTemplateOptions} />;
};

View File

@@ -1,13 +0,0 @@
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
import { TProduct } from "@formbricks/types/product";
import { TXMTemplate } from "@formbricks/types/templates";
// replace all occurences of productName with the actual product name in the current template
export const replacePresetPlaceholders = (template: TXMTemplate, product: TProduct) => {
const survey = structuredClone(template);
survey.name = survey.name.replace("{{productName}}", product.name);
survey.questions = survey.questions.map((question) => {
return replaceQuestionPresetPlaceholders(question, product);
});
return { ...template, ...survey };
};

View File

@@ -1,406 +0,0 @@
import { createId } from "@paralleldrive/cuid2";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
export const XMSurveyDefault: TXMTemplate = {
name: "",
endings: [getDefaultEndingCard([])],
questions: [],
styling: {
overwriteThemeStyling: true,
},
};
const NPSSurvey = (): TXMTemplate => {
return {
...XMSurveyDefault,
name: "NPS Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
required: true,
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any other comments, feedback, or concerns?" },
required: false,
inputType: "text",
},
],
};
};
const StarRatingSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "{{productName}}'s Rating Survey",
questions: [
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[0],
type: "question",
},
operator: "isLessThanOrEqual",
rightOperand: {
type: "static",
value: 3,
},
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: reusableQuestionIds[2],
},
],
},
],
range: 5,
scale: "number",
headline: { default: "How do you like {{productName}}?" },
required: true,
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[1],
type: "question",
},
operator: "isClicked",
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: "Write review" },
buttonExternal: true,
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
required: true,
subheader: { default: "Help us improve your experience." },
buttonLabel: { default: "Send" },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const CSATSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "{{productName}} CSAT",
questions: [
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[0],
type: "question",
},
operator: "isLessThanOrEqual",
rightOperand: {
type: "static",
value: 3,
},
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: reusableQuestionIds[2],
},
],
},
],
range: 5,
scale: "smiley",
headline: { default: "How satisfied are you with your {{productName}} experience?" },
required: true,
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
type: TSurveyQuestionTypeEnum.OpenText,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[1],
type: "question",
},
operator: "isSubmitted",
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
required: false,
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" },
required: false,
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const CESSurvey = (): TXMTemplate => {
return {
...XMSurveyDefault,
name: "CES Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" },
required: true,
lowerLabel: { default: "Disagree strongly" },
upperLabel: { default: "Agree strongly" },
isColorCodingEnabled: false,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" },
required: true,
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const SmileysRatingSurvey = (): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "Smileys Survey",
questions: [
{
id: reusableQuestionIds[0],
type: TSurveyQuestionTypeEnum.Rating,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[0],
type: "question",
},
operator: "isLessThanOrEqual",
rightOperand: {
type: "static",
value: 3,
},
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: reusableQuestionIds[2],
},
],
},
],
range: 5,
scale: "smiley",
headline: { default: "How do you like {{productName}}?" },
required: true,
lowerLabel: { default: "Not good" },
upperLabel: { default: "Very satisfied" },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: reusableQuestionIds[1],
type: "question",
},
operator: "isClicked",
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
},
],
},
],
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: "Write review" },
buttonExternal: true,
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
required: true,
subheader: { default: "Help us improve your experience." },
buttonLabel: { default: "Send" },
placeholder: { default: "Type your answer here..." },
inputType: "text",
},
],
};
};
const eNPSSurvey = (): TXMTemplate => {
return {
...XMSurveyDefault,
name: "eNPS Survey",
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: "How likely are you to recommend working at this company to a friend or colleague?",
},
required: false,
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any other comments, feedback, or concerns?" },
required: false,
inputType: "text",
},
],
};
};
export const XMTemplates: TXMTemplate[] = [
NPSSurvey(),
StarRatingSurvey(),
CSATSurvey(),
CESSurvey(),
SmileysRatingSurvey(),
eNPSSurvey(),
];

View File

@@ -1,60 +0,0 @@
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
import { getProductByEnvironmentId, getProducts } from "@formbricks/lib/product/service";
import { getUser } from "@formbricks/lib/user/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface XMTemplatePageProps {
params: {
environmentId: string;
};
}
const Page = async ({ params }: XMTemplatePageProps) => {
const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId);
if (!session) {
throw new Error("Session not found");
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
}
if (!environment) {
throw new Error("Environment not found");
}
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error("Product not found");
}
const products = await getProducts(organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title="What kind of feedback would you like to get?" />
<XMTemplateList product={product} user={user} environmentId={environment.id} />
{products.length >= 2 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={`/environments/${environment.id}/surveys`}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -1,12 +1,13 @@
import { TProductConfigChannel } from "@formbricks/types/product";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
switch (channel) {
case "website":
return "Let's get the most out of your website traffic!";
case "app":
return "Let's research what your users need!";
default:
return "You maintain a product, how exciting!";
}
export const getCustomHeadline = (channel: TProductConfigChannel, industry: TProductConfigIndustry) => {
const combinations = {
"website+eCommerce": "web shop",
"website+saas": "landing page",
"website+other": "website",
"app+eCommerce": "shopping app",
"app+saas": "SaaS app",
"app+other": "app",
};
return combinations[`${channel}+${industry}`] || "product";
};

View File

@@ -7,7 +7,7 @@ import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
import { ToasterClient } from "@formbricks/ui/ToasterClient";
const ProductOnboardingLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);

View File

@@ -1,8 +1,8 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ChannelPageProps {
params: {
@@ -17,14 +17,14 @@ const Page = async ({ params }: ChannelPageProps) => {
description: "Run well-timed pop-up surveys.",
icon: GlobeIcon,
iconText: "Built for scale",
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
},
{
title: "App with sign up",
description: "Run highly-targeted micro-surveys.",
icon: GlobeLockIcon,
iconText: "Enrich user profiles",
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
},
{
channel: "link",
@@ -32,7 +32,7 @@ const Page = async ({ params }: ChannelPageProps) => {
description: "Reach people anywhere online.",
icon: LinkIcon,
iconText: "Anywhere online",
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
href: `/organizations/${params.organizationId}/products/new/industry?channel=link`,
},
];

View File

@@ -3,8 +3,8 @@ import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface IndustryPageProps {
params: {

View File

@@ -1,47 +0,0 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface ModePageProps {
params: {
organizationId: string;
};
}
const Page = async ({ params }: ModePageProps) => {
const channelOptions = [
{
title: "Formbricks Surveys",
description: "Multi-purpose survey platform for web, app and email surveys.",
icon: ListTodoIcon,
href: `/organizations/${params.organizationId}/products/new/channel`,
},
{
title: "Formbricks CX",
description: "Surveys and reports to understand what your customers need.",
icon: HeartIcon,
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
},
];
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title="What are you here for?" />
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -1,5 +1,6 @@
"use client";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import Image from "next/image";
@@ -7,17 +8,16 @@ import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { FORMBRICKS_PRODUCT_ID_LS, FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
import {
TProductConfigChannel,
TProductConfigIndustry,
TProductMode,
TProductUpdateInput,
ZProductUpdateInput,
} from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
import { Button } from "@formbricks/ui/Button";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import {
FormControl,
FormDescription,
@@ -26,13 +26,12 @@ import {
FormItem,
FormLabel,
FormProvider,
} from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
import { SurveyInline } from "@formbricks/ui/components/Survey";
} from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { SurveyInline } from "@formbricks/ui/Survey";
interface ProductSettingsProps {
organizationId: string;
productMode: TProductMode;
channel: TProductConfigChannel;
industry: TProductConfigIndustry;
defaultBrandColor: string;
@@ -40,7 +39,6 @@ interface ProductSettingsProps {
export const ProductSettings = ({
organizationId,
productMode,
channel,
industry,
defaultBrandColor,
@@ -64,16 +62,16 @@ export const ProductSettings = ({
);
if (productionEnvironment) {
if (typeof window !== "undefined") {
localStorage.setItem(FORMBRICKS_PRODUCT_ID_LS, productionEnvironment.productId);
// Rmove filters when creating a new product
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
}
}
if (channel === "app" || channel === "website") {
if (channel !== "link") {
router.push(`/environments/${productionEnvironment?.id}/connect`);
} else if (channel === "link") {
} else {
router.push(`/environments/${productionEnvironment?.id}/surveys`);
} else if (productMode === "cx") {
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
}
} else {
const errorMessage = getFormattedErrorMessage(createProductResponse);
@@ -130,7 +128,9 @@ export const ProductSettings = ({
<FormItem className="w-full space-y-4">
<div>
<FormLabel>Product name</FormLabel>
<FormDescription>What is your product called?</FormDescription>
<FormDescription>
What is your {getCustomHeadline(channel, industry)} called?
</FormDescription>
</div>
<FormControl>
<div>

View File

@@ -1,11 +1,13 @@
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
import { startsWithVowel } from "@formbricks/lib/utils/strings";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ProductSettingsPageProps {
params: {
@@ -14,34 +16,31 @@ interface ProductSettingsPageProps {
searchParams: {
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
mode?: TProductMode;
};
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const channel = searchParams.channel || null;
const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys";
const customHeadline = getCustomHeadline(channel);
const channel = searchParams.channel;
const industry = searchParams.industry;
if (!channel || !industry) return notFound();
const customHeadline = getCustomHeadline(channel, industry);
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" || mode === "cx" ? (
{channel === "link" ? (
<Header
title="Match your brand, get 2x more responses."
subtitle="When people recognize your brand, they are much more likely to start and complete responses."
/>
) : (
<Header
title={customHeadline}
title={`You maintain ${startsWithVowel(customHeadline) ? "an " + customHeadline : "a " + customHeadline}, how exciting!`}
subtitle="Get 2x more responses matching surveys with your brand and UI"
/>
)}
<ProductSettings
organizationId={params.organizationId}
productMode={mode}
channel={channel}
industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR}

View File

@@ -1,59 +1,41 @@
import { LucideProps } from "lucide-react";
import Link from "next/link";
import { ForwardRefExoticComponent, RefAttributes } from "react";
import { cn } from "@formbricks/lib/cn";
import { OptionCard } from "@formbricks/ui/components/OptionCard";
import { OptionCard } from "@formbricks/ui/OptionCard";
interface OnboardingOptionsContainerProps {
options: {
title: string;
description: string;
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
iconText?: string;
href?: string;
onClick?: () => void;
isLoading?: boolean;
iconText: string;
href: string;
}[];
}
export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContainerProps) => {
const getOptionCard = (option) => {
const Icon = option.icon;
return (
<OptionCard
size="md"
key={option.title}
title={option.title}
onSelect={option.onClick}
description={option.description}
loading={option.isLoading || false}>
<div className="flex flex-col items-center">
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
{option.iconText && (
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
{option.iconText}
</p>
)}
</div>
</OptionCard>
);
};
return (
<div
className={cn({
"grid w-5/6 grid-cols-3 gap-8 text-center lg:w-2/3": options.length >= 3,
"flex justify-center gap-8": options.length < 3,
})}>
{options.map((option) =>
option.href ? (
<Link key={option.title} href={option.href}>
{getOptionCard(option)}
<div className="grid w-5/6 grid-cols-3 gap-8 text-center lg:w-2/3">
{options.map((option, index) => {
const Icon = option.icon;
return (
<Link href={option.href}>
<OptionCard
size="md"
key={index}
title={option.title}
description={option.description}
loading={false}>
<div className="flex flex-col items-center">
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
{option.iconText}
</p>
</div>
</OptionCard>
</Link>
) : (
getOptionCard(option)
)
)}
);
})}
</div>
);
};

View File

@@ -9,8 +9,8 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import { ToasterClient } from "@formbricks/ui/ToasterClient";
const EnvLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);

View File

@@ -2,7 +2,7 @@
import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
import { CreateNewActionTab } from "./CreateNewActionTab";
import { SavedActionsTab } from "./SavedActionsTab";

View File

@@ -1,29 +1,21 @@
"use client";
import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/app/lib/questions";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import {
CXQuestionTypes,
getQuestionDefaults,
questionTypes,
universalQuestionPresets,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
product: TProduct;
isCxMode: boolean;
}
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonProps) => {
const [open, setOpen] = useState(false);
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
return (
<Collapsible.Root
open={open}
@@ -45,7 +37,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
{/* <hr className="py-1 text-slate-600" /> */}
{availableQuestionTypes.map((questionType) => (
{questionTypes.map((questionType) => (
<button
type="button"
key={questionType.id}

View File

@@ -4,9 +4,9 @@ import { PlusIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface AddressQuestionFormProps {
localSurvey: TSurvey;

View File

@@ -1,6 +1,6 @@
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { LogicEditor } from "./LogicEditor";
import { UpdateQuestionId } from "./UpdateQuestionId";
interface AdvancedSettingsProps {
@@ -19,14 +19,16 @@ export const AdvancedSettings = ({
attributeClasses,
}: AdvancedSettingsProps) => {
return (
<div className="flex flex-col gap-4">
<ConditionalLogic
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
<div>
<div className="mb-4">
<LogicEditor
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
attributeClasses={attributeClasses}
/>
</div>
<UpdateQuestionId
question={question}

View File

@@ -6,9 +6,9 @@ import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
import { Badge } from "@formbricks/ui/components/Badge";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
import { Slider } from "@formbricks/ui/components/Slider";
import { Badge } from "@formbricks/ui/Badge";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Slider } from "@formbricks/ui/Slider";
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
interface BackgroundStylingCardProps {

View File

@@ -4,10 +4,10 @@ import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
const options = [
{

View File

@@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface CalQuestionFormProps {
localSurvey: TSurvey;

View File

@@ -8,12 +8,12 @@ import { cn } from "@formbricks/lib/cn";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
import { Badge } from "@formbricks/ui/components/Badge";
import { CardArrangementTabs } from "@formbricks/ui/components/CardArrangementTabs";
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
import { Slider } from "@formbricks/ui/components/Slider";
import { Switch } from "@formbricks/ui/components/Switch";
import { Badge } from "@formbricks/ui/Badge";
import { CardArrangementTabs } from "@formbricks/ui/CardArrangementTabs";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Slider } from "@formbricks/ui/Slider";
import { Switch } from "@formbricks/ui/Switch";
type CardStylingSettingsProps = {
open: boolean;

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
interface ColorSurveyBgProps {
handleBgChange: (bg: string, bgType: string) => void;

View File

@@ -1,199 +0,0 @@
import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
import {
getDefaultOperatorForQuestion,
replaceEndingCardHeadlineRecall,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import {
ArrowDownIcon,
ArrowUpIcon,
CopyIcon,
EllipsisVerticalIcon,
PlusIcon,
SplitIcon,
TrashIcon,
} from "lucide-react";
import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { Label } from "@formbricks/ui/components/Label";
interface ConditionalLogicProps {
localSurvey: TSurvey;
questionIdx: number;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
}
export function ConditionalLogic({
attributeClasses,
localSurvey,
question,
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
return modifiedSurvey;
}, [localSurvey, attributeClasses]);
const addLogic = () => {
const operator = getDefaultOperatorForQuestion(question);
const initialCondition: TSurveyLogic = {
id: createId(),
conditions: {
id: createId(),
connector: "and",
conditions: [
{
id: createId(),
leftOperand: {
value: question.id,
type: "question",
},
operator,
},
],
},
actions: [
{
id: createId(),
objective: "jumpToQuestion",
target: "",
},
],
};
updateQuestion(questionIdx, {
logic: [...(question?.logic ?? []), initialCondition],
});
};
const handleRemoveLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
logicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const moveLogic = (from: number, to: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const [movedItem] = logicCopy.splice(from, 1);
logicCopy.splice(to, 0, movedItem);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const duplicateLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const logicItem = logicCopy[logicItemIdx];
const newLogicItem = duplicateLogicItem(logicItem);
logicCopy.splice(logicItemIdx + 1, 0, newLogicItem);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
return (
<div className="mt-2">
<Label className="flex gap-2">
Conditional Logic
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
{question.logic && question.logic.length > 0 && (
<div className="mt-2 flex flex-col gap-4">
{question.logic.map((logicItem, logicItemIdx) => (
<div
key={logicItem.id}
className="flex w-full grow items-start gap-2 rounded-lg border border-slate-200 bg-slate-50 p-4">
<LogicEditor
localSurvey={transformedSurvey}
logicItem={logicItem}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
isLast={logicItemIdx === (question.logic ?? []).length - 1}
/>
<DropdownMenu>
<DropdownMenuTrigger>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
duplicateLogic(logicItemIdx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={logicItemIdx === 0}
onClick={() => {
moveLogic(logicItemIdx, logicItemIdx - 1);
}}>
<ArrowUpIcon className="h-4 w-4" />
Move up
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={logicItemIdx === (question.logic ?? []).length - 1}
onClick={() => {
moveLogic(logicItemIdx, logicItemIdx + 1);
}}>
<ArrowDownIcon className="h-4 w-4" />
Move down
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleRemoveLogic(logicItemIdx);
}}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
)}
<div className="mt-2 flex items-center space-x-2">
<Button
id="logicJumps"
className="bg-slate-100 hover:bg-slate-50"
type="button"
name="logicJumps"
size="sm"
variant="secondary"
EndIcon={PlusIcon}
onClick={addLogic}>
Add logic
</Button>
</div>
</div>
);
}

View File

@@ -4,8 +4,8 @@ import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface ConsentQuestionFormProps {
localSurvey: TSurvey;

View File

@@ -11,13 +11,13 @@ import {
ZActionClassInput,
} from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { TabToggle } from "@formbricks/ui/components/TabToggle";
import { CodeActionForm } from "@formbricks/ui/components/organisms/CodeActionForm";
import { NoCodeActionForm } from "@formbricks/ui/components/organisms/NoCodeActionForm";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { TabToggle } from "@formbricks/ui/TabToggle";
import { CodeActionForm } from "@formbricks/ui/organisms/CodeActionForm";
import { NoCodeActionForm } from "@formbricks/ui/organisms/NoCodeActionForm";
import { createActionClassAction } from "../actions";
interface CreateNewActionTabProps {

View File

@@ -2,10 +2,10 @@ import { PlusIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface IDateQuestionFormProps {
localSurvey: TSurvey;

View File

@@ -3,7 +3,7 @@
import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2";
@@ -14,8 +14,8 @@ import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
import { TooltipRenderer } from "@formbricks/ui/Tooltip";
interface EditEndingCardProps {
localSurvey: TSurvey;
@@ -32,6 +32,11 @@ interface EditEndingCardProps {
isFormbricksCloud: boolean;
}
const endingCardTypes = [
{ value: "endScreen", label: "Ending card" },
{ value: "redirectToUrl", label: "Redirect to Url" },
];
export const EditEndingCard = ({
localSurvey,
endingCardIndex,
@@ -47,16 +52,9 @@ export const EditEndingCard = ({
isFormbricksCloud,
}: EditEndingCardProps) => {
const endingCard = localSurvey.endings[endingCardIndex];
const isRedirectToUrlDisabled = isFormbricksCloud
? plan === "free" && endingCard.type !== "redirectToUrl"
: false;
const endingCardTypes = [
{ value: "endScreen", label: "Ending card" },
{ value: "redirectToUrl", label: "Redirect to Url", disabled: isRedirectToUrlDisabled },
];
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: endingCard.id,
});
@@ -206,16 +204,14 @@ export const EditEndingCard = ({
<OptionsSwitch
options={endingCardTypes}
currentOption={endingCard.type}
handleOptionChange={(newType) => {
const selectedOption = endingCardTypes.find((option) => option.value === newType);
if (!selectedOption?.disabled) {
if (newType === "redirectToUrl") {
updateSurvey({ type: "redirectToUrl" });
} else {
updateSurvey({ type: "endScreen" });
}
handleOptionChange={() => {
if (endingCard.type === "endScreen") {
updateSurvey({ type: "redirectToUrl" });
} else {
updateSurvey({ type: "endScreen" });
}
}}
disabled={isRedirectToUrlDisabled}
/>
</TooltipRenderer>
{endingCard.type === "endScreen" && (

View File

@@ -8,10 +8,10 @@ import { LocalizedEditor } from "@formbricks/ee/multi-language/components/locali
import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
import { FileInput } from "@formbricks/ui/components/FileInput";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Switch } from "@formbricks/ui/components/Switch";
import { FileInput } from "@formbricks/ui/FileInput";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Switch } from "@formbricks/ui/Switch";
interface EditWelcomeCardProps {
localSurvey: TSurvey;

View File

@@ -1,15 +1,10 @@
"use client";
import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@/app/lib/questions";
import { createId } from "@paralleldrive/cuid2";
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import {
CX_QUESTIONS_NAME_MAP,
QUESTIONS_ICON_MAP,
QUESTIONS_NAME_MAP,
getQuestionDefaults,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
import {
TSurvey,
@@ -17,8 +12,9 @@ import {
TSurveyQuestion,
TSurveyQuestionTypeEnum,
TSurveyRedirectUrlCard,
ZSurveyQuestion,
} from "@formbricks/types/surveys/types";
import { ConfirmationModal } from "@formbricks/ui/components/ConfirmationModal";
import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,7 +23,7 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
} from "@formbricks/ui/DropdownMenu";
interface EditorCardMenuProps {
survey: TSurvey;
@@ -41,7 +37,6 @@ interface EditorCardMenuProps {
addCard: (question: any, index?: number) => void;
cardType: "question" | "ending";
product?: TProduct;
isCxMode?: boolean;
}
export const EditorCardMenu = ({
@@ -56,75 +51,76 @@ export const EditorCardMenu = ({
updateCard,
addCard,
cardType,
isCxMode = false,
}: EditorCardMenuProps) => {
const [logicWarningModal, setLogicWarningModal] = useState(false);
const [changeToType, setChangeToType] = useState(() => {
if (card.type !== "endScreen" && card.type !== "redirectToUrl") {
return card.type;
}
return undefined;
});
const [changeToType, setChangeToType] = useState(
card.type !== "endScreen" && card.type !== "redirectToUrl" ? card.type : undefined
);
const isDeleteDisabled =
cardType === "question"
? survey.questions.length === 1
: survey.type === "link" && survey.endings.length === 1;
const availableQuestionTypes = isCxMode ? CX_QUESTIONS_NAME_MAP : QUESTIONS_NAME_MAP;
const changeQuestionType = (type?: TSurveyQuestionTypeEnum) => {
if (!type) return;
const parseResult = ZSurveyQuestion.safeParse(card);
if (parseResult.success && type) {
const question = parseResult.data;
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } = question;
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
card as TSurveyQuestion;
const questionDefaults = getQuestionDefaults(type, product);
const questionDefaults = getQuestionDefaults(type, product);
// if going from single select to multi select or vice versa, we need to keep the choices as well
if (
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) ||
(type === TSurveyQuestionTypeEnum.MultipleChoiceMulti &&
question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle)
) {
updateCard(cardIdx, {
choices: question.choices,
type,
logic: undefined,
});
return;
}
if (
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
card.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti) ||
(type === TSurveyQuestionTypeEnum.MultipleChoiceMulti &&
card.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle)
) {
updateCard(cardIdx, {
choices: card.choices,
...questionDefaults,
type,
headline,
subheader,
required,
imageUrl,
videoUrl,
buttonLabel,
backButtonLabel,
logic: undefined,
});
return;
}
updateCard(cardIdx, {
...questionDefaults,
type,
headline,
subheader,
required,
imageUrl,
videoUrl,
buttonLabel,
backButtonLabel,
logic: undefined,
});
};
const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => {
const questionDefaults = getQuestionDefaults(type, product);
const parseResult = ZSurveyQuestion.safeParse(card);
if (parseResult.success) {
const question = parseResult.data;
const questionDefaults = getQuestionDefaults(type, product);
addCard(
{
...questionDefaults,
type,
id: createId(),
required: true,
},
cardIdx + 1
);
addCard(
{
...questionDefaults,
type,
id: createId(),
required: true,
},
cardIdx + 1
);
const section = document.getElementById(`${card.id}`);
section?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
// scroll to the new question
const section = document.getElementById(`${question.id}`);
section?.scrollIntoView({ behavior: "smooth", block: "end", inline: "end" });
}
};
const addEndingCardBelow = () => {
@@ -171,25 +167,29 @@ export const EditorCardMenu = ({
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2 border border-slate-200 text-slate-600 hover:text-slate-700">
{Object.entries(availableQuestionTypes).map(([type, name]) => {
if (type === card.type) return null;
return (
<DropdownMenuItem
key={type}
className="min-h-8 cursor-pointer"
onClick={() => {
setChangeToType(type as TSurveyQuestionTypeEnum);
if ((card as TSurveyQuestion).logic) {
setLogicWarningModal(true);
return;
}
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
const parsedResult = ZSurveyQuestion.safeParse(card);
if (parsedResult.success) {
const question = parsedResult.data;
if (type === question.type) return null;
return (
<DropdownMenuItem
key={type}
className="min-h-8 cursor-pointer"
onClick={() => {
setChangeToType(type as TSurveyQuestionTypeEnum);
if (question.logic) {
setLogicWarningModal(true);
return;
}
changeQuestionType(type as TSurveyQuestionTypeEnum);
}}>
{QUESTIONS_ICON_MAP[type as TSurveyQuestionTypeEnum]}
<span className="ml-2">{name}</span>
</DropdownMenuItem>
);
changeQuestionType(type as TSurveyQuestionTypeEnum);
}}>
{QUESTIONS_ICON_MAP[type as TSurveyQuestionTypeEnum]}
<span className="ml-2">{name}</span>
</DropdownMenuItem>
);
}
})}
</DropdownMenuSubContent>
</DropdownMenuSub>
@@ -212,7 +212,7 @@ export const EditorCardMenu = ({
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-4 border border-slate-200">
{Object.entries(availableQuestionTypes).map(([type, name]) => {
{Object.entries(QUESTIONS_NAME_MAP).map(([type, name]) => {
return (
<DropdownMenuItem
key={type}

View File

@@ -4,10 +4,10 @@ import { useState } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Switch } from "@formbricks/ui/components/Switch";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Switch } from "@formbricks/ui/Switch";
interface EndScreenFormProps {
localSurvey: TSurvey;

View File

@@ -11,10 +11,10 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface FileUploadFormProps {
localSurvey: TSurvey;

View File

@@ -9,9 +9,9 @@ import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { mixColor } from "@formbricks/lib/utils/colors";
import { TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
import { Button } from "@formbricks/ui/Button";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
type FormStylingSettingsProps = {
open: boolean;

View File

@@ -1,6 +1,5 @@
"use client";
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import * as Collapsible from "@radix-ui/react-collapsible";
import { EyeOff } from "lucide-react";
import { useState } from "react";
@@ -9,11 +8,11 @@ import { cn } from "@formbricks/lib/cn";
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys/types";
import { validateId } from "@formbricks/types/surveys/validation";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { Switch } from "@formbricks/ui/components/Switch";
import { Tag } from "@formbricks/ui/components/Tag";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import { Tag } from "@formbricks/ui/Tag";
interface HiddenFieldsCardProps {
localSurvey: TSurvey;
@@ -66,25 +65,6 @@ export const HiddenFieldsCard = ({
});
};
const handleDeleteHiddenField = (fieldId: string) => {
const quesIdx = findHiddenFieldUsedInLogic(localSurvey, fieldId);
if (quesIdx !== -1) {
toast.error(
`${fieldId} is used in logic of question ${quesIdx + 1}. Please remove it from logic first.`
);
return;
}
updateSurvey(
{
enabled: true,
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
},
fieldId
);
};
return (
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
<div
@@ -131,7 +111,15 @@ export const HiddenFieldsCard = ({
return (
<Tag
key={fieldId}
onDelete={(fieldId) => handleDeleteHiddenField(fieldId)}
onDelete={() => {
updateSurvey(
{
enabled: true,
fieldIds: localSurvey.hiddenFields?.fieldIds?.filter((q) => q !== fieldId),
},
fieldId
);
}}
tagId={fieldId}
tagName={fieldId}
/>

View File

@@ -10,9 +10,9 @@ import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
import { Badge } from "@formbricks/ui/components/Badge";
import { Label } from "@formbricks/ui/components/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/components/RadioGroup";
import { Badge } from "@formbricks/ui/Badge";
import { Label } from "@formbricks/ui/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
interface HowToSendCardProps {
localSurvey: TSurvey;
@@ -231,7 +231,7 @@ export const HowToSendCard = ({
You can also use Formbricks to run {promotedFeaturesString} surveys.{" "}
<Link
target="_blank"
href={`/organizations/${organizationId}/products/new/mode`}
href={`/organizations/${organizationId}/products/new/channel`}
className="font-medium underline decoration-slate-400 underline-offset-2">
Create a new product
</Link>{" "}

View File

@@ -1,4 +1,4 @@
import { FileInput } from "@formbricks/ui/components/FileInput";
import { FileInput } from "@formbricks/ui/FileInput";
interface UploadImageSurveyBgProps {
environmentId: string;

View File

@@ -1,51 +1,477 @@
import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions";
import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import { ArrowRightIcon } from "lucide-react";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
import {
ArrowDownIcon,
ChevronDown,
CornerDownRightIcon,
HelpCircle,
SplitIcon,
TrashIcon,
} from "lucide-react";
import Image from "next/image";
import { useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
TSurvey,
TSurveyLogic,
TSurveyLogicCondition,
TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/Button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
interface LogicEditorProps {
localSurvey: TSurvey;
logicItem: TSurveyLogic;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
questionIdx: number;
logicIdx: number;
isLast: boolean;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
attributeClasses: TAttributeClass[];
}
export function LogicEditor({
type LogicConditions = {
[K in TSurveyLogicCondition]: {
label: string;
values: string[] | null;
unique?: boolean;
multiSelect?: boolean;
};
};
const conditions = {
openText: ["submitted", "skipped"],
multipleChoiceSingle: ["submitted", "skipped", "equals", "notEquals", "includesOne"],
multipleChoiceMulti: ["submitted", "skipped", "includesAll", "includesOne", "equals"],
nps: [
"equals",
"notEquals",
"lessThan",
"lessEqual",
"greaterThan",
"greaterEqual",
"submitted",
"skipped",
],
rating: [
"equals",
"notEquals",
"lessThan",
"lessEqual",
"greaterThan",
"greaterEqual",
"submitted",
"skipped",
],
cta: ["clicked", "skipped"],
consent: ["skipped", "accepted"],
pictureSelection: ["submitted", "skipped", "includesAll", "includesOne", "equals"],
fileUpload: ["uploaded", "notUploaded"],
cal: ["skipped", "booked"],
matrix: ["isCompletelySubmitted", "isPartiallySubmitted", "skipped"],
address: ["submitted", "skipped"],
ranking: ["submitted", "skipped"],
};
export const LogicEditor = ({
localSurvey,
logicItem,
updateQuestion,
question,
questionIdx,
logicIdx,
isLast,
}: LogicEditorProps) {
updateQuestion,
attributeClasses,
}: LogicEditorProps) => {
const [searchValue, setSearchValue] = useState<string>("");
const showDropdownSearch = question.type !== "pictureSelection";
const transformedSurvey = useMemo(() => {
return replaceHeadlineRecall(localSurvey, "default", attributeClasses);
}, [localSurvey, attributeClasses]);
const questionValues: string[] = useMemo(() => {
if ("choices" in question) {
if (question.type === "pictureSelection") {
return question.choices.map((choice) => choice.id);
} else {
return question.choices.map((choice) => getLocalizedValue(choice.label, "default"));
}
} else if ("range" in question) {
return Array.from({ length: question.range ? question.range : 0 }, (_, i) => (i + 1).toString());
} else if (question.type === TSurveyQuestionTypeEnum.NPS) {
return Array.from({ length: 11 }, (_, i) => (i + 0).toString());
}
return [];
}, [question]);
const logicConditions: LogicConditions = {
submitted: {
label: "is submitted",
values: null,
unique: true,
},
skipped: {
label: "is skipped",
values: null,
unique: true,
},
accepted: {
label: "is accepted",
values: null,
unique: true,
},
clicked: {
label: "is clicked",
values: null,
unique: true,
},
equals: {
label: "equals",
values: questionValues,
},
notEquals: {
label: "does not equal",
values: questionValues,
},
lessThan: {
label: "is less than",
values: questionValues,
},
lessEqual: {
label: "is less or equal to",
values: questionValues,
},
greaterThan: {
label: "is greater than",
values: questionValues,
},
greaterEqual: {
label: "is greater or equal to",
values: questionValues,
},
includesAll: {
label: "includes all of",
values: questionValues,
multiSelect: true,
},
includesOne: {
label: "includes one of",
values: questionValues,
multiSelect: true,
},
uploaded: {
label: "has uploaded file",
values: null,
unique: true,
},
notUploaded: {
label: "has not uploaded file",
values: null,
unique: true,
},
booked: {
label: "has a call booked",
values: null,
unique: true,
},
isCompletelySubmitted: {
label: "is completely submitted",
values: null,
unique: true,
},
isPartiallySubmitted: {
label: "is partially submitted",
values: null,
unique: true,
},
};
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,
value: undefined,
destination: undefined,
});
updateQuestion(questionIdx, { logic: newLogic });
};
const updateLogic = (logicIdx: number, updatedAttributes: any) => {
const currentLogic: any = question.logic ? question.logic[logicIdx] : undefined;
if (!currentLogic) return;
// clean logic value if not needed or if condition changed between multiSelect and singleSelect conditions
const updatedCondition = updatedAttributes?.condition;
const currentCondition = currentLogic?.condition;
const updatedLogicCondition = logicConditions[updatedCondition];
const currentLogicCondition = logicConditions[currentCondition];
if (updatedCondition) {
if (updatedLogicCondition?.multiSelect && !currentLogicCondition?.multiSelect) {
updatedAttributes.value = [];
} else if (
(!updatedLogicCondition?.multiSelect && currentLogicCondition?.multiSelect) ||
updatedLogicCondition?.values === null
) {
updatedAttributes.value = undefined;
}
}
const newLogic = !question.logic
? []
: question.logic.map((logic, idx) => {
if (idx === logicIdx) {
return { ...logic, ...updatedAttributes };
}
return logic;
});
updateQuestion(questionIdx, { logic: newLogic });
};
const updateMultiSelectLogic = (logicIdx: number, checked: boolean, value: string) => {
const newLogic = !question.logic
? []
: question.logic.map((logic, idx) => {
if (idx === logicIdx) {
const newValues = !logic.value ? [] : logic.value;
if (checked) {
newValues.push(value);
} else {
newValues.splice(newValues.indexOf(value), 1);
}
return { ...logic, value: Array.from(new Set(newValues)) };
}
return logic;
});
updateQuestion(questionIdx, { logic: newLogic });
};
const deleteLogic = (logicIdx: number) => {
const updatedLogic = !question.logic ? [] : structuredClone(question.logic);
updatedLogic.splice(logicIdx, 1);
updateQuestion(questionIdx, { logic: updatedLogic });
};
if (!(question.type in conditions)) {
return <></>;
}
const getLogicDisplayValue = (value: string | string[]): string => {
if (question.type === "pictureSelection") {
if (Array.isArray(value)) {
return value
.map((val) => {
const choiceIndex = question.choices.findIndex((choice) => choice.id === val);
return `Picture ${choiceIndex + 1}`;
})
.join(", ");
} else {
const choiceIndex = question.choices.findIndex((choice) => choice.id === value);
return `Picture ${choiceIndex + 1}`;
}
} else if (Array.isArray(value)) {
return value.join(", ");
}
return value;
};
const getOptionPreview = (value: string) => {
if (question.type === "pictureSelection") {
const choice = question.choices.find((choice) => choice.id === value);
if (choice) {
return <Image src={choice.imageUrl} alt={"Picture"} width={20} height={12} className="rounded-xs" />;
}
}
};
return (
<div className="flex w-full grow flex-col gap-4 overflow-x-auto pb-2 text-sm">
<LogicEditorConditions
conditions={logicItem.conditions}
updateQuestion={updateQuestion}
question={question}
questionIdx={questionIdx}
localSurvey={localSurvey}
logicIdx={logicIdx}
/>
<LogicEditorActions
logicItem={logicItem}
logicIdx={logicIdx}
question={question}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
/>
{isLast ? (
<div className="flex flex-wrap items-center space-x-2">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
<div className="mt-3">
<Label>Logic Jumps</Label>
{question?.logic && question?.logic?.length !== 0 && (
<div className="mt-2 space-y-3">
{question?.logic?.map((logic, logicIdx) => (
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
<div>
<CornerDownRightIcon className="h-4 w-4" />
</div>
<p className="text-slate-800">If this answer</p>
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
<SelectTrigger className="min-w-fit flex-1">
<SelectValue placeholder="Select condition" className="text-xs lg:text-sm" />
</SelectTrigger>
<SelectContent>
{conditions[question.type].map(
(condition) =>
!(question.required && (condition === "skipped" || condition === "notUploaded")) && (
<SelectItem
key={condition}
value={condition}
title={logicConditions[condition].label}
className="text-xs lg:text-sm">
{logicConditions[condition].label}
</SelectItem>
)
)}
</SelectContent>
</Select>
{logic.condition && logicConditions[logic.condition].values != null && (
<DropdownMenu>
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<div className="flex h-10 w-full items-center justify-between overflow-hidden rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
{!logic.value || logic.value?.length === 0 ? (
<p className="line-clamp-1 text-slate-400" title="Select match type">
Select match type
</p>
) : (
<p className="line-clamp-1" title={getLogicDisplayValue(logic.value)}>
{getLogicDisplayValue(logic.value)}
</p>
)}
<ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-40 bg-slate-50 text-slate-700"
align="start"
side="bottom">
{showDropdownSearch && (
<Input
autoFocus
placeholder="Search options"
className="mb-1 w-full bg-white"
onChange={(e) => setSearchValue(e.target.value)}
value={searchValue}
onKeyDown={(e) => e.stopPropagation()}
/>
)}
<div className="max-h-72 overflow-y-auto overflow-x-hidden">
{logicConditions[logic.condition].values
?.filter((value) => value.includes(searchValue))
?.map((value) => (
<DropdownMenuCheckboxItem
key={value}
title={value}
checked={
!logicConditions[logic.condition].multiSelect
? logic.value === value
: logic.value?.includes(value)
}
onSelect={(e) => e.preventDefault()}
onCheckedChange={(e) =>
!logicConditions[logic.condition].multiSelect
? updateLogic(logicIdx, { value })
: updateMultiSelectLogic(logicIdx, e, value)
}>
<div className="flex space-x-2">
{question.type === "pictureSelection" && getOptionPreview(value)}
<p>{getLogicDisplayValue(value)}</p>
</div>
</DropdownMenuCheckboxItem>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
)}
<p className="text-slate-800">jump to</p>
<Select
value={logic.destination}
onValueChange={(e) => updateLogic(logicIdx, { destination: e })}>
<SelectTrigger className="w-fit overflow-hidden">
<SelectValue placeholder="Select question" />
</SelectTrigger>
<SelectContent>
{transformedSurvey.questions.map(
(question, idx) =>
idx !== questionIdx && (
<SelectItem
key={question.id}
value={question.id}
title={getLocalizedValue(question.headline, "default")}>
<div className="w-96">
<p className="truncate text-left">
{idx + 1}
{". "}
{getLocalizedValue(question.headline, "default")}
</p>
</div>
</SelectItem>
)
)}
{localSurvey.endings.map((ending) => {
return (
<SelectItem value={ending.id}>
{ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default")
: ending.label}
</SelectItem>
);
})}
</SelectContent>
</Select>
<div>
<TrashIcon
className="h-4 w-4 cursor-pointer text-slate-400"
onClick={() => deleteLogic(logicIdx)}
/>
</div>
</div>
))}
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">
<ArrowDownIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
</div>
</div>
) : null}
)}
<div className="mt-2 flex items-center space-x-2">
<Button
id="logicJumps"
type="button"
name="logicJumps"
size="sm"
variant="secondary"
StartIcon={SplitIcon}
onClick={() => addLogic()}>
Add logic
</Button>
<TooltipProvider delayDuration={50}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="ml-2 inline h-4 w-4 cursor-default text-slate-500" />
</TooltipTrigger>
<TooltipContent className="max-w-[300px]" side="top">
With logic jumps you can skip questions based on the responses users give.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
}
};

View File

@@ -1,236 +0,0 @@
import {
actionObjectiveOptions,
getActionOperatorOptions,
getActionTargetOptions,
getActionValueOptions,
getActionVariableOptions,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils";
import {
TActionNumberVariableCalculateOperator,
TActionObjective,
TActionTextVariableCalculateOperator,
TActionVariableValueType,
TSurvey,
TSurveyLogic,
TSurveyLogicAction,
TSurveyQuestion,
} from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { InputCombobox } from "@formbricks/ui/components/InputCombobox";
interface LogicEditorActions {
localSurvey: TSurvey;
logicItem: TSurveyLogic;
logicIdx: number;
question: TSurveyQuestion;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
questionIdx: number;
}
export function LogicEditorActions({
localSurvey,
logicItem,
logicIdx,
question,
updateQuestion,
questionIdx,
}: LogicEditorActions) {
const actions = logicItem.actions;
const handleActionsChange = (
operation: "remove" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: TSurveyLogicAction
) => {
const logicCopy = structuredClone(question.logic) ?? [];
const currentLogicItem = logicCopy[logicIdx];
const actionsClone = currentLogicItem.actions;
switch (operation) {
case "remove":
actionsClone.splice(actionIdx, 1);
break;
case "addBelow":
actionsClone.splice(actionIdx + 1, 0, { id: createId(), objective: "jumpToQuestion", target: "" });
break;
case "duplicate":
actionsClone.splice(actionIdx + 1, 0, { ...actionsClone[actionIdx], id: createId() });
break;
case "update":
if (!action) return;
actionsClone[actionIdx] = action;
break;
}
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleObjectiveChange = (actionIdx: number, objective: TActionObjective) => {
const action = actions[actionIdx];
const actionBody = getUpdatedActionBody(action, objective);
handleActionsChange("update", actionIdx, actionBody);
};
const handleValuesChange = (actionIdx: number, values: Partial<TSurveyLogicAction>) => {
const action = actions[actionIdx];
const actionBody = { ...action, ...values } as TSurveyLogicAction;
handleActionsChange("update", actionIdx, actionBody);
};
return (
<div className="flex grow gap-2">
<CornerDownRightIcon className="mt-3 h-4 w-4 shrink-0" />
<div className="flex grow flex-col gap-y-2">
{actions?.map((action, idx) => (
<div key={action.id} className="flex grow items-center justify-between gap-x-2">
<div className="block w-9 shrink-0">{idx === 0 ? "Then" : "and"}</div>
<div className="flex grow items-center gap-x-2">
<InputCombobox
id={`action-${idx}-objective`}
key={`objective-${action.id}`}
showSearch={false}
options={actionObjectiveOptions}
value={action.objective}
onChangeValue={(val: TActionObjective) => {
handleObjectiveChange(idx, val);
}}
comboboxClasses="grow"
/>
{action.objective !== "calculate" && (
<InputCombobox
id={`action-${idx}-target`}
key={`target-${action.id}`}
showSearch={false}
options={getActionTargetOptions(action, localSurvey, questionIdx)}
value={action.target}
onChangeValue={(val: string) => {
handleValuesChange(idx, {
target: val,
});
}}
comboboxClasses="grow"
/>
)}
{action.objective === "calculate" && (
<>
<InputCombobox
id={`action-${idx}-variableId`}
key={`variableId-${action.id}`}
showSearch={false}
options={getActionVariableOptions(localSurvey)}
value={action.variableId}
onChangeValue={(val: string) => {
handleValuesChange(idx, {
variableId: val,
value: {
type: "static",
value: "",
},
});
}}
comboboxClasses="grow"
emptyDropdownText="Add a variable to calculate"
/>
<InputCombobox
id={`action-${idx}-operator`}
key={`operator-${action.id}`}
showSearch={false}
options={getActionOperatorOptions(
localSurvey.variables.find((v) => v.id === action.variableId)?.type
)}
value={action.operator}
onChangeValue={(
val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator
) => {
handleValuesChange(idx, {
operator: val,
});
}}
comboboxClasses="grow"
/>
<InputCombobox
id={`action-${idx}-value`}
key={`value-${action.id}`}
withInput={true}
clearable={true}
value={action.value?.value ?? ""}
inputProps={{
placeholder: "Value",
type: localSurvey.variables.find((v) => v.id === action.variableId)?.type || "text",
}}
groupedOptions={getActionValueOptions(action.variableId, localSurvey)}
onChangeValue={(val, option, fromInput) => {
const fieldType = option?.meta?.type as TActionVariableValueType;
if (!fromInput && fieldType !== "static") {
handleValuesChange(idx, {
value: {
type: fieldType,
value: val as string,
},
});
} else if (fromInput) {
handleValuesChange(idx, {
value: {
type: "static",
value: val as string,
},
});
}
}}
comboboxClasses="grow shrink-0"
/>
</>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger id={`actions-${idx}-dropdown`}>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("addBelow", idx);
}}>
<PlusIcon className="h-4 w-4" />
Add action below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={actions.length === 1}
onClick={() => {
handleActionsChange("remove", idx);
}}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("duplicate", idx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,347 +0,0 @@
import {
getConditionOperatorOptions,
getConditionValueOptions,
getDefaultOperatorForQuestion,
getMatchValueProps,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import {
addConditionBelow,
createGroupFromResource,
duplicateCondition,
isConditionGroup,
removeCondition,
toggleGroupConnector,
updateCondition,
} from "@formbricks/lib/surveyLogic/utils";
import {
TConditionGroup,
TDynamicLogicField,
TRightOperand,
TSingleCondition,
TSurvey,
TSurveyLogicConditionsOperator,
TSurveyQuestion,
} from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { InputCombobox, TComboboxOption } from "@formbricks/ui/components/InputCombobox";
interface LogicEditorConditionsProps {
conditions: TConditionGroup;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
localSurvey: TSurvey;
questionIdx: number;
logicIdx: number;
depth?: number;
}
export function LogicEditorConditions({
conditions,
logicIdx,
question,
localSurvey,
questionIdx,
updateQuestion,
depth = 0,
}: LogicEditorConditionsProps) {
const handleAddConditionBelow = (resourceId: string) => {
const operator = getDefaultOperatorForQuestion(question);
const condition: TSingleCondition = {
id: createId(),
leftOperand: {
value: question.id,
type: "question",
},
operator,
};
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
addConditionBelow(logicItem.conditions, resourceId, condition);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleConnectorChange = (groupId: string) => {
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
toggleGroupConnector(logicItem.conditions, groupId);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleRemoveCondition = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
removeCondition(logicItem.conditions, resourceId);
// Remove the logic item if there are zero conditions left
if (logicItem.conditions.conditions.length === 0) {
logicCopy.splice(logicIdx, 1);
}
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleDuplicateCondition = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
duplicateCondition(logicItem.conditions, resourceId);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleCreateGroup = (resourceId: string) => {
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
createGroupFromResource(logicItem.conditions, resourceId);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleUpdateCondition = (resourceId: string, updateConditionBody: Partial<TSingleCondition>) => {
const logicCopy = structuredClone(question.logic) ?? [];
const logicItem = logicCopy[logicIdx];
updateCondition(logicItem.conditions, resourceId, updateConditionBody);
updateQuestion(questionIdx, {
logic: logicCopy,
});
};
const handleQuestionChange = (condition: TSingleCondition, value: string, option?: TComboboxOption) => {
handleUpdateCondition(condition.id, {
leftOperand: {
value,
type: option?.meta?.type as TDynamicLogicField,
},
operator: "isSkipped",
rightOperand: undefined,
});
};
const handleOperatorChange = (condition: TSingleCondition, value: TSurveyLogicConditionsOperator) => {
if (value !== condition.operator) {
handleUpdateCondition(condition.id, {
operator: value,
rightOperand: undefined,
});
}
};
const handleRightOperandChange = (
condition: TSingleCondition,
value: string | number | string[],
option?: TComboboxOption
) => {
const type = (option?.meta?.type as TRightOperand["type"]) || "static";
switch (type) {
case "question":
case "hiddenField":
case "variable":
handleUpdateCondition(condition.id, {
rightOperand: {
value: value as string,
type,
},
});
break;
case "static":
handleUpdateCondition(condition.id, {
rightOperand: {
value,
type,
},
});
break;
}
};
const renderCondition = (
condition: TSingleCondition | TConditionGroup,
index: number,
parentConditionGroup: TConditionGroup
) => {
const connector = parentConditionGroup.connector;
if (isConditionGroup(condition)) {
return (
<div key={condition.id} className="flex items-start justify-between gap-4">
{index === 0 ? (
<div>When</div>
) : (
<div
className={cn("w-14", index === 1 && "cursor-pointer underline")}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);
}}>
{connector}
</div>
)}
<div className="rounded-lg border border-slate-400 p-3">
<LogicEditorConditions
conditions={condition}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
logicIdx={logicIdx}
depth={depth + 1}
/>
</div>
<div className="mt-2">
<DropdownMenu>
<DropdownMenuTrigger>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleAddConditionBelow(condition.id);
}}>
<PlusIcon className="h-4 w-4" />
Add condition below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.id)}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
}
const conditionValueOptions = getConditionValueOptions(localSurvey, questionIdx);
const conditionOperatorOptions = getConditionOperatorOptions(condition, localSurvey);
const { show, options, showInput = false, inputType } = getMatchValueProps(condition, localSurvey);
const allowMultiSelect = ["equalsOneOf", "includesAllOf", "includesOneOf"].includes(condition.operator);
return (
<div key={condition.id} className="flex items-center gap-x-2">
<div className="w-10 shrink-0">
{index === 0 ? (
"When"
) : (
<div
className={cn("w-14", index === 1 && "cursor-pointer underline")}
onClick={() => {
if (index !== 1) return;
handleConnectorChange(parentConditionGroup.id);
}}>
{connector}
</div>
)}
</div>
<InputCombobox
id={`condition-${depth}-${index}-conditionValue`}
key="conditionValue"
showSearch={false}
groupedOptions={conditionValueOptions}
value={condition.leftOperand.value}
onChangeValue={(val: string, option) => {
handleQuestionChange(condition, val, option);
}}
comboboxClasses="grow"
/>
<InputCombobox
id={`condition-${depth}-${index}-conditionOperator`}
key="conditionOperator"
showSearch={false}
options={conditionOperatorOptions}
value={condition.operator}
onChangeValue={(val: TSurveyLogicConditionsOperator) => {
handleOperatorChange(condition, val);
}}
comboboxClasses="grow min-w-[150px]"
/>
{show && (
<InputCombobox
id={`condition-${depth}-${index}-conditionMatchValue`}
withInput={showInput}
inputProps={{
type: inputType,
placeholder: "Value",
}}
key="conditionMatchValue"
showSearch={false}
groupedOptions={options}
allowMultiSelect={allowMultiSelect}
showCheckIcon={allowMultiSelect}
comboboxClasses="grow min-w-[180px] max-w-[300px]"
value={condition.rightOperand?.value}
clearable={true}
onChangeValue={(val, option) => {
handleRightOperandChange(condition, val, option);
}}
/>
)}
<DropdownMenu>
<DropdownMenuTrigger id={`condition-${depth}-${index}-dropdown`}>
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleAddConditionBelow(condition.id);
}}>
<PlusIcon className="h-4 w-4" />
Add condition below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.id)}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => handleDuplicateCondition(condition.id)}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => handleCreateGroup(condition.id)}>
<WorkflowIcon className="h-4 w-4" />
Create group
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
return (
<div className="flex flex-col gap-y-2">
{conditions?.conditions.map((condition, index) => renderCondition(condition, index, conditions))}
</div>
);
}

View File

@@ -4,9 +4,9 @@ import { PlusIcon, TrashIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface MatrixQuestionFormProps {

View File

@@ -1,13 +1,11 @@
"use client";
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
TI18nString,
@@ -16,16 +14,10 @@ import {
TSurveyMultipleChoiceQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
import { QuestionOptionChoice } from "./QuestionOptionChoice";
interface OpenQuestionFormProps {
@@ -76,6 +68,8 @@ export const MultipleChoiceQuestionForm = ({
};
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
const newLabel = updatedAttributes.label.en;
const oldLabel = question.choices[choiceIdx].label;
let newChoices: any[] = [];
if (question.choices) {
newChoices = question.choices.map((choice, idx) => {
@@ -84,9 +78,19 @@ export const MultipleChoiceQuestionForm = ({
});
}
updateQuestion(questionIdx, {
choices: newChoices,
let newLogic: any[] = [];
question.logic?.forEach((logic) => {
let newL: string | string[] | undefined = logic.value;
if (Array.isArray(logic.value)) {
newL = logic.value.map((value) =>
value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : value
);
} else {
newL = logic.value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : logic.value;
}
newLogic.push({ ...logic, value: newL });
});
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
};
const addChoice = (choiceIdx?: number) => {
@@ -128,27 +132,23 @@ export const MultipleChoiceQuestionForm = ({
};
const deleteChoice = (choiceIdx: number) => {
const choiceToDelete = question.choices[choiceIdx].id;
if (choiceToDelete !== "other") {
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, choiceToDelete);
if (questionIdx !== -1) {
toast.error(
`This option is used in logic for question ${questionIdx + 1}. Please fix the logic first before deleting.`
);
return;
}
}
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
if (isInvalidValue === choiceValue) {
setisInvalidValue(null);
}
updateQuestion(questionIdx, {
choices: newChoices,
let newLogic: any[] = [];
question.logic?.forEach((logic) => {
let newL: string | string[] | undefined = logic.value;
if (Array.isArray(logic.value)) {
newL = logic.value.filter((value) => value !== choiceValue);
} else {
newL = logic.value !== choiceValue ? logic.value : undefined;
}
newLogic.push({ ...logic, value: newL });
});
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
};
useEffect(() => {
@@ -219,7 +219,6 @@ export const MultipleChoiceQuestionForm = ({
<Label htmlFor="choices">Options*</Label>
<div className="mt-2" id="choices">
<DndContext
id="multi-choice-choices"
onDragEnd={(event) => {
const { active, over } = event;

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