mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-23 07:18:57 -06:00
feat: Multi language Surveys (#1630)
Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com> Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
committed by
GitHub
parent
5c9e59b136
commit
8b5328aa74
@@ -34,7 +34,12 @@ export default function AppPage({}) {
|
||||
|
||||
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
|
||||
const isUserId = window.location.href.includes("userId=true");
|
||||
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
const defaultAttributes = {
|
||||
language: "gu",
|
||||
};
|
||||
const userInitAttributes = { "Init Attribute 1": "eight", "Init Attribute 2": "two" };
|
||||
|
||||
const attributes = isUserId ? { ...defaultAttributes, ...userInitAttributes } : defaultAttributes;
|
||||
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export const metadata = {
|
||||
title: "Enterprise License to unlock advanced functionality",
|
||||
description:
|
||||
"Request a self-hosting licenses to unlock advanced enterprise functionality",
|
||||
description: "Request a enterprise licenses to unlock advanced enterprise functionality",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
@@ -14,13 +13,17 @@ Additional to the AGPL licensed Formbricks core, the Formbricks repository conta
|
||||
|
||||
**Please note:** Sooner than later we will introduce a enterprise license pricing. For a free beta key, fill out this form:
|
||||
|
||||
<div style={{ position: 'relative', height: '100vh', maxHeight: '100vh', overflow: 'auto', borderRadius:'12px' }}>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "100vh",
|
||||
maxHeight: "100vh",
|
||||
overflow: "auto",
|
||||
borderRadius: "12px",
|
||||
}}>
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
|
||||
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', border: 0 }}
|
||||
>
|
||||
</iframe>
|
||||
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", border: 0 }}></iframe>
|
||||
</div>
|
||||
|
||||
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import HtmlBody from "./HtmlBody";
|
||||
import { TSurveyCTAQuestion } from "./types";
|
||||
|
||||
interface CTAQuestionProps {
|
||||
question: TSurveyCTAQuestion;
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import PreviewSurvey from "./PreviewSurvey";
|
||||
import { findTemplateByName } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
interface DemoPreviewProps {
|
||||
template: string;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import PreviewSurvey from "./PreviewSurvey";
|
||||
import TemplateList from "./TemplateList";
|
||||
import { templates } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
export default function SurveyTemplatesPage({}) {
|
||||
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyMultipleChoiceMultiQuestion } from "./types";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
question: TSurveyMultipleChoiceMultiQuestion;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyMultipleChoiceSingleQuestion } from "./types";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
question: TSurveyMultipleChoiceSingleQuestion;
|
||||
@@ -20,6 +20,7 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyNPSQuestion } from "./types";
|
||||
|
||||
interface NPSQuestionProps {
|
||||
question: TSurveyNPSQuestion;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyOpenTextQuestion } from "./types";
|
||||
|
||||
interface OpenTextQuestionProps {
|
||||
question: TSurveyOpenTextQuestion;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import QuestionConditional from "./QuestionConditional";
|
||||
import ThankYouCard from "./ThankYouCard";
|
||||
import { TSurvey, TSurveyQuestion } from "./types";
|
||||
|
||||
interface PreviewSurveyProps {
|
||||
localSurvey?: TSurvey;
|
||||
@@ -67,8 +66,8 @@ export default function PreviewSurvey({
|
||||
{activeQuestionId == "thank-you-card" ? (
|
||||
<ThankYouCard
|
||||
brandColor={brandColor}
|
||||
headline={localSurvey?.thankYouCard?.headline || ""}
|
||||
subheader={localSurvey?.thankYouCard?.subheader || ""}
|
||||
headline={localSurvey?.thankYouCard?.headline!}
|
||||
subheader={localSurvey?.thankYouCard?.subheader!}
|
||||
/>
|
||||
) : (
|
||||
questions.map(
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
|
||||
import CTAQuestion from "./CTAQuestion";
|
||||
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
|
||||
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
|
||||
import NPSQuestion from "./NPSQuestion";
|
||||
import OpenTextQuestion from "./OpenTextQuestion";
|
||||
import RatingQuestion from "./RatingQuestion";
|
||||
import { TSurveyQuestion, TSurveyQuestionType } from "./types";
|
||||
|
||||
interface QuestionConditionalProps {
|
||||
question: TSurveyQuestion;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import { TSurveyRatingQuestion } from "./types";
|
||||
|
||||
interface RatingQuestionProps {
|
||||
question: TSurveyRatingQuestion;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
import { templates } from "./templates";
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
type TemplateList = {
|
||||
onTemplateClick: (template: TTemplate) => void;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import {
|
||||
AppPieChartIcon,
|
||||
ArrowRightCircleIcon,
|
||||
@@ -27,14 +26,17 @@ import {
|
||||
VideoTabletAdjustIcon,
|
||||
} from "@formbricks/ui/icons";
|
||||
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
const thankYouCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "We appreciate your feedback.",
|
||||
subheader: "TWe appreciate your feedback.",
|
||||
};
|
||||
|
||||
const welcomeCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Welcome!",
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
};
|
||||
|
||||
501
apps/formbricks-com/components/dummyUI/types.ts
Normal file
501
apps/formbricks-com/components/dummyUI/types.ts
Normal file
@@ -0,0 +1,501 @@
|
||||
import z from "zod";
|
||||
|
||||
export enum TSurveyQuestionType {
|
||||
FileUpload = "fileUpload",
|
||||
OpenText = "openText",
|
||||
MultipleChoiceSingle = "multipleChoiceSingle",
|
||||
MultipleChoiceMulti = "multipleChoiceMulti",
|
||||
NPS = "nps",
|
||||
CTA = "cta",
|
||||
Rating = "rating",
|
||||
Consent = "consent",
|
||||
PictureSelection = "pictureSelection",
|
||||
Cal = "cal",
|
||||
Date = "date",
|
||||
}
|
||||
|
||||
export const ZAllowedFileExtension = z.enum([
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"pdf",
|
||||
"doc",
|
||||
"docx",
|
||||
"xls",
|
||||
"xlsx",
|
||||
"ppt",
|
||||
"pptx",
|
||||
"plain",
|
||||
"csv",
|
||||
"mp4",
|
||||
"mov",
|
||||
"avi",
|
||||
"mkv",
|
||||
"webm",
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
"tar",
|
||||
]);
|
||||
|
||||
export type TAllowedFileExtension = z.infer<typeof ZAllowedFileExtension>;
|
||||
|
||||
export const ZUserObjective = z.enum([
|
||||
"increase_conversion",
|
||||
"improve_user_retention",
|
||||
"increase_user_adoption",
|
||||
"sharpen_marketing_messaging",
|
||||
"support_sales",
|
||||
"other",
|
||||
]);
|
||||
|
||||
export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
|
||||
export const ZSurveyWelcomeCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
headline: z.optional(z.string()),
|
||||
html: z.string().optional(),
|
||||
fileUrl: z.string().optional(),
|
||||
buttonLabel: z.string().optional(),
|
||||
timeToFinish: z.boolean().default(true),
|
||||
showResponseCount: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type TSurveyWelcomeCard = z.infer<typeof ZSurveyWelcomeCard>;
|
||||
|
||||
export const ZSurveyThankYouCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
headline: z.optional(z.string()),
|
||||
subheader: z.optional(z.string()),
|
||||
buttonLabel: z.optional(z.string()),
|
||||
buttonLink: z.optional(z.string()),
|
||||
imageUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyThankYouCard = z.infer<typeof ZSurveyThankYouCard>;
|
||||
|
||||
export const ZSurveyHiddenFields = z.object({
|
||||
enabled: z.boolean(),
|
||||
fieldIds: z.optional(z.array(z.string())),
|
||||
});
|
||||
|
||||
export type TSurveyHiddenFields = z.infer<typeof ZSurveyHiddenFields>;
|
||||
|
||||
export const ZSurveyChoice = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyChoice = z.infer<typeof ZSurveyChoice>;
|
||||
|
||||
export const ZSurveyPictureChoice = z.object({
|
||||
id: z.string(),
|
||||
imageUrl: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureChoice = z.infer<typeof ZSurveyPictureChoice>;
|
||||
|
||||
export const ZSurveyLogicCondition = z.enum([
|
||||
"accepted",
|
||||
"clicked",
|
||||
"submitted",
|
||||
"skipped",
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"includesAll",
|
||||
"includesOne",
|
||||
"uploaded",
|
||||
"notUploaded",
|
||||
"booked",
|
||||
]);
|
||||
|
||||
export type TSurveyLogicCondition = z.infer<typeof ZSurveyLogicCondition>;
|
||||
|
||||
export const ZSurveyLogicBase = z.object({
|
||||
condition: ZSurveyLogicCondition.optional(),
|
||||
value: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
destination: z.union([z.string(), z.literal("end")]).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyFileUploadLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["uploaded", "notUploaded"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["skipped", "accepted"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped", "equals", "notEquals"]).optional(),
|
||||
value: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped", "includesAll", "includesOne", "equals"]).optional(),
|
||||
value: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyNPSLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.enum([
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"submitted",
|
||||
"skipped",
|
||||
])
|
||||
.optional(),
|
||||
value: z.union([z.string(), z.number()]).optional(),
|
||||
});
|
||||
|
||||
const ZSurveyCTALogic = ZSurveyLogicBase.extend({
|
||||
// "submitted" condition is legacy and should be removed later
|
||||
condition: z.enum(["clicked", "submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyRatingLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.enum([
|
||||
"equals",
|
||||
"notEquals",
|
||||
"lessThan",
|
||||
"lessEqual",
|
||||
"greaterThan",
|
||||
"greaterEqual",
|
||||
"submitted",
|
||||
"skipped",
|
||||
])
|
||||
.optional(),
|
||||
value: z.union([z.string(), z.number()]).optional(),
|
||||
});
|
||||
|
||||
const ZSurveyPictureSelectionLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["submitted", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyCalLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["booked", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyLogic = z.union([
|
||||
ZSurveyOpenTextLogic,
|
||||
ZSurveyConsentLogic,
|
||||
ZSurveyMultipleChoiceSingleLogic,
|
||||
ZSurveyMultipleChoiceMultiLogic,
|
||||
ZSurveyNPSLogic,
|
||||
ZSurveyCTALogic,
|
||||
ZSurveyRatingLogic,
|
||||
ZSurveyPictureSelectionLogic,
|
||||
ZSurveyFileUploadLogic,
|
||||
ZSurveyCalLogic,
|
||||
]);
|
||||
|
||||
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
|
||||
|
||||
const ZSurveyQuestionBase = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
headline: z.string(),
|
||||
subheader: z.string().optional(),
|
||||
imageUrl: z.string().optional(),
|
||||
required: z.boolean(),
|
||||
buttonLabel: z.string().optional(),
|
||||
backButtonLabel: z.string().optional(),
|
||||
scale: z.enum(["number", "smiley", "star"]).optional(),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
|
||||
logic: z.array(ZSurveyLogic).optional(),
|
||||
isDraft: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url", "number", "phone"]);
|
||||
export type TSurveyOpenTextQuestionInputType = z.infer<typeof ZSurveyOpenTextQuestionInputType>;
|
||||
|
||||
export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.OpenText),
|
||||
placeholder: z.string().optional(),
|
||||
longAnswer: z.boolean().optional(),
|
||||
logic: z.array(ZSurveyOpenTextLogic).optional(),
|
||||
inputType: ZSurveyOpenTextQuestionInputType.optional().default("text"),
|
||||
});
|
||||
|
||||
export type TSurveyOpenTextQuestion = z.infer<typeof ZSurveyOpenTextQuestion>;
|
||||
|
||||
export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Consent),
|
||||
html: z.string().optional(),
|
||||
label: z.string(),
|
||||
dismissButtonLabel: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
logic: z.array(ZSurveyConsentLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
|
||||
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
|
||||
otherOptionPlaceholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyMultipleChoiceSingleQuestion = z.infer<typeof ZSurveyMultipleChoiceSingleQuestion>;
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
|
||||
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
|
||||
otherOptionPlaceholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TSurveyMultipleChoiceMultiQuestion = z.infer<typeof ZSurveyMultipleChoiceMultiQuestion>;
|
||||
|
||||
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.NPS),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
logic: z.array(ZSurveyNPSLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyNPSQuestion = z.infer<typeof ZSurveyNPSQuestion>;
|
||||
|
||||
export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.CTA),
|
||||
html: z.string().optional(),
|
||||
buttonUrl: z.string().optional(),
|
||||
buttonExternal: z.boolean(),
|
||||
dismissButtonLabel: z.string().optional(),
|
||||
logic: z.array(ZSurveyCTALogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyCTAQuestion = z.infer<typeof ZSurveyCTAQuestion>;
|
||||
|
||||
// export const ZSurveyWelcomeQuestion = ZSurveyQuestionBase.extend({
|
||||
// type: z.literal(TSurveyQuestionType.Welcome),
|
||||
// html: z.string().optional(),
|
||||
// fileUrl: z.string().optional(),
|
||||
// buttonUrl: z.string().optional(),
|
||||
// timeToFinish: z.boolean().default(false),
|
||||
// logic: z.array(ZSurveyCTALogic).optional(),
|
||||
// });
|
||||
|
||||
// export type TSurveyWelcomeQuestion = z.infer<typeof ZSurveyWelcomeQuestion>;
|
||||
|
||||
export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Rating),
|
||||
scale: z.enum(["number", "smiley", "star"]),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
logic: z.array(ZSurveyRatingLogic).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Date),
|
||||
html: z.string().optional(),
|
||||
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
|
||||
});
|
||||
|
||||
export type TSurveyDateQuestion = z.infer<typeof ZSurveyDateQuestion>;
|
||||
|
||||
export type TSurveyRatingQuestion = z.infer<typeof ZSurveyRatingQuestion>;
|
||||
|
||||
export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.PictureSelection),
|
||||
allowMulti: z.boolean().optional().default(false),
|
||||
choices: z.array(ZSurveyPictureChoice),
|
||||
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyPictureSelectionQuestion = z.infer<typeof ZSurveyPictureSelectionQuestion>;
|
||||
|
||||
export const ZSurveyFileUploadQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.FileUpload),
|
||||
allowMultipleFiles: z.boolean(),
|
||||
maxSizeInMB: z.number().optional(),
|
||||
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
||||
logic: z.array(ZSurveyFileUploadLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyFileUploadQuestion = z.infer<typeof ZSurveyFileUploadQuestion>;
|
||||
|
||||
export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal(TSurveyQuestionType.Cal),
|
||||
calUserName: z.string(),
|
||||
logic: z.array(ZSurveyCalLogic).optional(),
|
||||
});
|
||||
|
||||
export type TSurveyCalQuestion = z.infer<typeof ZSurveyCalQuestion>;
|
||||
|
||||
export const ZSurveyQuestion = z.union([
|
||||
ZSurveyOpenTextQuestion,
|
||||
ZSurveyConsentQuestion,
|
||||
ZSurveyMultipleChoiceSingleQuestion,
|
||||
ZSurveyMultipleChoiceMultiQuestion,
|
||||
ZSurveyNPSQuestion,
|
||||
ZSurveyCTAQuestion,
|
||||
ZSurveyRatingQuestion,
|
||||
ZSurveyPictureSelectionQuestion,
|
||||
ZSurveyDateQuestion,
|
||||
ZSurveyFileUploadQuestion,
|
||||
ZSurveyCalQuestion,
|
||||
]);
|
||||
|
||||
export type TSurveyQuestion = z.infer<typeof ZSurveyQuestion>;
|
||||
|
||||
export const ZSurveyQuestions = z.array(ZSurveyQuestion);
|
||||
|
||||
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
|
||||
|
||||
export const ZSurveyClosedMessage = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
heading: z.string().optional(),
|
||||
subheading: z.string().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional();
|
||||
|
||||
export type TSurveyClosedMessage = z.infer<typeof ZSurveyClosedMessage>;
|
||||
|
||||
export const ZSurveyAttributeFilter = z.object({
|
||||
attributeClassId: z.string().cuid2(),
|
||||
condition: z.enum(["equals", "notEquals"]),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type TSurveyAttributeFilter = z.infer<typeof ZSurveyAttributeFilter>;
|
||||
|
||||
export const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
|
||||
|
||||
export type TSurveyType = z.infer<typeof ZSurveyType>;
|
||||
|
||||
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
|
||||
|
||||
export type TSurveyStatus = z.infer<typeof ZSurveyStatus>;
|
||||
|
||||
const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
|
||||
|
||||
export type TSurveyDisplayOption = z.infer<typeof ZSurveyDisplayOption>;
|
||||
|
||||
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
|
||||
|
||||
export const ZPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);
|
||||
|
||||
export type TPlacement = z.infer<typeof ZPlacement>;
|
||||
|
||||
export const ZSurveyProductOverwrites = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
placement: ZPlacement.nullish(),
|
||||
clickOutsideClose: z.boolean().nullish(),
|
||||
darkOverlay: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
|
||||
|
||||
export const ZSurveyStylingBackground = z.object({
|
||||
bg: z.string().nullish(),
|
||||
bgType: z.enum(["animation", "color", "image"]).nullish(),
|
||||
brightness: z.number().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyStylingBackground = z.infer<typeof ZSurveyStylingBackground>;
|
||||
|
||||
export const ZSurveyStyling = z.object({
|
||||
background: ZSurveyStylingBackground.nullish(),
|
||||
hideProgressBar: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type TSurveyStyling = z.infer<typeof ZSurveyStyling>;
|
||||
|
||||
export const ZSurveySingleUse = z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
heading: z.optional(z.string()),
|
||||
subheading: z.optional(z.string()),
|
||||
isEncrypted: z.boolean(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
export type TSurveySingleUse = z.infer<typeof ZSurveySingleUse>;
|
||||
|
||||
export const ZSurveyVerifyEmail = z
|
||||
.object({
|
||||
name: z.optional(z.string()),
|
||||
subheading: z.optional(z.string()),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type TSurveyVerifyEmail = z.infer<typeof ZSurveyVerifyEmail>;
|
||||
|
||||
export const ZSurvey = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
name: z.string(),
|
||||
type: ZSurveyType,
|
||||
environmentId: z.string(),
|
||||
createdBy: z.string().nullable(),
|
||||
status: ZSurveyStatus,
|
||||
attributeFilters: z.array(ZSurveyAttributeFilter),
|
||||
displayOption: ZSurveyDisplayOption,
|
||||
autoClose: z.number().nullable(),
|
||||
triggers: z.array(z.string()),
|
||||
redirectUrl: z.string().url().nullable(),
|
||||
recontactDays: z.number().nullable(),
|
||||
welcomeCard: ZSurveyWelcomeCard,
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
delay: z.number(),
|
||||
autoComplete: z.number().nullable(),
|
||||
closeOnDate: z.date().nullable(),
|
||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||
styling: ZSurveyStyling.nullable(),
|
||||
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
|
||||
singleUse: ZSurveySingleUse.nullable(),
|
||||
verifyEmail: ZSurveyVerifyEmail.nullable(),
|
||||
pin: z.string().nullable().optional(),
|
||||
resultShareKey: z.string().nullable(),
|
||||
displayPercentage: z.number().min(1).max(100).nullable(),
|
||||
});
|
||||
|
||||
export type TSurvey = z.infer<typeof ZSurvey>;
|
||||
|
||||
export const ZTemplate = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.any().optional(),
|
||||
category: z
|
||||
.enum(["Product Experience", "Exploration", "Growth", "Increase Revenue", "Customer Success"])
|
||||
.optional(),
|
||||
objectives: z.array(ZUserObjective).optional(),
|
||||
preset: z.object({
|
||||
name: z.string(),
|
||||
welcomeCard: ZSurveyWelcomeCard,
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TTemplate = z.infer<typeof ZTemplate>;
|
||||
@@ -25,6 +25,16 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">Language</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
{person.attributes.language ? (
|
||||
<span>{person.attributes.language}</span>
|
||||
) : (
|
||||
<span className="text-slate-300">Not provided</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-slate-500">User Id</dt>
|
||||
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
|
||||
@@ -43,7 +53,7 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
</div>
|
||||
|
||||
{Object.entries(person.attributes)
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId")
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
|
||||
.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
|
||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
@@ -92,7 +93,7 @@ const ResponseSurveyCard = ({
|
||||
{survey && (
|
||||
<SingleResponseCard
|
||||
response={response}
|
||||
survey={survey}
|
||||
survey={checkForRecallInHeadline(survey, "default")}
|
||||
user={user}
|
||||
pageType="people"
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -4,11 +4,16 @@ import HeadingSection from "@/app/(app)/environments/[environmentId]/(peopleAndS
|
||||
import ResponseSection from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponseSection";
|
||||
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
|
||||
export default async function PersonPage({ params }) {
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
const environmentTags = await getTagsByEnvironmentId(params.environmentId);
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Navigation from "@/app/(app)/environments/[environmentId]/components/Navigation";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { IS_FORMBRICKS_CLOUD, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
@@ -11,7 +12,6 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
interface EnvironmentsNavbarProps {
|
||||
environmentId: string;
|
||||
session: Session;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) {
|
||||
@@ -25,6 +25,8 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
|
||||
return <ErrorComponent />;
|
||||
}
|
||||
|
||||
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
|
||||
|
||||
const [products, environments] = await Promise.all([
|
||||
getProducts(team.id),
|
||||
getEnvironments(environment.productId),
|
||||
@@ -46,6 +48,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CreditCardIcon,
|
||||
FileCheckIcon,
|
||||
HeartIcon,
|
||||
LanguagesIcon,
|
||||
LinkIcon,
|
||||
LogOutIcon,
|
||||
MailIcon,
|
||||
@@ -69,6 +70,7 @@ interface NavigationProps {
|
||||
isFormbricksCloud: boolean;
|
||||
webAppUrl: string;
|
||||
membershipRole?: TMembershipRole;
|
||||
isMultiLanguageAllowed: boolean;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
@@ -81,6 +83,7 @@ export default function Navigation({
|
||||
isFormbricksCloud,
|
||||
webAppUrl,
|
||||
membershipRole,
|
||||
isMultiLanguageAllowed,
|
||||
}: NavigationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -166,6 +169,12 @@ export default function Navigation({
|
||||
href: `/environments/${environment.id}/settings/lookandfeel`,
|
||||
hidden: isViewer,
|
||||
},
|
||||
{
|
||||
icon: LanguagesIcon,
|
||||
label: "Survey Languages",
|
||||
href: `/environments/${environment.id}/settings/language`,
|
||||
hidden: !isMultiLanguageAllowed,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
@@ -334,7 +335,7 @@ export default function AddIntegrationModal(props: AddIntegrationModalProps) {
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
@@ -354,7 +355,7 @@ export default function AddIntegrationModal(props: AddIntegrationModalProps) {
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{question.headline}</span>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getAirtableTables } from "@formbricks/lib/airtable/service";
|
||||
import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationAirtable } from "@formbricks/types/integration/airtable";
|
||||
@@ -19,6 +20,10 @@ export default async function Airtable({ params }) {
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const airtableIntegration: TIntegrationAirtable | undefined = integrations?.find(
|
||||
(integration): integration is TIntegrationAirtable => integration.type === "airtable"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import {
|
||||
@@ -274,7 +275,7 @@ export default function AddIntegrationModal({
|
||||
<Label htmlFor="Surveys">Questions</Label>
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((question) => (
|
||||
<div key={question.id} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={question.id} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
@@ -287,7 +288,7 @@ export default function AddIntegrationModal({
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{question.headline}</span>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getSpreadSheets } from "@formbricks/lib/googleSheet/service";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TIntegrationItem } from "@formbricks/types/integration";
|
||||
import { TIntegrationGoogleSheets } from "@formbricks/types/integration/googleSheet";
|
||||
@@ -24,6 +25,10 @@ export default async function GoogleSheet({ params }) {
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const googleSheetIntegration: TIntegrationGoogleSheets | undefined = integrations?.find(
|
||||
(integration): integration is TIntegrationGoogleSheets => integration.type === "googleSheets"
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function AddIntegrationModal({
|
||||
|
||||
const questionItems = useMemo(() => {
|
||||
const questions = selectedSurvey
|
||||
? checkForRecallInHeadline(selectedSurvey)?.questions.map((q) => ({
|
||||
? checkForRecallInHeadline(selectedSurvey, "default")?.questions.map((q) => ({
|
||||
id: q.id,
|
||||
name: q.headline,
|
||||
type: q.type,
|
||||
@@ -226,7 +226,7 @@ export default function AddIntegrationModal({
|
||||
return questionItems.filter((q) => !selectedQuestionIds.includes(q.id));
|
||||
};
|
||||
|
||||
const createCopy = (item) => JSON.parse(JSON.stringify(item));
|
||||
const createCopy = (item) => structuredClone(item);
|
||||
|
||||
const MappingRow = ({ idx }: { idx: number }) => {
|
||||
const filteredQuestionItems = getFilteredQuestionItems(idx);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
@@ -42,11 +41,7 @@ export default async function EnvironmentLayout({ children, params }) {
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
<EnvironmentsNavbar
|
||||
environmentId={params.environmentId}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
<EnvironmentsNavbar environmentId={params.environmentId} session={session} />
|
||||
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
|
||||
{children}
|
||||
<main />
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function PricingTableComponent({
|
||||
},
|
||||
{
|
||||
title: "Multi-Language Surveys",
|
||||
comingSoon: true,
|
||||
comingSoon: false,
|
||||
},
|
||||
{
|
||||
title: "Unlimited Responses",
|
||||
@@ -162,7 +162,7 @@ export default function PricingTableComponent({
|
||||
},
|
||||
{
|
||||
title: "Multi-Language Surveys",
|
||||
comingSoon: true,
|
||||
comingSoon: false,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FileSearch2Icon,
|
||||
HashIcon,
|
||||
KeyIcon,
|
||||
LanguagesIcon,
|
||||
LinkIcon,
|
||||
SlidersIcon,
|
||||
UserCircleIcon,
|
||||
@@ -28,19 +29,23 @@ import { TProduct } from "@formbricks/types/product";
|
||||
import { TTeam } from "@formbricks/types/teams";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
|
||||
interface SettingsNavbarProps {
|
||||
environmentId: string;
|
||||
isFormbricksCloud: boolean;
|
||||
team: TTeam;
|
||||
product: TProduct;
|
||||
membershipRole?: TMembershipRole;
|
||||
isMultiLanguageAllowed: boolean;
|
||||
}
|
||||
|
||||
export default function SettingsNavbar({
|
||||
environmentId,
|
||||
isFormbricksCloud,
|
||||
team,
|
||||
product,
|
||||
membershipRole,
|
||||
}: {
|
||||
environmentId: string;
|
||||
isFormbricksCloud: boolean;
|
||||
team: TTeam;
|
||||
product: TProduct;
|
||||
membershipRole?: TMembershipRole;
|
||||
}) {
|
||||
isMultiLanguageAllowed,
|
||||
}: SettingsNavbarProps) {
|
||||
const pathname = usePathname();
|
||||
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
|
||||
const { isAdmin, isOwner, isViewer } = getAccessFlags(membershipRole);
|
||||
@@ -100,6 +105,13 @@ export default function SettingsNavbar({
|
||||
current: pathname?.includes("/lookandfeel"),
|
||||
hidden: isViewer,
|
||||
},
|
||||
{
|
||||
name: "Survey Languages",
|
||||
href: `/environments/${environmentId}/settings/language`,
|
||||
icon: LanguagesIcon,
|
||||
current: pathname?.includes("/language"),
|
||||
hidden: !isMultiLanguageAllowed,
|
||||
},
|
||||
{
|
||||
name: "API Keys",
|
||||
href: `/environments/${environmentId}/settings/api-keys`,
|
||||
@@ -206,7 +218,7 @@ export default function SettingsNavbar({
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
[environmentId, isFormbricksCloud, pathname, isPricingDisabled, isViewer]
|
||||
[environmentId, pathname, isViewer, isMultiLanguageAllowed, isFormbricksCloud, isPricingDisabled]
|
||||
);
|
||||
|
||||
if (!navigation) return null;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import SettingsCard from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard";
|
||||
import SettingsTitle from "@/app/(app)/environments/[environmentId]/settings/components/SettingsTitle";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import EditLanguage from "@formbricks/ee/multiLanguage/components/EditLanguage";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTeam } from "@formbricks/lib/team/service";
|
||||
|
||||
export default async function LanguageSettingsPage({ params }: { params: { environmentId: string } }) {
|
||||
const product = await getProductByEnvironmentId(params.environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const team = await getTeam(product?.teamId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
|
||||
|
||||
if (!isMultiLanguageAllowed) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsTitle title="Survey Languages" />
|
||||
<SettingsCard
|
||||
title="Multi-language surveys"
|
||||
description="Add languages to create multi-language surveys.">
|
||||
<EditLanguage product={product} environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
@@ -19,9 +20,11 @@ export default async function SettingsLayout({ children, params }) {
|
||||
getProductByEnvironmentId(params.environmentId),
|
||||
getServerSession(authOptions),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
@@ -30,6 +33,8 @@ export default async function SettingsLayout({ children, params }) {
|
||||
throw new Error("Unauthenticated");
|
||||
}
|
||||
|
||||
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
|
||||
return (
|
||||
@@ -41,6 +46,7 @@ export default async function SettingsLayout({ children, params }) {
|
||||
team={team}
|
||||
product={product}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
/>
|
||||
<div className="w-full md:ml-64">
|
||||
<div className="max-w-4xl px-20 pb-6 pt-14 md:pt-6">
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function AddMemberModal({
|
||||
<UpgradePlanNotice
|
||||
message="To manage access roles for your team,"
|
||||
url="https://formbricks.com/docs/self-hosting/enterprise"
|
||||
textForUrl="get a self-hosting license (free)."
|
||||
textForUrl="get a enterprise license."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ const ResponsePage = ({
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey);
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
@@ -133,7 +133,7 @@ const ResponsePage = ({
|
||||
/>
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
activeId="responses"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate";
|
||||
import { generateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -18,17 +17,6 @@ type TSendEmailActionArgs = {
|
||||
html: string;
|
||||
};
|
||||
|
||||
export async function generateSingleUseIdAction(surveyId: string, isEncrypted: boolean): Promise<string> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const hasUserSurveyAccess = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
|
||||
if (!hasUserSurveyAccess) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return generateSurveySingleUseId(isEncrypted);
|
||||
}
|
||||
|
||||
export const sendEmailAction = async ({ html, subject, to }: TSendEmailActionArgs) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
|
||||
import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryCta } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
|
||||
@@ -15,7 +16,7 @@ export default function CTASummary({ questionSummary }: CTASummaryProps) {
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
|
||||
@@ -2,6 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
|
||||
import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryCal } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
|
||||
@@ -16,7 +17,7 @@ export default function CalSummary({ questionSummary }: CalSummaryProps) {
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
|
||||
@@ -2,6 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
|
||||
import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryConsent } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
|
||||
@@ -15,7 +16,7 @@ export default function ConsentSummary({ questionSummary }: ConsentSummaryProps)
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
|
||||
@@ -20,8 +21,7 @@ export default function DateQuestionSummary({ questionSummary, environmentId }:
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { DownloadIcon, FileIcon, InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
@@ -20,7 +21,7 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { TSurveySummaryMultipleChoice } from "@formbricks/types/responses";
|
||||
import { PersonAvatar } from "@formbricks/ui/Avatars";
|
||||
@@ -33,7 +34,7 @@ export default function MultipleChoiceSummary({
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -2,6 +2,7 @@ import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId
|
||||
import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryNps } from "@formbricks/types/responses";
|
||||
import { HalfCircle, ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
|
||||
@@ -15,7 +16,7 @@ export default function NPSSummary({ questionSummary }: NPSSummaryProps) {
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TSurveySummaryOpenText } from "@formbricks/types/responses";
|
||||
@@ -19,7 +20,8 @@ export default function OpenTextSummary({ questionSummary, environmentId }: Open
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
|
||||
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryPictureSelection } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
|
||||
@@ -19,7 +20,7 @@ export default function PictureChoiceSummary({ questionSummary }: PictureChoiceS
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { questionTypes } from "@/app/lib/questions";
|
||||
import { CircleSlash2, InboxIcon, SmileIcon, StarIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurveySummaryRating } from "@formbricks/types/responses";
|
||||
import { ProgressBar } from "@formbricks/ui/ProgressBar";
|
||||
import { RatingResponse } from "@formbricks/ui/RatingResponse";
|
||||
@@ -24,7 +25,7 @@ export default function RatingSummary({ questionSummary }: RatingSummaryProps) {
|
||||
return (
|
||||
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<Headline headline={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
|
||||
<div className="flex items-center rounded-lg bg-slate-100 p-2">
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/actions";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
BellRing,
|
||||
BlocksIcon,
|
||||
Code2Icon,
|
||||
CopyIcon,
|
||||
LinkIcon,
|
||||
MailIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import { ArrowLeftIcon, BellRing, BlocksIcon, Code2Icon, LinkIcon, MailIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Dialog, DialogContent } from "@formbricks/ui/Dialog";
|
||||
import { ShareSurveyLink } from "@formbricks/ui/ShareSurveyLink";
|
||||
|
||||
import EmailTab from "./shareEmbedTabs/EmailTab";
|
||||
import LinkTab from "./shareEmbedTabs/LinkTab";
|
||||
@@ -32,7 +21,6 @@ interface ShareEmbedSurveyProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
webAppUrl: string;
|
||||
product: TProduct;
|
||||
user: TUser;
|
||||
}
|
||||
export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, user }: ShareEmbedSurveyProps) {
|
||||
@@ -49,35 +37,8 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
const [showInitialPage, setShowInitialPage] = useState(true);
|
||||
const linkTextRef = useRef(null);
|
||||
const [surveyUrl, setSurveyUrl] = useState("");
|
||||
|
||||
const getUrl = useCallback(async () => {
|
||||
let url = webAppUrl + "/s/" + survey.id;
|
||||
if (survey.singleUse?.enabled) {
|
||||
const singleUseId = await generateSingleUseIdAction(survey.id, survey.singleUse.isEncrypted);
|
||||
url += "?suId=" + singleUseId;
|
||||
}
|
||||
setSurveyUrl(url);
|
||||
}, [survey, webAppUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
getUrl();
|
||||
}, [survey, webAppUrl, getUrl]);
|
||||
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setActiveId(tabs[0].id);
|
||||
setOpen(open);
|
||||
@@ -91,11 +52,6 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
setShowInitialPage(!showInitialPage);
|
||||
};
|
||||
|
||||
const generateNewSingleUseLink = () => {
|
||||
getUrl();
|
||||
toast.success("New single use link generated");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className=" w-full max-w-xl bg-white p-0 md:max-w-3xl lg:h-[700px] lg:max-w-5xl">
|
||||
@@ -103,39 +59,12 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
<div className="h-full max-w-full overflow-hidden">
|
||||
<div className="flex h-[200px] w-full flex-col items-center justify-center space-y-6 p-8 text-center lg:h-2/5">
|
||||
<p className="pt-2 text-xl font-semibold text-slate-800">Your survey is public 🎉</p>
|
||||
<div className="flex max-w-full flex-col items-center justify-center space-x-2 lg:flex-row">
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="mt-2 max-w-[80%] overflow-hidden rounded-lg border border-slate-300 bg-slate-50 px-3 py-2 text-slate-800"
|
||||
style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
|
||||
onClick={() => handleTextSelection()}>
|
||||
{surveyUrl}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="inline"
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
EndIcon={CopyIcon}>
|
||||
Copy Link
|
||||
</Button>
|
||||
{survey.singleUse?.enabled && (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="inline"
|
||||
title="Regenerate single use survey link"
|
||||
aria-label="Regenerate single use survey link"
|
||||
onClick={() => generateNewSingleUseLink()}>
|
||||
<RefreshCcw className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex h-[300px] flex-col items-center justify-center gap-8 rounded-b-lg bg-slate-50 px-8 lg:h-3/5">
|
||||
<p className="-mt-8 text-sm text-slate-500">What's next?</p>
|
||||
@@ -202,10 +131,10 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
<WebpageTab surveyUrl={surveyUrl} />
|
||||
) : activeId === "link" ? (
|
||||
<LinkTab
|
||||
surveyUrl={surveyUrl}
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
generateNewSingleUseLink={generateNewSingleUseLink}
|
||||
isSingleUseLinkSurvey={isSingleUseLinkSurvey}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Confetti } from "@formbricks/ui/Confetti";
|
||||
@@ -16,18 +15,10 @@ interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
product: TProduct;
|
||||
user: TUser;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
export default function SuccessMessage({
|
||||
environment,
|
||||
survey,
|
||||
webAppUrl,
|
||||
product,
|
||||
user,
|
||||
}: SummaryMetadataProps) {
|
||||
export default function SuccessMessage({ environment, survey, webAppUrl, user }: SummaryMetadataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
@@ -63,7 +54,6 @@ export default function SuccessMessage({
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
{confetti && <Confetti />}
|
||||
|
||||
@@ -2,6 +2,7 @@ import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/
|
||||
import CalSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/CalSummary";
|
||||
import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary";
|
||||
import HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
|
||||
import PictureChoiceSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TSurveySummary } from "@formbricks/types/responses";
|
||||
@@ -15,7 +16,6 @@ import FileUploadSummary from "./FileUploadSummary";
|
||||
import MultipleChoiceSummary from "./MultipleChoiceSummary";
|
||||
import NPSSummary from "./NPSSummary";
|
||||
import OpenTextSummary from "./OpenTextSummary";
|
||||
import PictureChoiceSummary from "./PictureChoiceSummary";
|
||||
import RatingSummary from "./RatingSummary";
|
||||
|
||||
interface SummaryListProps {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function SummaryMetadata({
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-2 lg:col-span-1">
|
||||
<div className="text-right text-xs text-slate-400">
|
||||
Last updated: {timeSinceConditionally(survey.updatedAt.toISOString())}
|
||||
Last updated: {timeSinceConditionally(survey.updatedAt.toString())}
|
||||
</div>
|
||||
<Button
|
||||
variant="minimal"
|
||||
|
||||
@@ -91,8 +91,9 @@ const SummaryPage = ({
|
||||
}, [filters, surveyId]);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey);
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
}, [survey]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -114,7 +115,7 @@ const SummaryPage = ({
|
||||
/>
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
activeId="summary"
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
import UrlShortenerForm from "@/app/(app)/environments/[environmentId]/components/UrlShortenerForm";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { RefreshCcw } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRef } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ShareSurveyLink } from "@formbricks/ui/ShareSurveyLink";
|
||||
|
||||
interface LinkTabProps {
|
||||
surveyUrl: string;
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
generateNewSingleUseLink: () => void;
|
||||
isSingleUseLinkSurvey: boolean;
|
||||
surveyUrl: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
}
|
||||
|
||||
export default function LinkTab({
|
||||
surveyUrl,
|
||||
webAppUrl,
|
||||
generateNewSingleUseLink,
|
||||
isSingleUseLinkSurvey,
|
||||
}: LinkTabProps) {
|
||||
const linkTextRef = useRef(null);
|
||||
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function LinkTab({ survey, webAppUrl, surveyUrl, setSurveyUrl }: LinkTabProps) {
|
||||
const docsLinks = [
|
||||
{
|
||||
title: "Identify users",
|
||||
@@ -62,39 +39,12 @@ export default function LinkTab({
|
||||
<div className="flex h-full grow flex-col gap-6">
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-slate-800">Share the link to get responses</p>
|
||||
<div className="mt-2 flex max-w-full flex-col items-center space-x-2 lg:flex-row">
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="mt-2 max-w-[65%] overflow-hidden rounded-lg border border-slate-300 bg-white px-3 py-3 text-sm text-slate-800"
|
||||
style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
|
||||
onClick={() => handleTextSelection()}>
|
||||
{surveyUrl}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-center space-x-2">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="inline"
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
EndIcon={CopyIcon}>
|
||||
Copy Link
|
||||
</Button>
|
||||
{isSingleUseLinkSurvey && (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="inline"
|
||||
title="Regenerate single use survey link"
|
||||
aria-label="Regenerate single use survey link"
|
||||
onClick={() => generateNewSingleUseLink()}>
|
||||
<RefreshCcw className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<p className="pt-2 font-semibold text-slate-700">You can do a lot more with links surveys 💡</p>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CalendarDaysIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
@@ -50,17 +51,17 @@ export const getEmailTemplateHtml = async (surveyId) => {
|
||||
const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) => {
|
||||
const url = `${surveyUrl}?preview=true`;
|
||||
const urlWithPrefilling = `${surveyUrl}?preview=true&`;
|
||||
|
||||
const defaultLanguageCode = "default";
|
||||
const firstQuestion = survey.questions[0];
|
||||
switch (firstQuestion.type) {
|
||||
case TSurveyQuestionType.OpenText:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-slate-200 bg-slate-50" />
|
||||
<EmailFooter />
|
||||
@@ -70,14 +71,20 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
<Text
|
||||
className="m-0 p-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
|
||||
}}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 font-medium text-slate-800">
|
||||
<Text className="m-0 inline-block">{firstQuestion.label}</Text>
|
||||
<Text className="m-0 inline-block">
|
||||
{getLocalizedValue(firstQuestion.label, defaultLanguageCode)}
|
||||
</Text>
|
||||
</Container>
|
||||
<Container className="mx-0 mt-4 flex max-w-none justify-end">
|
||||
{!firstQuestion.required && (
|
||||
@@ -104,10 +111,10 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex w-max flex-col">
|
||||
<Section className="block overflow-hidden rounded-md border border-slate-200">
|
||||
@@ -123,10 +130,14 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<Section className="mt-2 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block w-max p-0">{firstQuestion.lowerLabel}</Text>
|
||||
<Text className="m-0 inline-block w-max p-0">
|
||||
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">
|
||||
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
@@ -139,10 +150,14 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="mt-2 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
<Text
|
||||
className="m-0 p-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
|
||||
}}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mx-0 mt-4 max-w-none">
|
||||
@@ -150,7 +165,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
{firstQuestion.dismissButtonLabel || "Skip"}
|
||||
{getLocalizedValue(firstQuestion.dismissButtonLabel, defaultLanguageCode) || "Skip"}
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
@@ -159,7 +174,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
"bg-brand-color inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium",
|
||||
isLight(brandColor) ? "text-black" : "text-white"
|
||||
)}>
|
||||
{firstQuestion.buttonLabel}
|
||||
{getLocalizedValue(firstQuestion.buttonLabel, defaultLanguageCode)}
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
@@ -170,10 +185,10 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section className=" w-full">
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 w-full items-center justify-center">
|
||||
<Section
|
||||
@@ -205,10 +220,14 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<Section className="m-0 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block p-0">{firstQuestion.lowerLabel}</Text>
|
||||
<Text className="m-0 inline-block p-0">
|
||||
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
<Text className="m-0 inline-block p-0 text-right">
|
||||
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
@@ -221,17 +240,17 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices.map((choice) => (
|
||||
<Section
|
||||
className="mt-2 block w-full rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800"
|
||||
key={choice.id}>
|
||||
{choice.label}
|
||||
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
||||
</Section>
|
||||
))}
|
||||
</Container>
|
||||
@@ -242,10 +261,10 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices
|
||||
@@ -255,7 +274,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
key={choice.id}
|
||||
className="mt-2 block rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.label}`}>
|
||||
{choice.label}
|
||||
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
@@ -266,10 +285,10 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Section className="mx-0">
|
||||
{firstQuestion.choices.map((choice) =>
|
||||
@@ -295,7 +314,7 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
You have been invited to schedule a meet via cal.com Open Survey to continue{" "}
|
||||
@@ -307,10 +326,10 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Section className="mt-4 flex h-12 w-full items-center justify-center rounded-lg border border-solid border-slate-200 bg-white">
|
||||
<CalendarDaysIcon className="mb-1 inline h-4 w-4" />
|
||||
|
||||
@@ -51,7 +51,6 @@ export default async function Page({ params }) {
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import clsx from "clsx";
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import useClickOutside from "@formbricks/lib/useClickOutside";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "@formbricks/ui/Command";
|
||||
@@ -21,7 +23,7 @@ type QuestionFilterComboBoxProps = {
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
onChangeFilterComboBoxValue: (o: string | string[]) => void;
|
||||
type: TSurveyQuestionType | "Attributes" | "Tags" | undefined;
|
||||
type: OptionsType.METADATA | TSurveyQuestionType | OptionsType.ATTRIBUTES | OptionsType.TAGS | undefined;
|
||||
handleRemoveMultiSelect: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -40,6 +42,7 @@ const QuestionFilterComboBox = ({
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [openFilterValue, setOpenFilterValue] = React.useState<boolean>(false);
|
||||
const commandRef = React.useRef(null);
|
||||
const defaultLanguageCode = "default";
|
||||
useClickOutside(commandRef, () => setOpen(false));
|
||||
|
||||
// multiple when question type is multi selection
|
||||
@@ -150,14 +153,21 @@ const QuestionFilterComboBox = ({
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
!isMultiple
|
||||
? onChangeFilterComboBoxValue(o)
|
||||
? onChangeFilterComboBoxValue(
|
||||
typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o
|
||||
)
|
||||
: onChangeFilterComboBoxValue(
|
||||
Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, o] : [o]
|
||||
Array.isArray(filterComboBoxValue)
|
||||
? [
|
||||
...filterComboBoxValue,
|
||||
typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o,
|
||||
]
|
||||
: [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o]
|
||||
);
|
||||
!isMultiple && setOpen(false);
|
||||
}}
|
||||
className="cursor-pointer">
|
||||
{o}
|
||||
{typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -8,14 +8,16 @@ import {
|
||||
HashIcon,
|
||||
HelpCircleIcon,
|
||||
ImageIcon,
|
||||
LanguagesIcon,
|
||||
ListIcon,
|
||||
MousePointerClickIcon,
|
||||
Rows3Icon,
|
||||
SmartphoneIcon,
|
||||
StarIcon,
|
||||
TagIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import useClickOutside from "@formbricks/lib/useClickOutside";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import {
|
||||
@@ -32,6 +34,7 @@ export enum OptionsType {
|
||||
QUESTIONS = "Questions",
|
||||
TAGS = "Tags",
|
||||
ATTRIBUTES = "Attributes",
|
||||
METADATA = "Metadata",
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
@@ -53,31 +56,37 @@ interface QuestionComboBoxProps {
|
||||
|
||||
const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
|
||||
const getIconType = () => {
|
||||
if (type === OptionsType.QUESTIONS) {
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionType.Rating:
|
||||
return <StarIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.CTA:
|
||||
return <MousePointerClickIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.OpenText:
|
||||
return <HelpCircleIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceMulti:
|
||||
return <ListIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceSingle:
|
||||
return <Rows3Icon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.NPS:
|
||||
return <NetPromoterScoreIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.Consent:
|
||||
return <CheckIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.PictureSelection:
|
||||
return <ImageIcon width={18} className="text-white" />;
|
||||
}
|
||||
}
|
||||
if (type === OptionsType.ATTRIBUTES) {
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
}
|
||||
if (type === OptionsType.TAGS) {
|
||||
return <TagIcon width={18} className="text-white" />;
|
||||
switch (type) {
|
||||
case OptionsType.QUESTIONS:
|
||||
switch (questionType) {
|
||||
case TSurveyQuestionType.Rating:
|
||||
return <StarIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.CTA:
|
||||
return <MousePointerClickIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.OpenText:
|
||||
return <HelpCircleIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceMulti:
|
||||
return <ListIcon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.MultipleChoiceSingle:
|
||||
return <Rows3Icon width={18} className="text-white" />;
|
||||
case TSurveyQuestionType.NPS:
|
||||
return <NetPromoterScoreIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.Consent:
|
||||
return <CheckIcon width={18} height={18} className="text-white" />;
|
||||
case TSurveyQuestionType.PictureSelection:
|
||||
return <ImageIcon width={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.ATTRIBUTES:
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
case OptionsType.METADATA:
|
||||
switch (label) {
|
||||
case "Language":
|
||||
return <LanguagesIcon width={18} height={18} className="text-white" />;
|
||||
case "Device Type":
|
||||
return <SmartphoneIcon width={18} height={18} className="text-white" />;
|
||||
}
|
||||
case OptionsType.TAGS:
|
||||
return <HashIcon width={18} className="text-white" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,6 +95,8 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
return "bg-indigo-500";
|
||||
} else if (type === OptionsType.QUESTIONS) {
|
||||
return "bg-brand-dark";
|
||||
} else if (type === OptionsType.TAGS) {
|
||||
return "bg-indigo-500";
|
||||
} else {
|
||||
return "bg-amber-500";
|
||||
}
|
||||
@@ -93,7 +104,9 @@ const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOpti
|
||||
return (
|
||||
<div className="flex h-5 w-[12rem] items-center sm:w-4/5">
|
||||
<span className={clsx("rounded-md p-1", getColor())}>{getIconType()}</span>
|
||||
<p className="ml-3 truncate text-base text-slate-600">{label}</p>
|
||||
<p className="ml-3 truncate text-base text-slate-600">
|
||||
{typeof label === "string" ? label : getLocalizedValue(label, "default")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover"
|
||||
import QuestionsComboBox, { OptionsType, QuestionOption } from "./QuestionsComboBox";
|
||||
|
||||
export type QuestionFilterOptions = {
|
||||
type: TSurveyTSurveyQuestionType | "Attributes" | "Tags";
|
||||
type: TSurveyTSurveyQuestionType | "Attributes" | "Tags" | "Languages";
|
||||
filterOptions: string[];
|
||||
filterComboBoxOptions: string[];
|
||||
id: string;
|
||||
@@ -200,7 +200,9 @@ const ResponseFilter = () => {
|
||||
key={`${s.questionType.id}-${i}`}
|
||||
filterOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) => q.type === s.questionType.type || q.type === s.questionType.questionType
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
)?.filterOptions
|
||||
}
|
||||
filterComboBoxOptions={
|
||||
@@ -240,15 +242,15 @@ const ResponseFilter = () => {
|
||||
</>
|
||||
))}
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Button size="sm" variant="darkCTA" onClick={handleAddNewFilter}>
|
||||
<Button size="sm" variant="secondary" onClick={handleAddNewFilter}>
|
||||
Add filter
|
||||
<Plus width={18} height={18} className="ml-2" />
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="primary" onClick={handleApplyFilters}>
|
||||
Apply Filters
|
||||
<Button size="sm" variant="darkCTA" onClick={handleApplyFilters}>
|
||||
Apply filters
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={handleClearAllFilters}>
|
||||
<Button size="sm" variant="minimal" onClick={handleClearAllFilters}>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { DownloadIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import {
|
||||
@@ -25,13 +24,11 @@ import ShareSurveyResults from "../(analysis)/summary/components/ShareSurveyResu
|
||||
|
||||
interface ResultsShareButtonProps {
|
||||
survey: TSurvey;
|
||||
className?: string;
|
||||
webAppUrl: string;
|
||||
product: TProduct;
|
||||
user: TUser;
|
||||
}
|
||||
|
||||
export default function ResultsShareButton({ survey, webAppUrl, product, user }: ResultsShareButtonProps) {
|
||||
export default function ResultsShareButton({ survey, webAppUrl, user }: ResultsShareButtonProps) {
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [showResultsLinkModal, setShowResultsLinkModal] = useState(false);
|
||||
|
||||
@@ -146,7 +143,6 @@ export default function ResultsShareButton({ survey, webAppUrl, product, user }:
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
product={product}
|
||||
webAppUrl={webAppUrl}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -104,13 +104,7 @@ const SummaryHeader = ({
|
||||
<DropdownMenuContent align="end" className="p-2">
|
||||
{survey.type === "link" && (
|
||||
<>
|
||||
<ResultsShareButton
|
||||
className="flex w-full justify-center p-1"
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
@@ -189,19 +183,12 @@ const SummaryHeader = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<SuccessMessage
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
<SuccessMessage environment={environment} survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
{showShareSurveyModal && (
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showShareSurveyModal}
|
||||
setOpen={setShowShareSurveyModal}
|
||||
product={product}
|
||||
webAppUrl={webAppUrl}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function AddQuestionButton({ addQuestion, product }: AddQuestionB
|
||||
className="mx-2 inline-flex items-center rounded p-0.5 px-4 py-2 font-medium text-slate-700 last:mb-2 hover:bg-slate-100 hover:text-slate-800"
|
||||
onClick={() => {
|
||||
addQuestion({
|
||||
id: createId(),
|
||||
type: questionType.id,
|
||||
...universalQuestionPresets,
|
||||
...getQuestionDefaults(questionType.id, product),
|
||||
id: createId(),
|
||||
type: questionType.id,
|
||||
});
|
||||
setOpen(false);
|
||||
}}>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { BackButtonInput } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard";
|
||||
import { useState } from "react";
|
||||
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
import { Editor } from "@formbricks/ui/Editor";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
|
||||
interface CTAQuestionFormProps {
|
||||
@@ -17,6 +15,8 @@ interface CTAQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -27,38 +27,38 @@ export default function CTAQuestionForm({
|
||||
lastQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: CTAQuestionFormProps): JSX.Element {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Description</Label>
|
||||
<div className="mt-2">
|
||||
<Editor
|
||||
getText={() =>
|
||||
md.render(
|
||||
question.html || "We would love to talk to you and learn more about how you use our product."
|
||||
)
|
||||
}
|
||||
setText={(value: string) => {
|
||||
updateQuestion(questionIdx, { html: value });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
<LocalizedEditor
|
||||
id="subheader"
|
||||
value={question.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={questionIdx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,24 +82,33 @@ export default function CTAQuestionForm({
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="mt-2 flex justify-between gap-8">
|
||||
<div className="flex w-full space-x-2">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
{questionIdx !== 0 && (
|
||||
<BackButtonInput
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -124,12 +133,16 @@ export default function CTAQuestionForm({
|
||||
<div className="mt-3 flex-1">
|
||||
<Label htmlFor="buttonLabel">Skip Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
<QuestionFormInput
|
||||
id="dismissButtonLabel"
|
||||
name="dismissButtonLabel"
|
||||
value={question.dismissButtonLabel}
|
||||
placeholder="Skip"
|
||||
onChange={(e) => updateQuestion(questionIdx, { dismissButtonLabel: e.target.value })}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
interface CalQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -13,6 +14,8 @@ interface CalQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -21,43 +24,49 @@ export default function CalQuestionForm({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
}: CalQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -65,7 +74,13 @@ export default function CalQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys";
|
||||
import { Editor } from "@formbricks/ui/Editor";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
interface ConsentQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyConsentQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -23,65 +23,54 @@ export default function ConsentQuestionForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: ConsentQuestionFormProps): JSX.Element {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Description</Label>
|
||||
<div className="mt-2">
|
||||
<Editor
|
||||
getText={() =>
|
||||
md.render(
|
||||
question.html || "We would love to talk to you and learn more about how you use our product."
|
||||
)
|
||||
}
|
||||
setText={(value: string) => {
|
||||
updateQuestion(questionIdx, { html: value });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
<LocalizedEditor
|
||||
id="subheader"
|
||||
value={question.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={questionIdx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="label">Checkbox Label</Label>
|
||||
<Input
|
||||
id="label"
|
||||
name="label"
|
||||
className="mt-2"
|
||||
value={question.label}
|
||||
placeholder="I agree to the terms and conditions"
|
||||
onChange={(e) => updateQuestion(questionIdx, { label: e.target.value })}
|
||||
isInvalid={isInvalid && question.label.trim() === ""}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="mt-3">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
className="mt-2"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div> */}
|
||||
<QuestionFormInput
|
||||
id="label"
|
||||
label="Checkbox Label"
|
||||
placeholder="I agree to the terms and conditions"
|
||||
value={question.label}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { OptionsSwitcher } from "@formbricks/ui/QuestionTypeSelector";
|
||||
|
||||
interface IDateQuestionFormProps {
|
||||
@@ -13,6 +14,8 @@ interface IDateQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -37,42 +40,48 @@ export default function DateQuestionForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: IDateQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
type="headline"
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!showSubheader && (
|
||||
@@ -81,7 +90,13 @@ export default function DateQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -4,10 +4,11 @@ import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
interface EditThankYouCardProps {
|
||||
@@ -15,6 +16,9 @@ interface EditThankYouCardProps {
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId: string | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
}
|
||||
|
||||
export default function EditThankYouCard({
|
||||
@@ -22,11 +26,16 @@ export default function EditThankYouCard({
|
||||
setLocalSurvey,
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: EditThankYouCardProps) {
|
||||
// const [open, setOpen] = useState(false);
|
||||
let open = activeQuestionId == "end";
|
||||
const [showThankYouCardCTA, setshowThankYouCardCTA] = useState<boolean>(
|
||||
localSurvey.thankYouCard.buttonLabel || localSurvey.thankYouCard.buttonLink ? true : false
|
||||
getLocalizedValue(localSurvey.thankYouCard.buttonLabel, "default") || localSurvey.thankYouCard.buttonLink
|
||||
? true
|
||||
: false
|
||||
);
|
||||
const setOpen = (e) => {
|
||||
if (e) {
|
||||
@@ -37,13 +46,14 @@ export default function EditThankYouCard({
|
||||
};
|
||||
|
||||
const updateSurvey = (data) => {
|
||||
setLocalSurvey({
|
||||
const updatedSurvey = {
|
||||
...localSurvey,
|
||||
thankYouCard: {
|
||||
...localSurvey.thankYouCard,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
};
|
||||
setLocalSurvey(updatedSurvey);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -54,8 +64,9 @@ export default function EditThankYouCard({
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
open ? "bg-slate-50" : "",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none",
|
||||
isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50"
|
||||
)}>
|
||||
<p>🙏</p>
|
||||
</div>
|
||||
@@ -97,28 +108,26 @@ export default function EditThankYouCard({
|
||||
<Collapsible.CollapsibleContent className="px-4 pb-6">
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={localSurvey?.thankYouCard?.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={false}
|
||||
questionId="end"
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
localSurvey={localSurvey}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={false}
|
||||
questionId="end"
|
||||
questionIdx={localSurvey.questions.length}
|
||||
updateSurvey={updateSurvey}
|
||||
type="subheader"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={localSurvey.thankYouCard.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch
|
||||
@@ -129,7 +138,7 @@ export default function EditThankYouCard({
|
||||
updateSurvey({ buttonLabel: undefined, buttonLink: undefined });
|
||||
} else {
|
||||
updateSurvey({
|
||||
buttonLabel: "Create your own Survey",
|
||||
buttonLabel: { default: "Create your own Survey" },
|
||||
buttonLink: "https://formbricks.com/signup",
|
||||
});
|
||||
}
|
||||
@@ -146,16 +155,20 @@ export default function EditThankYouCard({
|
||||
</Label>
|
||||
</div>
|
||||
{showThankYouCardCTA && (
|
||||
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4">
|
||||
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4 pt-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Button Label</Label>
|
||||
<Input
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
className="bg-white"
|
||||
label="Button Label"
|
||||
placeholder="Create your own Survey"
|
||||
className="bg-white"
|
||||
value={localSurvey.thankYouCard.buttonLabel}
|
||||
onChange={(e) => updateSurvey({ buttonLabel: e.target.value })}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,13 +4,12 @@ import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Editor } from "@formbricks/ui/Editor";
|
||||
import FileInput from "@formbricks/ui/FileInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
interface EditWelcomeCardProps {
|
||||
@@ -18,6 +17,9 @@ interface EditWelcomeCardProps {
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
setActiveQuestionId: (id: string | null) => void;
|
||||
activeQuestionId: string | null;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
}
|
||||
|
||||
export default function EditWelcomeCard({
|
||||
@@ -25,6 +27,9 @@ export default function EditWelcomeCard({
|
||||
setLocalSurvey,
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: EditWelcomeCardProps) {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const path = usePathname();
|
||||
@@ -59,8 +64,9 @@ export default function EditWelcomeCard({
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
|
||||
open ? "bg-slate-50" : "",
|
||||
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none",
|
||||
isInvalid ? "bg-red-400" : "bg-white group-hover:bg-slate-50"
|
||||
)}>
|
||||
<p>✋</p>
|
||||
</div>
|
||||
@@ -84,7 +90,7 @@ export default function EditWelcomeCard({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="welcome-toggle">Enabled</Label>
|
||||
<Label htmlFor="welcome-toggle">{localSurvey?.welcomeCard?.enabled ? "On" : "Off"}</Label>
|
||||
|
||||
<Switch
|
||||
id="welcome-toggle"
|
||||
@@ -114,34 +120,32 @@ export default function EditWelcomeCard({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Headline</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="headline"
|
||||
name="headline"
|
||||
defaultValue={localSurvey?.welcomeCard?.headline}
|
||||
onChange={(e) => {
|
||||
updateSurvey({ headline: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={localSurvey.welcomeCard.headline}
|
||||
label="Headline"
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={-1}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Welcome Message</Label>
|
||||
<div className="mt-2">
|
||||
<Editor
|
||||
getText={() =>
|
||||
md.render(
|
||||
localSurvey?.welcomeCard?.html || "Thanks for providing your feedback - let's go!"
|
||||
)
|
||||
}
|
||||
setText={(value: string) => {
|
||||
updateSurvey({ html: value });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
<LocalizedEditor
|
||||
id="html"
|
||||
value={localSurvey.welcomeCard.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,15 +153,18 @@ export default function EditWelcomeCard({
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="flex w-full space-x-2">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
defaultValue={localSurvey?.welcomeCard?.buttonLabel || "Next"}
|
||||
onChange={(e) => updateSurvey({ buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={localSurvey.welcomeCard.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={-1}
|
||||
maxLength={48}
|
||||
placeholder={"Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { PlusIcon, TrashIcon, XCircleIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { useGetBillingInfo } from "@formbricks/lib/team/hooks/useGetBillingInfo";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
@@ -11,7 +13,7 @@ import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
interface FileUploadFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -20,6 +22,8 @@ interface FileUploadFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -30,6 +34,8 @@ export default function FileUploadQuestionForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
product,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: FileUploadFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [extension, setExtension] = useState("");
|
||||
@@ -38,6 +44,7 @@ export default function FileUploadQuestionForm({
|
||||
error: billingInfoError,
|
||||
isLoading: billingInfoLoading,
|
||||
} = useGetBillingInfo(product?.teamId ?? "");
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setExtension(event.target.value);
|
||||
@@ -103,41 +110,42 @@ export default function FileUploadQuestionForm({
|
||||
return 10;
|
||||
}, [billingInfo, billingInfoError, billingInfoLoading]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -145,7 +153,13 @@ export default function FileUploadQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -74,7 +74,9 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="hidden-fields-toggle">Enabled</Label>
|
||||
<Label htmlFor="hidden-fields-toggle">
|
||||
{localSurvey?.hiddenFields?.enabled ? "On" : "Off"}
|
||||
</Label>
|
||||
|
||||
<Switch
|
||||
id="hidden-fields-toggle"
|
||||
@@ -106,7 +108,9 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-sm italic text-slate-500">No hidden fields yet. Add the first one below.</p>
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
No hidden fields yet. Add the first one below.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
@@ -173,7 +177,17 @@ const validateHiddenField = (
|
||||
}
|
||||
// no key words -- userId & suid & existing question ids
|
||||
if (
|
||||
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(field) ||
|
||||
[
|
||||
"userId",
|
||||
"source",
|
||||
"suid",
|
||||
"end",
|
||||
"start",
|
||||
"welcomeCard",
|
||||
"hidden",
|
||||
"verifiedEmail",
|
||||
"multiLanguage",
|
||||
].includes(field) ||
|
||||
existingQuestions.findIndex((q) => q.id === field) !== -1
|
||||
) {
|
||||
return "Question not allowed";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
export const LoadingSkeleton = () => (
|
||||
<div className="flex h-full w-full flex-col items-center justify-between p-6">
|
||||
{/* Top Part - Loading Navbar */}
|
||||
<div className="flex h-[10vh] w-full animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
|
||||
|
||||
{/* Bottom Part - Divided into Left and Right */}
|
||||
<div className="mt-4 flex h-[85%] w-full flex-row">
|
||||
{/* Left Part - 7 Horizontal Bars */}
|
||||
<div className="flex h-full w-1/2 flex-col justify-between space-y-2">
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
{/* Right Part - Simple Box */}
|
||||
<div className="ml-4 flex h-full w-1/2 flex-col">
|
||||
<div className="ph-no-capture h-full animate-pulse rounded-lg bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -4,6 +4,7 @@ import { useMemo } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { BsArrowDown, BsArrowReturnRight } from "react-icons/bs";
|
||||
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -46,12 +47,12 @@ export default function LogicEditor({
|
||||
updateQuestion,
|
||||
}: LogicEditorProps): JSX.Element {
|
||||
localSurvey = useMemo(() => {
|
||||
return checkForRecallInHeadline(localSurvey);
|
||||
return checkForRecallInHeadline(localSurvey, "default");
|
||||
}, [localSurvey]);
|
||||
|
||||
const questionValues = useMemo(() => {
|
||||
if ("choices" in question) {
|
||||
return question.choices.map((choice) => choice.label);
|
||||
return question.choices.map((choice) => getLocalizedValue(choice.label, "default"));
|
||||
} else if ("range" in question) {
|
||||
return Array.from({ length: question.range ? question.range : 0 }, (_, i) => (i + 1).toString());
|
||||
} else if (question.type === TSurveyQuestionType.NPS) {
|
||||
@@ -238,7 +239,7 @@ export default function LogicEditor({
|
||||
};
|
||||
|
||||
const deleteLogic = (logicIdx: number) => {
|
||||
const updatedLogic = !question.logic ? [] : JSON.parse(JSON.stringify(question.logic));
|
||||
const updatedLogic = !question.logic ? [] : structuredClone(question.logic);
|
||||
updatedLogic.splice(logicIdx, 1);
|
||||
updateQuestion(questionIdx, { logic: updatedLogic });
|
||||
};
|
||||
@@ -348,9 +349,14 @@ export default function LogicEditor({
|
||||
{localSurvey.questions.map(
|
||||
(question, idx) =>
|
||||
idx !== questionIdx && (
|
||||
<SelectItem key={question.id} value={question.id} title={question.headline}>
|
||||
<div className="max-w-[6rem]">
|
||||
<p className="truncate text-left">{question.headline}</p>
|
||||
<SelectItem
|
||||
key={question.id}
|
||||
value={question.id}
|
||||
title={getLocalizedValue(question.headline, "default")}>
|
||||
<div className="w-40">
|
||||
<p className="truncate text-left">
|
||||
{getLocalizedValue(question.headline, "default")}
|
||||
</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey, TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
|
||||
import {
|
||||
createI18nString,
|
||||
extractLanguageCodes,
|
||||
isLabelValidForAllLanguages,
|
||||
} from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TI18nString, TSurvey, TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
@@ -18,6 +23,8 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -27,12 +34,15 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const shuffleOptionsTypes = {
|
||||
none: {
|
||||
@@ -52,8 +62,8 @@ export default function MultipleChoiceMultiForm({
|
||||
},
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => {
|
||||
const newLabel = updatedAttributes.label;
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
const newLabel = updatedAttributes.label.en;
|
||||
const oldLabel = question.choices[choiceIdx].label;
|
||||
let newChoices: any[] = [];
|
||||
if (question.choices) {
|
||||
@@ -67,9 +77,11 @@ export default function MultipleChoiceMultiForm({
|
||||
question.logic?.forEach((logic) => {
|
||||
let newL: string | string[] | undefined = logic.value;
|
||||
if (Array.isArray(logic.value)) {
|
||||
newL = logic.value.map((value) => (value === oldLabel ? newLabel : value));
|
||||
newL = logic.value.map((value) =>
|
||||
newLabel && value === oldLabel[selectedLanguageCode] ? newLabel : value
|
||||
);
|
||||
} else {
|
||||
newL = logic.value === oldLabel ? newLabel : logic.value;
|
||||
newL = logic.value === oldLabel[selectedLanguageCode] ? newLabel : logic.value;
|
||||
}
|
||||
newLogic.push({ ...logic, value: newL });
|
||||
});
|
||||
@@ -79,21 +91,17 @@ export default function MultipleChoiceMultiForm({
|
||||
const findDuplicateLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
for (let j = i + 1; j < question.choices.length; j++) {
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
if (
|
||||
getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim() ===
|
||||
getLocalizedValue(question.choices[j].label, selectedLanguageCode).trim()
|
||||
) {
|
||||
return getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx?: number) => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
@@ -101,7 +109,10 @@ export default function MultipleChoiceMultiForm({
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
const newChoice = { id: createId(), label: "" };
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
if (choiceIdx !== undefined) {
|
||||
newChoices.splice(choiceIdx + 1, 0, newChoice);
|
||||
} else {
|
||||
@@ -116,7 +127,10 @@ export default function MultipleChoiceMultiForm({
|
||||
const addOther = () => {
|
||||
if (question.choices.filter((c) => c.id === "other").length === 0) {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
newChoices.push({
|
||||
id: "other",
|
||||
label: createI18nString("Other", surveyLanguageCodes),
|
||||
});
|
||||
updateQuestion(questionIdx, {
|
||||
choices: newChoices,
|
||||
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
|
||||
@@ -129,7 +143,7 @@ export default function MultipleChoiceMultiForm({
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
@@ -160,43 +174,43 @@ export default function MultipleChoiceMultiForm({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
ref={questionRef}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -204,7 +218,13 @@ export default function MultipleChoiceMultiForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -213,45 +233,55 @@ export default function MultipleChoiceMultiForm({
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="choices">Options</Label>
|
||||
<div className="mt-2 space-y-2" id="choices">
|
||||
<div className="mt-2 -space-y-2" id="choices">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="inline-flex w-full items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
<div className="w-full space-x-2">
|
||||
<QuestionFormInput
|
||||
key={choice.id}
|
||||
id={`choice-${choiceIdx}`}
|
||||
localSurvey={localSurvey}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
questionIdx={questionIdx}
|
||||
value={choice.label}
|
||||
onBlur={() => {
|
||||
const duplicateLabel = findDuplicateLabel();
|
||||
if (duplicateLabel) {
|
||||
toast.error("Duplicate choices");
|
||||
setisInvalidValue(duplicateLabel);
|
||||
} else if (findEmptyLabel()) {
|
||||
setisInvalidValue("");
|
||||
} else {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
}}
|
||||
updateChoice={updateChoice}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguageCodes)
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<Input
|
||||
id="otherInputLabel"
|
||||
name="otherInputLabel"
|
||||
value={question.otherOptionPlaceholder ?? "Please specify"}
|
||||
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { otherOptionPlaceholder: e.target.value });
|
||||
}}
|
||||
<QuestionFormInput
|
||||
id="otherOptionPlaceholder"
|
||||
localSurvey={localSurvey}
|
||||
placeholder={"Please specify"}
|
||||
questionIdx={questionIdx}
|
||||
value={
|
||||
question.otherOptionPlaceholder
|
||||
? question.otherOptionPlaceholder
|
||||
: createI18nString("Please specify", surveyLanguageCodes)
|
||||
}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguageCodes)
|
||||
}
|
||||
className="border border-dashed"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { isLabelValidForAllLanguages } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/Validation";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey, TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TI18nString, TSurvey, TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
@@ -18,6 +20,8 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -27,13 +31,16 @@ export default function MultipleChoiceSingleForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const surveyLanguages = localSurvey.languages ?? [];
|
||||
const shuffleOptionsTypes = {
|
||||
none: {
|
||||
id: "none",
|
||||
@@ -55,23 +62,19 @@ export default function MultipleChoiceSingleForm({
|
||||
const findDuplicateLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
for (let j = i + 1; j < question.choices.length; j++) {
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
if (
|
||||
getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim() ===
|
||||
getLocalizedValue(question.choices[j].label, selectedLanguageCode).trim()
|
||||
) {
|
||||
return getLocalizedValue(question.choices[i].label, selectedLanguageCode).trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => {
|
||||
const newLabel = updatedAttributes.label;
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
const newLabel = updatedAttributes.label.en;
|
||||
const oldLabel = question.choices[choiceIdx].label;
|
||||
let newChoices: any[] = [];
|
||||
if (question.choices) {
|
||||
@@ -87,7 +90,7 @@ export default function MultipleChoiceSingleForm({
|
||||
if (Array.isArray(logic.value)) {
|
||||
newL = logic.value.map((value) => (value === oldLabel ? newLabel : value));
|
||||
} else {
|
||||
newL = logic.value === oldLabel ? newLabel : logic.value;
|
||||
newL = logic.value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : logic.value;
|
||||
}
|
||||
newLogic.push({ ...logic, value: newL });
|
||||
});
|
||||
@@ -101,7 +104,10 @@ export default function MultipleChoiceSingleForm({
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
const newChoice = { id: createId(), label: "" };
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
if (choiceIdx !== undefined) {
|
||||
newChoices.splice(choiceIdx + 1, 0, newChoice);
|
||||
} else {
|
||||
@@ -116,7 +122,10 @@ export default function MultipleChoiceSingleForm({
|
||||
const addOther = () => {
|
||||
if (question.choices.filter((c) => c.id === "other").length === 0) {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((c) => c.id !== "other");
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
newChoices.push({
|
||||
id: "other",
|
||||
label: createI18nString("Other", surveyLanguageCodes),
|
||||
});
|
||||
updateQuestion(questionIdx, {
|
||||
choices: newChoices,
|
||||
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
|
||||
@@ -128,8 +137,7 @@ export default function MultipleChoiceSingleForm({
|
||||
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
@@ -160,43 +168,43 @@ export default function MultipleChoiceSingleForm({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
ref={questionRef}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -204,7 +212,13 @@ export default function MultipleChoiceSingleForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -213,45 +227,55 @@ export default function MultipleChoiceSingleForm({
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="choices">Options</Label>
|
||||
<div className="mt-2 space-y-2" id="choices">
|
||||
<div className="mt-2 -space-y-2" id="choices">
|
||||
{question.choices &&
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
<QuestionFormInput
|
||||
key={choice.id}
|
||||
id={`choice-${choiceIdx}`}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
value={choice.label}
|
||||
onBlur={() => {
|
||||
const duplicateLabel = findDuplicateLabel();
|
||||
if (duplicateLabel) {
|
||||
toast.error("Duplicate choices");
|
||||
setisInvalidValue(duplicateLabel);
|
||||
} else if (findEmptyLabel()) {
|
||||
setisInvalidValue("");
|
||||
} else {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
updateChoice={updateChoice}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<Input
|
||||
id="otherInputLabel"
|
||||
name="otherInputLabel"
|
||||
value={question.otherOptionPlaceholder ?? "Please specify"}
|
||||
placeholder={question.otherOptionPlaceholder ?? "Please specify"}
|
||||
className={cn(choice.id === "other" && "border-dashed")}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { otherOptionPlaceholder: e.target.value });
|
||||
}}
|
||||
<QuestionFormInput
|
||||
id="otherOptionPlaceholder"
|
||||
localSurvey={localSurvey}
|
||||
placeholder={"Please specify"}
|
||||
questionIdx={questionIdx}
|
||||
value={
|
||||
question.otherOptionPlaceholder
|
||||
? question.otherOptionPlaceholder
|
||||
: createI18nString("Please specify", surveyLanguageCodes)
|
||||
}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
}
|
||||
className="border border-dashed"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
interface NPSQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -15,6 +14,8 @@ interface NPSQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -25,44 +26,48 @@ export default function NPSQuestionForm({
|
||||
lastQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: NPSQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className=" flex w-full items-center">
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -70,50 +75,60 @@ export default function NPSQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex justify-between">
|
||||
<div>
|
||||
<Label htmlFor="subheader">Lower label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="subheader"
|
||||
name="subheader"
|
||||
value={question.lowerLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { lowerLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex justify-between space-x-2">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="lowerLabel"
|
||||
value={question.lowerLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="subheader">Upper label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="subheader"
|
||||
name="subheader"
|
||||
value={question.upperLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { upperLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="upperLabel"
|
||||
value={question.upperLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!question.required && (
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="buttonLabel">Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@@ -11,15 +11,15 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import {
|
||||
TSurvey,
|
||||
TSurveyOpenTextQuestion,
|
||||
TSurveyOpenTextQuestionInputType,
|
||||
} from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { OptionsSwitcher } from "@formbricks/ui/QuestionTypeSelector";
|
||||
|
||||
const questionTypes = [
|
||||
@@ -36,6 +36,8 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -45,55 +47,58 @@ export default function OpenQuestionForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
|
||||
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
const handleInputChange = (inputType: TSurveyOpenTextQuestionInputType) => {
|
||||
const updatedAttributes = {
|
||||
inputType: inputType,
|
||||
placeholder: getPlaceholderByInputType(inputType),
|
||||
placeholder: createI18nString(getPlaceholderByInputType(inputType), surveyLanguageCodes),
|
||||
longAnswer: inputType === "text" ? question.longAnswer : false,
|
||||
};
|
||||
updateQuestion(questionIdx, updatedAttributes);
|
||||
};
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -101,23 +106,32 @@ export default function OpenQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="placeholder">Placeholder</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="placeholder"
|
||||
name="placeholder"
|
||||
value={question.placeholder ?? defaultPlaceholder}
|
||||
onChange={(e) => updateQuestion(questionIdx, { placeholder: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<QuestionFormInput
|
||||
id="placeholder"
|
||||
value={
|
||||
question.placeholder
|
||||
? question.placeholder
|
||||
: createI18nString(defaultPlaceholder, surveyLanguageCodes)
|
||||
}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Add a dropdown to select the question type */}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import FileInput from "@formbricks/ui/FileInput";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
interface PictureSelectionFormProps {
|
||||
@@ -16,6 +17,8 @@ interface PictureSelectionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -24,44 +27,50 @@ export default function PictureSelectionForm({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
}: PictureSelectionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -69,7 +78,13 @@ export default function PictureSelectionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -26,9 +26,9 @@ import { Draggable } from "react-beautiful-dnd";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { TI18nString, TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import CTAQuestionForm from "./CTAQuestionForm";
|
||||
@@ -44,7 +44,7 @@ import RatingQuestionForm from "./RatingQuestionForm";
|
||||
|
||||
interface QuestionCardProps {
|
||||
localSurvey: TSurvey;
|
||||
product?: TProduct;
|
||||
product: TProduct;
|
||||
questionIdx: number;
|
||||
moveQuestion: (questionIndex: number, up: boolean) => void;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
@@ -53,35 +53,11 @@ interface QuestionCardProps {
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
export function BackButtonInput({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: {
|
||||
value: string | undefined;
|
||||
onChange: (e: any) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Label htmlFor="backButtonLabel">"Back" Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="backButtonLabel"
|
||||
name="backButtonLabel"
|
||||
value={value}
|
||||
placeholder="Back"
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function QuestionCard({
|
||||
localSurvey,
|
||||
product,
|
||||
@@ -93,6 +69,8 @@ export default function QuestionCard({
|
||||
activeQuestionId,
|
||||
setActiveQuestionId,
|
||||
lastQuestion,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
}: QuestionCardProps) {
|
||||
const question = localSurvey.questions[questionIdx];
|
||||
@@ -118,10 +96,10 @@ export default function QuestionCard({
|
||||
});
|
||||
};
|
||||
|
||||
const updateEmptyNextButtonLabels = (labelValue: string) => {
|
||||
const updateEmptyNextButtonLabels = (labelValue: TI18nString) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel?.trim() === "") {
|
||||
if (!q.buttonLabel || q.buttonLabel[selectedLanguageCode]?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
}
|
||||
});
|
||||
@@ -188,8 +166,14 @@ export default function QuestionCard({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(question.headline, localSurvey, true)
|
||||
? formatTextWithSlashes(recallToHeadline(question.headline, localSurvey, true))
|
||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
] ?? ""
|
||||
)
|
||||
: getTSurveyQuestionTypeName(question.type)}
|
||||
</p>
|
||||
{!open && question?.required && (
|
||||
@@ -219,6 +203,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
@@ -228,6 +214,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
@@ -237,6 +225,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
@@ -246,6 +236,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
@@ -255,6 +247,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
@@ -264,6 +258,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
@@ -272,6 +268,8 @@ export default function QuestionCard({
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
@@ -281,6 +279,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
@@ -290,6 +290,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||
@@ -300,6 +302,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
@@ -309,6 +313,8 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : null}
|
||||
@@ -327,34 +333,43 @@ export default function QuestionCard({
|
||||
{question.type !== TSurveyQuestionType.NPS &&
|
||||
question.type !== TSurveyQuestionType.Rating &&
|
||||
question.type !== TSurveyQuestionType.CTA ? (
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="buttonLabel">"Next" Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
maxLength={48}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => {
|
||||
updateQuestion(questionIdx, { buttonLabel: e.target.value });
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
//If it is the last question then do not update labels
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
onBlur={(e) => {
|
||||
if (!question.buttonLabel) return;
|
||||
let translatedNextButtonLabel = {
|
||||
...question.buttonLabel,
|
||||
[selectedLanguageCode]: e.target.value,
|
||||
};
|
||||
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(translatedNextButtonLabel);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
<BackButtonInput
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { backButtonLabel: e.target.value });
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -363,12 +378,17 @@ export default function QuestionCard({
|
||||
question.type === TSurveyQuestionType.NPS) &&
|
||||
questionIdx !== 0 && (
|
||||
<div className="mt-4">
|
||||
<BackButtonInput
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { backButtonLabel: e.target.value });
|
||||
}}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface QuestionsAudienceTabsProps {
|
||||
|
||||
export default function QuestionsAudienceTabs({ activeId, setActiveId }: QuestionsAudienceTabsProps) {
|
||||
return (
|
||||
<div className="fixed z-10 flex h-14 w-full items-center justify-center border bg-white md:w-1/2">
|
||||
<div className="fixed z-20 flex h-14 w-full items-center justify-center border bg-white md:w-1/2">
|
||||
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { DragDropContext } from "react-beautiful-dnd";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { MultiLanguageCard } from "@formbricks/ee/multiLanguage/components/MultiLanguageCard";
|
||||
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
|
||||
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
@@ -15,7 +17,7 @@ import EditThankYouCard from "./EditThankYouCard";
|
||||
import EditWelcomeCard from "./EditWelcomeCard";
|
||||
import QuestionCard from "./QuestionCard";
|
||||
import { StrictModeDroppable } from "./StrictModeDroppable";
|
||||
import { validateQuestion } from "./Validation";
|
||||
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "./Validation";
|
||||
|
||||
interface QuestionsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -23,8 +25,12 @@ interface QuestionsViewProps {
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
product: TProduct;
|
||||
invalidQuestions: String[] | null;
|
||||
setInvalidQuestions: (invalidQuestions: String[] | null) => void;
|
||||
invalidQuestions: string[] | null;
|
||||
setInvalidQuestions: (invalidQuestions: string[] | null) => void;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default function QuestionsView({
|
||||
@@ -35,6 +41,10 @@ export default function QuestionsView({
|
||||
product,
|
||||
invalidQuestions,
|
||||
setInvalidQuestions,
|
||||
setSelectedLanguageCode,
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
}: QuestionsViewProps) {
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
return localSurvey.questions.reduce((acc, question) => {
|
||||
@@ -42,13 +52,15 @@ export default function QuestionsView({
|
||||
return acc;
|
||||
}, {});
|
||||
}, [localSurvey.questions]);
|
||||
|
||||
const surveyLanguages = localSurvey.languages;
|
||||
const [backButtonLabel, setbackButtonLabel] = useState(null);
|
||||
|
||||
const handleQuestionLogicChange = (survey: TSurvey, compareId: string, updatedId: string): TSurvey => {
|
||||
survey.questions.forEach((question) => {
|
||||
if (question.headline.includes(`recall:${compareId}`)) {
|
||||
question.headline = question.headline.replaceAll(`recall:${compareId}`, `recall:${updatedId}`);
|
||||
if (question.headline[selectedLanguageCode].includes(`recall:${compareId}`)) {
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replaceAll(
|
||||
`recall:${compareId}`,
|
||||
`recall:${updatedId}`
|
||||
);
|
||||
}
|
||||
if (!question.logic) return;
|
||||
question.logic.forEach((rule) => {
|
||||
@@ -61,13 +73,13 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
// function to validate individual questions
|
||||
const validateSurvey = (question: TSurveyQuestion) => {
|
||||
const validateSurveyQuestion = (question: TSurveyQuestion) => {
|
||||
// prevent this function to execute further if user hasnt still tried to save the survey
|
||||
if (invalidQuestions === null) {
|
||||
return;
|
||||
}
|
||||
let temp = JSON.parse(JSON.stringify(invalidQuestions));
|
||||
if (validateQuestion(question)) {
|
||||
let temp = structuredClone(invalidQuestions);
|
||||
if (validateQuestion(question, surveyLanguages)) {
|
||||
temp = invalidQuestions.filter((id) => id !== question.id);
|
||||
setInvalidQuestions(temp);
|
||||
} else if (!invalidQuestions.includes(question.id)) {
|
||||
@@ -94,7 +106,6 @@ export default function QuestionsView({
|
||||
delete internalQuestionIdMap[localSurvey.questions[questionIdx].id];
|
||||
setActiveQuestionId(updatedAttributes.id);
|
||||
}
|
||||
|
||||
updatedSurvey.questions[questionIdx] = {
|
||||
...updatedSurvey.questions[questionIdx],
|
||||
...updatedAttributes,
|
||||
@@ -107,7 +118,7 @@ export default function QuestionsView({
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
setLocalSurvey(updatedSurvey);
|
||||
validateSurvey(updatedSurvey.questions[questionIdx]);
|
||||
validateSurveyQuestion(updatedSurvey.questions[questionIdx]);
|
||||
};
|
||||
|
||||
const deleteQuestion = (questionIdx: number) => {
|
||||
@@ -117,16 +128,18 @@ export default function QuestionsView({
|
||||
|
||||
// check if we are recalling from this question
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
if (question.headline.includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(question.headline);
|
||||
if (question.headline[selectedLanguageCode].includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(question.headline, selectedLanguageCode));
|
||||
if (recallInfo) {
|
||||
question.headline = question.headline.replace(recallInfo, "");
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replace(
|
||||
recallInfo,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
updatedSurvey.questions.splice(questionIdx, 1);
|
||||
updatedSurvey = handleQuestionLogicChange(updatedSurvey, questionId, "end");
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
delete internalQuestionIdMap[questionId];
|
||||
if (questionId === activeQuestionIdTemp) {
|
||||
@@ -140,7 +153,7 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
const duplicateQuestion = (questionIdx: number) => {
|
||||
const questionToDuplicate = JSON.parse(JSON.stringify(localSurvey.questions[questionIdx]));
|
||||
const questionToDuplicate = structuredClone(localSurvey.questions[questionIdx]);
|
||||
|
||||
const newQuestionId = createId();
|
||||
|
||||
@@ -166,8 +179,9 @@ export default function QuestionsView({
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
|
||||
updatedSurvey.questions.push({ ...question, isDraft: true });
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const translatedQuestion = translateQuestion(question, languageSymbols);
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setActiveQuestionId(question.id);
|
||||
@@ -195,7 +209,67 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey);
|
||||
if (invalidQuestions === null) return;
|
||||
|
||||
const updateInvalidQuestions = (card, cardId, currentInvalidQuestions) => {
|
||||
if (card.enabled && !isCardValid(card, cardId, surveyLanguages)) {
|
||||
return currentInvalidQuestions.includes(cardId)
|
||||
? currentInvalidQuestions
|
||||
: [...currentInvalidQuestions, cardId];
|
||||
}
|
||||
return currentInvalidQuestions.filter((id) => id !== cardId);
|
||||
};
|
||||
|
||||
const updatedQuestionsStart = updateInvalidQuestions(localSurvey.welcomeCard, "start", invalidQuestions);
|
||||
const updatedQuestionsEnd = updateInvalidQuestions(
|
||||
localSurvey.thankYouCard,
|
||||
"end",
|
||||
updatedQuestionsStart
|
||||
);
|
||||
|
||||
setInvalidQuestions(updatedQuestionsEnd);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey.welcomeCard, localSurvey.thankYouCard]);
|
||||
|
||||
//useEffect to validate survey when changes are made to languages
|
||||
useEffect(() => {
|
||||
if (!invalidQuestions) return;
|
||||
let updatedInvalidQuestions: string[] = invalidQuestions;
|
||||
// Validate each question
|
||||
localSurvey.questions.forEach((question) => {
|
||||
updatedInvalidQuestions = validateSurveyQuestionsInBatch(
|
||||
question,
|
||||
updatedInvalidQuestions,
|
||||
surveyLanguages
|
||||
);
|
||||
});
|
||||
|
||||
// Check welcome card
|
||||
if (localSurvey.welcomeCard.enabled && !isCardValid(localSurvey.welcomeCard, "start", surveyLanguages)) {
|
||||
if (!updatedInvalidQuestions.includes("start")) {
|
||||
updatedInvalidQuestions.push("start");
|
||||
}
|
||||
} else {
|
||||
updatedInvalidQuestions = updatedInvalidQuestions.filter((questionId) => questionId !== "start");
|
||||
}
|
||||
|
||||
// Check thank you card
|
||||
if (localSurvey.thankYouCard.enabled && !isCardValid(localSurvey.thankYouCard, "end", surveyLanguages)) {
|
||||
if (!updatedInvalidQuestions.includes("end")) {
|
||||
updatedInvalidQuestions.push("end");
|
||||
}
|
||||
} else {
|
||||
updatedInvalidQuestions = updatedInvalidQuestions.filter((questionId) => questionId !== "end");
|
||||
}
|
||||
|
||||
if (JSON.stringify(updatedInvalidQuestions) !== JSON.stringify(invalidQuestions)) {
|
||||
setInvalidQuestions(updatedInvalidQuestions);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey.languages, localSurvey.questions]);
|
||||
|
||||
useEffect(() => {
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
if (questionWithEmptyFallback) {
|
||||
setActiveQuestionId(questionWithEmptyFallback.id);
|
||||
if (activeQuestionId === questionWithEmptyFallback.id) {
|
||||
@@ -206,13 +280,16 @@ export default function QuestionsView({
|
||||
}, [activeQuestionId, setActiveQuestionId]);
|
||||
|
||||
return (
|
||||
<div className="mt-12 px-5 py-4">
|
||||
<div className="mt-16 px-5 py-4">
|
||||
<div className="mb-5 flex flex-col gap-5">
|
||||
<EditWelcomeCard
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("start") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
@@ -230,6 +307,8 @@ export default function QuestionsView({
|
||||
moveQuestion={moveQuestion}
|
||||
updateQuestion={updateQuestion}
|
||||
duplicateQuestion={duplicateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
deleteQuestion={deleteQuestion}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
@@ -250,6 +329,9 @@ export default function QuestionsView({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" ? (
|
||||
@@ -260,6 +342,17 @@ export default function QuestionsView({
|
||||
activeQuestionId={activeQuestionId}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<MultiLanguageCard
|
||||
localSurvey={localSurvey}
|
||||
product={product}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { HashIcon, PlusIcon, SmileIcon, StarIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
import Dropdown from "./RatingTypeDropdown";
|
||||
|
||||
@@ -15,6 +15,8 @@ interface RatingQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -22,47 +24,51 @@ export default function RatingQuestionForm({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: RatingQuestionFormProps) {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="headline"
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
type="subheader"
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
<TrashIcon
|
||||
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: undefined });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button
|
||||
@@ -70,7 +76,13 @@ export default function RatingQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -113,44 +125,47 @@ export default function RatingQuestionForm({
|
||||
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="lowerLabel">Lower label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="lowerLabel"
|
||||
name="lowerLabel"
|
||||
placeholder="Not good"
|
||||
value={question.lowerLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { lowerLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="lowerLabel"
|
||||
placeholder="Not good"
|
||||
value={question.lowerLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="upperLabel">Upper label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="upperLabel"
|
||||
name="upperLabel"
|
||||
placeholder="Very satisfied"
|
||||
value={question.upperLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { upperLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="upperLabel"
|
||||
placeholder="Very satisfied"
|
||||
value={question.upperLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{!question.required && (
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="buttonLabel">Dismiss Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="dismissButtonLabel"
|
||||
name="dismissButtonLabel"
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import Loading from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/loading";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
|
||||
import { extractLanguageCodes, getEnabledLanguages } from "@formbricks/lib/i18n/utils";
|
||||
import useDocumentVisibility from "@formbricks/lib/useDocumentVisibility";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { TAttributeClass } from "@formbricks/types/attributeClasses";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
@@ -30,6 +32,7 @@ interface SurveyEditorProps {
|
||||
membershipRole?: TMembershipRole;
|
||||
colours: string[];
|
||||
isUserTargetingAllowed?: boolean;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
@@ -42,6 +45,7 @@ export default function SurveyEditor({
|
||||
segments,
|
||||
responseCount,
|
||||
membershipRole,
|
||||
isMultiLanguageAllowed,
|
||||
colours,
|
||||
isUserTargetingAllowed = false,
|
||||
isFormbricksCloud,
|
||||
@@ -49,9 +53,30 @@ export default function SurveyEditor({
|
||||
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||
const [localSurvey, setLocalSurvey] = useState<TSurvey | null>(survey);
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(null);
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<string[] | null>(null);
|
||||
const [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [localProduct, setLocalProduct] = useState<TProduct>(product);
|
||||
|
||||
const fetchLatestProduct = useCallback(async () => {
|
||||
const latestProduct = await refetchProduct(localProduct.id);
|
||||
if (latestProduct) {
|
||||
setLocalProduct(latestProduct);
|
||||
}
|
||||
}, [localProduct.id]);
|
||||
|
||||
useDocumentVisibility(fetchLatestProduct);
|
||||
|
||||
useEffect(() => {
|
||||
if (survey) {
|
||||
const surveyClone = structuredClone(survey);
|
||||
setLocalSurvey(surveyClone);
|
||||
if (survey.questions.length > 0) {
|
||||
setActiveQuestionId(survey.questions[0].id);
|
||||
}
|
||||
}
|
||||
}, [survey]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
@@ -75,7 +100,6 @@ export default function SurveyEditor({
|
||||
if (localSurvey?.questions?.length && localSurvey.questions.length > 0) {
|
||||
setActiveQuestionId(localSurvey.questions[0].id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localSurvey?.type, survey?.questions]);
|
||||
|
||||
@@ -84,7 +108,6 @@ export default function SurveyEditor({
|
||||
if (!localSurvey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do nothing if its not an in-app survey
|
||||
if (localSurvey.type !== "web") {
|
||||
return;
|
||||
@@ -117,8 +140,16 @@ export default function SurveyEditor({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [environment.id, isUserTargetingAllowed, localSurvey?.type, survey.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localSurvey?.languages) return;
|
||||
const enabledLanguageCodes = extractLanguageCodes(getEnabledLanguages(localSurvey.languages ?? []));
|
||||
if (!enabledLanguageCodes.includes(selectedLanguageCode)) {
|
||||
setSelectedLanguageCode("default");
|
||||
}
|
||||
}, [localSurvey?.languages, selectedLanguageCode]);
|
||||
|
||||
if (!localSurvey) {
|
||||
return <Loading />;
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -134,10 +165,13 @@ export default function SurveyEditor({
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
product={localProduct}
|
||||
responseCount={responseCount}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<div className="relative z-0 flex flex-1 overflow-hidden">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none">
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none" ref={surveyEditorRef}>
|
||||
<QuestionsAudienceTabs activeId={activeView} setActiveId={setActiveView} />
|
||||
|
||||
{activeView === "questions" ? (
|
||||
<QuestionsView
|
||||
localSurvey={localSurvey}
|
||||
@@ -147,6 +181,10 @@ export default function SurveyEditor({
|
||||
product={localProduct}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
) : (
|
||||
<SettingsView
|
||||
@@ -164,6 +202,7 @@ export default function SurveyEditor({
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 py-6 md:flex md:flex-col">
|
||||
<PreviewSurvey
|
||||
survey={localSurvey}
|
||||
@@ -172,6 +211,7 @@ export default function SurveyEditor({
|
||||
product={localProduct}
|
||||
environment={environment}
|
||||
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||
languageCode={selectedLanguageCode}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Input } from "@formbricks/ui/Input";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
import { updateSurveyAction } from "../actions";
|
||||
import { isValidUrl, validateQuestion } from "./Validation";
|
||||
import { isCardValid, validateQuestion } from "./Validation";
|
||||
|
||||
interface SurveyMenuBarProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -32,9 +32,11 @@ interface SurveyMenuBarProps {
|
||||
environment: TEnvironment;
|
||||
activeId: "questions" | "settings";
|
||||
setActiveId: (id: "questions" | "settings") => void;
|
||||
setInvalidQuestions: (invalidQuestions: String[]) => void;
|
||||
setInvalidQuestions: (invalidQuestions: string[]) => void;
|
||||
product: TProduct;
|
||||
responseCount: number;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (selectedLanguage: string) => void;
|
||||
}
|
||||
|
||||
export default function SurveyMenuBar({
|
||||
@@ -47,6 +49,8 @@ export default function SurveyMenuBar({
|
||||
setInvalidQuestions,
|
||||
product,
|
||||
responseCount,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: SurveyMenuBarProps) {
|
||||
const router = useRouter();
|
||||
const [audiencePrompt, setAudiencePrompt] = useState(true);
|
||||
@@ -55,7 +59,7 @@ export default function SurveyMenuBar({
|
||||
const [isSurveySaving, setIsSurveySaving] = useState(false);
|
||||
const cautionText = "This survey received responses, make changes with caution.";
|
||||
|
||||
let faultyQuestions: String[] = [];
|
||||
let faultyQuestions: string[] = [];
|
||||
|
||||
useEffect(() => {
|
||||
if (audiencePrompt && activeId === "settings") {
|
||||
@@ -116,42 +120,33 @@ export default function SurveyMenuBar({
|
||||
|
||||
const validateSurvey = (survey: TSurvey) => {
|
||||
const existingQuestionIds = new Set();
|
||||
|
||||
faultyQuestions = [];
|
||||
if (survey.questions.length === 0) {
|
||||
toast.error("Please add at least one question");
|
||||
return;
|
||||
}
|
||||
|
||||
if (survey.welcomeCard.enabled) {
|
||||
if (!isCardValid(survey.welcomeCard, "start", survey.languages)) {
|
||||
faultyQuestions.push("start");
|
||||
}
|
||||
}
|
||||
|
||||
if (survey.thankYouCard.enabled) {
|
||||
if (!isCardValid(survey.thankYouCard, "end", survey.languages)) {
|
||||
faultyQuestions.push("end");
|
||||
}
|
||||
}
|
||||
|
||||
let pin = survey?.pin;
|
||||
if (pin !== null && pin!.toString().length !== 4) {
|
||||
toast.error("PIN must be a four digit number.");
|
||||
return;
|
||||
}
|
||||
|
||||
const { thankYouCard } = localSurvey;
|
||||
if (thankYouCard.enabled) {
|
||||
const { buttonLabel, buttonLink } = thankYouCard;
|
||||
|
||||
if (buttonLabel && !buttonLink) {
|
||||
toast.error("Button Link missing on Thank you card.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!buttonLabel && buttonLink) {
|
||||
toast.error("Button Label missing on Thank you card.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonLink && !isValidUrl(buttonLink)) {
|
||||
toast.error("Invalid URL on Thank You card.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
faultyQuestions = [];
|
||||
for (let index = 0; index < survey.questions.length; index++) {
|
||||
const question = survey.questions[index];
|
||||
const isValid = validateQuestion(question);
|
||||
const isValid = validateQuestion(question, survey.languages);
|
||||
|
||||
if (!isValid) {
|
||||
faultyQuestions.push(question.id);
|
||||
@@ -161,6 +156,7 @@ export default function SurveyMenuBar({
|
||||
// if there are any faulty questions, the user won't be allowed to save the survey
|
||||
if (faultyQuestions.length > 0) {
|
||||
setInvalidQuestions(faultyQuestions);
|
||||
setSelectedLanguageCode("default");
|
||||
toast.error("Please fill all required fields.");
|
||||
return false;
|
||||
}
|
||||
@@ -179,11 +175,15 @@ export default function SurveyMenuBar({
|
||||
question.type === TSurveyQuestionType.MultipleChoiceMulti
|
||||
) {
|
||||
const haveSameChoices =
|
||||
question.choices.some((element) => element.label.trim() === "") ||
|
||||
question.choices.some((element) => element.label[selectedLanguageCode]?.trim() === "") ||
|
||||
question.choices.some((element, index) =>
|
||||
question.choices
|
||||
.slice(index + 1)
|
||||
.some((nextElement) => nextElement.label.trim() === element.label.trim())
|
||||
.some(
|
||||
(nextElement) =>
|
||||
nextElement.label[selectedLanguageCode]?.trim() ===
|
||||
element.label[selectedLanguageCode].trim()
|
||||
)
|
||||
);
|
||||
|
||||
if (haveSameChoices) {
|
||||
@@ -237,7 +237,7 @@ export default function SurveyMenuBar({
|
||||
toast.error("Please add at least one question.");
|
||||
return;
|
||||
}
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey);
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
if (questionWithEmptyFallback) {
|
||||
toast.error("Fallback missing");
|
||||
return;
|
||||
|
||||
@@ -42,9 +42,17 @@ export default function UpdateQuestionId({
|
||||
toast.error("ID should not be empty.");
|
||||
return;
|
||||
} else if (
|
||||
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(
|
||||
currentValue
|
||||
)
|
||||
[
|
||||
"userId",
|
||||
"source",
|
||||
"suid",
|
||||
"end",
|
||||
"start",
|
||||
"welcomeCard",
|
||||
"hidden",
|
||||
"verifiedEmail",
|
||||
"multiLanguage",
|
||||
].includes(currentValue)
|
||||
) {
|
||||
setCurrentValue(prevValue);
|
||||
updateQuestion(questionIdx, { id: prevValue });
|
||||
|
||||
@@ -1,42 +1,130 @@
|
||||
// extend this object in order to add more validation rules
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import {
|
||||
TI18nString,
|
||||
TSurveyConsentQuestion,
|
||||
TSurveyLanguage,
|
||||
TSurveyMultipleChoiceMultiQuestion,
|
||||
TSurveyMultipleChoiceSingleQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
TSurveyQuestion,
|
||||
TSurveyThankYouCard,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys";
|
||||
|
||||
// Utility function to check if label is valid for all required languages
|
||||
const isLabelValidForAllLanguages = (label: TI18nString, surveyLanguages: TSurveyLanguage[]): boolean => {
|
||||
const filteredLanguages = surveyLanguages.filter((surveyLanguages) => {
|
||||
return surveyLanguages.enabled;
|
||||
});
|
||||
const languageCodes = extractLanguageCodes(filteredLanguages);
|
||||
const languages = languageCodes.length === 0 ? ["default"] : languageCodes;
|
||||
return languages.every((language) => label && label[language] && label[language].trim() !== "");
|
||||
};
|
||||
|
||||
// Validation logic for multiple choice questions
|
||||
const handleI18nCheckForMultipleChoice = (
|
||||
question: TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion,
|
||||
languages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
return question.choices.every((choice) => isLabelValidForAllLanguages(choice.label, languages));
|
||||
};
|
||||
|
||||
// Validation rules
|
||||
const validationRules = {
|
||||
multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion) => {
|
||||
return !question.choices.some((element) => element.label.trim() === "");
|
||||
multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion, languages: TSurveyLanguage[]) => {
|
||||
return handleI18nCheckForMultipleChoice(question, languages);
|
||||
},
|
||||
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion) => {
|
||||
return !question.choices.some((element) => element.label.trim() === "");
|
||||
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion, languages: TSurveyLanguage[]) => {
|
||||
return handleI18nCheckForMultipleChoice(question, languages);
|
||||
},
|
||||
consent: (question: TSurveyConsentQuestion) => {
|
||||
return question.label.trim() !== "";
|
||||
consent: (question: TSurveyConsentQuestion, languages: TSurveyLanguage[]) => {
|
||||
return isLabelValidForAllLanguages(question.label, languages);
|
||||
},
|
||||
pictureSelection: (question: TSurveyPictureSelectionQuestion) => {
|
||||
return question.choices.length >= 2;
|
||||
},
|
||||
defaultValidation: (question: TSurveyQuestion) => {
|
||||
return question.headline.trim() !== "";
|
||||
// Assuming headline is of type TI18nString
|
||||
defaultValidation: (question: TSurveyQuestion, languages: TSurveyLanguage[]) => {
|
||||
let isValid = isLabelValidForAllLanguages(question.headline, languages);
|
||||
let isValidCTADismissLabel = true;
|
||||
const defaultLanguageCode = "default";
|
||||
if (question.type === "cta" && !question.required && question.dismissButtonLabel) {
|
||||
isValidCTADismissLabel = isLabelValidForAllLanguages(question.dismissButtonLabel, languages);
|
||||
}
|
||||
const fieldsToValidate = [
|
||||
"subheader",
|
||||
"html",
|
||||
"buttonLabel",
|
||||
"upperLabel",
|
||||
"backButtonLabel",
|
||||
"lowerLabel",
|
||||
"placeholder",
|
||||
];
|
||||
|
||||
for (const field of fieldsToValidate) {
|
||||
if (question[field] && question[field][defaultLanguageCode]) {
|
||||
isValid =
|
||||
isValid && isLabelValidForAllLanguages(question[field], languages) && isValidCTADismissLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
},
|
||||
};
|
||||
|
||||
const validateQuestion = (question) => {
|
||||
// Main validation function
|
||||
const validateQuestion = (question: TSurveyQuestion, surveyLanguages: TSurveyLanguage[]): boolean => {
|
||||
const specificValidation = validationRules[question.type];
|
||||
const defaultValidation = validationRules.defaultValidation;
|
||||
|
||||
const specificValidationResult = specificValidation ? specificValidation(question) : true;
|
||||
const defaultValidationResult = defaultValidation(question);
|
||||
const specificValidationResult = specificValidation ? specificValidation(question, surveyLanguages) : true;
|
||||
const defaultValidationResult = defaultValidation(question, surveyLanguages);
|
||||
|
||||
// Return true only if both specific and default validation pass
|
||||
return specificValidationResult && defaultValidationResult;
|
||||
};
|
||||
|
||||
export { validateQuestion };
|
||||
export const validateSurveyQuestionsInBatch = (
|
||||
question: TSurveyQuestion,
|
||||
invalidQuestions: string[] | null,
|
||||
surveyLanguages: TSurveyLanguage[]
|
||||
) => {
|
||||
if (invalidQuestions === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (validateQuestion(question, surveyLanguages)) {
|
||||
return invalidQuestions.filter((id) => id !== question.id);
|
||||
} else if (!invalidQuestions.includes(question.id)) {
|
||||
return [...invalidQuestions, question.id];
|
||||
}
|
||||
|
||||
return invalidQuestions;
|
||||
};
|
||||
|
||||
export const isCardValid = (
|
||||
card: TSurveyWelcomeCard | TSurveyThankYouCard,
|
||||
cardType: "start" | "end",
|
||||
surveyLanguages: TSurveyLanguage[]
|
||||
): boolean => {
|
||||
const defaultLanguageCode = "default";
|
||||
const isContentValid = (content: Record<string, string> | undefined) => {
|
||||
return (
|
||||
!content || content[defaultLanguageCode] === "" || isLabelValidForAllLanguages(content, surveyLanguages)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
(card.headline ? isLabelValidForAllLanguages(card.headline, surveyLanguages) : true) &&
|
||||
isContentValid(
|
||||
cardType === "start" ? (card as TSurveyWelcomeCard).html : (card as TSurveyThankYouCard).subheader
|
||||
) &&
|
||||
isContentValid(card.buttonLabel)
|
||||
);
|
||||
};
|
||||
|
||||
export { validateQuestion, isLabelValidForAllLanguages };
|
||||
|
||||
export const isValidUrl = (string: string): boolean => {
|
||||
try {
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-between p-6">
|
||||
{/* Top Part - Loading Navbar */}
|
||||
<div className="flex h-[10vh] w-full animate-pulse rounded-lg bg-slate-200 font-medium text-slate-900"></div>
|
||||
|
||||
{/* Bottom Part - Divided into Left and Right */}
|
||||
<div className="mt-4 flex h-[85%] w-full flex-row">
|
||||
{/* Left Part - 7 Horizontal Bars */}
|
||||
<div className="flex h-full w-1/2 flex-col justify-between space-y-2">
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
<div className="ph-no-capture h-[10vh] animate-pulse rounded-lg bg-slate-200"></div>
|
||||
</div>
|
||||
|
||||
{/* Right Part - Simple Box */}
|
||||
<div className="ml-4 flex h-full w-1/2 flex-col">
|
||||
<div className="ph-no-capture h-full animate-pulse rounded-lg bg-slate-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
|
||||
import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
@@ -60,6 +60,7 @@ export default async function SurveysEditPage({ params }) {
|
||||
const isSurveyCreationDeletionDisabled = isViewer;
|
||||
|
||||
const isUserTargetingAllowed = getAdvancedTargetingPermission(team);
|
||||
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
|
||||
|
||||
if (
|
||||
!survey ||
|
||||
@@ -84,6 +85,7 @@ export default async function SurveysEditPage({ params }) {
|
||||
colours={colours}
|
||||
segments={segments}
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ interface PreviewSurveyProps {
|
||||
previewType?: TPreviewType;
|
||||
product: TProduct;
|
||||
environment: TEnvironment;
|
||||
languageCode: string;
|
||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||
}
|
||||
|
||||
@@ -62,6 +63,7 @@ export default function PreviewSurvey({
|
||||
previewType,
|
||||
product,
|
||||
environment,
|
||||
languageCode,
|
||||
onFileUpload,
|
||||
}: PreviewSurveyProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
@@ -213,6 +215,7 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
onClose={handlePreviewModalClose}
|
||||
/>
|
||||
@@ -227,6 +230,7 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
/>
|
||||
</div>
|
||||
@@ -284,6 +288,7 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
onClose={handlePreviewModalClose}
|
||||
/>
|
||||
@@ -299,6 +304,7 @@ export default function PreviewSurvey({
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +77,7 @@ export default function TemplateContainerWithPreview({
|
||||
product={product}
|
||||
environment={environment}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
languageCode={"default"}
|
||||
onFileUpload={async (file) => file.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ export const SignupForm = ({
|
||||
const [error, setError] = useState<string>("");
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
|
||||
|
||||
const inviteToken = searchParams?.get("inviteToken");
|
||||
const callbackUrl = useMemo(() => {
|
||||
@@ -85,7 +86,6 @@ export const SignupForm = ({
|
||||
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const [isButtonEnabled, setButtonEnabled] = useState(true);
|
||||
const [isPasswordFocused, setIsPasswordFocused] = useState(false);
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { headers } from "next/headers";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { CRON_SECRET } from "@formbricks/lib/constants";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
|
||||
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "./email";
|
||||
import { EnvironmentData, NotificationResponse, ProductData, Survey, SurveyResponse } from "./types";
|
||||
@@ -172,7 +173,6 @@ const getNotificationResponse = (environment: EnvironmentData, productName: stri
|
||||
};
|
||||
|
||||
const surveys: Survey[] = [];
|
||||
|
||||
// iterate through the surveys and calculate the overall insights
|
||||
for (const survey of environment.surveys) {
|
||||
const surveyData: Survey = {
|
||||
@@ -195,7 +195,7 @@ const getNotificationResponse = (environment: EnvironmentData, productName: stri
|
||||
if (answer === null || answer === "" || answer?.length === 0) {
|
||||
continue;
|
||||
}
|
||||
surveyResponse[headline] = answer;
|
||||
surveyResponse[getLocalizedValue(headline, "default")] = answer;
|
||||
}
|
||||
surveyData.responses.push(surveyResponse);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { writeData as airtableWriteData } from "@formbricks/lib/airtable/service";
|
||||
import { writeData } from "@formbricks/lib/googleSheet/service";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { writeData as writeNotionData } from "@formbricks/lib/notion/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TIntegration } from "@formbricks/types/integration";
|
||||
@@ -67,7 +68,7 @@ async function extractResponses(data: TPipelineInput, questionIds: string[]): Pr
|
||||
}
|
||||
|
||||
const question = survey?.questions.find((q) => q.id === questionId);
|
||||
questions.push(question?.headline || "");
|
||||
questions.push(getLocalizedValue(question?.headline, "default") || "");
|
||||
}
|
||||
|
||||
return [responses, questions];
|
||||
|
||||
@@ -6,6 +6,7 @@ import { prisma } from "@formbricks/database";
|
||||
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
|
||||
import { sendResponseFinishedEmail } from "@formbricks/lib/emails/emails";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
@@ -36,6 +37,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const { environmentId, surveyId, event, response } = inputValidation.data;
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
if (!product) return;
|
||||
|
||||
// get all webhooks of this environment where event in triggers
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
@@ -153,7 +156,7 @@ export async function POST(request: Request) {
|
||||
const survey = {
|
||||
id: surveyData.id,
|
||||
name: surveyData.name,
|
||||
questions: JSON.parse(JSON.stringify(surveyData.questions)) as TSurveyQuestion[],
|
||||
questions: structuredClone(surveyData.questions) as TSurveyQuestion[],
|
||||
};
|
||||
// send email to all users
|
||||
await Promise.all(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
|
||||
interface Context {
|
||||
@@ -69,6 +70,12 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveys(environmentId, person.id),
|
||||
getActionClasses(environmentId),
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
|
||||
interface Context {
|
||||
@@ -68,6 +69,12 @@ export async function POST(req: Request, context: Context): Promise<Response> {
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team not found");
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveys(environmentId, person.id),
|
||||
getActionClasses(environmentId),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PRICING_USERTARGETING_FREE_MTU,
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { reverseTranslateSurvey } from "@formbricks/lib/i18n/reverseTranslation";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys, getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
@@ -21,7 +22,7 @@ import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const transformLegacySurveys = (surveys: TSurvey[]): TSurveyWithTriggers[] => {
|
||||
const updatedSurveys = surveys.map((survey) => {
|
||||
const updatedSurvey: any = { ...survey };
|
||||
const updatedSurvey: any = { ...reverseTranslateSurvey(survey) };
|
||||
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
|
||||
return updatedSurvey;
|
||||
});
|
||||
@@ -91,6 +92,7 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
|
||||
const isPerson = Object.keys(person).length > 0;
|
||||
|
||||
let surveys;
|
||||
|
||||
if (isAppSurveyLimitReached) {
|
||||
surveys = [];
|
||||
} else if (isPerson) {
|
||||
|
||||
@@ -12,14 +12,17 @@ import {
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { createPerson, getIsPersonMonthlyActive, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getSyncSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
|
||||
import {
|
||||
getMonthlyActiveTeamPeopleCount,
|
||||
getMonthlyTeamResponseCount,
|
||||
getTeamByEnvironmentId,
|
||||
} from "@formbricks/lib/team/service";
|
||||
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
|
||||
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export async function OPTIONS(): Promise<Response> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -38,9 +41,10 @@ export async function GET(
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const { device } = userAgent(request);
|
||||
const apiVersion = request.nextUrl.searchParams.get("version");
|
||||
const version = request.nextUrl.searchParams.get("version");
|
||||
|
||||
// validate using zod
|
||||
|
||||
const inputValidation = ZJsPeopleUserIdInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
userId: params.userId,
|
||||
@@ -66,17 +70,17 @@ export async function GET(
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
// check if MAU limit is reached
|
||||
let isMauLimitReached = false;
|
||||
let isInAppSurveyLimitReached = false;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
// check userTargeting subscription
|
||||
const hasUserTargetingSubscription =
|
||||
team.billing.features.userTargeting.status &&
|
||||
@@ -121,13 +125,14 @@ export async function GET(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isInAppSurveyLimitReached) {
|
||||
await sendFreeLimitReachedEventToPosthogBiWeekly(environmentId, "inAppSurvey");
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveys(environmentId, person.id, device.type === "mobile" ? "phone" : "desktop", {
|
||||
version: apiVersion ?? undefined,
|
||||
version: version ?? undefined,
|
||||
}),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
@@ -136,16 +141,41 @@ export async function GET(
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
const languageAttribute = person.attributes.language;
|
||||
const isLanguageAvailable = Boolean(languageAttribute);
|
||||
|
||||
const personData = version
|
||||
? {
|
||||
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
|
||||
}
|
||||
: {
|
||||
id: person.id,
|
||||
userId: person.userId,
|
||||
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
|
||||
};
|
||||
|
||||
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
|
||||
let transformedSurveys: TLegacySurvey[] | TSurvey[];
|
||||
|
||||
// Backwards compatibility for versions less than 1.7.0 (no multi-language support).
|
||||
if (version && isVersionGreaterThanOrEqualTo(version, "1.7.0")) {
|
||||
// Scenario 1: Multi language supported
|
||||
// Use the surveys as they are.
|
||||
transformedSurveys = surveys;
|
||||
} else {
|
||||
// Scenario 2: Multi language not supported
|
||||
// Convert to legacy surveys with default language.
|
||||
transformedSurveys = await Promise.all(
|
||||
surveys.map((survey) => {
|
||||
const languageCode = "default";
|
||||
return transformToLegacySurvey(survey, languageCode);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsStateSync = {
|
||||
person: apiVersion
|
||||
? undefined
|
||||
: {
|
||||
id: person.id,
|
||||
userId: person.userId,
|
||||
},
|
||||
surveys: !isInAppSurveyLimitReached ? surveys : [],
|
||||
person: personData,
|
||||
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
@@ -12,35 +12,46 @@ import {
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { createSurvey, getSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
|
||||
import { getMonthlyTeamResponseCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
|
||||
import { TLegacySurvey } from "@formbricks/types/LegacySurvey";
|
||||
import { TJsStateSync, ZJsPublicSyncInput } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export async function OPTIONS(): Promise<Response> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_: NextRequest,
|
||||
request: NextRequest,
|
||||
{ params }: { params: { environmentId: string } }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
// validate using zod
|
||||
const environmentIdValidation = ZJsPublicSyncInput.safeParse({
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const version =
|
||||
searchParams.get("version") === "undefined" || searchParams.get("version") === null
|
||||
? undefined
|
||||
: searchParams.get("version");
|
||||
const syncInputValidation = ZJsPublicSyncInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
if (!syncInputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
transformErrorToDetails(syncInputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId } = environmentIdValidation.data;
|
||||
const { environmentId } = syncInputValidation.data;
|
||||
|
||||
const environment = await getEnvironment(environmentId);
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
@@ -50,11 +61,7 @@ export async function GET(
|
||||
let isInAppSurveyLimitReached = false;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
// check inAppSurvey subscription
|
||||
const hasInAppSurveySubscription =
|
||||
team.billing.features.inAppSurvey.status &&
|
||||
@@ -83,15 +90,36 @@ export async function GET(
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// Common filter condition for selecting surveys that are in progress, are of type 'web' and have no active segment filtering.
|
||||
let filteredSurveys = surveys.filter(
|
||||
(survey) =>
|
||||
survey.status === "inProgress" &&
|
||||
survey.type === "web" &&
|
||||
(!survey.segment || survey.segment.filters.length === 0)
|
||||
);
|
||||
|
||||
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
|
||||
let transformedSurveys: TLegacySurvey[] | TSurvey[];
|
||||
|
||||
// Backwards compatibility for versions less than 1.7.0 (no multi-language support).
|
||||
if (version && isVersionGreaterThanOrEqualTo(version, "1.7.0")) {
|
||||
// Scenario 1: Multi language supported
|
||||
// Use the surveys as they are.
|
||||
transformedSurveys = filteredSurveys;
|
||||
} else {
|
||||
// Scenario 2: Multi language not supported
|
||||
// Convert to legacy surveys with default language.
|
||||
transformedSurveys = await Promise.all(
|
||||
filteredSurveys.map((survey) => {
|
||||
const languageCode = "default";
|
||||
return transformToLegacySurvey(survey, languageCode);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create the 'state' object with surveys, noCodeActionClasses, product, and person.
|
||||
const state: TJsStateSync = {
|
||||
surveys: !isInAppSurveyLimitReached
|
||||
? surveys.filter(
|
||||
(survey) =>
|
||||
survey.status === "inProgress" &&
|
||||
survey.type === "web" &&
|
||||
(!survey.segment || survey.segment.filters.length === 0)
|
||||
)
|
||||
: [],
|
||||
surveys: isInAppSurveyLimitReached ? [] : transformedSurveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
person: null,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { authenticateRequest } from "@/app/api/v1/auth";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
|
||||
import { translateSurvey } from "@formbricks/lib/i18n/utils";
|
||||
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { DatabaseError } from "@formbricks/types/errors";
|
||||
import { ZSurveyInput } from "@formbricks/types/surveys";
|
||||
@@ -29,7 +30,14 @@ export async function POST(request: Request): Promise<Response> {
|
||||
try {
|
||||
const authentication = await authenticateRequest(request);
|
||||
if (!authentication) return responses.notAuthenticatedResponse();
|
||||
const surveyInput = await request.json();
|
||||
let surveyInput = await request.json();
|
||||
if (surveyInput?.questions && surveyInput.questions[0].headline) {
|
||||
const questionHeadline = surveyInput.questions[0].headline;
|
||||
if (typeof questionHeadline === "string") {
|
||||
// its a legacy survey
|
||||
surveyInput = translateSurvey(surveyInput, []);
|
||||
}
|
||||
}
|
||||
const inputValidation = ZSurveyInput.safeParse(surveyInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
|
||||
@@ -32,9 +32,9 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask for a text-based answer",
|
||||
icon: MessageSquareTextIcon,
|
||||
preset: {
|
||||
headline: "Who let the dogs out?",
|
||||
subheader: "Who? Who? Who?",
|
||||
placeholder: "Type your answer here...",
|
||||
headline: { default: "Who let the dogs out?" },
|
||||
subheader: { default: "Who? Who? Who?" },
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
longAnswer: true,
|
||||
inputType: "text",
|
||||
},
|
||||
@@ -45,11 +45,11 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "A single choice from a list of options (radio buttons)",
|
||||
icon: Rows3Icon,
|
||||
preset: {
|
||||
headline: "What do you do?",
|
||||
subheader: "Can't do both.",
|
||||
headline: { default: "What do you do?" },
|
||||
subheader: { default: "Can't do both." },
|
||||
choices: [
|
||||
{ id: createId(), label: "Eat the cake 🍰" },
|
||||
{ id: createId(), label: "Have the cake 🎂" },
|
||||
{ id: createId(), label: { default: "Eat the cake 🍰" } },
|
||||
{ id: createId(), label: { default: "Have the cake 🎂" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
@@ -60,11 +60,11 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Number of choices from a list of options (checkboxes)",
|
||||
icon: ListIcon,
|
||||
preset: {
|
||||
headline: "What's important on vacay?",
|
||||
headline: { default: "What's important on vacay?" },
|
||||
choices: [
|
||||
{ id: createId(), label: "Sun ☀️" },
|
||||
{ id: createId(), label: "Ocean 🌊" },
|
||||
{ id: createId(), label: "Palms 🌴" },
|
||||
{ id: createId(), label: { default: "Sun ☀️" } },
|
||||
{ id: createId(), label: { default: "Ocean 🌊" } },
|
||||
{ id: createId(), label: { default: "Palms 🌴" } },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
@@ -75,8 +75,8 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask respondents to select one or more pictures",
|
||||
icon: ImageIcon,
|
||||
preset: {
|
||||
headline: "Which is the cutest puppy?",
|
||||
subheader: "You can also pick both.",
|
||||
headline: { default: "Which is the cutest puppy?" },
|
||||
subheader: { default: "You can also pick both." },
|
||||
allowMulti: true,
|
||||
choices: [
|
||||
{
|
||||
@@ -96,12 +96,12 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask respondents for a rating",
|
||||
icon: StarIcon,
|
||||
preset: {
|
||||
headline: "How would you rate {{productName}}",
|
||||
subheader: "Don't worry, be honest.",
|
||||
headline: { default: "How would you rate {{productName}}" },
|
||||
subheader: { default: "Don't worry, be honest." },
|
||||
scale: "star",
|
||||
range: 5,
|
||||
lowerLabel: "Not good",
|
||||
upperLabel: "Very good",
|
||||
lowerLabel: { default: "Not good" },
|
||||
upperLabel: { default: "Very good" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,9 +110,9 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Rate satisfaction on a 0-10 scale",
|
||||
icon: PresentationIcon,
|
||||
preset: {
|
||||
headline: "How likely are you to recommend {{productName}} to a friend or colleague?",
|
||||
lowerLabel: "Not at all likely",
|
||||
upperLabel: "Extremely likely",
|
||||
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
|
||||
lowerLabel: { default: "Not at all likely" },
|
||||
upperLabel: { default: "Extremely likely" },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -121,8 +121,9 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Prompt respondents to perform an action",
|
||||
icon: MousePointerClickIcon,
|
||||
preset: {
|
||||
headline: "You are one of our power users!",
|
||||
buttonLabel: "Book interview",
|
||||
headline: { default: "You are one of our power users!" },
|
||||
html: { default: "" },
|
||||
buttonLabel: { default: "Book interview" },
|
||||
buttonExternal: false,
|
||||
dismissButtonLabel: "Skip",
|
||||
},
|
||||
@@ -133,8 +134,9 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask respondents for consent",
|
||||
icon: CheckIcon,
|
||||
preset: {
|
||||
headline: "Terms and Conditions",
|
||||
label: "I agree to the terms and conditions",
|
||||
headline: { default: "Terms and Conditions" },
|
||||
html: { default: "" },
|
||||
label: { default: "I agree to the terms and conditions" },
|
||||
dismissButtonLabel: "Skip",
|
||||
},
|
||||
},
|
||||
@@ -144,7 +146,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask your users to select a date",
|
||||
icon: CalendarDaysIcon,
|
||||
preset: {
|
||||
headline: "When is your birthday?",
|
||||
headline: { default: "When is your birthday?" },
|
||||
format: "M-d-y",
|
||||
},
|
||||
},
|
||||
@@ -154,7 +156,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Allow respondents to upload a file",
|
||||
icon: ArrowUpFromLine,
|
||||
preset: {
|
||||
headline: "File Upload",
|
||||
headline: { default: "File Upload" },
|
||||
allowMultipleFiles: false,
|
||||
},
|
||||
},
|
||||
@@ -164,8 +166,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Allow respondents to schedule a meet",
|
||||
icon: PhoneIcon,
|
||||
preset: {
|
||||
headline: "Schedule a call with me",
|
||||
buttonLabel: "Skip",
|
||||
headline: { default: "Schedule a call with me" },
|
||||
calUserName: "rick/get-rick-rolled",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
export const getQuestionResponseMapping = (
|
||||
survey: { questions: TSurveyQuestion[] },
|
||||
response: TResponse
|
||||
): { question: string; answer: string }[] => {
|
||||
const questionResponseMapping: { question: string; answer: string }[] = [];
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const answer = response.data[question.id];
|
||||
|
||||
questionResponseMapping.push({
|
||||
question: question.headline,
|
||||
answer: typeof answer !== "undefined" ? answer.toString() : "",
|
||||
});
|
||||
}
|
||||
|
||||
return questionResponseMapping;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import {
|
||||
OptionsType,
|
||||
QuestionOption,
|
||||
QuestionOptions,
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
@@ -22,6 +23,7 @@ const conditionOptions = {
|
||||
rating: ["Is equal to", "Is less than", "Is more than", "Submitted", "Skipped"],
|
||||
cta: ["is"],
|
||||
tags: ["is"],
|
||||
languages: ["Equals", "Not equals"],
|
||||
pictureSelection: ["Includes all", "Includes either"],
|
||||
userAttributes: ["Equals", "Not equals"],
|
||||
consent: ["is"],
|
||||
@@ -35,7 +37,7 @@ const filterOptions = {
|
||||
consent: ["Accepted", "Dismissed"],
|
||||
};
|
||||
|
||||
// creating the options for the filtering to be selected there are three types questions, attributes and tags
|
||||
// creating the options for the filtering to be selected there are 4 types questions, attributes, tags and metadata
|
||||
export const generateQuestionAndFilterOptions = (
|
||||
survey: TSurvey,
|
||||
environmentTags: TTag[] | undefined,
|
||||
@@ -44,7 +46,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
} => {
|
||||
let questionOptions: any = [];
|
||||
let questionOptions: QuestionOptions[] = [];
|
||||
let questionFilterOptions: any = [];
|
||||
|
||||
let questionsOptions: any = [];
|
||||
@@ -125,6 +127,20 @@ export const generateQuestionAndFilterOptions = (
|
||||
});
|
||||
}
|
||||
|
||||
let metadataOptions: QuestionOption[] = [];
|
||||
//can be extended to include more properties
|
||||
if (survey.languages?.length > 0) {
|
||||
metadataOptions.push({ label: "Language", type: OptionsType.METADATA, id: "language" });
|
||||
const languageOptions = survey.languages.map((sl) => sl.language.code);
|
||||
questionFilterOptions.push({
|
||||
type: "Metadata",
|
||||
filterOptions: conditionOptions.languages,
|
||||
filterComboBoxOptions: languageOptions,
|
||||
id: "language",
|
||||
});
|
||||
}
|
||||
questionOptions = [...questionOptions, { header: OptionsType.METADATA, option: metadataOptions }];
|
||||
|
||||
return { questionOptions: [...questionOptions], questionFilterOptions: [...questionFilterOptions] };
|
||||
};
|
||||
|
||||
@@ -135,19 +151,20 @@ export const getFormattedFilters = (
|
||||
dateRange: DateRange
|
||||
): TResponseFilterCriteria => {
|
||||
const filters: TResponseFilterCriteria = {};
|
||||
|
||||
const [questions, tags, attributes] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
const [questions, tags, attributes, metadata] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
if (filter.questionType?.type === "Questions") {
|
||||
result[0].push(filter);
|
||||
} else if (filter.questionType?.type === "Tags") {
|
||||
result[1].push(filter);
|
||||
} else if (filter.questionType?.type === "Attributes") {
|
||||
result[2].push(filter);
|
||||
} else if (filter.questionType?.type === "Metadata") {
|
||||
result[3].push(filter);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[[], [], []]
|
||||
[[], [], [], []]
|
||||
);
|
||||
|
||||
// for completed responses
|
||||
@@ -306,6 +323,24 @@ export const getFormattedFilters = (
|
||||
});
|
||||
}
|
||||
|
||||
// for metadata
|
||||
if (metadata.length) {
|
||||
metadata.forEach(({ filterType, questionType }) => {
|
||||
if (!filters.metadata) filters.metadata = {};
|
||||
|
||||
if (filterType.filterValue === "Equals") {
|
||||
filters.metadata[questionType.label ?? ""] = {
|
||||
op: "equals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
} else if (filterType.filterValue === "Not equals") {
|
||||
filters.metadata[questionType.label ?? ""] = {
|
||||
op: "notEquals",
|
||||
value: filterType.filterComboBoxValue as string,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
return filters;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,33 @@
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurveyQuestion } from "@formbricks/types/surveys";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
|
||||
export const replaceQuestionPresetPlaceholders = (question: TSurveyQuestion, product) => {
|
||||
if (!question) return;
|
||||
export const replaceQuestionPresetPlaceholders = (
|
||||
question: TSurveyQuestion,
|
||||
product: TProduct
|
||||
): TSurveyQuestion => {
|
||||
if (!product) return question;
|
||||
const newQuestion = JSON.parse(JSON.stringify(question));
|
||||
const newQuestion = structuredClone(question);
|
||||
const defaultLanguageCode = "default";
|
||||
if (newQuestion.headline) {
|
||||
newQuestion.headline = newQuestion.headline.replace("{{productName}}", product.name);
|
||||
newQuestion.headline[defaultLanguageCode] = getLocalizedValue(
|
||||
newQuestion.headline,
|
||||
defaultLanguageCode
|
||||
).replace("{{productName}}", product.name);
|
||||
}
|
||||
if (newQuestion.subheader) {
|
||||
newQuestion.subheader = newQuestion.subheader?.replace("{{productName}}", product.name);
|
||||
newQuestion.subheader[defaultLanguageCode] = getLocalizedValue(
|
||||
newQuestion.subheader,
|
||||
defaultLanguageCode
|
||||
)?.replace("{{productName}}", product.name);
|
||||
}
|
||||
return newQuestion;
|
||||
};
|
||||
|
||||
// replace all occurences of productName with the actual product name in the current template
|
||||
export const replacePresetPlaceholders = (template: TTemplate, product: any) => {
|
||||
const preset = JSON.parse(JSON.stringify(template.preset));
|
||||
const preset = structuredClone(template.preset);
|
||||
preset.name = preset.name.replace("{{productName}}", product.name);
|
||||
preset.questions = preset.questions.map((question) => {
|
||||
return replaceQuestionPresetPlaceholders(question, product);
|
||||
|
||||
12
apps/web/app/s/[surveyId]/components/InvalidLanguage.tsx
Normal file
12
apps/web/app/s/[surveyId]/components/InvalidLanguage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { StackedCardsContainer } from "@formbricks/ui/StackedCardsContainer";
|
||||
|
||||
export default function InvalidLanguage() {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||
<StackedCardsContainer>
|
||||
<span className="h-24 w-24 rounded-full bg-slate-200 p-6 text-5xl">🈂️</span>
|
||||
<p className="mt-8 text-4xl font-bold">Survey not available in specified language</p>
|
||||
</StackedCardsContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user