mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-31 12:43:19 -05:00
Compare commits
1 Commits
ReviewBot/
...
ReviewBot/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b301482868 |
1
.github/workflows/kamal.yml
vendored
1
.github/workflows/kamal.yml
vendored
@@ -70,7 +70,6 @@ jobs:
|
||||
S3_REGION: ${{ vars.S3_REGION }}
|
||||
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
|
||||
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
|
||||
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
|
||||
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -34,12 +34,7 @@ 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 defaultAttributes = {
|
||||
language: "gu",
|
||||
};
|
||||
const userInitAttributes = { "Init Attribute 1": "eight", "Init Attribute 2": "two" };
|
||||
|
||||
const attributes = isUserId ? { ...defaultAttributes, ...userInitAttributes } : defaultAttributes;
|
||||
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
|
||||
formbricks.init({
|
||||
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const metadata = {
|
||||
title: "Enterprise License to unlock advanced functionality",
|
||||
description: "Request a enterprise licenses to unlock advanced enterprise functionality",
|
||||
description:
|
||||
"Request a self-hosting licenses to unlock advanced enterprise functionality",
|
||||
};
|
||||
|
||||
#### Self-Hosting
|
||||
@@ -13,17 +14,13 @@ 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,6 +1,7 @@
|
||||
import { TSurveyCTAQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
import Headline from "./Headline";
|
||||
import HtmlBody from "./HtmlBody";
|
||||
import { TSurveyCTAQuestion } from "./types";
|
||||
|
||||
interface CTAQuestionProps {
|
||||
question: TSurveyCTAQuestion;
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
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,9 +1,10 @@
|
||||
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,7 +20,6 @@ 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,8 +1,9 @@
|
||||
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,9 +1,10 @@
|
||||
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;
|
||||
@@ -66,8 +67,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,10 +1,11 @@
|
||||
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,6 +1,7 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TTemplate } from "@formbricks/types/templates";
|
||||
import {
|
||||
AppPieChartIcon,
|
||||
ArrowRightCircleIcon,
|
||||
@@ -26,17 +27,14 @@ import {
|
||||
VideoTabletAdjustIcon,
|
||||
} from "@formbricks/ui/icons";
|
||||
|
||||
import { TTemplate } from "./types";
|
||||
|
||||
const thankYouCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Thank you!",
|
||||
subheader: "TWe appreciate your feedback.",
|
||||
subheader: "We appreciate your feedback.",
|
||||
};
|
||||
|
||||
const welcomeCardDefault = {
|
||||
enabled: true,
|
||||
headline: "Welcome!",
|
||||
timeToFinish: false,
|
||||
showResponseCount: false,
|
||||
};
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
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.string().optional(),
|
||||
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,16 +25,6 @@ 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">
|
||||
@@ -53,7 +43,7 @@ export default async function AttributesSection({ personId }: { personId: string
|
||||
</div>
|
||||
|
||||
{Object.entries(person.attributes)
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
|
||||
.filter(([key, _]) => key !== "email" && key !== "userId")
|
||||
.map(([key, value]) => (
|
||||
<div key={key}>
|
||||
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -93,7 +92,7 @@ const ResponseSurveyCard = ({
|
||||
{survey && (
|
||||
<SingleResponseCard
|
||||
response={response}
|
||||
survey={checkForRecallInHeadline(survey, "default")}
|
||||
survey={survey}
|
||||
user={user}
|
||||
pageType="people"
|
||||
environmentTags={environmentTags}
|
||||
|
||||
@@ -4,16 +4,11 @@ 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,7 +1,6 @@
|
||||
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";
|
||||
@@ -12,6 +11,7 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||
interface EnvironmentsNavbarProps {
|
||||
environmentId: string;
|
||||
session: Session;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) {
|
||||
@@ -25,8 +25,6 @@ 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),
|
||||
@@ -48,7 +46,6 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
webAppUrl={WEBAPP_URL}
|
||||
membershipRole={currentUserMembership?.role}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
CreditCardIcon,
|
||||
FileCheckIcon,
|
||||
HeartIcon,
|
||||
LanguagesIcon,
|
||||
LinkIcon,
|
||||
LogOutIcon,
|
||||
MailIcon,
|
||||
@@ -70,7 +69,6 @@ interface NavigationProps {
|
||||
isFormbricksCloud: boolean;
|
||||
webAppUrl: string;
|
||||
membershipRole?: TMembershipRole;
|
||||
isMultiLanguageAllowed: boolean;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
@@ -83,7 +81,6 @@ export default function Navigation({
|
||||
isFormbricksCloud,
|
||||
webAppUrl,
|
||||
membershipRole,
|
||||
isMultiLanguageAllowed,
|
||||
}: NavigationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -169,12 +166,6 @@ 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,7 +8,6 @@ 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 {
|
||||
@@ -335,7 +334,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, "default")?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey)?.questions.map((question) => (
|
||||
<Controller
|
||||
key={question.id}
|
||||
control={control}
|
||||
@@ -355,7 +354,7 @@ export default function AddIntegrationModal(props: AddIntegrationModalProps) {
|
||||
: field.onChange(field.value?.filter((value) => value !== question.id));
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
<span className="ml-2">{question.headline}</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -20,10 +19,6 @@ 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,7 +6,6 @@ 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 {
|
||||
@@ -275,7 +274,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, "default")?.questions.map((question) => (
|
||||
{checkForRecallInHeadline(selectedSurvey)?.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
|
||||
@@ -288,7 +287,7 @@ export default function AddIntegrationModal({
|
||||
handleCheckboxChange(question.id);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{getLocalizedValue(question.headline, "default")}</span>
|
||||
<span className="ml-2">{question.headline}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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";
|
||||
@@ -25,10 +24,6 @@ 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, "default")?.questions.map((q) => ({
|
||||
? checkForRecallInHeadline(selectedSurvey)?.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) => structuredClone(item);
|
||||
const createCopy = (item) => JSON.parse(JSON.stringify(item));
|
||||
|
||||
const MappingRow = ({ idx }: { idx: number }) => {
|
||||
const filteredQuestionItems = getFilteredQuestionItems(idx);
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
@@ -41,7 +42,11 @@ export default async function EnvironmentLayout({ children, params }) {
|
||||
/>
|
||||
<FormbricksClient session={session} />
|
||||
<ToasterClient />
|
||||
<EnvironmentsNavbar environmentId={params.environmentId} session={session} />
|
||||
<EnvironmentsNavbar
|
||||
environmentId={params.environmentId}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
<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: false,
|
||||
comingSoon: true,
|
||||
},
|
||||
{
|
||||
title: "Unlimited Responses",
|
||||
@@ -162,7 +162,7 @@ export default function PricingTableComponent({
|
||||
},
|
||||
{
|
||||
title: "Multi-Language Surveys",
|
||||
comingSoon: false,
|
||||
comingSoon: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
FileSearch2Icon,
|
||||
HashIcon,
|
||||
KeyIcon,
|
||||
LanguagesIcon,
|
||||
LinkIcon,
|
||||
SlidersIcon,
|
||||
UserCircleIcon,
|
||||
@@ -29,23 +28,19 @@ 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,
|
||||
isMultiLanguageAllowed,
|
||||
}: SettingsNavbarProps) {
|
||||
}: {
|
||||
environmentId: string;
|
||||
isFormbricksCloud: boolean;
|
||||
team: TTeam;
|
||||
product: TProduct;
|
||||
membershipRole?: TMembershipRole;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
|
||||
const { isAdmin, isOwner, isViewer } = getAccessFlags(membershipRole);
|
||||
@@ -105,13 +100,6 @@ 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`,
|
||||
@@ -218,7 +206,7 @@ export default function SettingsNavbar({
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
[environmentId, pathname, isViewer, isMultiLanguageAllowed, isFormbricksCloud, isPricingDisabled]
|
||||
[environmentId, isFormbricksCloud, pathname, isPricingDisabled, isViewer]
|
||||
);
|
||||
|
||||
if (!navigation) return null;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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,7 +1,6 @@
|
||||
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";
|
||||
@@ -20,11 +19,9 @@ 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");
|
||||
}
|
||||
@@ -33,8 +30,6 @@ 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 (
|
||||
@@ -46,7 +41,6 @@ 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 enterprise license."
|
||||
textForUrl="get a self-hosting license (free)."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -65,7 +65,7 @@ const ResponsePage = ({
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
return checkForRecallInHeadline(survey);
|
||||
}, [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} user={user} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
activeId="responses"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"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";
|
||||
|
||||
@@ -17,6 +18,17 @@ 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,7 +2,6 @@ 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";
|
||||
|
||||
@@ -16,7 +15,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +2,6 @@ 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";
|
||||
|
||||
@@ -17,7 +16,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +2,6 @@ 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";
|
||||
|
||||
@@ -16,7 +15,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +3,6 @@ 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";
|
||||
@@ -21,7 +20,8 @@ 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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +3,6 @@ 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";
|
||||
@@ -21,7 +20,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +3,6 @@ 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";
|
||||
@@ -34,7 +33,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +2,6 @@ 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";
|
||||
|
||||
@@ -16,7 +15,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +3,6 @@ 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";
|
||||
@@ -20,8 +19,7 @@ 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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
<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,7 +3,6 @@ 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";
|
||||
|
||||
@@ -20,7 +19,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,7 +3,6 @@ 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";
|
||||
@@ -25,7 +24,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={getLocalizedValue(questionSummary.question.headline, "default")} />
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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,16 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeftIcon, BellRing, BlocksIcon, Code2Icon, LinkIcon, MailIcon } from "lucide-react";
|
||||
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 Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
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";
|
||||
@@ -21,6 +32,7 @@ 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) {
|
||||
@@ -37,8 +49,35 @@ 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);
|
||||
@@ -52,6 +91,11 @@ 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">
|
||||
@@ -59,12 +103,39 @@ 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>
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
/>
|
||||
<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>
|
||||
</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>
|
||||
@@ -131,10 +202,10 @@ export default function ShareEmbedSurvey({ survey, open, setOpen, webAppUrl, use
|
||||
<WebpageTab surveyUrl={surveyUrl} />
|
||||
) : activeId === "link" ? (
|
||||
<LinkTab
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
webAppUrl={webAppUrl}
|
||||
generateNewSingleUseLink={generateNewSingleUseLink}
|
||||
isSingleUseLinkSurvey={isSingleUseLinkSurvey}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -15,10 +16,18 @@ interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
product: TProduct;
|
||||
user: TUser;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
export default function SuccessMessage({ environment, survey, webAppUrl, user }: SummaryMetadataProps) {
|
||||
export default function SuccessMessage({
|
||||
environment,
|
||||
survey,
|
||||
webAppUrl,
|
||||
product,
|
||||
user,
|
||||
}: SummaryMetadataProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
@@ -54,6 +63,7 @@ export default function SuccessMessage({ environment, survey, webAppUrl, user }:
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
{confetti && <Confetti />}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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";
|
||||
@@ -16,6 +15,7 @@ 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.toString())}
|
||||
Last updated: {timeSinceConditionally(survey.updatedAt.toISOString())}
|
||||
</div>
|
||||
<Button
|
||||
variant="minimal"
|
||||
|
||||
@@ -91,9 +91,8 @@ const SummaryPage = ({
|
||||
}, [filters, surveyId]);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
survey = useMemo(() => {
|
||||
return checkForRecallInHeadline(survey, "default");
|
||||
return checkForRecallInHeadline(survey);
|
||||
}, [survey]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -115,7 +114,7 @@ const SummaryPage = ({
|
||||
/>
|
||||
<div className="flex gap-1.5">
|
||||
<CustomFilter environmentTags={environmentTags} attributes={attributes} survey={survey} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} product={product} user={user} />
|
||||
</div>
|
||||
<SurveyResultsTabs
|
||||
activeId="summary"
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
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 { TSurvey } from "@formbricks/types/surveys";
|
||||
import { ShareSurveyLink } from "@formbricks/ui/ShareSurveyLink";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
interface LinkTabProps {
|
||||
survey: TSurvey;
|
||||
webAppUrl: string;
|
||||
surveyUrl: string;
|
||||
setSurveyUrl: (url: string) => void;
|
||||
webAppUrl: string;
|
||||
generateNewSingleUseLink: () => void;
|
||||
isSingleUseLinkSurvey: boolean;
|
||||
}
|
||||
|
||||
export default function LinkTab({ survey, webAppUrl, surveyUrl, setSurveyUrl }: LinkTabProps) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const docsLinks = [
|
||||
{
|
||||
title: "Identify users",
|
||||
@@ -39,12 +62,39 @@ export default function LinkTab({ survey, webAppUrl, surveyUrl, setSurveyUrl }:
|
||||
<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>
|
||||
<ShareSurveyLink
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
surveyUrl={surveyUrl}
|
||||
setSurveyUrl={setSurveyUrl}
|
||||
/>
|
||||
<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>
|
||||
</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,7 +15,6 @@ 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";
|
||||
@@ -51,17 +50,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-slate-200 bg-slate-50" />
|
||||
<EmailFooter />
|
||||
@@ -71,20 +70,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text
|
||||
className="m-0 p-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
|
||||
}}></Text>
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></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">
|
||||
{getLocalizedValue(firstQuestion.label, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 inline-block">{firstQuestion.label}</Text>
|
||||
</Container>
|
||||
<Container className="mx-0 mt-4 flex max-w-none justify-end">
|
||||
{!firstQuestion.required && (
|
||||
@@ -111,10 +104,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex w-max flex-col">
|
||||
<Section className="block overflow-hidden rounded-md border border-slate-200">
|
||||
@@ -130,14 +123,10 @@ 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">
|
||||
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 inline-block w-max p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">
|
||||
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
@@ -150,14 +139,10 @@ 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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="mt-2 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text
|
||||
className="m-0 p-0"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
|
||||
}}></Text>
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mx-0 mt-4 max-w-none">
|
||||
@@ -165,7 +150,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">
|
||||
{getLocalizedValue(firstQuestion.dismissButtonLabel, defaultLanguageCode) || "Skip"}
|
||||
{firstQuestion.dismissButtonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
@@ -174,7 +159,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"
|
||||
)}>
|
||||
{getLocalizedValue(firstQuestion.buttonLabel, defaultLanguageCode)}
|
||||
{firstQuestion.buttonLabel}
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
@@ -185,10 +170,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 w-full items-center justify-center">
|
||||
<Section
|
||||
@@ -220,14 +205,10 @@ 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">
|
||||
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 inline-block p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block p-0 text-right">
|
||||
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
|
||||
</Text>
|
||||
<Text className="m-0 inline-block p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
@@ -240,17 +221,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</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}>
|
||||
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
||||
{choice.label}
|
||||
</Section>
|
||||
))}
|
||||
</Container>
|
||||
@@ -261,10 +242,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices
|
||||
@@ -274,7 +255,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}`}>
|
||||
{getLocalizedValue(choice.label, defaultLanguageCode)}
|
||||
{choice.label}
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
@@ -285,10 +266,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Section className="mx-0">
|
||||
{firstQuestion.choices.map((choice) =>
|
||||
@@ -314,7 +295,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">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</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{" "}
|
||||
@@ -326,10 +307,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">
|
||||
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
|
||||
{firstQuestion.subheader}
|
||||
</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,6 +51,7 @@ 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,11 +1,9 @@
|
||||
"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";
|
||||
@@ -23,7 +21,7 @@ type QuestionFilterComboBoxProps = {
|
||||
filterComboBoxValue: string | string[] | undefined;
|
||||
onChangeFilterValue: (o: string) => void;
|
||||
onChangeFilterComboBoxValue: (o: string | string[]) => void;
|
||||
type: OptionsType.METADATA | TSurveyQuestionType | OptionsType.ATTRIBUTES | OptionsType.TAGS | undefined;
|
||||
type: TSurveyQuestionType | "Attributes" | "Tags" | undefined;
|
||||
handleRemoveMultiSelect: (value: string[]) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -42,7 +40,6 @@ 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
|
||||
@@ -153,21 +150,14 @@ const QuestionFilterComboBox = ({
|
||||
<CommandItem
|
||||
onSelect={() => {
|
||||
!isMultiple
|
||||
? onChangeFilterComboBoxValue(
|
||||
typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o
|
||||
)
|
||||
? onChangeFilterComboBoxValue(o)
|
||||
: onChangeFilterComboBoxValue(
|
||||
Array.isArray(filterComboBoxValue)
|
||||
? [
|
||||
...filterComboBoxValue,
|
||||
typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o,
|
||||
]
|
||||
: [typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o]
|
||||
Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, o] : [o]
|
||||
);
|
||||
!isMultiple && setOpen(false);
|
||||
}}
|
||||
className="cursor-pointer">
|
||||
{typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o}
|
||||
{o}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -8,16 +8,14 @@ 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 {
|
||||
@@ -34,7 +32,6 @@ export enum OptionsType {
|
||||
QUESTIONS = "Questions",
|
||||
TAGS = "Tags",
|
||||
ATTRIBUTES = "Attributes",
|
||||
METADATA = "Metadata",
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
@@ -56,37 +53,31 @@ interface QuestionComboBoxProps {
|
||||
|
||||
const SelectedCommandItem = ({ label, questionType, type }: Partial<QuestionOption>) => {
|
||||
const getIconType = () => {
|
||||
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" />;
|
||||
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" />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,8 +86,6 @@ 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";
|
||||
}
|
||||
@@ -104,9 +93,7 @@ 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">
|
||||
{typeof label === "string" ? label : getLocalizedValue(label, "default")}
|
||||
</p>
|
||||
<p className="ml-3 truncate text-base text-slate-600">{label}</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" | "Languages";
|
||||
type: TSurveyTSurveyQuestionType | "Attributes" | "Tags";
|
||||
filterOptions: string[];
|
||||
filterComboBoxOptions: string[];
|
||||
id: string;
|
||||
@@ -200,9 +200,7 @@ const ResponseFilter = () => {
|
||||
key={`${s.questionType.id}-${i}`}
|
||||
filterOptions={
|
||||
selectedOptions.questionFilterOptions.find(
|
||||
(q) =>
|
||||
(q.type === s.questionType.questionType || q.type === s.questionType.type) &&
|
||||
q.id === s.questionType.id
|
||||
(q) => q.type === s.questionType.type || q.type === s.questionType.questionType
|
||||
)?.filterOptions
|
||||
}
|
||||
filterComboBoxOptions={
|
||||
@@ -242,15 +240,15 @@ const ResponseFilter = () => {
|
||||
</>
|
||||
))}
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<Button size="sm" variant="secondary" onClick={handleAddNewFilter}>
|
||||
<Button size="sm" variant="darkCTA" onClick={handleAddNewFilter}>
|
||||
Add filter
|
||||
<Plus width={18} height={18} className="ml-2" />
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="darkCTA" onClick={handleApplyFilters}>
|
||||
Apply filters
|
||||
<Button size="sm" variant="primary" onClick={handleApplyFilters}>
|
||||
Apply Filters
|
||||
</Button>
|
||||
<Button size="sm" variant="minimal" onClick={handleClearAllFilters}>
|
||||
<Button size="sm" variant="secondary" onClick={handleClearAllFilters}>
|
||||
Clear all
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
@@ -24,11 +25,13 @@ 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, user }: ResultsShareButtonProps) {
|
||||
export default function ResultsShareButton({ survey, webAppUrl, product, user }: ResultsShareButtonProps) {
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [showResultsLinkModal, setShowResultsLinkModal] = useState(false);
|
||||
|
||||
@@ -143,6 +146,7 @@ export default function ResultsShareButton({ survey, webAppUrl, user }: ResultsS
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
product={product}
|
||||
webAppUrl={webAppUrl}
|
||||
user={user}
|
||||
/>
|
||||
|
||||
@@ -104,7 +104,13 @@ const SummaryHeader = ({
|
||||
<DropdownMenuContent align="end" className="p-2">
|
||||
{survey.type === "link" && (
|
||||
<>
|
||||
<ResultsShareButton survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
<ResultsShareButton
|
||||
className="flex w-full justify-center p-1"
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
user={user}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
@@ -183,12 +189,19 @@ const SummaryHeader = ({
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<SuccessMessage environment={environment} survey={survey} webAppUrl={webAppUrl} user={user} />
|
||||
<SuccessMessage
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
webAppUrl={webAppUrl}
|
||||
product={product}
|
||||
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({
|
||||
...universalQuestionPresets,
|
||||
...getQuestionDefaults(questionType.id, product),
|
||||
id: createId(),
|
||||
type: questionType.id,
|
||||
...universalQuestionPresets,
|
||||
...getQuestionDefaults(questionType.id, product),
|
||||
});
|
||||
setOpen(false);
|
||||
}}>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { BackButtonInput } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard";
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
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 {
|
||||
@@ -15,8 +17,6 @@ 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}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Description</Label>
|
||||
<div className="mt-2">
|
||||
<LocalizedEditor
|
||||
id="subheader"
|
||||
value={question.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
<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
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={questionIdx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,33 +82,24 @@ export default function CTAQuestionForm({
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<div className="mt-2 flex justify-between gap-8">
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="flex w-full space-x-2">
|
||||
<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 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>
|
||||
{questionIdx !== 0 && (
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -133,16 +124,12 @@ export default function CTAQuestionForm({
|
||||
<div className="mt-3 flex-1">
|
||||
<Label htmlFor="buttonLabel">Skip Button Label</Label>
|
||||
<div className="mt-2">
|
||||
<QuestionFormInput
|
||||
<Input
|
||||
id="dismissButtonLabel"
|
||||
name="dismissButtonLabel"
|
||||
value={question.dismissButtonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
placeholder="Skip"
|
||||
onChange={(e) => updateQuestion(questionIdx, { dismissButtonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
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;
|
||||
@@ -14,8 +13,6 @@ interface CalQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -24,49 +21,43 @@ 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}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -74,13 +65,7 @@ export default function CalQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { LocalizedEditor } from "@formbricks/ee/multiLanguage/components/LocalizedEditor";
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
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,54 +23,65 @@ 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}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Description</Label>
|
||||
<div className="mt-2">
|
||||
<LocalizedEditor
|
||||
id="subheader"
|
||||
value={question.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
<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
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={questionIdx}
|
||||
/>
|
||||
</div>
|
||||
</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}
|
||||
/>
|
||||
<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> */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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 {
|
||||
@@ -14,8 +13,6 @@ interface IDateQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -40,48 +37,42 @@ 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
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
localSurvey={localSurvey}
|
||||
type="headline"
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={localSurvey.environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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 && (
|
||||
@@ -90,13 +81,7 @@ export default function DateQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -4,11 +4,10 @@ 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 {
|
||||
@@ -16,9 +15,6 @@ 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({
|
||||
@@ -26,16 +22,11 @@ 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>(
|
||||
getLocalizedValue(localSurvey.thankYouCard.buttonLabel, "default") || localSurvey.thankYouCard.buttonLink
|
||||
? true
|
||||
: false
|
||||
localSurvey.thankYouCard.buttonLabel || localSurvey.thankYouCard.buttonLink ? true : false
|
||||
);
|
||||
const setOpen = (e) => {
|
||||
if (e) {
|
||||
@@ -46,14 +37,13 @@ export default function EditThankYouCard({
|
||||
};
|
||||
|
||||
const updateSurvey = (data) => {
|
||||
const updatedSurvey = {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
thankYouCard: {
|
||||
...localSurvey.thankYouCard,
|
||||
...data,
|
||||
},
|
||||
};
|
||||
setLocalSurvey(updatedSurvey);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -64,9 +54,8 @@ export default function EditThankYouCard({
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
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"
|
||||
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"
|
||||
)}>
|
||||
<p>🙏</p>
|
||||
</div>
|
||||
@@ -108,26 +97,28 @@ 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}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={localSurvey.thankYouCard.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
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>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Switch
|
||||
@@ -138,7 +129,7 @@ export default function EditThankYouCard({
|
||||
updateSurvey({ buttonLabel: undefined, buttonLink: undefined });
|
||||
} else {
|
||||
updateSurvey({
|
||||
buttonLabel: { default: "Create your own Survey" },
|
||||
buttonLabel: "Create your own Survey",
|
||||
buttonLink: "https://formbricks.com/signup",
|
||||
});
|
||||
}
|
||||
@@ -155,20 +146,16 @@ export default function EditThankYouCard({
|
||||
</Label>
|
||||
</div>
|
||||
{showThankYouCardCTA && (
|
||||
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4 pt-2">
|
||||
<div className="border-1 mt-4 space-y-4 rounded-md border bg-slate-100 p-4">
|
||||
<div className="space-y-2">
|
||||
<QuestionFormInput
|
||||
<Label>Button Label</Label>
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
label="Button Label"
|
||||
placeholder="Create your own Survey"
|
||||
name="buttonLabel"
|
||||
className="bg-white"
|
||||
placeholder="Create your own Survey"
|
||||
value={localSurvey.thankYouCard.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={localSurvey.questions.length}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
onChange={(e) => updateSurvey({ buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,12 +4,13 @@ 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 {
|
||||
@@ -17,9 +18,6 @@ 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({
|
||||
@@ -27,9 +25,6 @@ export default function EditWelcomeCard({
|
||||
setLocalSurvey,
|
||||
setActiveQuestionId,
|
||||
activeQuestionId,
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: EditWelcomeCardProps) {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const path = usePathname();
|
||||
@@ -64,9 +59,8 @@ export default function EditWelcomeCard({
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
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"
|
||||
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"
|
||||
)}>
|
||||
<p>✋</p>
|
||||
</div>
|
||||
@@ -90,7 +84,7 @@ export default function EditWelcomeCard({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="welcome-toggle">{localSurvey?.welcomeCard?.enabled ? "On" : "Off"}</Label>
|
||||
<Label htmlFor="welcome-toggle">Enabled</Label>
|
||||
|
||||
<Switch
|
||||
id="welcome-toggle"
|
||||
@@ -120,32 +114,34 @@ export default function EditWelcomeCard({
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={localSurvey.welcomeCard.headline}
|
||||
label="Headline"
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={-1}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="subheader">Welcome Message</Label>
|
||||
<div className="mt-2">
|
||||
<LocalizedEditor
|
||||
id="html"
|
||||
value={localSurvey.welcomeCard.html}
|
||||
localSurvey={localSurvey}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
<Editor
|
||||
getText={() =>
|
||||
md.render(
|
||||
localSurvey?.welcomeCard?.html || "Thanks for providing your feedback - let's go!"
|
||||
)
|
||||
}
|
||||
setText={(value: string) => {
|
||||
updateSurvey({ html: value });
|
||||
}}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
disableLists
|
||||
firstRender={firstRender}
|
||||
setFirstRender={setFirstRender}
|
||||
questionIdx={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -153,18 +149,15 @@ 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">
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={localSurvey.welcomeCard.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={-1}
|
||||
maxLength={48}
|
||||
placeholder={"Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,6 @@ 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";
|
||||
@@ -13,7 +11,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;
|
||||
@@ -22,8 +20,6 @@ interface FileUploadFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -34,8 +30,6 @@ export default function FileUploadQuestionForm({
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
product,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: FileUploadFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const [extension, setExtension] = useState("");
|
||||
@@ -44,7 +38,6 @@ export default function FileUploadQuestionForm({
|
||||
error: billingInfoError,
|
||||
isLoading: billingInfoLoading,
|
||||
} = useGetBillingInfo(product?.teamId ?? "");
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setExtension(event.target.value);
|
||||
@@ -110,42 +103,41 @@ export default function FileUploadQuestionForm({
|
||||
return 10;
|
||||
}, [billingInfo, billingInfoError, billingInfoLoading]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -153,13 +145,7 @@ export default function FileUploadQuestionForm({
|
||||
className="mt-3"
|
||||
variant="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
|
||||
@@ -74,9 +74,7 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="hidden-fields-toggle">
|
||||
{localSurvey?.hiddenFields?.enabled ? "On" : "Off"}
|
||||
</Label>
|
||||
<Label htmlFor="hidden-fields-toggle">Enabled</Label>
|
||||
|
||||
<Switch
|
||||
id="hidden-fields-toggle"
|
||||
@@ -108,9 +106,7 @@ const HiddenFieldsCard: FC<HiddenFieldsCardProps> = ({
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="mt-2 text-sm italic text-slate-500">
|
||||
No hidden fields yet. Add the first one below.
|
||||
</p>
|
||||
<p className="text-sm italic text-slate-500">No hidden fields yet. Add the first one below.</p>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
@@ -177,17 +173,7 @@ const validateHiddenField = (
|
||||
}
|
||||
// no key words -- userId & suid & existing question ids
|
||||
if (
|
||||
[
|
||||
"userId",
|
||||
"source",
|
||||
"suid",
|
||||
"end",
|
||||
"start",
|
||||
"welcomeCard",
|
||||
"hidden",
|
||||
"verifiedEmail",
|
||||
"multiLanguage",
|
||||
].includes(field) ||
|
||||
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(field) ||
|
||||
existingQuestions.findIndex((q) => q.id === field) !== -1
|
||||
) {
|
||||
return "Question not allowed";
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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,7 +4,6 @@ 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,
|
||||
@@ -47,12 +46,12 @@ export default function LogicEditor({
|
||||
updateQuestion,
|
||||
}: LogicEditorProps): JSX.Element {
|
||||
localSurvey = useMemo(() => {
|
||||
return checkForRecallInHeadline(localSurvey, "default");
|
||||
return checkForRecallInHeadline(localSurvey);
|
||||
}, [localSurvey]);
|
||||
|
||||
const questionValues = useMemo(() => {
|
||||
if ("choices" in question) {
|
||||
return question.choices.map((choice) => getLocalizedValue(choice.label, "default"));
|
||||
return question.choices.map((choice) => choice.label);
|
||||
} else if ("range" in question) {
|
||||
return Array.from({ length: question.range ? question.range : 0 }, (_, i) => (i + 1).toString());
|
||||
} else if (question.type === TSurveyQuestionType.NPS) {
|
||||
@@ -239,7 +238,7 @@ export default function LogicEditor({
|
||||
};
|
||||
|
||||
const deleteLogic = (logicIdx: number) => {
|
||||
const updatedLogic = !question.logic ? [] : structuredClone(question.logic);
|
||||
const updatedLogic = !question.logic ? [] : JSON.parse(JSON.stringify(question.logic));
|
||||
updatedLogic.splice(logicIdx, 1);
|
||||
updateQuestion(questionIdx, { logic: updatedLogic });
|
||||
};
|
||||
@@ -349,14 +348,9 @@ export default function LogicEditor({
|
||||
{localSurvey.questions.map(
|
||||
(question, idx) =>
|
||||
idx !== questionIdx && (
|
||||
<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>
|
||||
<SelectItem key={question.id} value={question.id} title={question.headline}>
|
||||
<div className="max-w-[6rem]">
|
||||
<p className="truncate text-left">{question.headline}</p>
|
||||
</div>
|
||||
</SelectItem>
|
||||
)
|
||||
|
||||
@@ -3,18 +3,13 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
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 { cn } from "@formbricks/lib/cn";
|
||||
import { 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 {
|
||||
@@ -23,8 +18,6 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -34,15 +27,12 @@ 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: {
|
||||
@@ -62,8 +52,8 @@ export default function MultipleChoiceMultiForm({
|
||||
},
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
const newLabel = updatedAttributes.label.en;
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => {
|
||||
const newLabel = updatedAttributes.label;
|
||||
const oldLabel = question.choices[choiceIdx].label;
|
||||
let newChoices: any[] = [];
|
||||
if (question.choices) {
|
||||
@@ -77,11 +67,9 @@ 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) =>
|
||||
newLabel && value === oldLabel[selectedLanguageCode] ? newLabel : value
|
||||
);
|
||||
newL = logic.value.map((value) => (value === oldLabel ? newLabel : value));
|
||||
} else {
|
||||
newL = logic.value === oldLabel[selectedLanguageCode] ? newLabel : logic.value;
|
||||
newL = logic.value === oldLabel ? newLabel : logic.value;
|
||||
}
|
||||
newLogic.push({ ...logic, value: newL });
|
||||
});
|
||||
@@ -91,17 +79,21 @@ 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 (
|
||||
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
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const addChoice = (choiceIdx?: number) => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
@@ -109,10 +101,7 @@ export default function MultipleChoiceMultiForm({
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
const newChoice = { id: createId(), label: "" };
|
||||
if (choiceIdx !== undefined) {
|
||||
newChoices.splice(choiceIdx + 1, 0, newChoice);
|
||||
} else {
|
||||
@@ -127,10 +116,7 @@ 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: createI18nString("Other", surveyLanguageCodes),
|
||||
});
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
updateQuestion(questionIdx, {
|
||||
choices: newChoices,
|
||||
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
|
||||
@@ -143,7 +129,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[selectedLanguageCode];
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
@@ -174,43 +160,43 @@ export default function MultipleChoiceMultiForm({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
ref={questionRef}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -218,13 +204,7 @@ export default function MultipleChoiceMultiForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -233,55 +213,45 @@ 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="w-full space-x-2">
|
||||
<QuestionFormInput
|
||||
key={choice.id}
|
||||
id={`choice-${choiceIdx}`}
|
||||
localSurvey={localSurvey}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
questionIdx={questionIdx}
|
||||
<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")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
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={
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguageCodes)
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<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"
|
||||
<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 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
"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 { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
|
||||
import { TI18nString, TSurvey, TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { 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 {
|
||||
@@ -20,8 +18,6 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -31,16 +27,13 @@ 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",
|
||||
@@ -62,19 +55,23 @@ 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 (
|
||||
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
|
||||
if (question.choices[i].label.trim() === question.choices[j].label.trim()) {
|
||||
return question.choices[i].label.trim(); // Return the duplicate label
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: TI18nString }) => {
|
||||
const newLabel = updatedAttributes.label.en;
|
||||
const findEmptyLabel = () => {
|
||||
for (let i = 0; i < question.choices.length; i++) {
|
||||
if (question.choices[i].label.trim() === "") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => {
|
||||
const newLabel = updatedAttributes.label;
|
||||
const oldLabel = question.choices[choiceIdx].label;
|
||||
let newChoices: any[] = [];
|
||||
if (question.choices) {
|
||||
@@ -90,7 +87,7 @@ export default function MultipleChoiceSingleForm({
|
||||
if (Array.isArray(logic.value)) {
|
||||
newL = logic.value.map((value) => (value === oldLabel ? newLabel : value));
|
||||
} else {
|
||||
newL = logic.value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : logic.value;
|
||||
newL = logic.value === oldLabel ? newLabel : logic.value;
|
||||
}
|
||||
newLogic.push({ ...logic, value: newL });
|
||||
});
|
||||
@@ -104,10 +101,7 @@ export default function MultipleChoiceSingleForm({
|
||||
if (otherChoice) {
|
||||
newChoices = newChoices.filter((choice) => choice.id !== "other");
|
||||
}
|
||||
const newChoice = {
|
||||
id: createId(),
|
||||
label: createI18nString("", surveyLanguageCodes),
|
||||
};
|
||||
const newChoice = { id: createId(), label: "" };
|
||||
if (choiceIdx !== undefined) {
|
||||
newChoices.splice(choiceIdx + 1, 0, newChoice);
|
||||
} else {
|
||||
@@ -122,10 +116,7 @@ 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: createI18nString("Other", surveyLanguageCodes),
|
||||
});
|
||||
newChoices.push({ id: "other", label: "Other" });
|
||||
updateQuestion(questionIdx, {
|
||||
choices: newChoices,
|
||||
...(question.shuffleOption === shuffleOptionsTypes.all.id && {
|
||||
@@ -137,7 +128,8 @@ export default function MultipleChoiceSingleForm({
|
||||
|
||||
const deleteChoice = (choiceIdx: number) => {
|
||||
const newChoices = !question.choices ? [] : question.choices.filter((_, idx) => idx !== choiceIdx);
|
||||
const choiceValue = question.choices[choiceIdx].label[selectedLanguageCode];
|
||||
|
||||
const choiceValue = question.choices[choiceIdx].label;
|
||||
if (isInvalidValue === choiceValue) {
|
||||
setisInvalidValue(null);
|
||||
}
|
||||
@@ -168,43 +160,43 @@ export default function MultipleChoiceSingleForm({
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
ref={questionRef}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -212,13 +204,7 @@ export default function MultipleChoiceSingleForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -227,55 +213,45 @@ 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 className="inline-flex w-full items-center">
|
||||
<div key={choiceIdx} className="flex w-full items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<QuestionFormInput
|
||||
key={choice.id}
|
||||
id={`choice-${choiceIdx}`}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
<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")}
|
||||
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
|
||||
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}
|
||||
onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
!isLabelValidForAllLanguages(question.choices[choiceIdx].label, surveyLanguages)
|
||||
(isInvalidValue === "" && choice.label.trim() === "") ||
|
||||
(isInvalidValue !== null && choice.label.trim() === isInvalidValue.trim())
|
||||
}
|
||||
className={`${choice.id === "other" ? "border border-dashed" : ""}`}
|
||||
/>
|
||||
{choice.id === "other" && (
|
||||
<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"
|
||||
<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 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
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 { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import QuestionFormInput from "@formbricks/ui/QuestionFormInput";
|
||||
|
||||
interface NPSQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -14,8 +15,6 @@ interface NPSQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -26,48 +25,44 @@ export default function NPSQuestionForm({
|
||||
lastQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: NPSQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className=" flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -75,60 +70,50 @@ export default function NPSQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
</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 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>
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="upperLabel"
|
||||
value={question.upperLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{!question.required && (
|
||||
<div className="mt-3">
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
</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,8 +36,6 @@ interface OpenQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -47,58 +45,55 @@ 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: createI18nString(getPlaceholderByInputType(inputType), surveyLanguageCodes),
|
||||
placeholder: getPlaceholderByInputType(inputType),
|
||||
longAnswer: inputType === "text" ? question.longAnswer : false,
|
||||
};
|
||||
updateQuestion(questionIdx, updatedAttributes);
|
||||
};
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -106,32 +101,23 @@ export default function OpenQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
</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 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>
|
||||
|
||||
{/* Add a dropdown to select the question type */}
|
||||
|
||||
@@ -3,12 +3,11 @@ 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 {
|
||||
@@ -17,8 +16,6 @@ interface PictureSelectionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -27,50 +24,44 @@ 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}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -78,13 +69,7 @@ export default function PictureSelectionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => 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 { TI18nString, TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
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,11 +53,35 @@ 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,
|
||||
@@ -69,8 +93,6 @@ export default function QuestionCard({
|
||||
activeQuestionId,
|
||||
setActiveQuestionId,
|
||||
lastQuestion,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
}: QuestionCardProps) {
|
||||
const question = localSurvey.questions[questionIdx];
|
||||
@@ -96,10 +118,10 @@ export default function QuestionCard({
|
||||
});
|
||||
};
|
||||
|
||||
const updateEmptyNextButtonLabels = (labelValue: TI18nString) => {
|
||||
const updateEmptyNextButtonLabels = (labelValue: string) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel[selectedLanguageCode]?.trim() === "") {
|
||||
if (!q.buttonLabel || q.buttonLabel?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
}
|
||||
});
|
||||
@@ -166,14 +188,8 @@ export default function QuestionCard({
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">
|
||||
{recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(question.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
] ?? ""
|
||||
)
|
||||
{recallToHeadline(question.headline, localSurvey, true)
|
||||
? formatTextWithSlashes(recallToHeadline(question.headline, localSurvey, true))
|
||||
: getTSurveyQuestionTypeName(question.type)}
|
||||
</p>
|
||||
{!open && question?.required && (
|
||||
@@ -203,8 +219,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||
@@ -214,8 +228,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.MultipleChoiceMulti ? (
|
||||
@@ -225,8 +237,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.NPS ? (
|
||||
@@ -236,8 +246,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.CTA ? (
|
||||
@@ -247,8 +255,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Rating ? (
|
||||
@@ -258,8 +264,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Consent ? (
|
||||
@@ -268,8 +272,6 @@ export default function QuestionCard({
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
@@ -279,8 +281,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.PictureSelection ? (
|
||||
@@ -290,8 +290,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
||||
@@ -302,8 +300,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
@@ -313,8 +309,6 @@ export default function QuestionCard({
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isInvalid={isInvalid}
|
||||
/>
|
||||
) : null}
|
||||
@@ -333,43 +327,34 @@ export default function QuestionCard({
|
||||
{question.type !== TSurveyQuestionType.NPS &&
|
||||
question.type !== TSurveyQuestionType.Rating &&
|
||||
question.type !== TSurveyQuestionType.CTA ? (
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<div className="mt-4 flex space-x-2">
|
||||
<div className="w-full">
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
{questionIdx !== 0 && (
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { backButtonLabel: e.target.value });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -378,17 +363,12 @@ export default function QuestionCard({
|
||||
question.type === TSurveyQuestionType.NPS) &&
|
||||
questionIdx !== 0 && (
|
||||
<div className="mt-4">
|
||||
<QuestionFormInput
|
||||
id="backButtonLabel"
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
maxLength={48}
|
||||
placeholder={"Back"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
onChange={(e) => {
|
||||
if (e.target.value.trim() == "") e.target.value = "";
|
||||
updateQuestion(questionIdx, { backButtonLabel: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface QuestionsAudienceTabsProps {
|
||||
|
||||
export default function QuestionsAudienceTabs({ activeId, setActiveId }: QuestionsAudienceTabsProps) {
|
||||
return (
|
||||
<div className="fixed z-20 flex h-14 w-full items-center justify-center border bg-white md:w-1/2">
|
||||
<div className="fixed z-10 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,8 +6,6 @@ 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";
|
||||
@@ -17,7 +15,7 @@ import EditThankYouCard from "./EditThankYouCard";
|
||||
import EditWelcomeCard from "./EditWelcomeCard";
|
||||
import QuestionCard from "./QuestionCard";
|
||||
import { StrictModeDroppable } from "./StrictModeDroppable";
|
||||
import { isCardValid, validateQuestion, validateSurveyQuestionsInBatch } from "./Validation";
|
||||
import { validateQuestion } from "./Validation";
|
||||
|
||||
interface QuestionsViewProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -25,12 +23,8 @@ interface QuestionsViewProps {
|
||||
activeQuestionId: string | null;
|
||||
setActiveQuestionId: (questionId: string | null) => void;
|
||||
product: TProduct;
|
||||
invalidQuestions: string[] | null;
|
||||
setInvalidQuestions: (invalidQuestions: string[] | null) => void;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
invalidQuestions: String[] | null;
|
||||
setInvalidQuestions: (invalidQuestions: String[] | null) => void;
|
||||
}
|
||||
|
||||
export default function QuestionsView({
|
||||
@@ -41,10 +35,6 @@ export default function QuestionsView({
|
||||
product,
|
||||
invalidQuestions,
|
||||
setInvalidQuestions,
|
||||
setSelectedLanguageCode,
|
||||
selectedLanguageCode,
|
||||
isMultiLanguageAllowed,
|
||||
isFormbricksCloud,
|
||||
}: QuestionsViewProps) {
|
||||
const internalQuestionIdMap = useMemo(() => {
|
||||
return localSurvey.questions.reduce((acc, question) => {
|
||||
@@ -52,15 +42,13 @@ 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[selectedLanguageCode].includes(`recall:${compareId}`)) {
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replaceAll(
|
||||
`recall:${compareId}`,
|
||||
`recall:${updatedId}`
|
||||
);
|
||||
if (question.headline.includes(`recall:${compareId}`)) {
|
||||
question.headline = question.headline.replaceAll(`recall:${compareId}`, `recall:${updatedId}`);
|
||||
}
|
||||
if (!question.logic) return;
|
||||
question.logic.forEach((rule) => {
|
||||
@@ -73,13 +61,13 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
// function to validate individual questions
|
||||
const validateSurveyQuestion = (question: TSurveyQuestion) => {
|
||||
const validateSurvey = (question: TSurveyQuestion) => {
|
||||
// prevent this function to execute further if user hasnt still tried to save the survey
|
||||
if (invalidQuestions === null) {
|
||||
return;
|
||||
}
|
||||
let temp = structuredClone(invalidQuestions);
|
||||
if (validateQuestion(question, surveyLanguages)) {
|
||||
let temp = JSON.parse(JSON.stringify(invalidQuestions));
|
||||
if (validateQuestion(question)) {
|
||||
temp = invalidQuestions.filter((id) => id !== question.id);
|
||||
setInvalidQuestions(temp);
|
||||
} else if (!invalidQuestions.includes(question.id)) {
|
||||
@@ -106,6 +94,7 @@ export default function QuestionsView({
|
||||
delete internalQuestionIdMap[localSurvey.questions[questionIdx].id];
|
||||
setActiveQuestionId(updatedAttributes.id);
|
||||
}
|
||||
|
||||
updatedSurvey.questions[questionIdx] = {
|
||||
...updatedSurvey.questions[questionIdx],
|
||||
...updatedAttributes,
|
||||
@@ -118,7 +107,7 @@ export default function QuestionsView({
|
||||
setbackButtonLabel(updatedAttributes.backButtonLabel);
|
||||
}
|
||||
setLocalSurvey(updatedSurvey);
|
||||
validateSurveyQuestion(updatedSurvey.questions[questionIdx]);
|
||||
validateSurvey(updatedSurvey.questions[questionIdx]);
|
||||
};
|
||||
|
||||
const deleteQuestion = (questionIdx: number) => {
|
||||
@@ -128,18 +117,16 @@ export default function QuestionsView({
|
||||
|
||||
// check if we are recalling from this question
|
||||
updatedSurvey.questions.forEach((question) => {
|
||||
if (question.headline[selectedLanguageCode].includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(getLocalizedValue(question.headline, selectedLanguageCode));
|
||||
if (question.headline.includes(`recall:${questionId}`)) {
|
||||
const recallInfo = extractRecallInfo(question.headline);
|
||||
if (recallInfo) {
|
||||
question.headline[selectedLanguageCode] = question.headline[selectedLanguageCode].replace(
|
||||
recallInfo,
|
||||
""
|
||||
);
|
||||
question.headline = question.headline.replace(recallInfo, "");
|
||||
}
|
||||
}
|
||||
});
|
||||
updatedSurvey.questions.splice(questionIdx, 1);
|
||||
updatedSurvey = handleQuestionLogicChange(updatedSurvey, questionId, "end");
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
delete internalQuestionIdMap[questionId];
|
||||
if (questionId === activeQuestionIdTemp) {
|
||||
@@ -153,7 +140,7 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
const duplicateQuestion = (questionIdx: number) => {
|
||||
const questionToDuplicate = structuredClone(localSurvey.questions[questionIdx]);
|
||||
const questionToDuplicate = JSON.parse(JSON.stringify(localSurvey.questions[questionIdx]));
|
||||
|
||||
const newQuestionId = createId();
|
||||
|
||||
@@ -179,9 +166,8 @@ export default function QuestionsView({
|
||||
if (backButtonLabel) {
|
||||
question.backButtonLabel = backButtonLabel;
|
||||
}
|
||||
const languageSymbols = extractLanguageCodes(localSurvey.languages);
|
||||
const translatedQuestion = translateQuestion(question, languageSymbols);
|
||||
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
|
||||
|
||||
updatedSurvey.questions.push({ ...question, isDraft: true });
|
||||
|
||||
setLocalSurvey(updatedSurvey);
|
||||
setActiveQuestionId(question.id);
|
||||
@@ -209,67 +195,7 @@ export default function QuestionsView({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey);
|
||||
if (questionWithEmptyFallback) {
|
||||
setActiveQuestionId(questionWithEmptyFallback.id);
|
||||
if (activeQuestionId === questionWithEmptyFallback.id) {
|
||||
@@ -280,16 +206,13 @@ export default function QuestionsView({
|
||||
}, [activeQuestionId, setActiveQuestionId]);
|
||||
|
||||
return (
|
||||
<div className="mt-16 px-5 py-4">
|
||||
<div className="mt-12 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}>
|
||||
@@ -307,8 +230,6 @@ export default function QuestionsView({
|
||||
moveQuestion={moveQuestion}
|
||||
updateQuestion={updateQuestion}
|
||||
duplicateQuestion={duplicateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
deleteQuestion={deleteQuestion}
|
||||
activeQuestionId={activeQuestionId}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
@@ -329,9 +250,6 @@ export default function QuestionsView({
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
setActiveQuestionId={setActiveQuestionId}
|
||||
activeQuestionId={activeQuestionId}
|
||||
isInvalid={invalidQuestions ? invalidQuestions.includes("end") : false}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
/>
|
||||
|
||||
{localSurvey.type === "link" ? (
|
||||
@@ -342,17 +260,6 @@ 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,8 +15,6 @@ interface RatingQuestionFormProps {
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
@@ -24,51 +22,47 @@ export default function RatingQuestionForm({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: RatingQuestionFormProps) {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
type="headline"
|
||||
/>
|
||||
|
||||
<div>
|
||||
{showSubheader && (
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<>
|
||||
<div className="flex w-full items-center">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
environmentId={environmentId}
|
||||
isInvalid={isInvalid}
|
||||
questionId={question.id}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
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: "" });
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
@@ -76,13 +70,7 @@ export default function RatingQuestionForm({
|
||||
variant="minimal"
|
||||
className="mt-3"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
setShowSubheader(true);
|
||||
}}>
|
||||
{" "}
|
||||
onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
@@ -125,47 +113,44 @@ export default function RatingQuestionForm({
|
||||
|
||||
<div className="mt-3 flex justify-between gap-8">
|
||||
<div className="flex-1">
|
||||
<QuestionFormInput
|
||||
id="lowerLabel"
|
||||
placeholder="Not good"
|
||||
value={question.lowerLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<QuestionFormInput
|
||||
id="upperLabel"
|
||||
placeholder="Very satisfied"
|
||||
value={question.upperLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
{!question.required && (
|
||||
<div className="flex-1">
|
||||
<QuestionFormInput
|
||||
id="buttonLabel"
|
||||
value={question.buttonLabel}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
placeholder={"skip"}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Loading from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/loading";
|
||||
import { useEffect, 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";
|
||||
@@ -32,7 +30,6 @@ interface SurveyEditorProps {
|
||||
membershipRole?: TMembershipRole;
|
||||
colours: string[];
|
||||
isUserTargetingAllowed?: boolean;
|
||||
isMultiLanguageAllowed?: boolean;
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
@@ -45,7 +42,6 @@ export default function SurveyEditor({
|
||||
segments,
|
||||
responseCount,
|
||||
membershipRole,
|
||||
isMultiLanguageAllowed,
|
||||
colours,
|
||||
isUserTargetingAllowed = false,
|
||||
isFormbricksCloud,
|
||||
@@ -53,30 +49,9 @@ 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 [selectedLanguageCode, setSelectedLanguageCode] = useState<string>("default");
|
||||
const surveyEditorRef = useRef(null);
|
||||
const [invalidQuestions, setInvalidQuestions] = useState<String[] | null>(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") {
|
||||
@@ -100,6 +75,7 @@ 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]);
|
||||
|
||||
@@ -108,6 +84,7 @@ export default function SurveyEditor({
|
||||
if (!localSurvey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// do nothing if its not an in-app survey
|
||||
if (localSurvey.type !== "web") {
|
||||
return;
|
||||
@@ -140,16 +117,8 @@ 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 <LoadingSkeleton />;
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -165,13 +134,10 @@ 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" ref={surveyEditorRef}>
|
||||
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none">
|
||||
<QuestionsAudienceTabs activeId={activeView} setActiveId={setActiveView} />
|
||||
|
||||
{activeView === "questions" ? (
|
||||
<QuestionsView
|
||||
localSurvey={localSurvey}
|
||||
@@ -181,10 +147,6 @@ export default function SurveyEditor({
|
||||
product={localProduct}
|
||||
invalidQuestions={invalidQuestions}
|
||||
setInvalidQuestions={setInvalidQuestions}
|
||||
selectedLanguageCode={selectedLanguageCode ? selectedLanguageCode : "default"}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={isFormbricksCloud}
|
||||
/>
|
||||
) : (
|
||||
<SettingsView
|
||||
@@ -202,7 +164,6 @@ 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}
|
||||
@@ -211,7 +172,6 @@ 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 { isCardValid, validateQuestion } from "./Validation";
|
||||
import { isValidUrl, validateQuestion } from "./Validation";
|
||||
|
||||
interface SurveyMenuBarProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -32,11 +32,9 @@ 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({
|
||||
@@ -49,8 +47,6 @@ export default function SurveyMenuBar({
|
||||
setInvalidQuestions,
|
||||
product,
|
||||
responseCount,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
}: SurveyMenuBarProps) {
|
||||
const router = useRouter();
|
||||
const [audiencePrompt, setAudiencePrompt] = useState(true);
|
||||
@@ -59,7 +55,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") {
|
||||
@@ -120,33 +116,42 @@ 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, survey.languages);
|
||||
const isValid = validateQuestion(question);
|
||||
|
||||
if (!isValid) {
|
||||
faultyQuestions.push(question.id);
|
||||
@@ -156,7 +161,6 @@ 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;
|
||||
}
|
||||
@@ -175,15 +179,11 @@ export default function SurveyMenuBar({
|
||||
question.type === TSurveyQuestionType.MultipleChoiceMulti
|
||||
) {
|
||||
const haveSameChoices =
|
||||
question.choices.some((element) => element.label[selectedLanguageCode]?.trim() === "") ||
|
||||
question.choices.some((element) => element.label.trim() === "") ||
|
||||
question.choices.some((element, index) =>
|
||||
question.choices
|
||||
.slice(index + 1)
|
||||
.some(
|
||||
(nextElement) =>
|
||||
nextElement.label[selectedLanguageCode]?.trim() ===
|
||||
element.label[selectedLanguageCode].trim()
|
||||
)
|
||||
.some((nextElement) => nextElement.label.trim() === element.label.trim())
|
||||
);
|
||||
|
||||
if (haveSameChoices) {
|
||||
@@ -237,7 +237,7 @@ export default function SurveyMenuBar({
|
||||
toast.error("Please add at least one question.");
|
||||
return;
|
||||
}
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey, selectedLanguageCode);
|
||||
const questionWithEmptyFallback = checkForEmptyFallBackValue(localSurvey);
|
||||
if (questionWithEmptyFallback) {
|
||||
toast.error("Fallback missing");
|
||||
return;
|
||||
|
||||
@@ -42,17 +42,9 @@ export default function UpdateQuestionId({
|
||||
toast.error("ID should not be empty.");
|
||||
return;
|
||||
} else if (
|
||||
[
|
||||
"userId",
|
||||
"source",
|
||||
"suid",
|
||||
"end",
|
||||
"start",
|
||||
"welcomeCard",
|
||||
"hidden",
|
||||
"verifiedEmail",
|
||||
"multiLanguage",
|
||||
].includes(currentValue)
|
||||
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden", "verifiedEmail"].includes(
|
||||
currentValue
|
||||
)
|
||||
) {
|
||||
setCurrentValue(prevValue);
|
||||
updateQuestion(questionIdx, { id: prevValue });
|
||||
|
||||
@@ -1,130 +1,42 @@
|
||||
// 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, languages: TSurveyLanguage[]) => {
|
||||
return handleI18nCheckForMultipleChoice(question, languages);
|
||||
multipleChoiceMulti: (question: TSurveyMultipleChoiceMultiQuestion) => {
|
||||
return !question.choices.some((element) => element.label.trim() === "");
|
||||
},
|
||||
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion, languages: TSurveyLanguage[]) => {
|
||||
return handleI18nCheckForMultipleChoice(question, languages);
|
||||
multipleChoiceSingle: (question: TSurveyMultipleChoiceSingleQuestion) => {
|
||||
return !question.choices.some((element) => element.label.trim() === "");
|
||||
},
|
||||
consent: (question: TSurveyConsentQuestion, languages: TSurveyLanguage[]) => {
|
||||
return isLabelValidForAllLanguages(question.label, languages);
|
||||
consent: (question: TSurveyConsentQuestion) => {
|
||||
return question.label.trim() !== "";
|
||||
},
|
||||
pictureSelection: (question: TSurveyPictureSelectionQuestion) => {
|
||||
return question.choices.length >= 2;
|
||||
},
|
||||
// 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;
|
||||
defaultValidation: (question: TSurveyQuestion) => {
|
||||
return question.headline.trim() !== "";
|
||||
},
|
||||
};
|
||||
|
||||
// Main validation function
|
||||
const validateQuestion = (question: TSurveyQuestion, surveyLanguages: TSurveyLanguage[]): boolean => {
|
||||
const validateQuestion = (question) => {
|
||||
const specificValidation = validationRules[question.type];
|
||||
const defaultValidation = validationRules.defaultValidation;
|
||||
|
||||
const specificValidationResult = specificValidation ? specificValidation(question, surveyLanguages) : true;
|
||||
const defaultValidationResult = defaultValidation(question, surveyLanguages);
|
||||
const specificValidationResult = specificValidation ? specificValidation(question) : true;
|
||||
const defaultValidationResult = defaultValidation(question);
|
||||
|
||||
// Return true only if both specific and default validation pass
|
||||
return specificValidationResult && defaultValidationResult;
|
||||
};
|
||||
|
||||
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 { validateQuestion };
|
||||
|
||||
export const isValidUrl = (string: string): boolean => {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSkeleton />;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@formbricks/ee/lib/service";
|
||||
import { getAdvancedTargetingPermission } 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,7 +60,6 @@ export default async function SurveysEditPage({ params }) {
|
||||
const isSurveyCreationDeletionDisabled = isViewer;
|
||||
|
||||
const isUserTargetingAllowed = getAdvancedTargetingPermission(team);
|
||||
const isMultiLanguageAllowed = getMultiLanguagePermission(team);
|
||||
|
||||
if (
|
||||
!survey ||
|
||||
@@ -85,7 +84,6 @@ export default async function SurveysEditPage({ params }) {
|
||||
colours={colours}
|
||||
segments={segments}
|
||||
isUserTargetingAllowed={isUserTargetingAllowed}
|
||||
isMultiLanguageAllowed={isMultiLanguageAllowed}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ interface PreviewSurveyProps {
|
||||
previewType?: TPreviewType;
|
||||
product: TProduct;
|
||||
environment: TEnvironment;
|
||||
languageCode: string;
|
||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
||||
}
|
||||
|
||||
@@ -63,7 +62,6 @@ export default function PreviewSurvey({
|
||||
previewType,
|
||||
product,
|
||||
environment,
|
||||
languageCode,
|
||||
onFileUpload,
|
||||
}: PreviewSurveyProps) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||
@@ -215,7 +213,6 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
onClose={handlePreviewModalClose}
|
||||
/>
|
||||
@@ -230,7 +227,6 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.linkSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
/>
|
||||
</div>
|
||||
@@ -288,7 +284,6 @@ export default function PreviewSurvey({
|
||||
isBrandingEnabled={product.inAppSurveyBranding}
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
languageCode={languageCode}
|
||||
onFileUpload={onFileUpload}
|
||||
onClose={handlePreviewModalClose}
|
||||
/>
|
||||
@@ -304,7 +299,6 @@ export default function PreviewSurvey({
|
||||
onActiveQuestionChange={setActiveQuestionId}
|
||||
isRedirectDisabled={true}
|
||||
onFileUpload={onFileUpload}
|
||||
languageCode={languageCode}
|
||||
responseCount={42}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,6 @@ 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,7 +46,6 @@ 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(() => {
|
||||
@@ -86,6 +85,7 @@ 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,7 +3,6 @@ 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";
|
||||
@@ -173,6 +172,7 @@ 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[getLocalizedValue(headline, "default")] = answer;
|
||||
surveyResponse[headline] = answer;
|
||||
}
|
||||
surveyData.responses.push(surveyResponse);
|
||||
}
|
||||
|
||||
@@ -74,5 +74,6 @@ async function handleInit(req: NextRequest) {
|
||||
|
||||
async function loadAndAppendCode(path: string, append: string): Promise<string> {
|
||||
let jsCode = await fs.readFile(path, "utf-8");
|
||||
|
||||
return jsCode + append;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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";
|
||||
@@ -68,7 +67,7 @@ async function extractResponses(data: TPipelineInput, questionIds: string[]): Pr
|
||||
}
|
||||
|
||||
const question = survey?.questions.find((q) => q.id === questionId);
|
||||
questions.push(getLocalizedValue(question?.headline, "default") || "");
|
||||
questions.push(question?.headline || "");
|
||||
}
|
||||
|
||||
return [responses, questions];
|
||||
|
||||
@@ -6,7 +6,6 @@ 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";
|
||||
@@ -37,8 +36,6 @@ 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({
|
||||
@@ -156,7 +153,7 @@ export async function POST(request: Request) {
|
||||
const survey = {
|
||||
id: surveyData.id,
|
||||
name: surveyData.name,
|
||||
questions: structuredClone(surveyData.questions) as TSurveyQuestion[],
|
||||
questions: JSON.parse(JSON.stringify(surveyData.questions)) as TSurveyQuestion[],
|
||||
};
|
||||
// send email to all users
|
||||
await Promise.all(
|
||||
|
||||
@@ -8,7 +8,6 @@ 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 {
|
||||
@@ -70,12 +69,6 @@ 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,7 +8,6 @@ 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,12 +68,6 @@ 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,7 +6,6 @@ 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";
|
||||
@@ -22,7 +21,7 @@ import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const transformLegacySurveys = (surveys: TSurvey[]): TSurveyWithTriggers[] => {
|
||||
const updatedSurveys = surveys.map((survey) => {
|
||||
const updatedSurvey: any = { ...reverseTranslateSurvey(survey) };
|
||||
const updatedSurvey: any = { ...survey };
|
||||
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
|
||||
return updatedSurvey;
|
||||
});
|
||||
@@ -92,7 +91,6 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
|
||||
const isPerson = Object.keys(person).length > 0;
|
||||
|
||||
let surveys;
|
||||
|
||||
if (isAppSurveyLimitReached) {
|
||||
surveys = [];
|
||||
} else if (isPerson) {
|
||||
|
||||
@@ -12,17 +12,14 @@ 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, transformToLegacySurvey } from "@formbricks/lib/survey/service";
|
||||
import { getSyncSurveys } 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);
|
||||
@@ -41,10 +38,9 @@ export async function GET(
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const { device } = userAgent(request);
|
||||
const version = request.nextUrl.searchParams.get("version");
|
||||
const apiVersion = request.nextUrl.searchParams.get("version");
|
||||
|
||||
// validate using zod
|
||||
|
||||
const inputValidation = ZJsPeopleUserIdInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
userId: params.userId,
|
||||
@@ -70,17 +66,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 &&
|
||||
@@ -125,14 +121,13 @@ 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: version ?? undefined,
|
||||
version: apiVersion ?? undefined,
|
||||
}),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
@@ -141,41 +136,16 @@ 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: personData,
|
||||
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
|
||||
person: apiVersion
|
||||
? undefined
|
||||
: {
|
||||
id: person.id,
|
||||
userId: person.userId,
|
||||
},
|
||||
surveys: !isInAppSurveyLimitReached ? surveys : [],
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
@@ -12,46 +12,35 @@ import {
|
||||
} from "@formbricks/lib/constants";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { createSurvey, getSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
|
||||
import { createSurvey, getSurveys } 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(
|
||||
request: NextRequest,
|
||||
_: NextRequest,
|
||||
{ params }: { params: { environmentId: string } }
|
||||
): Promise<Response> {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const version =
|
||||
searchParams.get("version") === "undefined" || searchParams.get("version") === null
|
||||
? undefined
|
||||
: searchParams.get("version");
|
||||
const syncInputValidation = ZJsPublicSyncInput.safeParse({
|
||||
// validate using zod
|
||||
const environmentIdValidation = ZJsPublicSyncInput.safeParse({
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
|
||||
if (!syncInputValidation.success) {
|
||||
if (!environmentIdValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(syncInputValidation.error),
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId } = syncInputValidation.data;
|
||||
const { environmentId } = environmentIdValidation.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");
|
||||
@@ -61,7 +50,11 @@ 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 &&
|
||||
@@ -90,36 +83,15 @@ 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 ? [] : transformedSurveys,
|
||||
surveys: !isInAppSurveyLimitReached
|
||||
? surveys.filter(
|
||||
(survey) =>
|
||||
survey.status === "inProgress" &&
|
||||
survey.type === "web" &&
|
||||
(!survey.segment || survey.segment.filters.length === 0)
|
||||
)
|
||||
: [],
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
person: null,
|
||||
|
||||
@@ -2,7 +2,6 @@ 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";
|
||||
@@ -30,14 +29,7 @@ export async function POST(request: Request): Promise<Response> {
|
||||
try {
|
||||
const authentication = await authenticateRequest(request);
|
||||
if (!authentication) return responses.notAuthenticatedResponse();
|
||||
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 surveyInput = await request.json();
|
||||
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: { default: "Who let the dogs out?" },
|
||||
subheader: { default: "Who? Who? Who?" },
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
headline: "Who let the dogs out?",
|
||||
subheader: "Who? Who? Who?",
|
||||
placeholder: "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: { default: "What do you do?" },
|
||||
subheader: { default: "Can't do both." },
|
||||
headline: "What do you do?",
|
||||
subheader: "Can't do both.",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Eat the cake 🍰" } },
|
||||
{ id: createId(), label: { default: "Have the cake 🎂" } },
|
||||
{ id: createId(), label: "Eat the cake 🍰" },
|
||||
{ id: createId(), label: "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: { default: "What's important on vacay?" },
|
||||
headline: "What's important on vacay?",
|
||||
choices: [
|
||||
{ id: createId(), label: { default: "Sun ☀️" } },
|
||||
{ id: createId(), label: { default: "Ocean 🌊" } },
|
||||
{ id: createId(), label: { default: "Palms 🌴" } },
|
||||
{ id: createId(), label: "Sun ☀️" },
|
||||
{ id: createId(), label: "Ocean 🌊" },
|
||||
{ id: createId(), label: "Palms 🌴" },
|
||||
],
|
||||
shuffleOption: "none",
|
||||
},
|
||||
@@ -75,8 +75,8 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask respondents to select one or more pictures",
|
||||
icon: ImageIcon,
|
||||
preset: {
|
||||
headline: { default: "Which is the cutest puppy?" },
|
||||
subheader: { default: "You can also pick both." },
|
||||
headline: "Which is the cutest puppy?",
|
||||
subheader: "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: { default: "How would you rate {{productName}}" },
|
||||
subheader: { default: "Don't worry, be honest." },
|
||||
headline: "How would you rate {{productName}}",
|
||||
subheader: "Don't worry, be honest.",
|
||||
scale: "star",
|
||||
range: 5,
|
||||
lowerLabel: { default: "Not good" },
|
||||
upperLabel: { default: "Very good" },
|
||||
lowerLabel: "Not good",
|
||||
upperLabel: "Very good",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -110,9 +110,9 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Rate satisfaction on a 0-10 scale",
|
||||
icon: PresentationIcon,
|
||||
preset: {
|
||||
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
|
||||
lowerLabel: { default: "Not at all likely" },
|
||||
upperLabel: { default: "Extremely likely" },
|
||||
headline: "How likely are you to recommend {{productName}} to a friend or colleague?",
|
||||
lowerLabel: "Not at all likely",
|
||||
upperLabel: "Extremely likely",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -121,9 +121,8 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Prompt respondents to perform an action",
|
||||
icon: MousePointerClickIcon,
|
||||
preset: {
|
||||
headline: { default: "You are one of our power users!" },
|
||||
html: { default: "" },
|
||||
buttonLabel: { default: "Book interview" },
|
||||
headline: "You are one of our power users!",
|
||||
buttonLabel: "Book interview",
|
||||
buttonExternal: false,
|
||||
dismissButtonLabel: "Skip",
|
||||
},
|
||||
@@ -134,9 +133,8 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask respondents for consent",
|
||||
icon: CheckIcon,
|
||||
preset: {
|
||||
headline: { default: "Terms and Conditions" },
|
||||
html: { default: "" },
|
||||
label: { default: "I agree to the terms and conditions" },
|
||||
headline: "Terms and Conditions",
|
||||
label: "I agree to the terms and conditions",
|
||||
dismissButtonLabel: "Skip",
|
||||
},
|
||||
},
|
||||
@@ -146,7 +144,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Ask your users to select a date",
|
||||
icon: CalendarDaysIcon,
|
||||
preset: {
|
||||
headline: { default: "When is your birthday?" },
|
||||
headline: "When is your birthday?",
|
||||
format: "M-d-y",
|
||||
},
|
||||
},
|
||||
@@ -156,7 +154,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Allow respondents to upload a file",
|
||||
icon: ArrowUpFromLine,
|
||||
preset: {
|
||||
headline: { default: "File Upload" },
|
||||
headline: "File Upload",
|
||||
allowMultipleFiles: false,
|
||||
},
|
||||
},
|
||||
@@ -166,7 +164,8 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
description: "Allow respondents to schedule a meet",
|
||||
icon: PhoneIcon,
|
||||
preset: {
|
||||
headline: { default: "Schedule a call with me" },
|
||||
headline: "Schedule a call with me",
|
||||
buttonLabel: "Skip",
|
||||
calUserName: "rick/get-rick-rolled",
|
||||
},
|
||||
},
|
||||
|
||||
20
apps/web/app/lib/responses/questionResponseMapping.ts
Normal file
20
apps/web/app/lib/responses/questionResponseMapping.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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,7 +5,6 @@ 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";
|
||||
@@ -23,7 +22,6 @@ 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"],
|
||||
@@ -37,7 +35,7 @@ const filterOptions = {
|
||||
consent: ["Accepted", "Dismissed"],
|
||||
};
|
||||
|
||||
// creating the options for the filtering to be selected there are 4 types questions, attributes, tags and metadata
|
||||
// creating the options for the filtering to be selected there are three types questions, attributes and tags
|
||||
export const generateQuestionAndFilterOptions = (
|
||||
survey: TSurvey,
|
||||
environmentTags: TTag[] | undefined,
|
||||
@@ -46,7 +44,7 @@ export const generateQuestionAndFilterOptions = (
|
||||
questionOptions: QuestionOptions[];
|
||||
questionFilterOptions: QuestionFilterOptions[];
|
||||
} => {
|
||||
let questionOptions: QuestionOptions[] = [];
|
||||
let questionOptions: any = [];
|
||||
let questionFilterOptions: any = [];
|
||||
|
||||
let questionsOptions: any = [];
|
||||
@@ -127,20 +125,6 @@ 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] };
|
||||
};
|
||||
|
||||
@@ -151,20 +135,19 @@ export const getFormattedFilters = (
|
||||
dateRange: DateRange
|
||||
): TResponseFilterCriteria => {
|
||||
const filters: TResponseFilterCriteria = {};
|
||||
const [questions, tags, attributes, metadata] = selectedFilter.filter.reduce(
|
||||
(result: [FilterValue[], FilterValue[], FilterValue[], FilterValue[]], filter) => {
|
||||
|
||||
const [questions, tags, attributes] = selectedFilter.filter.reduce(
|
||||
(result: [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
|
||||
@@ -323,24 +306,6 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user