mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-27 08:50:38 -06:00
Compare commits
1 Commits
feature/fi
...
mattinannt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d2e6edd8 |
@@ -67,19 +67,7 @@ git clone https://github.com/formbricks/formbricks && cd formbricks
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. Setup Node.JS with nvm:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Setup Node version with nvm">
|
||||
|
||||
```bash
|
||||
nvm install && nvm use
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
3. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
|
||||
2. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Install dependencies via pnpm">
|
||||
@@ -91,7 +79,7 @@ pnpm install
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
4. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
|
||||
3. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Define environment variables">
|
||||
@@ -103,7 +91,7 @@ cp .env.example .env
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
5. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length:
|
||||
4. Generate & set some secret values mandatory for the `ENCRYPTION_KEY`, `NEXTAUTH_SECRET` and `CRON_SECRET` in the .env file. You can use the following command to generate the random string of required length:
|
||||
|
||||
- For Linux
|
||||
|
||||
@@ -133,7 +121,7 @@ sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
6. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
|
||||
5. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Start Formbricks Dev Setup">
|
||||
|
||||
@@ -48,7 +48,7 @@ Initialize the Formbricks JS Client for surveys. When used in a web app, pass a
|
||||
<CodeGroup title="Initialize Formbricks">
|
||||
|
||||
```javascript
|
||||
import formbricks from "@formbricks/js";
|
||||
import formbricks from "@formbricks/js/app";
|
||||
|
||||
formbricks.init({
|
||||
environmentId: "<your-environment-id>", // required
|
||||
|
||||
@@ -48,7 +48,7 @@ We have step-by-step guides to configure our third-party integrations with a sel
|
||||
<Note>
|
||||
{" "}
|
||||
Once you’ve configured your integration, See our Integration sections to see how to use them within your Formbricks
|
||||
app [here](/developer-docs/integrations/airtable)
|
||||
app [here](/how-to-formbricks/integrations)
|
||||
</Note>
|
||||
|
||||
### Step by Step Guides
|
||||
@@ -102,11 +102,12 @@ Enabling the Airtable Integration in a self-hosted environment requires creating
|
||||
/>
|
||||
|
||||
5. Click on the "Save" button and you are done
|
||||
6. Now just copy **Client ID** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||
6. Now just copy **Client ID** and **Redirect URL** for your integration & add it to your **Formbricks environment variables** as in the docker compose file:
|
||||
|
||||
- `AIRTABLE_CLIENT_ID`
|
||||
- `AIRTABLE_REDIRECT_URL`
|
||||
|
||||
Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Airtable Integration with Formbricks](/developer-docs/integrations/airtable) section to link an Airtable with Formbricks.
|
||||
Voila! You have successfully enabled the Airtable integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Airtable Integration with Formbricks](/integrations#airtable) section to link an Airtable with Formbricks.
|
||||
|
||||
## Google Sheets
|
||||
|
||||
@@ -151,7 +152,7 @@ Now just copy **GOOGLE_SHEETS_CLIENT_ID**, **GOOGLE_SHEETS_CLIENT_SECRET** and *
|
||||
- `GOOGLE_SHEETS_CLIENT_SECRET`
|
||||
- `GOOGLE_SHEETS_REDIRECT_URL`
|
||||
|
||||
Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Google Sheets Integration with Formbricks](/developer-docs/integrations/google-sheets) section to link a Google Sheet with Formbricks.
|
||||
Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Google Sheets Integration with Formbricks](/integrations#google-sheets) section to link a Google Sheet with Formbricks.
|
||||
|
||||
## Notion:
|
||||
|
||||
@@ -169,7 +170,7 @@ Enabling the Notion Integration in a self-hosted environment requires a setup us
|
||||
- `NOTION_OAUTH_CLIENT_ID` - OAuth Client ID
|
||||
- `NOTION_OAUTH_CLIENT_SECRET` - OAuth Client Secret
|
||||
|
||||
Voila! You have successfully enabled the Notion integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Notion Integration with Formbricks](/developer-docs/integrations/notion) section to link your Notion with Formbricks.
|
||||
Voila! You have successfully enabled the Notion integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in [Notion Integration with Formbricks](/integrations#notion) section to link your Notion with Formbricks.
|
||||
|
||||
## n8n
|
||||
|
||||
@@ -288,7 +289,7 @@ Once the execution is successful, you'll receive the content in the discord chan
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Voila! You have successfully enabled the n8n integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks](/developer-docs/integrations/n8n) Integrations section to know more about the capabilities with Formbricks with n8n.
|
||||
Voila! You have successfully enabled the n8n integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks](/integrations#n8n) Integrations section to know more about the capabilities with Formbricks with n8n.
|
||||
|
||||
## Slack
|
||||
|
||||
@@ -320,7 +321,7 @@ Enabling the Slack Integration in a self-hosted environment requires a setup usi
|
||||
8. Now, you need to enable the public distribution of your app. Go to the **Basic Information** tab and click on the **Manage distribution** button and click on the "Distribute App".
|
||||
9. Scroll down to the **Share your app with other workspaces** section, complete the checklist and click on the **Activate public distribution** button.
|
||||
|
||||
Voila! You have successfully enabled the Slack integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Slack Integration](/developer-docs/integrations/slack) section to link a Slack workspace with Formbricks.
|
||||
Voila! You have successfully enabled the Slack integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Slack Integration](/integrations#slack) section to link a Slack workspace with Formbricks.
|
||||
|
||||
## Zapier
|
||||
|
||||
@@ -347,7 +348,7 @@ Then, choose the event you want to trigger the Zap on:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Now you need an API key. Please refer to the [API Key Setup](/developer-docs/rest-api##how-to-generate-an-api-key) page to learn how to create one.
|
||||
Now you need an API key. Please refer to the [API Key Setup](/developer-docs/rest-api#api-key) page to learn how to create one.
|
||||
|
||||
Once you copied it in the newly opened Zapier window, you will be connected:
|
||||
|
||||
@@ -358,6 +359,6 @@ Once you copied it in the newly opened Zapier window, you will be connected:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Voila! You have successfully configured Zapier to work with your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Zapier Integration](/developer-docs/integrations/zapier) section to connect it with your Formbricks app and see it live.
|
||||
Voila! You have successfully configured Zapier to work with your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Zapier Integration](/integrations#zapier) section to connect it with your Formbricks app and see it live.
|
||||
|
||||
---
|
||||
|
||||
@@ -9,7 +9,7 @@ export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
width="1em"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}>
|
||||
<path d="M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z" />
|
||||
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
@@ -23,7 +22,6 @@ interface AddQuestionButtonProps {
|
||||
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
|
||||
|
||||
@@ -46,7 +44,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
|
||||
{/* <hr className="py-1 text-slate-600" /> */}
|
||||
{availableQuestionTypes.map((questionType) => (
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -92,7 +91,6 @@ export const AddressQuestionForm = ({
|
||||
question.country,
|
||||
]);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -108,7 +106,7 @@ export const AddressQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -40,14 +40,6 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB
|
||||
"/animated-bgs/Thumbnails/28_Thumb.mp4": "/animated-bgs/4K/28_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/29_Thumb.mp4": "/animated-bgs/4K/29_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/31_Thumb.mp4": "/animated-bgs/4K/31_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/32_Thumb.mp4": "/animated-bgs/4K/32_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/33_Thumb.mp4": "/animated-bgs/4K/33_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/34_Thumb.mp4": "/animated-bgs/4K/34_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/35_Thumb.mp4": "/animated-bgs/4K/35_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/36_Thumb.mp4": "/animated-bgs/4K/36_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/37_Thumb.mp4": "/animated-bgs/4K/37_4k.mp4",
|
||||
"/animated-bgs/Thumbnails/38_Thumb.mp4": "/animated-bgs/4K/38_4k.mp4",
|
||||
};
|
||||
|
||||
const togglePlayback = (index: number, type: "play" | "pause") => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
@@ -33,8 +32,6 @@ export const BackgroundStylingCard = ({
|
||||
isUnsplashConfigured,
|
||||
form,
|
||||
}: BackgroundStylingCardProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -75,7 +72,7 @@ export const BackgroundStylingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="pt-1 text-slate-600" />
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<FormField
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -42,9 +41,9 @@ export const CTAQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form ref={parent}>
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -43,7 +42,6 @@ export const CardStylingSettings = ({
|
||||
const appCardArrangement = form.watch("cardArrangement.appSurveys") ?? "simple";
|
||||
const roundness = form.watch("roundness") ?? 8;
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -80,7 +78,7 @@ export const CardStylingSettings = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getDefaultOperatorForQuestion,
|
||||
replaceEndingCardHeadlineRecall,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
@@ -112,17 +111,16 @@ export function ConditionalLogic({
|
||||
logic: logicCopy,
|
||||
});
|
||||
};
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="mt-4" ref={parent}>
|
||||
<div className="mt-4">
|
||||
<Label className="flex gap-2">
|
||||
Conditional Logic
|
||||
<SplitIcon className="h-4 w-4 rotate-90" />
|
||||
</Label>
|
||||
|
||||
{question.logic && question.logic.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-4" ref={parent}>
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
{question.logic.map((logicItem, logicItemIdx) => (
|
||||
<div
|
||||
key={logicItem.id}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -79,9 +78,6 @@ export const ContactInfoQuestionForm = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -97,7 +93,7 @@ export const ContactInfoQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -46,7 +45,7 @@ export const DateQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -61,7 +60,7 @@ export const DateQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -203,7 +203,7 @@ export const EditEndingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "mt-3 pb-6"}`}>
|
||||
<Collapsible.CollapsibleContent className="mt-3 px-4 pb-6">
|
||||
<TooltipRenderer
|
||||
shouldRender={endingCard.type === "endScreen" && isRedirectToUrlDisabled}
|
||||
tooltipContent={"Redirect To Url is not available on free plan"}
|
||||
|
||||
@@ -72,7 +72,7 @@ export const EditWelcomeCard = ({
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-200 ease-in-out">
|
||||
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out">
|
||||
<Collapsible.CollapsibleTrigger
|
||||
asChild
|
||||
className="flex cursor-pointer justify-between rounded-r-lg p-4 hover:bg-slate-50">
|
||||
@@ -102,7 +102,7 @@ export const EditWelcomeCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`}>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<form>
|
||||
<div className="mt-2">
|
||||
<Label htmlFor="companyLogo">Company Logo</Label>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon, XCircleIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -122,8 +121,6 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion(questionIdx, { maxSizeInMB: checked ? defaultMaxSizeInMB : undefined });
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -138,7 +135,7 @@ export const FileUploadQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon, SparklesIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
@@ -68,8 +67,6 @@ export const FormStylingSettings = ({
|
||||
}
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -106,10 +103,10 @@ export const FormStylingSettings = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<hr className="py-1 text-slate-600" key={"hello"} />
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-6 p-6 pt-2" key={"hjiii"}>
|
||||
<div className="flex flex-col gap-6 p-6 pt-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { EyeOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -86,9 +85,6 @@ export const HiddenFieldsCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Auto Animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className={cn(open ? "shadow-lg" : "shadow-md", "group z-10 flex flex-row rounded-lg bg-white")}>
|
||||
<div
|
||||
@@ -128,8 +124,8 @@ export const HiddenFieldsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-wrap gap-2" ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex gap-2">
|
||||
{localSurvey.hiddenFields?.fieldIds && localSurvey.hiddenFields?.fieldIds?.length > 0 ? (
|
||||
localSurvey.hiddenFields?.fieldIds?.map((fieldId) => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -90,8 +89,6 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
},
|
||||
];
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -117,7 +114,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
<RadioGroup
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getDefaultOperatorForQuestion,
|
||||
getMatchValueProps,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -53,8 +52,6 @@ export function LogicEditorConditions({
|
||||
updateQuestion,
|
||||
depth = 0,
|
||||
}: LogicEditorConditionsProps) {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const handleAddConditionBelow = (resourceId: string) => {
|
||||
const operator = getDefaultOperatorForQuestion(question);
|
||||
|
||||
@@ -337,7 +334,7 @@ export function LogicEditorConditions({
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={parent} className="flex flex-col gap-y-2">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{conditions?.conditions.map((condition, index) => renderCondition(condition, index, conditions))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -97,8 +96,7 @@ export const MatrixQuestionForm = ({
|
||||
show: true,
|
||||
},
|
||||
};
|
||||
/// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -113,7 +111,7 @@ export const MatrixQuestionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -152,7 +150,7 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Rows section */}
|
||||
<Label htmlFor="rows">Rows</Label>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.rows.map((_, index) => (
|
||||
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "row")}>
|
||||
<QuestionFormInput
|
||||
@@ -194,7 +192,7 @@ export const MatrixQuestionForm = ({
|
||||
<div>
|
||||
{/* Columns section */}
|
||||
<Label htmlFor="columns">Columns</Label>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.columns.map((_, index) => (
|
||||
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "column")}>
|
||||
<QuestionFormInput
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { findOptionUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -159,8 +158,6 @@ export const MultipleChoiceQuestionForm = ({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -176,7 +173,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -239,7 +236,7 @@ export const MultipleChoiceQuestionForm = ({
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col" ref={parent}>
|
||||
<div className="flex flex-col">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -33,9 +32,6 @@ export const NPSQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: NPSQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -51,7 +47,7 @@ export const NPSQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -55,8 +54,6 @@ export const OpenQuestionForm = ({
|
||||
updateQuestion(questionIdx, updatedAttributes);
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -72,7 +69,7 @@ export const OpenQuestionForm = ({
|
||||
label={"Question*"}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -65,8 +64,7 @@ export const PictureSelectionForm = ({
|
||||
choices: updatedChoices,
|
||||
});
|
||||
};
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -81,7 +79,7 @@ export const PictureSelectionForm = ({
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[e
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -86,18 +85,12 @@ export const QuestionCard = ({
|
||||
|
||||
const open = activeQuestionId === question.id;
|
||||
const [openAdvanced, setOpenAdvanced] = useState(question.logic && question.logic.length > 0);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const updateEmptyButtonLabels = (
|
||||
labelKey: "buttonLabel" | "backButtonLabel",
|
||||
labelValue: TI18nString,
|
||||
skipIndex: number
|
||||
) => {
|
||||
const updateEmptyNextButtonLabels = (labelValue: TI18nString) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === skipIndex) return;
|
||||
const currentLabel = q[labelKey];
|
||||
if (!currentLabel || currentLabel[selectedLanguageCode]?.trim() === "") {
|
||||
updateQuestion(index, { [labelKey]: labelValue });
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel[selectedLanguageCode]?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -231,7 +224,7 @@ export const QuestionCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-4"}`}>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-4">
|
||||
{question.type === TSurveyQuestionTypeEnum.OpenText ? (
|
||||
<OpenQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
@@ -425,7 +418,7 @@ export const QuestionCard = ({
|
||||
{openAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings"}
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col gap-4" ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="space-y-4">
|
||||
{question.type !== TSurveyQuestionTypeEnum.NPS &&
|
||||
question.type !== TSurveyQuestionTypeEnum.Rating &&
|
||||
question.type !== TSurveyQuestionTypeEnum.CTA ? (
|
||||
@@ -451,11 +444,7 @@ export const QuestionCard = ({
|
||||
};
|
||||
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyButtonLabels(
|
||||
"buttonLabel",
|
||||
translatedNextButtonLabel,
|
||||
localSurvey.questions.length - 1
|
||||
);
|
||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||
}}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
@@ -474,14 +463,6 @@ export const QuestionCard = ({
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
onBlur={(e) => {
|
||||
if (!question.backButtonLabel) return;
|
||||
let translatedBackButtonLabel = {
|
||||
...question.backButtonLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
updateEmptyButtonLabels("backButtonLabel", translatedBackButtonLabel, 0);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
|
||||
@@ -42,10 +41,8 @@ export const QuestionsDroppable = ({
|
||||
isFormbricksCloud,
|
||||
isCxMode,
|
||||
}: QuestionsDraggableProps) => {
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="group mb-5 flex w-full flex-col gap-5" ref={parent}>
|
||||
<div className="group mb-5 flex w-full flex-col gap-5">
|
||||
<SortableContext items={localSurvey.questions} strategy={verticalListSortingStrategy}>
|
||||
{localSurvey.questions.map((question, questionIdx) => (
|
||||
<QuestionCard
|
||||
|
||||
@@ -12,9 +12,8 @@ import {
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import React, { SetStateAction, useEffect, useMemo } from "react";
|
||||
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
|
||||
import { addMultiLanguageLabels, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
@@ -87,6 +86,7 @@ export const QuestionsView = ({
|
||||
}, [localSurvey.questions]);
|
||||
|
||||
const surveyLanguages = localSurvey.languages;
|
||||
const [backButtonLabel, setbackButtonLabel] = useState(null);
|
||||
|
||||
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
|
||||
const updateConditions = (conditions: TConditionGroup): TConditionGroup => {
|
||||
@@ -238,6 +238,22 @@ export const QuestionsView = ({
|
||||
...updatedAttributes,
|
||||
};
|
||||
|
||||
if ("backButtonLabel" in updatedAttributes) {
|
||||
const backButtonLabel = updatedSurvey.questions[questionIdx].backButtonLabel;
|
||||
// If the value of backbuttonLabel is equal to {default:""}, then delete backButtonLabel key
|
||||
if (
|
||||
backButtonLabel &&
|
||||
Object.keys(backButtonLabel).length === 1 &&
|
||||
backButtonLabel["default"].trim() === ""
|
||||
) {
|
||||
delete updatedSurvey.questions[questionIdx].backButtonLabel;
|
||||
} else {
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
question.backButtonLabel = updatedAttributes.backButtonLabel;
|
||||
});
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
}
|
||||
const attributesToCheck = ["buttonLabel", "upperLabel", "lowerLabel"];
|
||||
|
||||
// If the value of buttonLabel, lowerLabel or upperLabel is equal to {default:""}, then delete buttonLabel key
|
||||
@@ -316,6 +332,9 @@ export const QuestionsView = ({
|
||||
|
||||
const addQuestion = (question: TSurveyQuestion, index?: number) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const updatedQuestion = addMultiLanguageLabels(question, languageSymbols);
|
||||
@@ -411,9 +430,6 @@ export const QuestionsView = ({
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div className="mt-12 w-full px-5 py-4">
|
||||
{!isCxMode && (
|
||||
@@ -457,7 +473,7 @@ export const QuestionsView = ({
|
||||
</DndContext>
|
||||
|
||||
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} />
|
||||
<div className="mt-5 flex flex-col gap-5" ref={parent}>
|
||||
<div className="mt-5 flex flex-col gap-5">
|
||||
<hr className="border-t border-dashed" />
|
||||
<DndContext
|
||||
id="endings"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -108,8 +107,6 @@ export const RankingQuestionForm = ({
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -125,7 +122,7 @@ export const RankingQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
@@ -184,7 +181,7 @@ export const RankingQuestionForm = ({
|
||||
updateQuestion(questionIdx, { choices: newChoices });
|
||||
}}>
|
||||
<SortableContext items={question.choices} strategy={verticalListSortingStrategy}>
|
||||
<div className="flex flex-col" ref={parent}>
|
||||
<div className="flex flex-col">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<QuestionOptionChoice
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
@@ -32,7 +31,7 @@ export const RatingQuestionForm = ({
|
||||
attributeClasses,
|
||||
}: RatingQuestionFormProps) => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -48,7 +47,7 @@ export const RatingQuestionForm = ({
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div ref={parent}>
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -58,9 +57,6 @@ export const RecontactOptionsCard = ({
|
||||
);
|
||||
const [displayLimit, setDisplayLimit] = useState(localSurvey.displayLimit ?? 1);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const handleCheckMark = () => {
|
||||
if (ignoreWaiting) {
|
||||
const updatedSurvey = { ...localSurvey, recontactDays: null };
|
||||
@@ -123,7 +119,7 @@ export const RecontactOptionsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col ${open && "pb-3"}`} ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="pb-3">
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
<RadioGroup
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ArrowUpRight, CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -273,7 +272,6 @@ export const ResponseOptionsCard = ({
|
||||
return;
|
||||
}
|
||||
};
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -297,7 +295,7 @@ export const ResponseOptionsCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-3">
|
||||
{/* Close Survey on Limit */}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -71,8 +70,6 @@ export const SurveyPlacementCard = ({
|
||||
}
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -98,7 +95,7 @@ export const SurveyPlacementCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex ${open && "pb-3"}`} ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="pb-3">
|
||||
<hr className="py-1 text-slate-600" />
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { FileDigitIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
@@ -23,7 +22,6 @@ export const SurveyVariablesCard = ({
|
||||
setActiveQuestionId,
|
||||
}: SurveyVariablesCardProps) => {
|
||||
const open = activeQuestionId === variablesCardId;
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const setOpenState = (state: boolean) => {
|
||||
if (state) {
|
||||
@@ -59,8 +57,8 @@ export const SurveyVariablesCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<div className="flex flex-col gap-2" ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
{localSurvey.variables.length > 0 ? (
|
||||
localSurvey.variables.map((variable) => (
|
||||
<SurveyVariablesCardItem
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { AlertCircle, CheckIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -155,7 +154,6 @@ export const TargetingCard = ({
|
||||
() => (localSurvey?.segment ? localSurvey.segment?.surveys?.length > 1 : false),
|
||||
[localSurvey.segment]
|
||||
);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
@@ -178,7 +176,7 @@ export const TargetingCard = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="flex min-w-full flex-col overflow-auto" ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="min-w-full overflow-auto">
|
||||
<hr className="text-slate-600" />
|
||||
|
||||
<div className="flex flex-col gap-5 p-6">
|
||||
|
||||
@@ -110,13 +110,6 @@ const defaultImages = [
|
||||
regularWithAttribution: "/image-backgrounds/kittens.webp",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "windows",
|
||||
alt_description: "Windows",
|
||||
urls: {
|
||||
regularWithAttribution: "/image-backgrounds/windows.webp",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import {
|
||||
CheckIcon,
|
||||
@@ -127,9 +126,6 @@ export const WhenToSendCard = ({
|
||||
}
|
||||
}, [localSurvey.type]);
|
||||
|
||||
// Auto animate
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
const containsEmptyTriggers = useMemo(() => {
|
||||
return !localSurvey.triggers || !localSurvey.triggers.length || !localSurvey.triggers[0];
|
||||
}, [localSurvey]);
|
||||
@@ -171,7 +167,7 @@ export const WhenToSendCard = ({
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
|
||||
<Collapsible.CollapsibleContent className="flex flex-col" ref={parent}>
|
||||
<Collapsible.CollapsibleContent>
|
||||
<hr className="py-1 text-slate-600" />
|
||||
|
||||
<div className="px-3 pb-3 pt-1">
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
@@ -55,8 +54,6 @@ export const PersonTable = ({
|
||||
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const router = useRouter();
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
// Generate columns
|
||||
const columns = useMemo(
|
||||
() => generatePersonTableColumns(isExpanded ?? false, searchValue),
|
||||
@@ -192,7 +189,7 @@ export const PersonTable = ({
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody ref={parent}>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const BasicCreateSegmentModal = ({
|
||||
const [addFilterModalOpen, setAddFilterModalOpen] = useState(false);
|
||||
const [segment, setSegment] = useState<TSegment>(initialSegmentState);
|
||||
const [isCreatingSegment, setIsCreatingSegment] = useState(false);
|
||||
|
||||
const handleResetState = () => {
|
||||
setSegment(initialSegmentState);
|
||||
setOpen(false);
|
||||
|
||||
@@ -121,13 +121,20 @@ export const DisableTwoFactorModal = ({ open, setOpen }: TDisableTwoFactorModalP
|
||||
<Controller
|
||||
name="code"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<OTPInput
|
||||
value={field.value}
|
||||
valueLength={6}
|
||||
onChange={field.onChange}
|
||||
containerClassName="justify-start mt-4"
|
||||
/>
|
||||
render={({ field, formState: { errors } }) => (
|
||||
<>
|
||||
<OTPInput
|
||||
value={field.value}
|
||||
valueLength={6}
|
||||
onChange={field.onChange}
|
||||
containerClassName="justify-start mt-4"
|
||||
/>
|
||||
{errors.code && (
|
||||
<p className="mt-2 text-sm text-red-600" id="code-error">
|
||||
{errors.code.message}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -148,7 +148,7 @@ type TEnableCodeProps = {
|
||||
refreshData: () => void;
|
||||
};
|
||||
const EnterCode = ({ setCurrentStep, setOpen, refreshData }: TEnableCodeProps) => {
|
||||
const { control, handleSubmit } = useForm<TEnterCodeFormState>({
|
||||
const { control, handleSubmit, setError } = useForm<TEnterCodeFormState>({
|
||||
defaultValues: {
|
||||
code: "",
|
||||
},
|
||||
@@ -164,7 +164,8 @@ const EnterCode = ({ setCurrentStep, setOpen, refreshData }: TEnableCodeProps) =
|
||||
// refresh data to update the UI
|
||||
refreshData();
|
||||
} else {
|
||||
toast.error("The 2FA OTP is incorrect. Please try again.");
|
||||
const errorMessage = getFormattedErrorMessage(enableTwoFactorAuthResponse);
|
||||
setError("code", { message: errorMessage });
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err.message);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers";
|
||||
import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { VisibilityState, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -66,7 +65,6 @@ export const ResponseTable = ({
|
||||
const selectedResponse = responses?.find((response) => response.id === selectedResponseId) ?? null;
|
||||
const [isExpanded, setIsExpanded] = useState<boolean | null>(null);
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>([]);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
// Generate columns
|
||||
const columns = generateResponseTableColumns(survey, isExpanded ?? false, isViewer);
|
||||
@@ -201,7 +199,7 @@ export const ResponseTable = ({
|
||||
))}
|
||||
</TableHeader>
|
||||
|
||||
<TableBody ref={parent}>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SparklesIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/components/Alert";
|
||||
import { Badge } from "@formbricks/ui/components/Badge";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
|
||||
interface EnableInsightsBannerProps {
|
||||
@@ -28,34 +27,30 @@ export const EnableInsightsBanner = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert className="mb-6 mt-4 flex items-center gap-4 border-slate-400 bg-white">
|
||||
<div>
|
||||
<SparklesIcon strokeWidth={1.5} className="size-7 text-slate-700" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<AlertTitle>
|
||||
<span className="mr-2">Ready to test AI insights?</span>
|
||||
<Badge text="Beta" type="gray" size="normal" />
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex items-start justify-between gap-4">
|
||||
<Alert className="w-1/2 bg-white">
|
||||
<SparklesIcon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
<span>Ready to enable insights?</span>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex items-start justify-between gap-4">
|
||||
<span>
|
||||
You can enable the new insights feature for the survey to get AI-based insights for your open-text
|
||||
responses.
|
||||
</AlertDescription>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleInsightGeneration}
|
||||
loading={isGeneratingInsights}
|
||||
disabled={surveyResponseCount > maxResponseCount || isGeneratingInsights}
|
||||
tooltip={
|
||||
surveyResponseCount > maxResponseCount
|
||||
? "Kindly contact us at hola@formbricks.com to generate insights for this survey"
|
||||
: undefined
|
||||
}>
|
||||
Enable insights
|
||||
</Button>
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={handleInsightGeneration}
|
||||
disabled={surveyResponseCount > maxResponseCount || isGeneratingInsights}
|
||||
tooltip={
|
||||
surveyResponseCount > maxResponseCount
|
||||
? "Kindly contact us at hola@formbricks.com to generate insights for this survey"
|
||||
: undefined
|
||||
}>
|
||||
Enable Insights
|
||||
</Button>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,11 +8,10 @@ import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environment
|
||||
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
|
||||
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
|
||||
import { getSurveyFilterDataBySurveySharingKeyAction } from "@/app/share/[sharingKey]/actions";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Checkbox } from "@formbricks/ui/components/Checkbox";
|
||||
@@ -32,7 +31,6 @@ interface ResponseFilterProps {
|
||||
|
||||
export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
const params = useParams();
|
||||
const [parent] = useAutoAnimate();
|
||||
const sharingKey = params.sharingKey as string;
|
||||
const isSharingPage = !!sharingKey;
|
||||
|
||||
@@ -226,66 +224,63 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={parent}>
|
||||
{filterValue.filter?.map((s, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<div className="flex w-full flex-wrap gap-3 md:flex-nowrap">
|
||||
<div
|
||||
className="grid w-full grid-cols-1 items-center gap-3 md:grid-cols-2"
|
||||
key={`${s.questionType.id}-${i}`}>
|
||||
<QuestionsComboBox
|
||||
key={`${s.questionType.label}-${i}`}
|
||||
options={questionComboBoxOptions}
|
||||
selected={s.questionType}
|
||||
onChangeValue={(value) => handleOnChangeQuestionComboBoxValue(value, i)}
|
||||
/>
|
||||
<QuestionFilterComboBox
|
||||
key={`${s.questionType.id}-${i}`}
|
||||
filterOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
)?.filterOptions
|
||||
}
|
||||
filterComboBoxOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
)?.filterComboBoxOptions
|
||||
}
|
||||
filterValue={filterValue.filter[i].filterType.filterValue}
|
||||
filterComboBoxValue={filterValue.filter[i].filterType.filterComboBoxValue}
|
||||
type={
|
||||
s?.questionType?.type === OptionsType.QUESTIONS
|
||||
? s?.questionType?.questionType
|
||||
: s?.questionType?.type
|
||||
}
|
||||
handleRemoveMultiSelect={(value) => handleRemoveMultiSelect(value, i)}
|
||||
onChangeFilterComboBoxValue={(value) => handleOnChangeFilterComboBoxValue(value, i)}
|
||||
onChangeFilterValue={(value) => handleOnChangeFilterValue(value, i)}
|
||||
disabled={!s?.questionType?.label}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end gap-1 md:w-auto">
|
||||
<p className="block font-light text-slate-500 md:hidden">Delete</p>
|
||||
<TrashIcon
|
||||
className="w-4 cursor-pointer text-slate-500 md:text-black"
|
||||
onClick={() => handleDeleteFilter(i)}
|
||||
/>
|
||||
</div>
|
||||
{filterValue.filter?.map((s, i) => (
|
||||
<>
|
||||
<div className="flex w-full flex-wrap gap-3 md:flex-nowrap">
|
||||
<div
|
||||
className="grid w-full grid-cols-1 items-center gap-3 md:grid-cols-2"
|
||||
key={`${s.questionType.id}-${i}`}>
|
||||
<QuestionsComboBox
|
||||
key={`${s.questionType.label}-${i}`}
|
||||
options={questionComboBoxOptions}
|
||||
selected={s.questionType}
|
||||
onChangeValue={(value) => handleOnChangeQuestionComboBoxValue(value, i)}
|
||||
/>
|
||||
<QuestionFilterComboBox
|
||||
key={`${s.questionType.id}-${i}`}
|
||||
filterOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
)?.filterOptions
|
||||
}
|
||||
filterComboBoxOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
)?.filterComboBoxOptions
|
||||
}
|
||||
filterValue={filterValue.filter[i].filterType.filterValue}
|
||||
filterComboBoxValue={filterValue.filter[i].filterType.filterComboBoxValue}
|
||||
type={
|
||||
s?.questionType?.type === OptionsType.QUESTIONS
|
||||
? s?.questionType?.questionType
|
||||
: s?.questionType?.type
|
||||
}
|
||||
handleRemoveMultiSelect={(value) => handleRemoveMultiSelect(value, i)}
|
||||
onChangeFilterComboBoxValue={(value) => handleOnChangeFilterComboBoxValue(value, i)}
|
||||
onChangeFilterValue={(value) => handleOnChangeFilterValue(value, i)}
|
||||
disabled={!s?.questionType?.label}
|
||||
/>
|
||||
</div>
|
||||
{i !== filterValue.filter.length - 1 && (
|
||||
<div className="my-6 flex items-center">
|
||||
<p className="mr-6 text-base text-slate-600">And</p>
|
||||
<hr className="w-full text-slate-600" />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end gap-1 md:w-auto">
|
||||
<p className="block font-light text-slate-500 md:hidden">Delete</p>
|
||||
<TrashIcon
|
||||
className="w-4 cursor-pointer text-slate-500 md:text-black"
|
||||
onClick={() => handleDeleteFilter(i)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{i !== filterValue.filter.length - 1 && (
|
||||
<div className="my-6 flex items-center">
|
||||
<p className="mr-6 text-base text-slate-600">And</p>
|
||||
<hr className="w-full text-slate-600" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Button size="sm" variant="secondary" onClick={handleAddNewFilter}>
|
||||
Add filter
|
||||
|
||||
@@ -71,40 +71,41 @@ export const SurveyCard = ({
|
||||
}, [survey.status, survey.id, environment.id]);
|
||||
|
||||
return (
|
||||
<Link href={linkHref} key={survey.id} className="relative block">
|
||||
<div className="grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 pr-8 shadow-sm transition-colors ease-in-out hover:border-slate-400">
|
||||
<div className="col-span-2 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
|
||||
<div className="w-full truncate">{survey.name}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"col-span-1 flex w-fit items-center gap-2 whitespace-nowrap rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
|
||||
surveyStatusLabel === "Scheduled" && "bg-slate-200",
|
||||
surveyStatusLabel === "In Progress" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Completed" && "bg-slate-200",
|
||||
surveyStatusLabel === "Draft" && "bg-slate-100",
|
||||
surveyStatusLabel === "Paused" && "bg-slate-100"
|
||||
)}>
|
||||
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.responseCount}
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-between">
|
||||
<SurveyTypeIndicator type={survey.type} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{convertDateString(survey.createdAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{timeSince(survey.updatedAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.creator ? survey.creator.name : "-"}
|
||||
</div>
|
||||
<Link
|
||||
href={linkHref}
|
||||
key={survey.id}
|
||||
className="relative grid w-full grid-cols-8 place-items-center gap-3 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out hover:scale-[101%]">
|
||||
<div className="col-span-1 flex max-w-full items-center justify-self-start text-sm font-medium text-slate-900">
|
||||
<div className="w-full truncate">{survey.name}</div>
|
||||
</div>
|
||||
<div className="absolute right-3 top-3.5">
|
||||
<div
|
||||
className={cn(
|
||||
"col-span-1 flex w-fit items-center gap-2 whitespace-nowrap rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
|
||||
surveyStatusLabel === "Scheduled" && "bg-slate-200",
|
||||
surveyStatusLabel === "In Progress" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Completed" && "bg-slate-200",
|
||||
surveyStatusLabel === "Draft" && "bg-slate-100",
|
||||
surveyStatusLabel === "Paused" && "bg-slate-100"
|
||||
)}>
|
||||
<SurveyStatusIndicator status={survey.status} /> {surveyStatusLabel}{" "}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.responseCount}
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-between">
|
||||
<SurveyTypeIndicator type={survey.type} />
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{convertDateString(survey.createdAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{timeSince(survey.updatedAt.toString())}
|
||||
</div>
|
||||
<div className="col-span-1 max-w-full overflow-hidden text-ellipsis whitespace-nowrap text-sm text-slate-600">
|
||||
{survey.creator ? survey.creator.name : "-"}
|
||||
</div>
|
||||
<div className="col-span-1 place-self-end">
|
||||
<SurveyDropDownMenu
|
||||
survey={survey}
|
||||
key={`surveys-${survey.id}`}
|
||||
|
||||
@@ -3,18 +3,11 @@
|
||||
import {
|
||||
copySurveyToOtherEnvironmentAction,
|
||||
deleteSurveyAction,
|
||||
getSurveyAction,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { getSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import {
|
||||
ArrowUpFromLineIcon,
|
||||
CopyIcon,
|
||||
EyeIcon,
|
||||
LinkIcon,
|
||||
MoreVertical,
|
||||
SquarePenIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { ArrowUpFromLineIcon, CopyIcon, EyeIcon, LinkIcon, SquarePenIcon, TrashIcon } from "lucide-react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -106,7 +99,7 @@ export const SurveyDropDownMenu = ({
|
||||
onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenu open={isDropDownOpen} onOpenChange={setIsDropDownOpen}>
|
||||
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
|
||||
<div className="rounded-lg border bg-white p-2 hover:bg-slate-50">
|
||||
<div className="rounded-lg border p-2 hover:bg-slate-50">
|
||||
<span className="sr-only">Open options</span>
|
||||
<MoreVertical className="h-4 w-4" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { getSurveysAction } from "@/app/(app)/environments/[environmentId]/surveys/actions";
|
||||
import { getFormattedFilters } from "@/app/(app)/environments/[environmentId]/surveys/lib/utils";
|
||||
import { TSurvey } from "@/app/(app)/environments/[environmentId]/surveys/types/surveys";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -50,7 +49,6 @@ export const SurveysList = ({
|
||||
const [isFilterInitialized, setIsFilterInitialized] = useState(false);
|
||||
|
||||
const filters = useMemo(() => getFormattedFilters(surveyFilters, userId), [surveyFilters, userId]);
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -138,9 +136,9 @@ export const SurveysList = ({
|
||||
/>
|
||||
{surveys.length > 0 ? (
|
||||
<div>
|
||||
<div className="flex-col space-y-3" ref={parent}>
|
||||
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 pr-8 text-sm text-slate-800">
|
||||
<div className="col-span-2 place-self-start">Name</div>
|
||||
<div className="flex-col space-y-3">
|
||||
<div className="mt-6 grid w-full grid-cols-8 place-items-center gap-3 px-6 text-sm text-slate-800">
|
||||
<div className="col-span-1 place-self-start">Name</div>
|
||||
<div className="col-span-1">Status</div>
|
||||
<div className="col-span-1">Responses</div>
|
||||
<div className="col-span-1">Type</div>
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
export const SurveyLoading = () => {
|
||||
return (
|
||||
<div className="grid h-full w-full animate-pulse place-content-stretch gap-4">
|
||||
<div className="grid h-full w-full animate-pulse grid-cols-2 place-content-stretch gap-4 lg:grid-cols-3 2xl:grid-cols-5">
|
||||
{[1, 2, 3, 4, 5].map((i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="relative flex h-16 flex-col justify-between rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out">
|
||||
className="relative col-span-1 flex h-44 flex-col justify-between rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition-all ease-in-out">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="h-4 w-32 rounded-xl bg-slate-400"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-200"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-200"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-200"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-200"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-200"></div>
|
||||
<div className="h-4 w-24 rounded-xl bg-slate-300"></div>
|
||||
<div className="h-8 w-8 rounded-md bg-slate-300"></div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="h-4 w-24 rounded-xl bg-slate-400"></div>
|
||||
<div className="h-4 w-20 rounded-xl bg-slate-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
import { OTPInput } from "@formbricks/ui/components/OTPInput";
|
||||
|
||||
export const TwoFactor = () => {
|
||||
const { control } = useFormContext();
|
||||
const { control, formState: { errors } } = useFormContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -18,7 +18,14 @@ export const TwoFactor = () => {
|
||||
control={control}
|
||||
name="totpCode"
|
||||
render={({ field }) => (
|
||||
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
|
||||
<>
|
||||
<OTPInput value={field.value ?? ""} onChange={field.onChange} valueLength={6} />
|
||||
{errors.totpCode && (
|
||||
<p className="mt-2 text-sm text-red-600" id="totpCode-error">
|
||||
{errors.totpCode.message}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { attributeCache } from "@formbricks/lib/attribute/cache";
|
||||
import { getAttributesByUserId } from "@formbricks/lib/attribute/service";
|
||||
import { cache } from "@formbricks/lib/cache";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { displayCache } from "@formbricks/lib/display/cache";
|
||||
@@ -70,6 +71,7 @@ export const getPersonState = async ({
|
||||
const personResponses = await getResponsesByUserId(environmentId, userId);
|
||||
const personDisplays = await getDisplaysByUserId(environmentId, userId);
|
||||
const segments = await getPersonSegmentIds(environmentId, person, device);
|
||||
const attributes = await getAttributesByUserId(environmentId, userId);
|
||||
|
||||
// If the person exists, return the persons's state
|
||||
const userState: TJsPersonState["data"] = {
|
||||
@@ -79,6 +81,7 @@ export const getPersonState = async ({
|
||||
personDisplays?.map((display) => ({ surveyId: display.surveyId, createdAt: display.createdAt })) ??
|
||||
[],
|
||||
responses: personResponses?.map((response) => response.surveyId) ?? [],
|
||||
attributes,
|
||||
lastDisplayAt:
|
||||
personDisplays.length > 0
|
||||
? personDisplays.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())[0].createdAt
|
||||
|
||||
@@ -101,7 +101,7 @@ export const InsightView = ({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isFetching ? null : insights.length === 0 ? (
|
||||
<TableRow className="pointer-events-none">
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-8 text-center">
|
||||
<p className="text-slate-500">
|
||||
No insights found. Collect more survey responses or enable insights for your existing
|
||||
@@ -110,7 +110,7 @@ export const InsightView = ({
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : localInsights.length === 0 ? (
|
||||
<TableRow className="pointer-events-none">
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-8 text-center">
|
||||
<p className="text-slate-500">No insights found for this filter.</p>
|
||||
</TableCell>
|
||||
@@ -119,7 +119,7 @@ export const InsightView = ({
|
||||
localInsights.slice(0, visibleInsights).map((insight) => (
|
||||
<TableRow
|
||||
key={insight.id}
|
||||
className="group cursor-pointer hover:bg-slate-50"
|
||||
className="cursor-pointer hover:bg-slate-50"
|
||||
onClick={() => {
|
||||
setCurrentInsight(insight);
|
||||
setIsInsightSheetOpen(true);
|
||||
@@ -128,9 +128,7 @@ export const InsightView = ({
|
||||
{insight._count.documentInsights} <UserIcon className="ml-2 h-4 w-4" />
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{insight.title}</TableCell>
|
||||
<TableCell className="underline-offset-2 group-hover:underline">
|
||||
{insight.description}
|
||||
</TableCell>
|
||||
<TableCell>{insight.description}</TableCell>
|
||||
<TableCell>
|
||||
{insight.category === "complaint" ? (
|
||||
<Badge text="Complaint" type="error" size="tiny" />
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useState } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@formbricks/ui/components/Tabs";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@formbricks/ui/components/ToggleGroup";
|
||||
|
||||
interface DashboardProps {
|
||||
user: TUser;
|
||||
@@ -32,29 +32,26 @@ export const Dashboard = ({
|
||||
return (
|
||||
<div className="container mx-auto space-y-6 p-4">
|
||||
<Greeting userName={user.name} />
|
||||
<hr className="border-slate-200" />
|
||||
<Tabs
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={statsPeriod}
|
||||
onValueChange={(value) => value && setStatsPeriod(value as TStatsPeriod)}
|
||||
className="flex justify-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="day" aria-label="Toggle day">
|
||||
Today
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="week" aria-label="Toggle week">
|
||||
This week
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="month" aria-label="Toggle month">
|
||||
This month
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="quarter" aria-label="Toggle quarter">
|
||||
This quarter
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="all" aria-label="Toggle all">
|
||||
All time
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
onValueChange={(value) => value && setStatsPeriod(value as TStatsPeriod)}>
|
||||
<ToggleGroupItem value="day" aria-label="Toggle day">
|
||||
Today
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="week" aria-label="Toggle week">
|
||||
This week
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="month" aria-label="Toggle month">
|
||||
This month
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="quarter" aria-label="Toggle quarter">
|
||||
This quarter
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="all" aria-label="Toggle all">
|
||||
All time
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<ExperiencePageStats statsFrom={statsFrom} environmentId={environment.id} />
|
||||
<InsightsCard
|
||||
statsFrom={statsFrom}
|
||||
|
||||
@@ -134,7 +134,7 @@ export const InsightView = ({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{insights.length === 0 && !isFetching ? (
|
||||
<TableRow className="pointer-events-none">
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="py-8 text-center">
|
||||
<p className="text-slate-500">
|
||||
No insights found. Collect more survey responses or enable insights for your existing
|
||||
@@ -146,7 +146,7 @@ export const InsightView = ({
|
||||
insights.map((insight) => (
|
||||
<TableRow
|
||||
key={insight.id}
|
||||
className="group cursor-pointer hover:bg-slate-50"
|
||||
className="cursor-pointer hover:bg-slate-50"
|
||||
onClick={() => {
|
||||
setCurrentInsight(insight);
|
||||
setIsInsightSheetOpen(true);
|
||||
@@ -155,9 +155,7 @@ export const InsightView = ({
|
||||
{insight._count.documentInsights} <UserIcon className="ml-2 h-4 w-4" />
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{insight.title}</TableCell>
|
||||
<TableCell className="underline-offset-2 group-hover:underline">
|
||||
{insight.description}
|
||||
</TableCell>
|
||||
<TableCell>{insight.description}</TableCell>
|
||||
<TableCell>
|
||||
{insight.category === "complaint" ? (
|
||||
<Badge text="Complaint" type="error" size="tiny" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB |
@@ -1,12 +0,0 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 15.5.36
|
||||
- name: redis
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 20.1.5
|
||||
- name: traefik
|
||||
repository: https://helm.traefik.io/traefik
|
||||
version: 32.0.0
|
||||
digest: sha256:21923a92e214351c3f96348fe0c479cc6e98e7828d75d41edc1ab73839dd39ce
|
||||
generated: "2024-09-27T13:48:24.815107+03:00"
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: v2
|
||||
name: formbricks
|
||||
description: A Helm chart for Formbricks with PostgreSQL, Redis, Traefik, and cert-manager
|
||||
type: application
|
||||
version: 0.1.1
|
||||
appVersion: "1.0.0"
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 15.5.36
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: postgresql.enabled
|
||||
- name: redis
|
||||
version: 20.1.5
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: redis.enabled
|
||||
- name: traefik
|
||||
version: 32.0.0
|
||||
repository: https://helm.traefik.io/traefik
|
||||
condition: traefik.enabled
|
||||
@@ -1,653 +0,0 @@
|
||||
<div id="top"></div>
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://formbricks.com">
|
||||
|
||||
<img width="120" alt="Open Source Privacy First Experience Management Solution Qualtrics Alternative Logo" src="https://github.com/formbricks/formbricks/assets/72809645/0086704f-bee7-4d38-9cc8-fa42ee59e004">
|
||||
|
||||
</a>
|
||||
|
||||
<h3 align="center">Formbricks</h3>
|
||||
|
||||
<p align="center">
|
||||
Harvest user-insights, build irresistible experiences.
|
||||
<br />
|
||||
<a href="https://formbricks.com/">Website</a> | <a href="https://formbricks.com/discord">Join Discord community</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
# Formbricks Helm Chart: Comprehensive Documentation
|
||||
|
||||
1. [Introduction](#introduction)
|
||||
2. [Prerequisites](#prerequisites)
|
||||
3. [Chart Components](#chart-components)
|
||||
4. [Installation](#installation)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Usage Examples](#usage-examples)
|
||||
5. [Configuration](#configuration)
|
||||
6. [Environment Variables](#environment-variables)
|
||||
7. [Scaling](#scaling)
|
||||
8. [Upgrading Formbricks](#upgrading-formbricks)
|
||||
9. [Support](#support)
|
||||
10. [Full Values Documentation](#full-values-documentation)
|
||||
11. [Contribution](#contribution)
|
||||
12. [MicroK8s Installation and Formbricks Deployment](#microk8s-installation-and-formbricks-deployment)
|
||||
- [MicroK8s Quick Setup](#microk8s-quick-setup)
|
||||
- [Deploying Formbricks on MicroK8s](#deploying-formbricks-on-microk8s)
|
||||
|
||||
## Introduction
|
||||
|
||||
This Helm chart deploys Formbricks, an advanced open-source form builder and survey tool, along with its required dependencies (PostgreSQL and Redis) on a Kubernetes cluster. It also includes an optional Traefik ingress controller for easy access and SSL termination.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before installing the Formbricks Helm chart, ensure you have the following:
|
||||
|
||||
- Kubernetes cluster version 1.24 or later
|
||||
- Helm version 3.2.0 or later
|
||||
- Dynamic volume provisioning support in the underlying infrastructure (for PostgreSQL persistence)
|
||||
- Familiarity with Kubernetes concepts and Helm charts
|
||||
|
||||
## Chart Components
|
||||
|
||||
This Helm chart deploys the following components:
|
||||
|
||||
1. **Formbricks Application**: The core Formbricks service.
|
||||
2. **PostgreSQL Database**: (Optional) A relational database for Formbricks data.
|
||||
3. **Redis**: (Optional) An in-memory data structure store for caching.
|
||||
4. **Traefik Ingress Controller**: (Optional) A modern HTTP reverse proxy and load balancer.
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick Start
|
||||
|
||||
To quickly deploy Formbricks with default settings:
|
||||
|
||||
1. Add the Formbricks Helm repository:
|
||||
```bash
|
||||
helm repo add formbricks https://charts.formbricks.com
|
||||
helm repo update
|
||||
```
|
||||
|
||||
2. Install the chart:
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks --namespace formbricks --create-namespace --set postgresql.enabled=true
|
||||
```
|
||||
|
||||
This will deploy Formbricks with default settings, including a new PostgreSQL instance, Redis and Traefik disabled.
|
||||
|
||||
### Usage Examples
|
||||
|
||||
Here are various examples of how to install and configure the Formbricks Helm chart:
|
||||
|
||||
1. **Default Installation with Traefik enabled and Custom Hostname**:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Option 1: Installation without SSL (Not recommended for production)</summary>
|
||||
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set traefik.enabled=true \
|
||||
--set hostname=forms.example.com
|
||||
```
|
||||
|
||||
This command enables Traefik and sets a custom hostname. Replace `forms.example.com` with your actual domain name.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Option 2: Installation with SSL (Recommended for production)</summary>
|
||||
|
||||
1. First, download the values file:
|
||||
|
||||
```bash
|
||||
helm show values formbricks/formbricks > values.yaml
|
||||
```
|
||||
|
||||
2. Open the `values.yaml` file in a text editor and make the following changes:
|
||||
|
||||
```yaml
|
||||
traefik:
|
||||
enabled: true
|
||||
additionalArguments:
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
|
||||
```
|
||||
|
||||
Replace `your-email@example.com` with a valid email address where you want to receive Let's Encrypt notifications.
|
||||
|
||||
3. Install Formbricks with the updated values file:
|
||||
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
-f values.yaml \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set hostname=forms.example.com
|
||||
```
|
||||
|
||||
This command enables Traefik, sets a custom hostname, and uses the configured email address for Let's Encrypt. Remember to replace `forms.example.com` with your actual domain name.
|
||||
|
||||
</details>
|
||||
|
||||
These installation options provide flexibility in setting up Formbricks with Traefik. The SSL option is recommended for production environments to ensure secure communications.
|
||||
|
||||
2. **Community Advanced:**
|
||||
Provision a whole community setup with Formbricks, Postgres, Custom Domain with SSL
|
||||
|
||||
```bash
|
||||
helm install formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set postgres.enabled=true \
|
||||
--set traefik.enabled=true \
|
||||
--set hostname=forms.example.com \
|
||||
--set email=your-mail@example.com
|
||||
```
|
||||
|
||||
3. **Cluster Advanced:**
|
||||
Provision a ready to use cluster for enterprise customers with Formbricks (3 pods), Postgres, Redis and Custom Domain with SSL
|
||||
|
||||
```bash
|
||||
helm install formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set replicaCount=3
|
||||
--set redis.enabled=true \
|
||||
--set traefik.enabled=true \
|
||||
--set hostname=forms.example.com \
|
||||
--set email=your-mail@example.com
|
||||
```
|
||||
|
||||
4. **Installation with Redis and PostgreSQL Disabled, Using External Services**:
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set postgresql.enabled=false \
|
||||
--set postgresql.externalUrl=postgresql://user:password@your-postgres-url:5432/dbname \
|
||||
--set redis.enabled=false \
|
||||
--set redis.externalUrl=redis://your-redis-url:6379
|
||||
```
|
||||
|
||||
5. **High Availability Setup**:
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set replicaCount=3
|
||||
```
|
||||
|
||||
This command:
|
||||
1. Deploys the Formbricks application with 3 replicas.
|
||||
2. Enables PostgreSQL and Redis with default settings.
|
||||
|
||||
#### Scaling PostgreSQL and Redis
|
||||
|
||||
For advanced configuration and scaling of PostgreSQL and Redis, refer to their respective Helm chart documentation:
|
||||
|
||||
- PostgreSQL Helm Chart: https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||
- Redis Helm Chart: https://github.com/bitnami/charts/tree/master/bitnami/redis
|
||||
|
||||
These documents provide detailed information on scaling and configuring high availability for each component.
|
||||
|
||||
4. **Custom Configuration with Environment Variables**:
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set env.SMTP_HOST=smtp.example.com \
|
||||
--set env.SMTP_PORT=587 \
|
||||
--set env.SMTP_USER=user@example.com \
|
||||
--set env.SMTP_PASSWORD=password123
|
||||
```
|
||||
|
||||
5. **Installation with Custom Resource Limits**:
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--namespace formbricks \
|
||||
--create-namespace \
|
||||
--set resources.limits.cpu=1 \
|
||||
--set resources.limits.memory=1Gi \
|
||||
--set resources.requests.cpu=500m \
|
||||
--set resources.requests.memory=512Mi
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
For detailed configuration options, please refer to the [Full Values Documentation](#full-values-documentation) section at the end of this document.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Formbricks supports various environment variables for configuration. Here are some key variables:
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
|----------|-------------|----------|---------|
|
||||
| `WEBAPP_URL` | Base URL of the site | Yes | `http://localhost:3000` |
|
||||
| `NEXTAUTH_URL` | Location of the auth server | Yes | `http://localhost:3000` |
|
||||
| `DATABASE_URL` | Database URL with credentials | Yes | - |
|
||||
| `NEXTAUTH_SECRET` | Secret for NextAuth | Yes | (Generated) |
|
||||
| `ENCRYPTION_KEY` | Secret for data encryption | Yes | (Generated) |
|
||||
| `CRON_SECRET` | API Secret for running cron jobs | Yes | (Generated) |
|
||||
| `...` | ... | ... | ... |
|
||||
|
||||
For a comprehensive list of supported environment variables, refer to the [Formbricks Configuration Documentation](https://formbricks.com/docs/self-hosting/configuration).
|
||||
|
||||
|
||||
## Scaling
|
||||
|
||||
```bash
|
||||
kubectl scale deployment my-formbricks --replicas=5 -n formbricks
|
||||
```
|
||||
This command scales the Formbricks deployment to 5 replicas. Replace `my-formbricks` with your actual deployment name if different.
|
||||
|
||||
### With Auto Scaling (Kubernetes Metrics Server Requirement)
|
||||
The Formbricks Helm chart includes support for Horizontal Pod Autoscaling (HPA) to automatically adjust the number of pods based on CPU utilization. This feature is enabled by default and can be customized to suit your specific needs.
|
||||
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks --namespace formbricks --create-namespace \
|
||||
--set autoscaling.enabled=true
|
||||
```
|
||||
This configuration sets up autoscaling with a minimum of 2 replicas and a maximum of 5 replicas, targeting an average CPU utilization of 80%
|
||||
|
||||
### Customizing Autoscaling
|
||||
|
||||
To adjust the autoscaling settings, you can modify the values in your `values.yaml` file or use the `--set` flag when installing or upgrading the chart. Here are some common customizations:
|
||||
|
||||
1. Change the minimum and maximum number of replicas:
|
||||
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.minReplicas=3 \
|
||||
--set autoscaling.maxReplicas=10
|
||||
```
|
||||
|
||||
2. Adjust the target CPU utilization:
|
||||
|
||||
```bash
|
||||
helm install my-formbricks formbricks/formbricks \
|
||||
--set autoscaling.enabled=true \
|
||||
--set autoscaling.metrics[0].resource.target.averageUtilization=70
|
||||
```
|
||||
|
||||
3. Disable autoscaling:
|
||||
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks \
|
||||
--set autoscaling.enabled=false
|
||||
```
|
||||
|
||||
### Kubernetes Metrics Server Requirement
|
||||
|
||||
For autoscaling to function properly, the Kubernetes Metrics Server must be installed in your cluster. The Metrics Server collects resource metrics from Kubelets and exposes them in the Kubernetes API server through the Metrics API.
|
||||
|
||||
If you don't have the Metrics Server installed, you can typically add it using the following command:
|
||||
|
||||
```bash
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
|
||||
```
|
||||
|
||||
For more detailed information on installing and configuring the Metrics Server, please refer to the [official Kubernetes Metrics Server documentation](https://github.com/kubernetes-sigs/metrics-server).
|
||||
|
||||
### Advanced Autoscaling Configuration
|
||||
|
||||
The Formbricks Helm chart uses Kubernetes HPA v2, which allows for more advanced scaling behaviors. You can customize the `behavior` section in the `values.yaml` file to fine-tune how your application scales up and down. For more information on advanced HPA configurations, refer to the [Kubernetes HPA documentation](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/).
|
||||
|
||||
|
||||
## Upgrading Formbricks
|
||||
|
||||
This section provides guidance on how to upgrade your Formbricks deployment using Helm, including examples of common upgrade scenarios.
|
||||
|
||||
### Upgrade Process
|
||||
|
||||
To upgrade your Formbricks deployment, use the `helm upgrade` command. Always ensure you have the latest version of the Formbricks Helm chart by running `helm repo update` before upgrading.
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm upgrade my-formbricks formbricks/formbricks --namespace formbricks
|
||||
```
|
||||
|
||||
### Common Upgrade Scenarios
|
||||
|
||||
#### 1. Updating Environment Variables
|
||||
|
||||
To update or add new environment variables, use the `--set` flag with the `env` prefix:
|
||||
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks \
|
||||
--set env.SMTP_HOST=new-smtp.example.com \
|
||||
--set env.SMTP_PORT=587 \
|
||||
--set env.NEW_CUSTOM_VAR=newvalue
|
||||
```
|
||||
|
||||
This command updates the SMTP host and port, and adds a new custom environment variable.
|
||||
|
||||
#### 2. Enabling or Disabling Features
|
||||
|
||||
You can enable or disable features by updating their respective values:
|
||||
|
||||
```bash
|
||||
# Disable Redis
|
||||
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=false
|
||||
|
||||
# Enable Redis
|
||||
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=true
|
||||
```
|
||||
|
||||
#### 3. Scaling Resources
|
||||
|
||||
To adjust resource allocation:
|
||||
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks \
|
||||
--set resources.limits.cpu=1 \
|
||||
--set resources.limits.memory=2Gi \
|
||||
--set resources.requests.cpu=500m \
|
||||
--set resources.requests.memory=1Gi
|
||||
```
|
||||
|
||||
#### 4. Updating Autoscaling Configuration
|
||||
|
||||
To modify autoscaling settings:
|
||||
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks \
|
||||
--set autoscaling.minReplicas=3 \
|
||||
--set autoscaling.maxReplicas=10 \
|
||||
--set autoscaling.metrics[0].resource.target.averageUtilization=75
|
||||
```
|
||||
|
||||
#### 5. Changing Database Credentials
|
||||
|
||||
To update PostgreSQL database credentials:
|
||||
To switch from the built-in PostgreSQL to an external database or update the external database credentials:
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks \
|
||||
--set postgresql.enabled=false \
|
||||
--set postgresql.externalUrl="postgresql://newuser:newpassword@external-postgres-host:5432/newdatabase"
|
||||
```
|
||||
|
||||
This command disables the built-in PostgreSQL and configures Formbricks to use an external PostgreSQL database. Make sure your external database is set up and accessible before making this change.
|
||||
|
||||
### Using a Values File for Complex Upgrades
|
||||
|
||||
For more complex upgrades or when you need to change multiple values, it's recommended to use a values file:
|
||||
|
||||
1. Create a file named `upgrade-values.yaml` with your desired changes:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
SMTP_HOST: new-smtp.example.com
|
||||
SMTP_PORT: "587"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 2Gi
|
||||
autoscaling:
|
||||
minReplicas: 3
|
||||
maxReplicas: 10
|
||||
traefik:
|
||||
enabled: true
|
||||
postgresql:
|
||||
auth:
|
||||
username: newuser
|
||||
password: newpassword
|
||||
database: newdatabase
|
||||
```
|
||||
|
||||
2. Apply the upgrade using the values file:
|
||||
|
||||
```bash
|
||||
helm upgrade my-formbricks formbricks/formbricks -f upgrade-values.yaml
|
||||
```
|
||||
|
||||
Remember to always backup your data before performing upgrades, especially when modifying database-related settings.
|
||||
|
||||
## Support
|
||||
|
||||
For support with the Formbricks Helm chart:
|
||||
- Open an issue on the [Formbricks GitHub repository](https://github.com/formbricks/formbricks)
|
||||
- Join our [community Discord](https://discord.com/invite/3YFcABF2Ts)
|
||||
- For enterprise support, contact us at hola@formbricks.com
|
||||
|
||||
|
||||
## Full Values Documentation
|
||||
|
||||
Below is a comprehensive list of all configurable values in the Formbricks Helm chart:
|
||||
|
||||
| Field | Description | Default |
|
||||
|-------|-------------|---------|
|
||||
| `image.repository` | Docker image repository for Formbricks | `ghcr.io/formbricks/formbricks` |
|
||||
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
|
||||
| `image.tag` | Docker image tag | `"2.6.0"` |
|
||||
| `service.type` | Kubernetes service type | `ClusterIP` |
|
||||
| `service.port` | Kubernetes service port | `80` |
|
||||
| `service.targetPort` | Container port to expose | `3000` |
|
||||
| `resources.limits.cpu` | CPU resource limit | `500m` |
|
||||
| `resources.limits.memory` | Memory resource limit | `1Gi` |
|
||||
| `resources.requests.cpu` | Memory resource request | `null` |
|
||||
| `resources.requests.memory` | Memory resource request | `null` |
|
||||
| `autoscaling.enabled` | Enable autoscaling | `false` |
|
||||
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
|
||||
| `autoscaling.maxReplicas` | Maximum number of replicas | `5` |
|
||||
| `autoscaling.metrics[0].type` | Type of metric for autoscaling | `Resource` |
|
||||
| `autoscaling.metrics[0].resource.name` | Resource name for autoscaling metric | `cpu` |
|
||||
| `autoscaling.metrics[0].resource.target.type` | Target type for autoscaling | `Utilization` |
|
||||
| `autoscaling.metrics[0].resource.target.averageUtilization` | Average utilization target for autoscaling | `80` |
|
||||
| `autoscaling.behavior.scaleDown.stabilizationWindowSeconds` | Stabilization window for scaling down | `300` |
|
||||
| `autoscaling.behavior.scaleUp.stabilizationWindowSeconds` | Stabilization window for scaling up | `0` |
|
||||
| `replicaCount` | Number of replicas | `1` |
|
||||
| `formbricksConfig.nextAuthSecret` | NextAuth secret | `""` |
|
||||
| `formbricksConfig.encryptionKey` | Encryption key | `""` |
|
||||
| `formbricksConfig.cronSecret` | Cron secret | `""` |
|
||||
| `env` | Additional environment variables | `{}` |
|
||||
| `hostname` | Hostname for Formbricks | `""` |
|
||||
| `traefik.enabled` | Enable Traefik ingress | `false` |
|
||||
| `traefik.ingressRoute.dashboard.enabled` | Enable Traefik dashboard | `false` |
|
||||
| `traefik.additionalArguments` | Additional arguments for Traefik | [See values.yaml] |
|
||||
| `traefik.tls.enabled` | Enable TLS for Traefik | `true` |
|
||||
| `traefik.tls.certResolver` | Cert resolver for Traefik | `letsencrypt` |
|
||||
| `traefik.ports.web.port` | HTTP port for Traefik | `80` |
|
||||
| `traefik.ports.websecure.port` | HTTPS port for Traefik | `443` |
|
||||
| `traefik.persistence.enabled` | Enable persistence for Traefik | `true` |
|
||||
| `traefik.persistence.size` | Size of persistent volume for Traefik | `128Mi` |
|
||||
| `traefik.podSecurityContext.fsGroup` | fsGroup for Traefik pods | `0` |
|
||||
| `traefik.hostNetwork` | Use host network for Traefik | `true` |
|
||||
| `traefik.securityContext` | Security context for Traefik | [See values.yaml] |
|
||||
| `redis.enabled` | Enable Redis | `true` |
|
||||
| `redis.externalUrl` | External Redis URL | `""` |
|
||||
| `redis.architecture` | Redis architecture | `standalone` |
|
||||
| `redis.auth.enabled` | Enable Redis authentication | `true` |
|
||||
| `redis.auth.password` | Redis password | `redispassword` |
|
||||
| `redis.master.persistence.enabled` | Enable persistence for Redis master | `false` |
|
||||
| `redis.replica.replicaCount` | Number of Redis replicas | `0` |
|
||||
| `postgresql.enabled` | Enable PostgreSQL | `true` |
|
||||
| `postgresql.externalUrl` | External PostgreSQL URL | `""` |
|
||||
| `postgresql.auth.username` | PostgreSQL username | `formbricks` |
|
||||
| `postgresql.auth.password` | PostgreSQL password | `formbrickspassword` |
|
||||
| `postgresql.auth.database` | PostgreSQL database name | `formbricks` |
|
||||
| `postgresql.primary.persistence.enabled` | Enable persistence for PostgreSQL | `true` |
|
||||
| `postgresql.primary.persistence.size` | Size of persistent volume for PostgreSQL | `10Gi` |
|
||||
|
||||
This table provides a comprehensive overview of all configurable fields in the Formbricks Helm chart, along with their descriptions and default values. Users can refer to this table to understand what each field does and how they can customize their Formbricks deployment.
|
||||
|
||||
## Full Values Documentation
|
||||
|
||||
Below is a comprehensive list of all configurable values in the Formbricks Helm chart:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: ghcr.io/formbricks/formbricks
|
||||
pullPolicy: IfNotPresent
|
||||
tag: "2.6.0"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 2
|
||||
maxReplicas: 5
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 15
|
||||
selectPolicy: Max
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
formbricksConfig:
|
||||
nextAuthSecret: ""
|
||||
encryptionKey: ""
|
||||
cronSecret: ""
|
||||
|
||||
env: {}
|
||||
|
||||
hostname: ""
|
||||
|
||||
traefik:
|
||||
enabled: false
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: false
|
||||
additionalArguments:
|
||||
- "--providers.file.filename=/config/traefik.toml"
|
||||
tls:
|
||||
enabled: true
|
||||
certResolver: letsencrypt
|
||||
ports:
|
||||
web:
|
||||
port: 80
|
||||
websecure:
|
||||
port: 443
|
||||
tls:
|
||||
enabled: true
|
||||
certResolver: letsencrypt
|
||||
persistence:
|
||||
enabled: true
|
||||
name: traefik-acme
|
||||
accessMode: ReadWriteOnce
|
||||
size: 128Mi
|
||||
path: /data
|
||||
podSecurityContext:
|
||||
fsGroup: 0
|
||||
hostNetwork: true
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- NET_BIND_SERVICE
|
||||
- NET_BROADCAST
|
||||
- NET_RAW
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
runAsNonRoot: false
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
externalUrl: ""
|
||||
architecture: standalone
|
||||
auth:
|
||||
enabled: true
|
||||
password: redispassword
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
replica:
|
||||
replicaCount: 0
|
||||
|
||||
postgresql:
|
||||
enabled: false
|
||||
externalUrl: ""
|
||||
auth:
|
||||
username: formbricks
|
||||
password: formbrickspassword
|
||||
database: formbricks
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
```
|
||||
|
||||
You can customize these values by creating a `values.yaml` file or by using the `--set` flag when running `helm install` or `helm upgrade`.
|
||||
|
||||
## ✍️ Contribution
|
||||
|
||||
We are very happy if you are interested in contributing to Formbricks 🤗
|
||||
|
||||
Here are a few options:
|
||||
|
||||
- Star this repo.
|
||||
|
||||
- Create issues every time you feel something is missing or goes wrong.
|
||||
|
||||
- Upvote issues with 👍 reaction so we know what the demand for a particular issue is to prioritize it within the roadmap.
|
||||
|
||||
Please check out [our contribution guide](https://formbricks.com/docs/developer-docs/contributing/get-started) and our [list of open issues](https://github.com/formbricks/formbricks/issues) for more information.
|
||||
|
||||
|
||||
|
||||
## MicroK8s Installation and Formbricks Deployment
|
||||
|
||||
### MicroK8s Quick Setup
|
||||
|
||||
1. Install MicroK8s:
|
||||
```bash
|
||||
sudo snap install microk8s --classic
|
||||
```
|
||||
|
||||
2. Enable necessary add-ons:
|
||||
```bash
|
||||
microk8s enable dns storage ingress helm3
|
||||
```
|
||||
|
||||
### Deploying Formbricks on MicroK8s
|
||||
|
||||
1. Add the Formbricks Helm repository:
|
||||
```bash
|
||||
microk8s helm3 repo add formbricks https://charts.formbricks.com
|
||||
microk8s helm3 repo update
|
||||
```
|
||||
|
||||
2. Install Formbricks:
|
||||
```bash
|
||||
microk8s helm3 install my-formbricks formbricks/formbricks --namespace formbricks --create-namespace
|
||||
```
|
||||
|
||||
For more detailed information on MicroK8s, including advanced configuration and usage, please refer to the [official MicroK8s documentation](https://microk8s.io/docs).
|
||||
|
||||
For Formbricks Helm chart configuration options, see the [Configuration](#configuration) and [Full Values Documentation](#full-values-documentation) sections of this document.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,97 +0,0 @@
|
||||
NOTES:
|
||||
Thank you for installing Formbricks! Here's how you can access and manage your deployment:
|
||||
|
||||
1. Accessing Your Application:
|
||||
{{- if .Values.traefik.enabled }}
|
||||
Traefik is enabled for ingress routing.
|
||||
Your application should be available at: https://{{ .Values.hostname }}
|
||||
Note: Ensure your DNS is properly configured to point to your cluster's IP.
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
Your application is running inside the cluster with ClusterIP service type.
|
||||
To access it locally, run the following command:
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "formbricks.fullname" . }} 8080:{{ .Values.service.port }}
|
||||
Then visit http://localhost:8080 in your browser.
|
||||
{{- end }}
|
||||
|
||||
2. Database (PostgreSQL) Access:
|
||||
{{- if .Values.postgresql.enabled }}
|
||||
PostgreSQL is deployed within your cluster.
|
||||
To get the PostgreSQL password, run:
|
||||
kubectl get secret --namespace {{ .Release.Namespace }} {{ .Release.Name }}-postgresql -o jsonpath="{.data.postgres-password}" | base64 --decode
|
||||
|
||||
Database connection details:
|
||||
- Host: {{ .Release.Name }}-postgresql
|
||||
- Port: 5432
|
||||
- Database: {{ .Values.postgresql.auth.database }}
|
||||
- Username: {{ .Values.postgresql.auth.username }}
|
||||
{{- else if .Values.postgresql.externalUrl }}
|
||||
You're using an external PostgreSQL database.
|
||||
Connection URL: {{ .Values.postgresql.externalUrl }}
|
||||
{{- end }}
|
||||
|
||||
3. Redis Access:
|
||||
{{- if .Values.redis.enabled }}
|
||||
Redis is deployed within your cluster.
|
||||
To get the Redis password, run:
|
||||
kubectl get secret --namespace {{ .Release.Namespace }} {{ .Release.Name }}-redis -o jsonpath="{.data.redis-password}" | base64 --decode
|
||||
|
||||
Redis connection details:
|
||||
- Host: {{ .Release.Name }}-redis-master
|
||||
- Port: 6379
|
||||
{{- else if .Values.redis.externalUrl }}
|
||||
You're using an external Redis instance.
|
||||
Connection URL: {{ .Values.redis.externalUrl }}
|
||||
{{- else }}
|
||||
Redis is not enabled in your current configuration.
|
||||
{{- end }}
|
||||
|
||||
4. Environment Variables:
|
||||
The following environment variables have been automatically generated:
|
||||
- NEXTAUTH_SECRET: A random 32-character string
|
||||
- ENCRYPTION_KEY: A random 32-character string
|
||||
- CRON_SECRET: A random 32-character string
|
||||
|
||||
To view these secrets, run:
|
||||
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.NEXTAUTH_SECRET}" | base64 --decode
|
||||
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.ENCRYPTION_KEY}" | base64 --decode
|
||||
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}-secrets -o jsonpath="{.data.CRON_SECRET}" | base64 --decode
|
||||
|
||||
5. Scaling:
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
Horizontal Pod Autoscaling is enabled.
|
||||
- Minimum replicas: {{ .Values.autoscaling.minReplicas }}
|
||||
- Maximum replicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
- Target CPU utilization: {{ .Values.autoscaling.metrics.averageUtilization }}%
|
||||
|
||||
To check the current status of the HPA, run:
|
||||
kubectl get hpa -n {{ .Release.Namespace }} {{ include "formbricks.fullname" . }}
|
||||
{{- else }}
|
||||
Horizontal Pod Autoscaling is not enabled. Your deployment has a fixed number of {{ .Values.replicaCount }} replicas.
|
||||
To scale manually, use:
|
||||
kubectl scale deployment -n {{ .Release.Namespace }} {{ include "formbricks.fullname" . }} --replicas=<desired_number>
|
||||
{{- end }}
|
||||
|
||||
6. Persistence:
|
||||
- PostgreSQL data is persisted with a {{ .Values.postgresql.primary.persistence.size }} storage.
|
||||
{{- if .Values.redis.enabled }}
|
||||
- Redis data is not persisted (persistence is disabled).
|
||||
{{- end }}
|
||||
|
||||
7. Traefik Configuration:
|
||||
{{- if .Values.traefik.enabled }}
|
||||
Traefik is configured with the following settings:
|
||||
- TLS enabled with Let's Encrypt
|
||||
- HTTP to HTTPS redirect enabled
|
||||
- ACME challenge type: HTTP
|
||||
- Entrypoints: web (80) and websecure (443)
|
||||
{{- else }}
|
||||
Traefik is not enabled in your current configuration.
|
||||
{{- end }}
|
||||
|
||||
8. Formbricks Documentation and Support:
|
||||
For more information, advanced configuration options, and support, please visit:
|
||||
- Official Formbricks website: https://formbricks.com
|
||||
- Documentation: https://formbricks.com/docs
|
||||
|
||||
If you encounter any issues or have questions, please refer to the Formbricks documentation
|
||||
or reach out to the Formbricks community for support.
|
||||
@@ -1,51 +0,0 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "formbricks.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "formbricks.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "formbricks.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "formbricks.labels" -}}
|
||||
helm.sh/chart: {{ include "formbricks.chart" . }}
|
||||
{{ include "formbricks.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "formbricks.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "formbricks.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
@@ -1,34 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "formbricks.fullname" . }}
|
||||
labels:
|
||||
{{- include "formbricks.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "formbricks.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "formbricks.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: {{ include "formbricks.fullname" . }}-secrets
|
||||
env:
|
||||
{{- range $key, $value := .Values.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $value | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
@@ -1,39 +0,0 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "formbricks.fullname" . }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "formbricks.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 15
|
||||
selectPolicy: Max
|
||||
{{- end }}
|
||||
@@ -1,23 +0,0 @@
|
||||
{{- if .Values.traefik.enabled -}}
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: {{ include "formbricks.fullname" . }}
|
||||
labels:
|
||||
{{- include "formbricks.labels" . | nindent 4 }}
|
||||
{{- with .Values.traefik.ingressRoute.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`{{ .Values.hostname }}`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: {{ include "formbricks.fullname" . }}
|
||||
port: {{ .Values.service.port }}
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
{{- end }}
|
||||
@@ -1,19 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "formbricks.fullname" . }}-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- if .Values.postgresql.externalUrl }}
|
||||
DATABASE_URL: {{ .Values.postgresql.externalUrl }}
|
||||
{{- else if .Values.postgresql.enabled }}
|
||||
DATABASE_URL: postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }}
|
||||
{{- end }}
|
||||
{{- if .Values.redis.externalUrl }}
|
||||
REDIS_URL: {{ .Values.redis.externalUrl }}
|
||||
{{- else if .Values.redis.enabled }}
|
||||
REDIS_URL: redis://:{{ .Values.redis.auth.password }}@{{ .Release.Name }}-redis-master:6379
|
||||
{{- end }}
|
||||
NEXTAUTH_SECRET: {{ .Values.formbricksConfig.nextAuthSecret | default (randAlphaNum 32) | quote }}
|
||||
ENCRYPTION_KEY: {{ .Values.formbricksConfig.encryptionKey | default (randAlphaNum 32) | quote }}
|
||||
CRON_SECRET: {{ .Values.formbricksConfig.cronSecret | default (randAlphaNum 32) | quote }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "formbricks.fullname" . }}
|
||||
labels:
|
||||
{{- include "formbricks.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.targetPort | default 3000 }}
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "formbricks.selectorLabels" . | nindent 4 }}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "formbricks.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "formbricks.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "formbricks.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: traefik-config
|
||||
data:
|
||||
traefik.toml: |
|
||||
[certificatesResolvers.letsencrypt.acme]
|
||||
email = {{ .Values.email }}
|
||||
@@ -1,136 +0,0 @@
|
||||
image:
|
||||
repository: ghcr.io/formbricks/formbricks
|
||||
pullPolicy: IfNotPresent
|
||||
tag: v2.6.0
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 1Gi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 2
|
||||
maxReplicas: 5
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
behavior:
|
||||
scaleDown:
|
||||
stabilizationWindowSeconds: 300
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
scaleUp:
|
||||
stabilizationWindowSeconds: 0
|
||||
policies:
|
||||
- type: Percent
|
||||
value: 100
|
||||
periodSeconds: 15
|
||||
- type: Pods
|
||||
value: 4
|
||||
periodSeconds: 15
|
||||
selectPolicy: Max
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
formbricksConfig:
|
||||
nextAuthSecret: ""
|
||||
encryptionKey: ""
|
||||
cronSecret: ""
|
||||
|
||||
env: {}
|
||||
|
||||
hostname: ""
|
||||
email: ""
|
||||
|
||||
traefik:
|
||||
enabled: true
|
||||
ingressRoute:
|
||||
dashboard:
|
||||
enabled: false
|
||||
additionalArguments:
|
||||
- "--certificatesresolvers.letsencrypt.acme.email=your-email@example.com"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
|
||||
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
|
||||
- "--entrypoints.web.address=:80"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
|
||||
- "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-v02.api.letsencrypt.org/directory"
|
||||
- "--log.level=DEBUG"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
|
||||
- "--entrypoints.web.http.redirections.entryPoint.permanent=true"
|
||||
- "--providers.file.filename=/config/traefik.toml"
|
||||
volumes:
|
||||
- name: traefik-config
|
||||
mountPath: "/config"
|
||||
type: configMap
|
||||
tls:
|
||||
enabled: true
|
||||
certResolver: letsencrypt
|
||||
ports:
|
||||
web:
|
||||
port: 80
|
||||
websecure:
|
||||
port: 443
|
||||
tls:
|
||||
enabled: true
|
||||
certResolver: letsencrypt
|
||||
persistence:
|
||||
enabled: true
|
||||
name: traefik-acme
|
||||
accessMode: ReadWriteOnce
|
||||
size: 128Mi
|
||||
path: /data
|
||||
podSecurityContext:
|
||||
fsGroup: 0
|
||||
hostNetwork: true
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- NET_BIND_SERVICE
|
||||
- NET_BROADCAST
|
||||
- NET_RAW
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
runAsNonRoot: false
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
redis:
|
||||
enabled: false
|
||||
externalUrl: ""
|
||||
architecture: standalone
|
||||
auth:
|
||||
enabled: true
|
||||
password: redispassword
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
replica:
|
||||
replicaCount: 0
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
externalUrl: ""
|
||||
auth:
|
||||
username: formbricks
|
||||
password: formbrickspassword
|
||||
database: formbricks
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { ArrowUpRight, Languages } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -170,8 +169,6 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
|
||||
setLocalSurvey({ ...localSurvey, ...{ showLanguageSwitch: !localSurvey.showLanguageSwitch } });
|
||||
};
|
||||
|
||||
const [parent] = useAutoAnimate();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -215,7 +212,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`} ref={parent}>
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<div className="space-y-4">
|
||||
{!isMultiLanguageAllowed && !isFormbricksCloud && !isMultiLanguageActivated ? (
|
||||
<UpgradePlanNotice
|
||||
|
||||
@@ -55,7 +55,8 @@ export const updateAttribute = async (
|
||||
}
|
||||
return err({
|
||||
code: "network_error",
|
||||
status: 500,
|
||||
// @ts-expect-error
|
||||
status: res.error.status ?? 500,
|
||||
message: res.error.message ?? `Error updating person with userId ${userId}`,
|
||||
url: `${config.get().apiHost}/api/v1/client/${environmentId}/people/${userId}/attributes`,
|
||||
responseMessage: res.error.message,
|
||||
@@ -92,7 +93,7 @@ export const updateAttributes = async (
|
||||
const updatedAttributes = { ...attributes };
|
||||
|
||||
try {
|
||||
const existingAttributes = config.get().attributes;
|
||||
const existingAttributes = config.get().personState.data.attributes;
|
||||
if (existingAttributes) {
|
||||
for (const [key, value] of Object.entries(existingAttributes)) {
|
||||
if (updatedAttributes[key] === value) {
|
||||
@@ -139,7 +140,7 @@ export const updateAttributes = async (
|
||||
};
|
||||
|
||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
||||
if (config.get().attributes[key] === value) {
|
||||
if (config.get().personState.data.attributes[key] === value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -239,7 +239,6 @@ export const initialize = async (
|
||||
environmentState,
|
||||
personState,
|
||||
filteredSurveys,
|
||||
attributes: configInput.attributes || {},
|
||||
});
|
||||
|
||||
const surveyNames = filteredSurveys.map((s) => s.name);
|
||||
@@ -281,7 +280,6 @@ export const initialize = async (
|
||||
personState,
|
||||
environmentState,
|
||||
filteredSurveys,
|
||||
attributes: configInput.attributes || {},
|
||||
});
|
||||
} catch (e) {
|
||||
handleErrorOnFirstInit();
|
||||
@@ -295,9 +293,15 @@ export const initialize = async (
|
||||
if (updatedAttributes && Object.keys(updatedAttributes).length > 0) {
|
||||
config.update({
|
||||
...config.get(),
|
||||
attributes: {
|
||||
...config.get().attributes,
|
||||
...updatedAttributes,
|
||||
personState: {
|
||||
...config.get().personState,
|
||||
data: {
|
||||
...config.get().personState.data,
|
||||
attributes: {
|
||||
...config.get().personState.data.attributes,
|
||||
...updatedAttributes,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
||||
environmentId: config.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
...(userId && { userId }),
|
||||
attributes: config.get().attributes,
|
||||
attributes: config.get().personState.data.attributes,
|
||||
};
|
||||
await logoutPerson();
|
||||
try {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const DEFAULT_PERSON_STATE_NO_USER_ID: TJsPersonState = {
|
||||
segments: [],
|
||||
displays: [],
|
||||
responses: [],
|
||||
attributes: {},
|
||||
lastDisplayAt: null,
|
||||
},
|
||||
} as const;
|
||||
@@ -67,6 +68,7 @@ export const fetchPersonState = async (
|
||||
segments: [],
|
||||
displays: [],
|
||||
responses: [],
|
||||
attributes: {},
|
||||
lastDisplayAt: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ const renderWidget = async (
|
||||
}
|
||||
|
||||
const { product } = config.get().environmentState.data ?? {};
|
||||
const { attributes } = config.get() ?? {};
|
||||
const { attributes } = config.get().personState.data ?? {};
|
||||
|
||||
const isMultiLanguageSurvey = survey.languages.length > 1;
|
||||
let languageCode = "default";
|
||||
|
||||
@@ -1950,102 +1950,6 @@ const improveActivationRate = (): TTemplate => {
|
||||
};
|
||||
};
|
||||
|
||||
const employeeSatisfaction = (): TTemplate => {
|
||||
return {
|
||||
name: "Employee Satisfaction",
|
||||
role: "productManager",
|
||||
industries: ["saas", "other"],
|
||||
channels: ["app", "link"],
|
||||
description: "Gauge employee satisfaction and identify areas for improvement.",
|
||||
preset: {
|
||||
...surveyDefault,
|
||||
name: "Employee Satisfaction",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "star",
|
||||
headline: { default: "How satisfied are you with your current role?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not satisfied" },
|
||||
upperLabel: { default: "Very satisfied" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
shuffleOption: "none",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Extremely meaningful" } },
|
||||
{ id: createId(), label: { default: "Very meaningful" } },
|
||||
{ id: createId(), label: { default: "Moderately meaningful" } },
|
||||
{ id: createId(), label: { default: "Slightly meaningful" } },
|
||||
{ id: createId(), label: { default: "Not at all meaningful" } },
|
||||
],
|
||||
headline: { default: "How meaningful do you find your work?" },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "What do you enjoy most about working here?" },
|
||||
required: false,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
shuffleOption: "none",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Extremely well" } },
|
||||
{ id: createId(), label: { default: "Very well" } },
|
||||
{ id: createId(), label: { default: "Moderately well" } },
|
||||
{ id: createId(), label: { default: "Slightly well" } },
|
||||
{ id: createId(), label: { default: "Not at all well" } },
|
||||
],
|
||||
headline: { default: "How well do you feel your work is recognized?" },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: { default: "Rate the support you receive from your manager." },
|
||||
required: true,
|
||||
lowerLabel: { default: "Poor" },
|
||||
upperLabel: { default: "Excellent" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "What improvements would you suggest for our workplace?" },
|
||||
required: false,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.MultipleChoiceSingle,
|
||||
shuffleOption: "none",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Extremely likely" } },
|
||||
{ id: createId(), label: { default: "Very likely" } },
|
||||
{ id: createId(), label: { default: "Moderately likely" } },
|
||||
{ id: createId(), label: { default: "Slightly likely" } },
|
||||
{ id: createId(), label: { default: "Not at all likely" } },
|
||||
],
|
||||
headline: { default: "How likely are you to recommend our company to a friend?" },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const uncoverStrengthsAndWeaknesses = (): TTemplate => {
|
||||
return {
|
||||
name: "Uncover Strengths & Weaknesses",
|
||||
@@ -5252,77 +5156,6 @@ const understandLowEngagement = (): TTemplate => {
|
||||
};
|
||||
};
|
||||
|
||||
const employeeWellBeing = (): TTemplate => {
|
||||
return {
|
||||
name: "Employee Well-Being",
|
||||
role: "productManager",
|
||||
industries: ["eCommerce"],
|
||||
channels: ["link"],
|
||||
description: "Assess your employee well-being through work-life balance, workload, and environment.",
|
||||
preset: {
|
||||
...surveyDefault,
|
||||
name: "Employee Well-Being",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "I feel that I have a good balance between my work and personal life." },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 10,
|
||||
lowerLabel: {
|
||||
default: "Very poor balance",
|
||||
},
|
||||
upperLabel: {
|
||||
default: "Excellent balance",
|
||||
},
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: {
|
||||
default: "My workload is manageable, allowing me to stay productive without feeling overwhelmed.",
|
||||
},
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 10,
|
||||
lowerLabel: {
|
||||
default: "Overwhelming workload",
|
||||
},
|
||||
upperLabel: {
|
||||
default: "Perfectly manageable",
|
||||
},
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
headline: { default: "The work environment supports my physical and mental well-being." },
|
||||
required: true,
|
||||
scale: "number",
|
||||
range: 10,
|
||||
lowerLabel: {
|
||||
default: "Not supportive",
|
||||
},
|
||||
upperLabel: {
|
||||
default: "Highly supportive",
|
||||
},
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "What changes, if any, would improve your overall well-being at work?" },
|
||||
required: false,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const templates: TTemplate[] = [
|
||||
cartAbandonmentSurvey(),
|
||||
siteAbandonmentSurvey(),
|
||||
@@ -5363,8 +5196,6 @@ export const templates: TTemplate[] = [
|
||||
improveNewsletterContent(),
|
||||
evaluateAProductIdea(),
|
||||
understandLowEngagement(),
|
||||
employeeSatisfaction(),
|
||||
employeeWellBeing(),
|
||||
];
|
||||
|
||||
export const customSurvey = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatDistance, intlFormat } from "date-fns";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { intlFormat } from "date-fns";
|
||||
|
||||
export const convertDateString = (dateString: string) => {
|
||||
if (!dateString) {
|
||||
@@ -9,7 +10,7 @@ export const convertDateString = (dateString: string) => {
|
||||
date,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -126,6 +126,7 @@ export const ZJsPersonState = z.object({
|
||||
})
|
||||
),
|
||||
responses: z.array(ZId), // responded survey ids
|
||||
attributes: ZAttributes,
|
||||
lastDisplayAt: z.date().nullable(),
|
||||
}),
|
||||
});
|
||||
@@ -145,7 +146,6 @@ export const ZJsConfig = z.object({
|
||||
environmentState: ZJsEnvironmentState,
|
||||
personState: ZJsPersonState,
|
||||
filteredSurveys: z.array(ZSurvey).default([]),
|
||||
attributes: z.record(z.string()),
|
||||
status: z.object({
|
||||
value: z.enum(["success", "error"]),
|
||||
expiresAt: z.date().nullable(),
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-xl border p-3 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-3 [&>svg]:top-3 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-9",
|
||||
"relative w-full rounded-lg border p-3 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-3 [&>svg]:top-3 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-9",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -34,7 +34,7 @@ const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement> & { dangerouslySetInnerHTML?: { __html: string } }
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn("mb-1 cursor-default font-medium leading-none", className)} {...props} />
|
||||
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
|
||||
));
|
||||
AlertTitle.displayName = "AlertTitle";
|
||||
|
||||
@@ -42,7 +42,7 @@ const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement> & { dangerouslySetInnerHTML?: { __html: string } }
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("cursor-default text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
||||
));
|
||||
AlertDescription.displayName = "AlertDescription";
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user