Compare commits

..

80 Commits

Author SHA1 Message Date
pandeymangg ed383e7862 test perf 2024-09-26 20:08:14 +05:30
pandeymangg 3db7c7946b fix: testing perf 2024-09-26 15:00:40 +05:30
pandeymangg 2f63659739 fix: fixes debouncing in e2e tests 2024-09-25 18:50:35 +05:30
Piyush Gupta b3757ae7c1 fix: typo 2024-09-25 15:53:10 +05:30
Piyush Gupta 942bf818a5 chore: add docs 2024-09-25 15:50:45 +05:30
Piyush Gupta 74b03a54e1 fix: reviews 2024-09-25 12:22:34 +05:30
Piyush Gupta 5282637772 Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-25 10:30:09 +05:30
pandeymangg 9165daffe9 fix: perf 2024-09-25 10:26:41 +05:30
Piyush Gupta de05d2abdf Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-24 22:59:25 +05:30
Piyush Gupta a907179c7d chore: variable name 2024-09-24 20:10:27 +05:30
Piyush Gupta bc7d3e5da4 chore: code rabbit review changes 2024-09-24 20:08:32 +05:30
Johannes 7042d73e99 UI tweaks 2024-09-24 15:11:01 +02:00
Piyush Gupta fe7ca5a923 fix: functionality and code bugs 2024-09-24 17:01:48 +05:30
Piyush Gupta 960edfd3f0 removes error throwing from data-migration 2024-09-24 11:15:42 +05:30
Piyush Gupta 2b0df8280d fix: build error 2024-09-24 09:51:26 +05:30
Piyush Gupta 13ce552a39 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-23 23:23:50 +05:30
Piyush Gupta 4d6665ab3e Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-23 23:22:02 +05:30
Piyush Gupta 483bdc0eff fix: code review changes 2024-09-23 23:21:46 +05:30
pandeymangg 8238b502fe fix: error 2024-09-23 18:18:22 +05:30
pandeymangg 35ff935a27 fix: lag 2024-09-23 17:00:30 +05:30
pandeymangg 901cd42f56 Merge branch 'feat-advanced-logic-editor' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-23 16:33:12 +05:30
pandeymangg 63e1ac11cf fix: lag 2024-09-23 16:32:55 +05:30
Piyush Gupta 744f3410ae Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-23 15:45:23 +05:30
pandeymangg 0e73d81999 fix 2024-09-23 12:19:11 +05:30
pandeymangg ba46782da4 remove comment 2024-09-23 11:42:22 +05:30
pandeymangg 350c895d8c fixes 2024-09-20 09:51:35 +05:30
Piyush Gupta 4ed1747ee2 fix: variable and hidden field comparison 2024-09-15 21:29:30 +05:30
Piyush Gupta 88c492afd8 adds complex logic evaluate check 2024-09-13 12:25:51 +05:30
Piyush Gupta c7e0b02595 feat: adds unit tests 2024-09-13 11:52:40 +05:30
Piyush Gupta 15aa9b2731 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-12 22:05:32 +05:30
Piyush Gupta 6f7359abf6 feat: adds e2e tests 2024-09-12 22:05:25 +05:30
Piyush Gupta 0bf5e0fd4c fix: comparison fixes 2024-09-12 15:57:11 +05:30
Piyush Gupta 887c5c0eef feat: adds image support in inputCombobox 2024-09-11 16:48:23 +05:30
Piyush Gupta af4ae38564 fix: input clear on operand change 2024-09-11 14:10:26 +05:30
Piyush Gupta 6b829744a1 fix: dhru review changes 2024-09-10 19:47:12 +05:30
Piyush Gupta 7b0d4926e8 fix: updated storybook component 2024-09-10 12:28:55 +05:30
Piyush Gupta 334166b4b1 fix: refine logic completed 2024-09-10 12:17:04 +05:30
Piyush Gupta 1685e77a35 fix: evaluate logic 2024-09-10 10:14:11 +05:30
Piyush Gupta 927f97e9ad Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-10 09:28:33 +05:30
Piyush Gupta 76c437b16a Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-09 20:39:43 +05:30
Johannes 0c2d425a45 tweak 2024-09-09 12:27:20 +02:00
Piyush Gupta 7aa827d1e1 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-09 14:21:56 +05:30
Piyush Gupta d4961f1840 fix: dropoff and comparison funciton 2024-09-09 14:21:20 +05:30
Piyush Gupta 6772ea6be4 fix: build error 2024-09-06 19:28:57 +05:30
Piyush Gupta 9bacc88063 fix: advanced logic data migration 2024-09-06 19:23:59 +05:30
Piyush Gupta 005c777c9c Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-06 15:34:54 +05:30
Piyush Gupta 560dce3bbf feat: add includeVariables option to integration settings 2024-09-06 15:18:25 +05:30
Piyush Gupta ffd45a6f20 feat: adds variables in response table, response finished email, response card, download responses sheet 2024-09-06 14:27:16 +05:30
Piyush Gupta 1ddd82c084 fix: data migration for surveys with end destination 2024-09-06 14:25:16 +05:30
Piyush Gupta 647539c617 fix: undefined conditions or actions 2024-09-06 11:52:29 +05:30
Piyush Gupta a5a3161f7c Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-06 11:20:48 +05:30
Piyush Gupta 0535581f6d WIP: adds survey refine for advanced logic 2024-09-05 22:05:42 +05:30
Piyush Gupta 97b04f9e43 changed left operand id -> value 2024-09-05 17:00:39 +05:30
Piyush Gupta 6ec0861c49 fix: inputCombobox styling 2024-09-05 15:19:48 +05:30
Piyush Gupta 8542320a8e fix build error 2024-09-05 12:54:32 +05:30
Piyush Gupta 7d3c8d35e1 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-05 11:31:27 +05:30
Piyush Gupta 6abcf91a07 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-04 11:39:24 +05:30
Piyush Gupta 9a7887d9fd Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-09-03 16:01:37 +05:30
Piyush Gupta 7dd8bb95bd fix: delete logicItem in case of no conditions 2024-09-03 10:38:13 +05:30
Piyush Gupta df40c0ef11 adds ranking question logic 2024-09-02 16:19:42 +05:30
Piyush Gupta 7e14b86a63 fix: UI suggestions 2024-09-02 15:45:08 +05:30
Piyush Gupta a1ba3af439 fix: data migration for incomplete logic 2024-09-01 20:57:35 +05:30
Piyush Gupta 0a8c5e384d fix: adds data migration script 2024-08-30 16:41:15 +05:30
Piyush Gupta b20ce46a7b Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-08-29 20:29:45 +05:30
Piyush Gupta 58636a9d51 Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-08-29 09:16:23 +05:30
Piyush Gupta 328e3d0b9a adds conditional options in matchValue 2024-08-28 22:34:12 +05:30
Piyush Gupta f1a2ecaa3a feat: adds data migration script WIP 2024-08-28 17:55:34 +05:30
Piyush Gupta d56f05fb19 fix: question, hiddenfield, variable delete error if used in logic 2024-08-28 16:35:05 +05:30
Piyush Gupta c32ced20f1 fix: adds variable in response schema and updated config to store variables on each response 2024-08-27 19:57:40 +05:30
Piyush Gupta 387590986a fix: removes userAttributes based logic chaining 2024-08-27 16:41:27 +05:30
Piyush Gupta 4a5c0b1409 feat: updates survey template 2024-08-27 15:26:08 +05:30
Piyush Gupta 275731e381 replaces logic with advancedLogic 2024-08-27 06:30:09 +05:30
Piyush Gupta 71c3ac0e4e component render fix based on new schema 2024-08-25 23:16:19 +05:30
Piyush Gupta d876c495be new schema 2024-08-24 22:04:04 +05:30
Piyush Gupta 1ba885e5dc adds local schema updation handlers 2024-08-23 15:43:34 +05:30
Piyush Gupta 245972234e Merge branch 'main' of https://github.com/formbricks/formbricks into feat-advanced-logic-editor 2024-08-22 15:37:07 +05:30
Piyush Gupta f743709908 adds inputCombobox in actions 2024-08-22 15:14:14 +05:30
Piyush Gupta 79603293a0 adds inputCombobox storybook 2024-08-21 21:54:50 +05:30
Piyush Gupta 29e0cf96d4 adds condition handling, added logicInput component 2024-08-21 21:39:35 +05:30
Piyush Gupta 4eea6a11c8 feat: added types for advanced logic editor 2024-08-19 13:16:40 +05:30
705 changed files with 4173 additions and 6573 deletions
-6
View File
@@ -180,9 +180,3 @@ UNSPLASH_ACCESS_KEY=
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
# CUSTOM_CACHE_DISABLED=1
# Azure AI settings
# AI_AZURE_RESSOURCE_NAME=
# AI_AZURE_API_KEY=
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
# AI_AZURE_LLM_DEPLOYMENT_ID=
@@ -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
+1
View File
@@ -51,6 +51,7 @@ jobs:
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
sed -i "s/ENTERPRISE_LICENSE_KEY=.*/ENTERPRISE_LICENSE_KEY=${RANDOM_KEY}/" .env
echo "E2E_TESTING=1" >> .env
echo "NEXT_PUBLIC_E2E_TESTING=1" >> .env
shell: bash
- name: Build App
@@ -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
+2 -2
View File
@@ -1,9 +1,9 @@
Copyright (c) 2024 Formbricks GmbH
Copyright (c) 2023 Formbricks GmbH
Portions of this software are licensed as follows:
- All content that resides under the "packages/ee/" directory of this repository, if that directory exists, is licensed under the license defined in "packages/ee/LICENSE".
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All content that resides under the "packages/js/", "packages/errors/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
+1 -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 (
+2 -2
View File
@@ -60,7 +60,7 @@ const AppPage = ({}) => {
}, []);
return (
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="flex flex-col items-center gap-2 sm:flex-row">
<SurveySwitch value="app" formbricks={formbricks} />
@@ -117,7 +117,7 @@ const AppPage = ({}) => {
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
+3 -3
View File
@@ -56,8 +56,8 @@ const AppPage = ({}) => {
});
return (
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex flex-col items-center gap-2 sm:flex-row">
<SurveySwitch value="website" formbricks={formbricks} />
<div>
@@ -113,7 +113,7 @@ const AppPage = ({}) => {
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
+26
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;
}
+13 -5
View File
@@ -4,20 +4,25 @@ import I1 from "./images/I1.webp";
import I2 from "./images/I2.webp";
export const metadata = {
title: "Using Actions in Formbricks",
title: "Using Actions in Formbricks | Fine-tuning User Moments",
description:
"Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
};
#### App Surveys
# Actions
# Actions & Targeting
Actions are predefined events within your app that prompt Formbricks to display a survey when triggered. These are detected by the Formbricks widget, which then presents the appropriate survey based on your predefined settings.
Understanding user thoughts and feelings at critical moments in their journey is pivotal. To achieve this, Formbricks uses user-centric actions that trigger surveys at precisely the right time. Actions are essentially notifications sent from your application to Formbricks when predefined user activities occur, making it possible to gather insights during key interactions.
<Note>
Ensure that youve **initialized Formbricks with a userId** to fully utilize this feature along with other
app survey capabilities.
</Note>
## **How Do Actions Work?**
Actions in Formbricks App Surveys are deeply integrated with user activities within your app. When a user performs a specified action, the Formbricks widget detects this activity and can present a survey to that specific user if the trigger conditions match for that survey. This capability ensures that surveys are triggered at the right time. You can set up these actions through a user-friendly No-Code interface within the Formbricks dashboard.
Actions in Formbricks App Surveys are deeply integrated with user activities within your app. When a user performs a specified action, the Formbricks widget detects this activity and can present a survey to that specific user if the trigger conditions match of that survey, while also recording the event. This capability ensures that surveys are not only triggered at the right time but are also tailored to the users recent interactions within the app. You can set up these actions through a user-friendly No-Code interface within the Formbricks dashboard.
## **Why Are Actions Useful?**
@@ -25,7 +30,8 @@ Actions are invaluable for enhancing survey relevance and effectiveness:
- **Personalized Engagement**: Surveys triggered by user actions ensure content is highly relevant and engaging, matching each users current context.
- **User Attributes**: By tying surveys to specific user attributes, such as activity levels or feature usage, you can customize the survey experience to reflect individual user profiles.
- **User Targeting**: Precise targeting based on user attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey.
- **User Segments**: Analyze action data to create detailed user segments, targeting specific groups with surveys that are pertinent to their behaviors or interactions within the app.
- **User Targeting**: Precise targeting based on user actions and attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey.
## **Setting Up No-Code Actions**
@@ -121,3 +127,5 @@ return <button onClick={handleClick}>Click Me</button>;
</CodeGroup>
</Col>
This documentation frames actions around user interactions, emphasizing the connection between the user's activities and the survey experience. By leveraging user-centric actions, you can create highly targeted and timely surveys that resonate with users and yield valuable insights.
@@ -1,18 +1,39 @@
import { MdxImage } from "@/components/MdxImage";
import { ResponsiveVideo } from "@/components/ResponsiveVideo";
import GermansGpt from "./germans-gpt.webp";
import Hni from "./hni.webp";
import PowerUsers from "./power-users.webp";
import RideHailing from "./ride-hailing.webp";
import UpsellMiro from "./upsell-miro.webp";
export const metadata = {
title: "Advanced Targeting for App Surveys | Formbricks",
title: "Advanced Targeting for In-app Surveys | Formbricks",
description:
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, metadata, and other segments. This helps you get more relevant feedback and make data-driven decisions.",
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, user events, and metadata. This helps you get more relevant feedback and make data-driven decisions.",
};
#### App Surveys
# Advanced Targeting
<Note>
Targeting based on actions is deprecated in Advanced Targeting and will be removed soon. We recommend using
filters on user attributes to target the survey only to specific groups of users.
</Note>
Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code.
# How to setup Advanced Targeting
<ResponsiveVideo
title="Formbricks Multi-language Surveys"
src="https://www.youtube-nocookie.com/embed/0BQp6N4cXzU?si=KeBM7G7Ch1xtrsOm&amp;controls=0"
/>
<Note>Advanced Targeting is only available on the Pro plan!</Note>
## How to setup Advanced Targeting
<Note>
Advanced Targeting is available on the Pro plan!
</Note>
1. On the Formbricks dashboard, click on **People** tab from the top navigation bar.
@@ -20,7 +41,7 @@ Advanced Targeting allows you to show surveys to the right group of people. You
3. Give your segment a title & a description to help you remember what this segment is about.
4. Now click on the **Add Filter** button to add a filter. You can filter based on user attributes, other segments, devices, and more.
4. Now click on the **Add Filter** button to add a filter. You can filter based on actions, user attributes, other segments, devices, and more.
5. To group a set of filters together, click on the Three Dots icon on the right side of the filter and click on **Create Group**.
@@ -29,3 +50,32 @@ Advanced Targeting allows you to show surveys to the right group of people. You
7. Once you are happy with the segment, click on **Save Segment**.
8. Now, when you create a survey, you can select this segment to target your survey to.
## Examples:
1. Let's say you want to upsell to: Miro, Loom, Figma, Slack and Asana.
<MdxImage
src={UpsellMiro}
alt="Upselling Opportunity"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Post-experience surveying for a ride hailing app where users who have taken more than 1 ride are shown a survey.
<MdxImage
src={RideHailing}
alt="Ride Hailing Targeting"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Sneak Peak: How we at Formbricks automate inviting power users to chat with us
<MdxImage
src={PowerUsers}
alt="Automate inviting power users to chat with us at Formbricks"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -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 = () => {
@@ -37,9 +37,8 @@ To display the Product-Market Fit survey in your app you want to proceed as foll
3. Setup the user action to display survey at good point in time
<Note>
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(15mins).](/app-surveys/quickstart)
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (15mins).](/app-surveys/quickstart)
</Note>
### 1. Create new PMF survey
@@ -70,12 +69,25 @@ _Want to change the button color? You can do so in the product settings!_
Save, and move over to where the magic happens: The “Audience” tab.
### 3. Pre-segment your audience
### 3. Pre-segment your audience (coming soon)
<Note>
## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this
manual in the next days.
</Note>
To run this survey properly, you should pre-segment your user base. As touched upon earlier: if you ask every user youll get lots of opinions which are often misleading. You only want to gather feedback from people who invested the time to get to know and use your product:
**Filter by attribute**: You can keep the logic to decide if a user has (or has not) experienced value in your application. This makes most sense if you want to use historic usage data to decide if a user qualifies or not. Create your logic and if it applies, send an attribute to Formbricks by e.g. `formbricks.setAttribute("Loyalty", "Experienced Value");` Here is the full manual on how to [set attributes](/app-surveys/user-identification).
**Filter by actions (coming soon)**: Later, you can also segment users based on events tracked with Formbricks. However, this makes it impossible to use historic usage data (pre Formbricks usage). Here we will have a few options to achieve that:
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
- User has performed a combination of actions (e.g. created a report **and** invited a organization member)
This way you make sure that you separate potentially misleading opinions from valuable insights.
### 4. Set up a trigger for the Product-Market Fit survey:
You need a trigger to display the survey but in this case, the filtering does all the work. Its up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/app-surveys/actions/) if you are not sure how to set them up:
@@ -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 = [
@@ -6,7 +6,7 @@ import ApiKeySecret from "./images/api-key-secret.webp";
export const metadata = {
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, actions, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
};
#### API
@@ -23,6 +23,7 @@ The [Public Client API](https://documenter.getpostman.com/view/11026000/2sA3Bq5X
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
- [Actions API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#b8f3a10e-1642-4d82-a629-fef0a8c6c86c) - Create actions for a Person
- [Displays API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#949272bf-daec-4d72-9b52-47af3d74a62c) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#ee3d2188-4253-4bca-9238-6b76455805a9) - Create & Update a Person (e.g. attributes, email, userId, etc)
- [Responses API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#8c773032-536c-483c-a237-c7697347946e) - Create & Update a Response for a Survey
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

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

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

@@ -11,9 +11,11 @@ export const metadata = {
#### Website Surveys
# Actions
# Actions & Targeting
Actions are triggers based on interactions with your website, allowing you to capture feedback precisely when it's most relevant.
For public-facing websites, landing pages, and pages without user authentication walls, actions serve as effective triggers for displaying surveys. This method is particularly suitable for engaging general audiences, where **individual user tracking is not required or feasible**.
Actions in this context are straightforward triggers based on interactions with your website, allowing you to capture feedback precisely when it's most relevant.
<Note>
These actions operate **independently** as website surveys do not involve user identification. If you have
@@ -44,7 +46,7 @@ Formbricks provides an intuitive No-Code interface for configuring actions, enab
src={StepOne}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
className="max-w-full rounded-lg sm:max-w-3xl "
/>
2. Now click on “Add Action”
@@ -53,7 +55,7 @@ Formbricks provides an intuitive No-Code interface for configuring actions, enab
src={StepTwo}
alt="Choose a link survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
className="max-w-full rounded-lg sm:max-w-3xl "
/>
Here are four types of No-Code actions you can set up:
+10 -12
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>
);
};
+1 -4
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" },
],
},
],
@@ -57,7 +56,7 @@ export const navigation: Array<NavGroup> = [
{
title: "Features",
children: [
{ title: "Actions", href: "/website-surveys/actions" },
{ title: "Actions & Targeting", href: "/website-surveys/actions-and-targeting" },
{ title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website
{ title: "Recontact Options", href: "/app-surveys/recontact" },
{ title: "Hidden Fields", href: "/global/hidden-fields" }, // global
@@ -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" },
+1 -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,
@@ -1,12 +1,15 @@
"use client";
import Dance from "@/images/onboarding-dance.gif";
import Lost from "@/images/onboarding-lost.gif";
import { ArrowRight } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
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 {
@@ -59,21 +62,20 @@ export const ConnectWithFormbricks = ({
</div>
<div
className={cn(
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border text-center",
widgetSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border bg-slate-200 text-center shadow",
widgetSetupCompleted ? "border-green-500 bg-green-100" : ""
)}>
{widgetSetupCompleted ? (
<div>
<p className="text-3xl">Congrats!</p>
<p className="pt-4 text-sm font-medium text-slate-600">Well done! We&apos;re connected.</p>
<Image src={Dance} alt="lost" height={250} />
<p className="mt-6 text-xl font-bold">Connection successful </p>
</div>
) : (
<div className="flex animate-pulse flex-col items-center space-y-4">
<span className="relative flex h-10 w-10">
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
</span>
<p className="pt-4 text-sm font-medium text-slate-600">Waiting for your signal...</p>
<div className="space-y-4">
<Image src={Lost} alt="lost" height={250} />
<p className="animate-pulse pt-4 text-sm font-semibold text-slate-700">
Waiting for your signal...
</p>
</div>
)}
</div>
@@ -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;
@@ -102,7 +102,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
<div className="flex w-full justify-end space-x-2">
<Button
id="onboarding-inapp-invite-have-a-look-first"
className="text-slate-400"
className="font-normal text-slate-400"
variant="minimal"
onClick={(e) => {
e.preventDefault();
@@ -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 /> },
@@ -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: {
@@ -33,7 +33,7 @@ const Page = async ({ params }: InvitePageProps) => {
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
<Header
title="Who is your favorite engineer?"
subtitle="Invite your tech-savvy co-worker to help with the setup."
subtitle="Invite your tech-savvy co-worker to help with the setup 🤓"
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
@@ -1,10 +1,11 @@
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 { 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: {
@@ -25,10 +26,16 @@ const Page = async ({ params }: ConnectPageProps) => {
}
const channel = product.config.channel || null;
const industry = product.config.industry || null;
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`} subtitle="It takes less than 4 minutes." />
<Header
title={`Let's connect your ${customHeadline} with Formbricks`}
subtitle="It takes less than 4 minutes, pinky promise!"
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
<p className="text-sm text-slate-500"></p>
@@ -12,7 +12,7 @@ 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";
import { createSurveyAction } from "@formbricks/ui/TemplateList/actions";
interface XMTemplateListProps {
product: TProduct;
@@ -6,8 +6,8 @@ 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";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface XMTemplatePageProps {
params: {
+11 -10
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";
};
@@ -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);
@@ -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: {
@@ -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: {
@@ -1,8 +1,8 @@
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";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ModePageProps {
params: {
@@ -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,7 +8,7 @@ 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,
@@ -16,8 +17,8 @@ import {
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,9 +27,9 @@ 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;
@@ -64,6 +65,8 @@ 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);
}
@@ -130,7 +133,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>
@@ -3,9 +3,10 @@ import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organiz
import { XIcon } from "lucide-react";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { startsWithVowel } from "@formbricks/lib/utils/strings";
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } 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 ProductSettingsPageProps {
params: {
@@ -23,7 +24,7 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys";
const customHeadline = getCustomHeadline(channel);
const customHeadline = getCustomHeadline(channel, industry);
const products = await getProducts(params.organizationId);
return (
@@ -35,7 +36,7 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
/>
) : (
<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"
/>
)}
@@ -2,7 +2,7 @@ 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: {
@@ -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);
@@ -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";
@@ -1,13 +1,12 @@
"use client";
import { PlusIcon } from "lucide-react";
import { useEffect } from "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 { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
interface AddressQuestionFormProps {
localSurvey: TSurvey;
@@ -33,64 +32,6 @@ export const AddressQuestionForm = ({
}: AddressQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const fields = [
{
id: "addressLine1",
label: "Address Line 1",
...question.addressLine1,
},
{
id: "addressLine2",
label: "Address Line 2",
...question.addressLine2,
},
{
id: "city",
label: "City",
...question.city,
},
{
id: "state",
label: "State",
...question.state,
},
{
id: "zip",
label: "Zip",
...question.zip,
},
{
id: "country",
label: "Country",
...question.country,
},
];
useEffect(() => {
const allFieldsAreOptional = [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
updateQuestion(questionIdx, { required: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]);
return (
<form>
<QuestionFormInput
@@ -140,30 +81,73 @@ export const AddressQuestionForm = ({
Add Description
</Button>
)}
<QuestionToggleTable
type="address"
fields={fields}
onShowToggle={(field, show) => {
<div className="mt-2 font-medium">Settings</div>
<AdvancedOptionToggle
isChecked={question.isAddressLine1Required}
onToggle={() =>
updateQuestion(questionIdx, {
[field.id]: {
show,
required: field.required,
},
// when show changes, and the field is required, the question should be required
...(show && field.required && { required: true }),
});
}}
onRequiredToggle={(field, required) => {
updateQuestion(questionIdx, {
[field.id]: {
show: field.show,
required,
},
isAddressLine1Required: !question.isAddressLine1Required,
required: true,
});
}}
/>
})
}
htmlId="isAddressRequired"
title="Required: Address Line 1"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={question.isAddressLine2Required}
onToggle={() =>
updateQuestion(questionIdx, {
isAddressLine2Required: !question.isAddressLine2Required,
required: true,
})
}
htmlId="isAddressLine2Required"
title="Required: Address Line 2"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={question.isCityRequired}
onToggle={() =>
updateQuestion(questionIdx, { isCityRequired: !question.isCityRequired, required: true })
}
htmlId="isCityRequired"
title="Required: City / Town"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={question.isStateRequired}
onToggle={() =>
updateQuestion(questionIdx, { isStateRequired: !question.isStateRequired, required: true })
}
htmlId="isStateRequired"
title="Required: State / Region"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={question.isZipRequired}
onToggle={() =>
updateQuestion(questionIdx, { isZipRequired: !question.isZipRequired, required: true })
}
htmlId="isZipRequired"
title="Required: ZIP / Post Code"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={question.isCountryRequired}
onToggle={() =>
updateQuestion(questionIdx, { isCountryRequired: !question.isCountryRequired, required: true })
}
htmlId="iscountryRequired"
title="Required: Country"
description=""
childBorder
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
</div>
</form>
);
@@ -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 {
@@ -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 = [
{
@@ -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;
@@ -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;
@@ -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;
@@ -4,6 +4,7 @@ import {
replaceEndingCardHeadlineRecall,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { debounce } from "lodash";
import {
ArrowDownIcon,
ArrowUpIcon,
@@ -13,19 +14,19 @@ import {
SplitIcon,
TrashIcon,
} from "lucide-react";
import { useMemo } from "react";
import { useEffect, useMemo, useState } 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 { Button } from "@formbricks/ui/Button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { Label } from "@formbricks/ui/components/Label";
} from "@formbricks/ui/DropdownMenu";
import { Label } from "@formbricks/ui/Label";
interface ConditionalLogicProps {
localSurvey: TSurvey;
@@ -42,6 +43,16 @@ export function ConditionalLogic({
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const [questionLogic, setQuestionLogic] = useState(question.logic);
const debouncedUpdateQuestion = useMemo(() => debounce(updateQuestion, 500), [updateQuestion]);
useEffect(() => {
debouncedUpdateQuestion(questionIdx, {
logic: questionLogic,
});
}, [questionLogic]);
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
@@ -49,6 +60,10 @@ export function ConditionalLogic({
return modifiedSurvey;
}, [localSurvey, attributeClasses]);
const updateQuestionLogic = (_questionIdx: number, updatedAttributes: any) => {
setQuestionLogic(updatedAttributes.logic);
};
const addLogic = () => {
const operator = getDefaultOperatorForQuestion(question);
@@ -77,37 +92,37 @@ export function ConditionalLogic({
],
};
updateQuestion(questionIdx, {
logic: [...(question?.logic ?? []), initialCondition],
updateQuestionLogic(questionIdx, {
logic: [...(questionLogic ?? []), initialCondition],
});
};
const handleRemoveLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const logicCopy = structuredClone(questionLogic ?? []);
logicCopy.splice(logicItemIdx, 1);
updateQuestion(questionIdx, {
updateQuestionLogic(questionIdx, {
logic: logicCopy,
});
};
const moveLogic = (from: number, to: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const logicCopy = structuredClone(questionLogic ?? []);
const [movedItem] = logicCopy.splice(from, 1);
logicCopy.splice(to, 0, movedItem);
updateQuestion(questionIdx, {
updateQuestionLogic(questionIdx, {
logic: logicCopy,
});
};
const duplicateLogic = (logicItemIdx: number) => {
const logicCopy = structuredClone(question.logic ?? []);
const logicCopy = structuredClone(questionLogic ?? []);
const logicItem = logicCopy[logicItemIdx];
const newLogicItem = duplicateLogicItem(logicItem);
logicCopy.splice(logicItemIdx + 1, 0, newLogicItem);
updateQuestion(questionIdx, {
updateQuestionLogic(questionIdx, {
logic: logicCopy,
});
};
@@ -119,20 +134,21 @@ export function ConditionalLogic({
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
{question.logic && question.logic.length > 0 && (
{questionLogic && questionLogic.length > 0 && (
<div className="mt-2 flex flex-col gap-4">
{question.logic.map((logicItem, logicItemIdx) => (
{questionLogic.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}
updateQuestion={updateQuestionLogic}
question={question}
questionLogic={questionLogic}
questionIdx={questionIdx}
logicIdx={logicItemIdx}
isLast={logicItemIdx === (question.logic ?? []).length - 1}
isLast={logicItemIdx === (questionLogic ?? []).length - 1}
/>
<DropdownMenu>
@@ -159,7 +175,7 @@ export function ConditionalLogic({
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={logicItemIdx === (question.logic ?? []).length - 1}
disabled={logicItemIdx === (questionLogic ?? []).length - 1}
onClick={() => {
moveLogic(logicItemIdx, logicItemIdx + 1);
}}>
@@ -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;
@@ -1,157 +0,0 @@
"use client";
import { PlusIcon } from "lucide-react";
import { useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
interface ContactInfoQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyContactInfoQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyContactInfoQuestion>) => void;
lastQuestion: boolean;
isInvalid: boolean;
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
}
export const ContactInfoQuestionForm = ({
question,
questionIdx,
updateQuestion,
isInvalid,
localSurvey,
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
}: ContactInfoQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const fields = [
{
id: "firstName",
label: "First Name",
...question.firstName,
},
{
id: "lastName",
label: "Last Name",
...question.lastName,
},
{
id: "email",
label: "Email",
...question.email,
},
{
id: "phone",
label: "Phone",
...question.phone,
},
{
id: "company",
label: "Company",
...question.company,
},
];
useEffect(() => {
const allFieldsAreOptional = [
question.firstName,
question.lastName,
question.email,
question.phone,
question.company,
]
.filter((field) => field.show)
.every((field) => !field.required);
if (allFieldsAreOptional) {
updateQuestion(questionIdx, { required: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
return (
<form>
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
/>
<div>
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
/>
</div>
</div>
)}
{question.subheader === undefined && (
<Button
size="sm"
variant="minimal"
className="mt-4"
type="button"
onClick={() => {
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>
)}
<QuestionToggleTable
type="contact"
fields={fields}
onShowToggle={(field, show) => {
updateQuestion(questionIdx, {
[field.id]: {
show,
required: field.required,
},
// when show changes, and the field is required, the question should be required
...(show && field.required && { required: true }),
});
}}
onRequiredToggle={(field, required) => {
updateQuestion(questionIdx, {
[field.id]: {
show: field.show,
required,
},
required: true,
});
}}
/>
</div>
</form>
);
};
@@ -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 {
@@ -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;
@@ -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;
@@ -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;
@@ -18,7 +18,7 @@ import {
TSurveyQuestionTypeEnum,
TSurveyRedirectUrlCard,
} from "@formbricks/types/surveys/types";
import { ConfirmationModal } from "@formbricks/ui/components/ConfirmationModal";
import { ConfirmationModal } from "@formbricks/ui/ConfirmationModal";
import {
DropdownMenu,
DropdownMenuContent,
@@ -27,7 +27,7 @@ import {
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
} from "@formbricks/ui/DropdownMenu";
interface EditorCardMenuProps {
survey: TSurvey;
@@ -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;
@@ -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;
@@ -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;
@@ -9,11 +9,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;
@@ -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;
@@ -1,4 +1,4 @@
import { FileInput } from "@formbricks/ui/components/FileInput";
import { FileInput } from "@formbricks/ui/FileInput";
interface UploadImageSurveyBgProps {
environmentId: string;
@@ -8,6 +8,7 @@ interface LogicEditorProps {
logicItem: TSurveyLogic;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
question: TSurveyQuestion;
questionLogic: TSurveyLogic[];
questionIdx: number;
logicIdx: number;
isLast: boolean;
@@ -18,6 +19,7 @@ export function LogicEditor({
logicItem,
updateQuestion,
question,
questionLogic,
questionIdx,
logicIdx,
isLast,
@@ -35,7 +37,7 @@ export function LogicEditor({
<LogicEditorActions
logicItem={logicItem}
logicIdx={logicIdx}
question={question}
questionLogic={questionLogic}
updateQuestion={updateQuestion}
localSurvey={localSurvey}
questionIdx={questionIdx}
@@ -0,0 +1,390 @@
import {
actionObjectiveOptions,
getActionOperatorOptions,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import {
CopyIcon,
EllipsisVerticalIcon,
EyeOffIcon,
FileDigitIcon,
FileType2Icon,
PlusIcon,
TrashIcon,
} from "lucide-react";
import React, { useCallback, useMemo } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { questionIconMapping } from "@formbricks/lib/utils/questions";
import {
TActionNumberVariableCalculateOperator,
TActionObjective,
TActionTextVariableCalculateOperator,
TActionVariableValueType,
TSurvey,
TSurveyLogicAction,
TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/DropdownMenu";
import { InputCombobox, TComboboxGroupedOption, TComboboxOption } from "@formbricks/ui/InputCombobox";
interface LogicEditorActionProps {
action: TSurveyLogicAction;
actionIdx: number;
handleObjectiveChange: (actionIdx: number, val: TActionObjective) => void;
handleValuesChange: (actionIdx: number, values: any) => void;
handleActionsChange: (operation: "remove" | "addBelow" | "duplicate", actionIdx: number) => void;
isRemoveDisabled: boolean;
questions: TSurveyQuestion[];
endings: TSurvey["endings"];
variables: TSurvey["variables"];
questionIdx: number;
hiddenFields: {
enabled: boolean;
fieldIds?: string[] | undefined;
};
}
const _LogicEditorAction = ({
action,
actionIdx,
handleActionsChange,
handleObjectiveChange,
handleValuesChange,
isRemoveDisabled,
questions,
endings,
variables,
questionIdx,
hiddenFields,
}: LogicEditorActionProps) => {
const actionTargetOptions = useMemo((): TComboboxOption[] => {
let filteredQuestions = questions.filter((_, idx) => idx !== questionIdx);
if (action.objective === "requireAnswer") {
filteredQuestions = filteredQuestions.filter((question) => !question.required);
}
const questionOptions = filteredQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
};
});
if (action.objective === "requireAnswer") return questionOptions;
const endingCardOptions = endings.map((ending) => {
return {
label:
ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default") || "End Screen"
: ending.label || "Redirect Thank you card",
value: ending.id,
};
});
return [...questionOptions, ...endingCardOptions];
}, [action.objective, endings, questionIdx, questions]);
const actionVariableOptions = useMemo((): TComboboxOption[] => {
return variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
variableType: variable.type,
},
};
});
}, [variables]);
const getActionValueOptions = useCallback(
(variableId: string): TComboboxGroupedOption[] => {
const hiddenFieldIds = hiddenFields?.fieldIds ?? [];
const hiddenFieldsOptions = hiddenFieldIds.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});
const selectedVariable = variables.find((variable) => variable.id === variableId);
const filteredVariables = variables.filter((variable) => variable.id !== variableId);
if (!selectedVariable) return [];
if (selectedVariable.type === "text") {
const allowedQuestions = questions.filter((question) =>
[
TSurveyQuestionTypeEnum.OpenText,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
TSurveyQuestionTypeEnum.Rating,
TSurveyQuestionTypeEnum.NPS,
TSurveyQuestionTypeEnum.Date,
].includes(question.type)
);
const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});
const stringVariables = filteredVariables.filter((variable) => variable.type === "text");
const variableOptions = stringVariables.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});
const groupedOptions: TComboboxGroupedOption[] = [];
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
return groupedOptions;
} else if (selectedVariable.type === "number") {
const allowedQuestions = questions.filter((question) =>
[TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS].includes(question.type)
);
const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});
const numberVariables = filteredVariables.filter((variable) => variable.type === "number");
const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});
const groupedOptions: TComboboxGroupedOption[] = [];
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
return groupedOptions;
}
return [];
},
[hiddenFields?.fieldIds, questions, variables]
);
return (
<div key={action.id} className="flex grow items-center justify-between gap-x-2">
<div className="block w-9 shrink-0">{actionIdx === 0 ? "Then" : "and"}</div>
<div className="flex grow items-center gap-x-2">
<InputCombobox
id={`action-${actionIdx}-objective`}
key={`objective-${action.id}`}
showSearch={false}
options={actionObjectiveOptions}
value={action.objective}
onChangeValue={(val: TActionObjective) => {
handleObjectiveChange(actionIdx, val);
}}
comboboxClasses="grow"
/>
{action.objective !== "calculate" && (
<InputCombobox
id={`action-${actionIdx}-target`}
key={`target-${action.id}`}
showSearch={false}
options={actionTargetOptions}
value={action.target}
onChangeValue={(val: string) => {
handleValuesChange(actionIdx, {
target: val,
});
}}
comboboxClasses="grow"
/>
)}
{action.objective === "calculate" && (
<>
<InputCombobox
id={`action-${actionIdx}-variableId`}
key={`variableId-${action.id}`}
showSearch={false}
options={actionVariableOptions}
value={action.variableId}
onChangeValue={(val: string) => {
handleValuesChange(actionIdx, {
variableId: val,
value: {
type: "static",
value: "",
},
});
}}
comboboxClasses="grow"
emptyDropdownText="Add a variable to calculate"
/>
<InputCombobox
id={`action-${actionIdx}-operator`}
key={`operator-${action.id}`}
showSearch={false}
options={getActionOperatorOptions(variables.find((v) => v.id === action.variableId)?.type)}
value={action.operator}
onChangeValue={(
val: TActionTextVariableCalculateOperator | TActionNumberVariableCalculateOperator
) => {
handleValuesChange(actionIdx, {
operator: val,
});
}}
comboboxClasses="grow"
/>
<InputCombobox
id={`action-${actionIdx}-value`}
key={`value-${action.id}`}
withInput={true}
clearable={true}
value={action.value?.value ?? ""}
inputProps={{
placeholder: "Value",
type: variables.find((v) => v.id === action.variableId)?.type || "text",
}}
groupedOptions={getActionValueOptions(action.variableId)}
onChangeValue={(val, option, fromInput) => {
const fieldType = option?.meta?.type as TActionVariableValueType;
if (!fromInput && fieldType !== "static") {
handleValuesChange(actionIdx, {
value: {
type: fieldType,
value: val as string,
},
});
} else if (fromInput) {
handleValuesChange(actionIdx, {
value: {
type: "static",
value: val as string,
},
});
}
}}
comboboxClasses="grow shrink-0"
/>
</>
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger id={`actions-${actionIdx}-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", actionIdx);
}}>
<PlusIcon className="h-4 w-4" />
Add action below
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
disabled={isRemoveDisabled}
onClick={() => {
handleActionsChange("remove", actionIdx);
}}>
<TrashIcon className="h-4 w-4" />
Remove
</DropdownMenuItem>
<DropdownMenuItem
className="flex items-center gap-2"
onClick={() => {
handleActionsChange("duplicate", actionIdx);
}}>
<CopyIcon className="h-4 w-4" />
Duplicate
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export const LogicEditorAction = React.memo(_LogicEditorAction);
@@ -1,236 +1,110 @@
import {
actionObjectiveOptions,
getActionOperatorOptions,
getActionTargetOptions,
getActionValueOptions,
getActionVariableOptions,
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { LogicEditorAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorAction";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { CornerDownRightIcon } from "lucide-react";
import React, { useCallback, useMemo } from "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";
import { TActionObjective, TSurvey, TSurveyLogic, TSurveyLogicAction } from "@formbricks/types/surveys/types";
interface LogicEditorActions {
localSurvey: TSurvey;
logicItem: TSurveyLogic;
logicIdx: number;
question: TSurveyQuestion;
questionLogic: TSurveyLogic[];
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
questionIdx: number;
}
export function LogicEditorActions({
export const LogicEditorActions = ({
localSurvey,
logicItem,
logicIdx,
question,
questionLogic,
updateQuestion,
questionIdx,
}: LogicEditorActions) {
}: 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;
const handleActionsChange = useCallback(
(
operation: "remove" | "addBelow" | "duplicate" | "update",
actionIdx: number,
action?: TSurveyLogicAction
) => {
const currentLogicCopy = structuredClone(logicItem);
const actionsClone = currentLogicCopy.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;
}
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 updatedLogic = questionLogic.map((item, idx) => (idx === logicIdx ? currentLogicCopy : item));
const handleObjectiveChange = (actionIdx: number, objective: TActionObjective) => {
const action = actions[actionIdx];
const actionBody = getUpdatedActionBody(action, objective);
handleActionsChange("update", actionIdx, actionBody);
};
updateQuestion(questionIdx, {
logic: updatedLogic,
});
},
[logicIdx, logicItem, questionIdx, questionLogic]
);
const handleValuesChange = (actionIdx: number, values: Partial<TSurveyLogicAction>) => {
const action = actions[actionIdx];
const actionBody = { ...action, ...values } as TSurveyLogicAction;
handleActionsChange("update", actionIdx, actionBody);
};
const handleObjectiveChange = useCallback(
(actionIdx: number, objective: TActionObjective) => {
const action = actions[actionIdx];
const actionBody = getUpdatedActionBody(action, objective);
handleActionsChange("update", actionIdx, actionBody);
},
[actions]
);
const handleValuesChange = useCallback(
(actionIdx: number, values: Partial<TSurveyLogicAction>) => {
const action = actions[actionIdx];
const actionBody = { ...action, ...values } as TSurveyLogicAction;
handleActionsChange("update", actionIdx, actionBody);
},
[actions]
);
const questions = useMemo(() => localSurvey.questions, [localSurvey.questions]);
const endings = useMemo(() => localSurvey.endings, [localSurvey.endings]);
const variables = useMemo(() => localSurvey.variables, [localSurvey.variables]);
const hiddenFields = useMemo(() => localSurvey.hiddenFields, [localSurvey.hiddenFields]);
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>
<LogicEditorAction
action={action}
actionIdx={idx}
handleActionsChange={handleActionsChange}
handleObjectiveChange={handleObjectiveChange}
handleValuesChange={handleValuesChange}
endings={endings}
isRemoveDisabled={actions.length === 1}
questions={questions}
variables={variables}
questionIdx={questionIdx}
hiddenFields={hiddenFields}
/>
))}
</div>
</div>
);
}
};
@@ -30,8 +30,8 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@formbricks/ui/components/DropdownMenu";
import { InputCombobox, TComboboxOption } from "@formbricks/ui/components/InputCombobox";
} from "@formbricks/ui/DropdownMenu";
import { InputCombobox, TComboboxOption } from "@formbricks/ui/InputCombobox";
interface LogicEditorConditionsProps {
conditions: TConditionGroup;
@@ -88,7 +88,7 @@ export function LogicEditorConditions({
const logicItem = logicCopy[logicIdx];
removeCondition(logicItem.conditions, resourceId);
// Remove the logic item if there are zero conditions left
// Remove the logic item if there are no conditions left
if (logicItem.conditions.conditions.length === 0) {
logicCopy.splice(logicIdx, 1);
}
@@ -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 {
@@ -16,16 +16,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 {
@@ -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, TSurveyNPSQuestion } 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 NPSQuestionFormProps {
localSurvey: TSurvey;
@@ -8,10 +8,10 @@ import {
TSurveyOpenTextQuestion,
TSurveyOpenTextQuestionInputType,
} 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";
const questionTypes = [
{ value: "text", label: "Text", icon: <MessageSquareTextIcon className="h-4 w-4" /> },
@@ -4,11 +4,11 @@ import { cn } from "@formbricks/lib/cn";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
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 { Button } from "@formbricks/ui/Button";
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 PictureSelectionFormProps {
localSurvey: TSurvey;
@@ -2,9 +2,9 @@
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { Label } from "@formbricks/ui/components/Label";
import { getPlacementStyle } from "@formbricks/ui/components/PreviewSurvey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/components/RadioGroup";
import { Label } from "@formbricks/ui/Label";
import { getPlacementStyle } from "@formbricks/ui/PreviewSurvey/lib/utils";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
@@ -1,6 +1,5 @@
"use client";
import { ContactInfoQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ContactInfoQuestionForm";
import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RankingQuestionForm";
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { useSortable } from "@dnd-kit/sortable";
@@ -19,9 +18,9 @@ import {
TSurveyQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Switch } from "@formbricks/ui/components/Switch";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Switch } from "@formbricks/ui/Switch";
import { AddressQuestionForm } from "./AddressQuestionForm";
import { AdvancedSettings } from "./AdvancedSettings";
import { CTAQuestionForm } from "./CTAQuestionForm";
@@ -95,25 +94,16 @@ export const QuestionCard = ({
};
const getIsRequiredToggleDisabled = (): boolean => {
if (question.type === TSurveyQuestionTypeEnum.Address) {
if (question.type === "address") {
return [
question.addressLine1,
question.addressLine2,
question.city,
question.state,
question.zip,
question.country,
]
.filter((field) => field.show)
.some((condition) => condition.required === true);
question.isAddressLine1Required,
question.isAddressLine2Required,
question.isCityRequired,
question.isCountryRequired,
question.isStateRequired,
question.isZipRequired,
].some((condition) => condition === true);
}
if (question.type === TSurveyQuestionTypeEnum.ContactInfo) {
return [question.firstName, question.lastName, question.email, question.phone, question.company]
.filter((field) => field.show)
.some((condition) => condition.required === true);
}
return false;
};
@@ -393,18 +383,6 @@ export const QuestionCard = ({
isInvalid={isInvalid}
attributeClasses={attributeClasses}
/>
) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? (
<ContactInfoQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
/>
) : null}
<div className="mt-4">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
@@ -12,7 +12,7 @@ import {
TSurveyQuestionChoice,
TSurveyRankingQuestion,
} from "@formbricks/types/surveys/types";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface ChoiceProps {
@@ -264,6 +264,7 @@ export const QuestionsView = ({
}
}
});
setLocalSurvey(updatedSurvey);
validateSurveyQuestion(updatedSurvey.questions[questionIdx]);
};
@@ -13,16 +13,10 @@ import {
TSurvey,
TSurveyRankingQuestion,
} 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 RankingQuestionFormProps {
@@ -2,10 +2,10 @@ import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
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 { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { Dropdown } from "./RatingTypeDropdown";
interface RatingQuestionFormProps {
@@ -5,10 +5,10 @@ import { CheckIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TSurvey } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/components/RadioGroup";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
interface DisplayOption {
id: "displayOnce" | "displayMultiple" | "respondMultiple" | "displaySome";
@@ -1,6 +1,6 @@
import { TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
interface RedirectUrlFormProps {
endingCard: TSurveyRedirectUrlCard;
@@ -7,11 +7,11 @@ import { KeyboardEventHandler, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TSurvey } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { DatePicker } from "@formbricks/ui/components/DatePicker";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { Switch } from "@formbricks/ui/components/Switch";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { DatePicker } from "@formbricks/ui/DatePicker";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
interface ResponseOptionsCardProps {
localSurvey: TSurvey;
@@ -2,7 +2,7 @@ import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useState } from "react";
import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { Input } from "@formbricks/ui/components/Input";
import { Input } from "@formbricks/ui/Input";
interface SavedActionsTabProps {
actionClasses: TActionClass[];
@@ -73,6 +73,7 @@ export const SettingsView = ({
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
attributeClasses={attributeClasses}
actionClasses={actionClasses}
segments={segments}
initialSegment={segments.find((segment) => segment.id === localSurvey.segment?.id)}
/>
@@ -8,8 +8,8 @@ import { TEnvironment } from "@formbricks/types/environment";
import { TProduct, TProductStyling } from "@formbricks/types/product";
import { TBaseStyling } from "@formbricks/types/styling";
import { TSurvey, TSurveyStyling } from "@formbricks/types/surveys/types";
import { AlertDialog } from "@formbricks/ui/components/AlertDialog";
import { Button } from "@formbricks/ui/components/Button";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import {
FormControl,
FormDescription,
@@ -17,8 +17,8 @@ import {
FormItem,
FormLabel,
FormProvider,
} from "@formbricks/ui/components/Form";
import { Switch } from "@formbricks/ui/components/Switch";
} from "@formbricks/ui/Form";
import { Switch } from "@formbricks/ui/Switch";
import { BackgroundStylingCard } from "./BackgroundStylingCard";
import { CardStylingSettings } from "./CardStylingSettings";
import { FormStylingSettings } from "./FormStylingSettings";
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { TabBar } from "@formbricks/ui/components/TabBar";
import { TabBar } from "@formbricks/ui/TabBar";
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
import { ColorSurveyBg } from "./ColorSurveyBg";
import { UploadImageSurveyBg } from "./ImageSurveyBg";
@@ -12,7 +12,7 @@ import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
import { PreviewSurvey } from "@formbricks/ui/components/PreviewSurvey";
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
import { refetchProductAction } from "../actions";
import { LoadingSkeleton } from "./LoadingSkeleton";
import { QuestionsAudienceTabs } from "./QuestionsStylingSettingsTabs";
@@ -132,97 +132,101 @@ export const SurveyEditor = ({
}
return (
<div className="flex h-full w-full flex-col">
<SurveyMenuBar
setLocalSurvey={setLocalSurvey}
localSurvey={localSurvey}
survey={survey}
environment={environment}
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
product={localProduct}
responseCount={responseCount}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isCxMode={isCxMode}
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main
className="relative z-0 w-1/2 flex-1 overflow-y-auto bg-slate-50 focus:outline-none"
ref={surveyEditorRef}>
<QuestionsAudienceTabs
activeId={activeView}
setActiveId={setActiveView}
isCxMode={isCxMode}
isStylingTabVisible={!!product.styling.allowStyleOverwrite}
/>
{activeView === "questions" && (
<QuestionsView
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
product={localProduct}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
setSelectedLanguageCode={setSelectedLanguageCode}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
attributeClasses={attributeClasses}
plan={plan}
<>
<div className="flex h-full w-full flex-col">
<SurveyMenuBar
setLocalSurvey={setLocalSurvey}
localSurvey={localSurvey}
survey={survey}
environment={environment}
activeId={activeView}
setActiveId={setActiveView}
setInvalidQuestions={setInvalidQuestions}
product={localProduct}
responseCount={responseCount}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isCxMode={isCxMode}
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main
className="relative z-0 w-1/2 flex-1 overflow-y-auto bg-slate-50 focus:outline-none"
ref={surveyEditorRef}>
<QuestionsAudienceTabs
activeId={activeView}
setActiveId={setActiveView}
isCxMode={isCxMode}
isStylingTabVisible={!!product.styling.allowStyleOverwrite}
/>
)}
{activeView === "styling" && product.styling.allowStyleOverwrite && (
<StylingView
colors={colors}
environment={environment}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
{activeView === "questions" && (
<QuestionsView
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
product={localProduct}
invalidQuestions={invalidQuestions}
setInvalidQuestions={setInvalidQuestions}
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
setSelectedLanguageCode={setSelectedLanguageCode}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
attributeClasses={attributeClasses}
plan={plan}
isCxMode={isCxMode}
/>
)}
{activeView === "styling" && product.styling.allowStyleOverwrite && (
<StylingView
colors={colors}
environment={environment}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
product={localProduct}
styling={styling ?? null}
setStyling={setStyling}
localStylingChanges={localStylingChanges}
setLocalStylingChanges={setLocalStylingChanges}
isUnsplashConfigured={isUnsplashConfigured}
isCxMode={isCxMode}
/>
)}
{activeView === "settings" && (
<SettingsView
environment={environment}
organizationId={organizationId}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
segments={segments}
responseCount={responseCount}
membershipRole={membershipRole}
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
product={localProduct}
/>
)}
</main>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}
product={localProduct}
styling={styling ?? null}
setStyling={setStyling}
localStylingChanges={localStylingChanges}
setLocalStylingChanges={setLocalStylingChanges}
isUnsplashConfigured={isUnsplashConfigured}
isCxMode={isCxMode}
/>
)}
{activeView === "settings" && (
<SettingsView
environment={environment}
organizationId={organizationId}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
segments={segments}
responseCount={responseCount}
membershipRole={membershipRole}
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
product={localProduct}
previewType={
localSurvey.type === "app" || localSurvey.type === "website" ? "modal" : "fullwidth"
}
languageCode={selectedLanguageCode}
onFileUpload={async (file) => file.name}
/>
)}
</main>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-200 bg-slate-100 shadow-inner md:flex md:flex-col">
<PreviewSurvey
survey={localSurvey}
questionId={activeQuestionId}
product={localProduct}
environment={environment}
previewType={localSurvey.type === "app" || localSurvey.type === "website" ? "modal" : "fullwidth"}
languageCode={selectedLanguageCode}
onFileUpload={async (file) => file.name}
/>
</aside>
</aside>
</div>
</div>
</div>
</>
);
};
@@ -20,10 +20,10 @@ import {
ZSurveyEndScreenCard,
ZSurveyRedirectUrlCard,
} from "@formbricks/types/surveys/types";
import { AlertDialog } from "@formbricks/ui/components/AlertDialog";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/components/Tooltip";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
import { updateSurveyAction } from "../actions";
import { isSurveyValid } from "../lib/validation";
@@ -6,8 +6,8 @@ import Link from "next/link";
import { useState } from "react";
import { TPlacement } from "@formbricks/types/common";
import { TSurvey, TSurveyProductOverwrites } from "@formbricks/types/surveys/types";
import { Label } from "@formbricks/ui/components/Label";
import { Switch } from "@formbricks/ui/components/Switch";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import { Placement } from "./Placement";
interface SurveyPlacementCardProps {
@@ -8,17 +8,11 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { extractRecallInfo } from "@formbricks/lib/utils/recall";
import { TSurvey, TSurveyVariable } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/components/Form";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@formbricks/ui/components/Select";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormField, FormItem, FormProvider } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
interface SurveyVariablesCardItemProps {
variable?: TSurveyVariable;
@@ -13,15 +13,15 @@ import { isAdvancedSegment } from "@formbricks/lib/segment/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TBaseFilter, TSegment, TSegmentCreateInput, TSegmentUpdateInput } from "@formbricks/types/segment";
import { TSurvey } from "@formbricks/types/surveys/types";
import { AlertDialog } from "@formbricks/ui/components/AlertDialog";
import { BasicAddFilterModal } from "@formbricks/ui/components/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/components/BasicSegmentEditor";
import { Button } from "@formbricks/ui/components/Button";
import { LoadSegmentModal } from "@formbricks/ui/components/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/components/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/components/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/components/TargetingIndicator";
import { UpgradePlanNotice } from "@formbricks/ui/components/UpgradePlanNotice";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { BasicAddFilterModal } from "@formbricks/ui/BasicAddFilterModal";
import { BasicSegmentEditor } from "@formbricks/ui/BasicSegmentEditor";
import { Button } from "@formbricks/ui/Button";
import { LoadSegmentModal } from "@formbricks/ui/LoadSegmentModal";
import { SaveAsNewSegmentModal } from "@formbricks/ui/SaveAsNewSegmentModal";
import { SegmentTitle } from "@formbricks/ui/SegmentTitle";
import { TargetingIndicator } from "@formbricks/ui/TargetingIndicator";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import {
cloneBasicSegmentAction,
createBasicSegmentAction,
@@ -6,9 +6,9 @@ import UnsplashImage from "next/image";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { TSurveyBackgroundBgType } from "@formbricks/types/surveys/types";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
import { getImagesFromUnsplashAction, triggerDownloadUnsplashImageAction } from "../actions";
interface ImageFromUnsplashSurveyBgProps {
@@ -4,9 +4,9 @@ import { useState } from "react";
import toast from "react-hot-toast";
import { TSurvey, TSurveyQuestion } 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 { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
interface UpdateQuestionIdProps {
localSurvey: TSurvey;
@@ -14,9 +14,9 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TActionClass } from "@formbricks/types/action-classes";
import { TMembershipRole } from "@formbricks/types/memberships";
import { TSurvey } 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 { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { AddActionModal } from "./AddActionModal";
interface WhenToSendCardProps {
@@ -1,8 +1,8 @@
import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react";
import { HTMLInputTypeAttribute } from "react";
import { HTMLInputTypeAttribute, useMemo } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { questionTypes } from "@formbricks/lib/utils/questions";
import { questionIconMapping, questionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
@@ -18,7 +18,7 @@ import {
TSurveyQuestionTypeEnum,
TSurveyVariable,
} from "@formbricks/types/surveys/types";
import { TComboboxGroupedOption, TComboboxOption } from "@formbricks/ui/components/InputCombobox";
import { TComboboxGroupedOption, TComboboxOption } from "@formbricks/ui/InputCombobox";
import { TLogicRuleOption, logicRules } from "./logicRuleEngine";
// formats the text to highlight specific parts of the text with slashes
@@ -40,14 +40,6 @@ export const formatTextWithSlashes = (text: string) => {
});
};
const questionIconMapping = questionTypes.reduce(
(prev, curr) => ({
...prev,
[curr.id]: curr.icon,
}),
{}
);
export const getConditionValueOptions = (
localSurvey: TSurvey,
currQuestionIdx: number
@@ -756,55 +748,6 @@ export const getMatchValueProps = (
return { show: false, options: [] };
};
export const getActionTargetOptions = (
action: TSurveyLogicAction,
localSurvey: TSurvey,
currQuestionIdx: number
): TComboboxOption[] => {
let questions = localSurvey.questions.filter((_, idx) => idx !== currQuestionIdx);
if (action.objective === "requireAnswer") {
questions = questions.filter((question) => !question.required);
}
const questionOptions = questions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
};
});
if (action.objective === "requireAnswer") return questionOptions;
const endingCardOptions = localSurvey.endings.map((ending) => {
return {
label:
ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default") || "End Screen"
: ending.label || "Redirect Thank you card",
value: ending.id,
};
});
return [...questionOptions, ...endingCardOptions];
};
export const getActionVariableOptions = (localSurvey: TSurvey): TComboboxOption[] => {
const variables = localSurvey.variables ?? [];
return variables.map((variable) => {
return {
icon: variable.type === "number" ? FileDigitIcon : FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
variableType: variable.type,
},
};
});
};
export const getActionOperatorOptions = (variableType?: TSurveyVariable["type"]): TComboboxOption[] => {
if (variableType === "number") {
return [
@@ -844,151 +787,6 @@ export const getActionOperatorOptions = (variableType?: TSurveyVariable["type"])
return [];
};
export const getActionValueOptions = (variableId: string, localSurvey: TSurvey): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
let variables = localSurvey.variables ?? [];
const questions = localSurvey.questions;
const hiddenFieldsOptions = hiddenFields.map((field) => {
return {
icon: EyeOffIcon,
label: field,
value: field,
meta: {
type: "hiddenField",
},
};
});
const selectedVariable = variables.find((variable) => variable.id === variableId);
variables = variables.filter((variable) => variable.id !== variableId);
if (!selectedVariable) return [];
if (selectedVariable.type === "text") {
const allowedQuestions = questions.filter((question) =>
[
TSurveyQuestionTypeEnum.OpenText,
TSurveyQuestionTypeEnum.MultipleChoiceSingle,
TSurveyQuestionTypeEnum.Rating,
TSurveyQuestionTypeEnum.NPS,
TSurveyQuestionTypeEnum.Date,
].includes(question.type)
);
const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});
const stringVariables = variables.filter((variable) => variable.type === "text");
const variableOptions = stringVariables.map((variable) => {
return {
icon: FileType2Icon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});
const groupedOptions: TComboboxGroupedOption[] = [];
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
return groupedOptions;
} else if (selectedVariable.type === "number") {
const allowedQuestions = questions.filter((question) =>
[TSurveyQuestionTypeEnum.Rating, TSurveyQuestionTypeEnum.NPS].includes(question.type)
);
const questionOptions = allowedQuestions.map((question) => {
return {
icon: questionIconMapping[question.type],
label: getLocalizedValue(question.headline, "default"),
value: question.id,
meta: {
type: "question",
},
};
});
const numberVariables = variables.filter((variable) => variable.type === "number");
const variableOptions = numberVariables.map((variable) => {
return {
icon: FileDigitIcon,
label: variable.name,
value: variable.id,
meta: {
type: "variable",
},
};
});
const groupedOptions: TComboboxGroupedOption[] = [];
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
value: "questions",
options: questionOptions,
});
}
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
value: "variables",
options: variableOptions,
});
}
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
value: "hiddenFields",
options: hiddenFieldsOptions,
});
}
return groupedOptions;
}
return [];
};
const isUsedInLeftOperand = (
leftOperand: TLeftOperand,
type: "question" | "hiddenField" | "variable",
@@ -178,11 +178,6 @@ export const isEndingCardValid = (
surveyLanguages: TSurveyLanguage[]
) => {
if (card.type === "endScreen") {
const parseResult = z.string().url().safeParse(card.buttonLink);
if (card.buttonLabel !== undefined && !parseResult.success) {
return false;
}
return (
isContentValid(card.headline, surveyLanguages) &&
isContentValid(card.subheader, surveyLanguages) &&
@@ -193,6 +188,7 @@ export const isEndingCardValid = (
if (parseResult.success) {
return card.label?.trim() !== "";
} else {
toast.error("Invalid Redirect Url in Ending card");
return false;
}
}
@@ -12,7 +12,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { SurveyEditor } from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
@@ -2,7 +2,7 @@
import { ArrowLeftIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@formbricks/ui/components/Button";
import { Button } from "@formbricks/ui/Button";
export const BackButton = () => {
const router = useRouter();
@@ -7,9 +7,9 @@ import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
import { TUser } from "@formbricks/types/user";
import { PreviewSurvey } from "@formbricks/ui/components/PreviewSurvey";
import { SearchBar } from "@formbricks/ui/components/SearchBar";
import { TemplateList } from "@formbricks/ui/components/TemplateList";
import { PreviewSurvey } from "@formbricks/ui/PreviewSurvey";
import { SearchBox } from "@formbricks/ui/SearchBox";
import { TemplateList } from "@formbricks/ui/TemplateList";
import { minimalSurvey } from "../../lib/minimalSurvey";
type TemplateContainerWithPreviewProps = {
@@ -39,11 +39,14 @@ export const TemplateContainerWithPreview = ({
<div className="mb-3 ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-end">
<h1 className="text-2xl font-bold text-slate-800">Create a new survey</h1>
<div className="px-6">
<SearchBar
<SearchBox
autoFocus
value={templateSearch ?? ""}
onChange={setTemplateSearch}
onChange={(e) => setTemplateSearch(e.target.value)}
placeholder={"Search..."}
className="border-slate-700"
className="block rounded-md border border-slate-100 bg-white shadow-sm focus:border-slate-500 focus:outline-none focus:ring-0 sm:text-sm md:w-auto"
type="search"
name="search"
/>
</div>
</div>
@@ -1,4 +1,4 @@
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
import { LoadingSpinner } from "@formbricks/ui/LoadingSpinner";
const Loading = () => {
return <LoadingSpinner />;
@@ -1,8 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@formbricks/ui/components/Button";
import { Confetti } from "@formbricks/ui/components/Confetti";
import { Button } from "@formbricks/ui/Button";
import { Confetti } from "@formbricks/ui/Confetti";
interface ConfirmationPageProps {
environmentId: string;

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