mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
feat: New Question Type Meet Scheduling with Cal.com (#1722)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com> Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
|
||||
import { questionTypes } from "@/app/lib/questions";
|
||||
import { InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import { TSurveyCalQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
|
||||
import { PersonAvatar } from "@formbricks/ui/Avatars";
|
||||
|
||||
interface CalSummaryProps {
|
||||
questionSummary: TSurveyQuestionSummary<TSurveyCalQuestion>;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export default function CalSummary({ questionSummary, environmentId }: CalSummaryProps) {
|
||||
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
|
||||
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
|
||||
<Headline headline={questionSummary.question.headline} />
|
||||
|
||||
<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 " />}
|
||||
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
|
||||
</div>
|
||||
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
|
||||
<InboxStackIcon className="mr-2 h-4 w-4" />
|
||||
{questionSummary.responses.length} Responses
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-b-lg bg-white ">
|
||||
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
|
||||
<div className="pl-4 md:pl-6">User</div>
|
||||
<div className="col-span-2 pl-4 md:pl-6">Response</div>
|
||||
<div className="px-4 md:px-6">Time</div>
|
||||
</div>
|
||||
{questionSummary.responses.map((response) => {
|
||||
const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
|
||||
return (
|
||||
<div
|
||||
key={response.id}
|
||||
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
|
||||
<div className="pl-4 md:pl-6">
|
||||
{response.person ? (
|
||||
<Link
|
||||
className="ph-no-capture group flex items-center"
|
||||
href={`/environments/${environmentId}/people/${response.person.id}`}>
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId={response.person.id} />
|
||||
</div>
|
||||
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
|
||||
{displayIdentifier}
|
||||
</p>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="group flex items-center">
|
||||
<div className="hidden md:flex">
|
||||
<PersonAvatar personId="anonymous" />
|
||||
</div>
|
||||
<p className="break-all text-slate-600 md:ml-2">Anonymous</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold capitalize">
|
||||
{response.value}
|
||||
</div>
|
||||
<div className="px-4 text-slate-500 md:px-6">{timeSince(response.updatedAt.toISOString())}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
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";
|
||||
|
||||
@@ -6,6 +7,7 @@ import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import type {
|
||||
TSurveyCalQuestion,
|
||||
TSurveyDateQuestion,
|
||||
TSurveyFileUploadQuestion,
|
||||
TSurveyPictureSelectionQuestion,
|
||||
@@ -159,6 +161,16 @@ export default function SummaryList({ environment, survey, responses, responsesP
|
||||
);
|
||||
}
|
||||
|
||||
if (questionSummary.question.type === TSurveyQuestionType.Cal) {
|
||||
return (
|
||||
<CalSummary
|
||||
key={questionSummary.question.id}
|
||||
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyCalQuestion>}
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
|
||||
@@ -284,6 +284,18 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case TSurveyQuestionType.Cal:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</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{" "}
|
||||
</Text>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case TSurveyQuestionType.Date:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
interface CalQuestionFormProps {
|
||||
question: TSurveyCalQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
lastQuestion: boolean;
|
||||
isInValid: boolean;
|
||||
}
|
||||
|
||||
export default function CalQuestionForm({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
isInValid,
|
||||
}: CalQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
|
||||
isInvalid={isInValid && question.headline.trim() === ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
{showSubheader && (
|
||||
<>
|
||||
<Label htmlFor="subheader">Description</Label>
|
||||
<div className="mt-2 inline-flex w-full items-center">
|
||||
<Input
|
||||
id="subheader"
|
||||
name="subheader"
|
||||
value={question.subheader}
|
||||
onChange={(e) => updateQuestion(questionIdx, { subheader: e.target.value })}
|
||||
/>
|
||||
<TrashIcon
|
||||
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||
onClick={() => {
|
||||
setShowSubheader(false);
|
||||
updateQuestion(questionIdx, { subheader: "" });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!showSubheader && (
|
||||
<Button size="sm" variant="minimal" type="button" onClick={() => setShowSubheader(true)}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="calUserName">Add your Cal.com username or username/event</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="calUserName"
|
||||
name="calUserName"
|
||||
value={question.calUserName}
|
||||
onChange={(e) => updateQuestion(questionIdx, { calUserName: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export default function LogicEditor({
|
||||
consent: ["skipped", "accepted"],
|
||||
pictureSelection: ["submitted", "skipped"],
|
||||
fileUpload: ["uploaded", "notUploaded"],
|
||||
cal: ["skipped", "booked"],
|
||||
};
|
||||
|
||||
const logicConditions: LogicConditions = {
|
||||
@@ -101,16 +102,6 @@ export default function LogicEditor({
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
uploaded: {
|
||||
label: "has uploaded file",
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
notUploaded: {
|
||||
label: "has not uploaded file",
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
clicked: {
|
||||
label: "is clicked",
|
||||
values: null,
|
||||
@@ -150,6 +141,21 @@ export default function LogicEditor({
|
||||
values: questionValues,
|
||||
multiSelect: true,
|
||||
},
|
||||
uploaded: {
|
||||
label: "has uploaded file",
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
notUploaded: {
|
||||
label: "has not uploaded file",
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
booked: {
|
||||
label: "has a call booked",
|
||||
values: null,
|
||||
unique: true,
|
||||
},
|
||||
};
|
||||
|
||||
const addLogic = () => {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
CursorArrowRippleIcon,
|
||||
ListBulletIcon,
|
||||
PhoneIcon,
|
||||
PhotoIcon,
|
||||
PresentationChartBarIcon,
|
||||
QueueListIcon,
|
||||
@@ -31,6 +32,7 @@ import { Label } from "@formbricks/ui/Label";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
|
||||
import CTAQuestionForm from "./CTAQuestionForm";
|
||||
import CalQuestionForm from "./CalQuestionForm";
|
||||
import ConsentQuestionForm from "./ConsentQuestionForm";
|
||||
import FileUploadQuestionForm from "./FileUploadQuestionForm";
|
||||
import MultipleChoiceMultiForm from "./MultipleChoiceMultiForm";
|
||||
@@ -152,6 +154,8 @@ export default function QuestionCard({
|
||||
<PhotoIcon />
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<CalendarDaysIcon />
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<PhoneIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
@@ -268,6 +272,14 @@ export default function QuestionCard({
|
||||
lastQuestion={lastQuestion}
|
||||
isInValid={isInValid}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestionForm
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
lastQuestion={lastQuestion}
|
||||
isInValid={isInValid}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mt-4">
|
||||
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CheckIcon,
|
||||
CursorArrowRippleIcon,
|
||||
ListBulletIcon,
|
||||
PhoneIcon,
|
||||
PhotoIcon,
|
||||
PresentationChartBarIcon,
|
||||
QueueListIcon,
|
||||
@@ -156,6 +157,17 @@ export const questionTypes: TSurveyQuestionType[] = [
|
||||
allowMultipleFiles: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: QuestionId.Cal,
|
||||
label: "Schedule a meeting",
|
||||
description: "Allow respondents to schedule a meet",
|
||||
icon: PhoneIcon,
|
||||
preset: {
|
||||
headline: "Schedule a call with me",
|
||||
buttonLabel: "Skip",
|
||||
calUserName: "rick/get-rick-rolled",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const universalQuestionPresets = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "1.2.8",
|
||||
"version": "1.2.9",
|
||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"homepage": "https://formbricks.com",
|
||||
"repository": {
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"vite-plugin-dts": "^3.6.4",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"serve": "14.2.1",
|
||||
"concurrently": "8.2.2"
|
||||
"concurrently": "8.2.2",
|
||||
"@calcom/embed-snippet": "1.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
56
packages/surveys/src/components/general/CalEmbed.tsx
Normal file
56
packages/surveys/src/components/general/CalEmbed.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import snippet from "@calcom/embed-snippet";
|
||||
import { useEffect, useMemo } from "preact/hooks";
|
||||
|
||||
import { TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
interface CalEmbedProps {
|
||||
question: TSurveyCalQuestion;
|
||||
onSuccessfulBooking: () => void;
|
||||
}
|
||||
|
||||
export default function CalEmbed({ question, onSuccessfulBooking }: CalEmbedProps) {
|
||||
const cal = useMemo(() => {
|
||||
const calInline = snippet("https://cal.com/embed.js");
|
||||
|
||||
const calCssVars = {
|
||||
"cal-border-subtle": "transparent",
|
||||
"cal-border-booker": "transparent",
|
||||
};
|
||||
|
||||
calInline("ui", {
|
||||
theme: "light",
|
||||
cssVarsPerTheme: {
|
||||
light: {
|
||||
...calCssVars,
|
||||
},
|
||||
dark: {
|
||||
"cal-bg-muted": "transparent",
|
||||
"cal-bg": "transparent",
|
||||
...calCssVars,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
calInline("on", {
|
||||
action: "bookingSuccessful",
|
||||
callback: () => {
|
||||
onSuccessfulBooking();
|
||||
},
|
||||
});
|
||||
|
||||
return calInline;
|
||||
}, [onSuccessfulBooking]);
|
||||
|
||||
useEffect(() => {
|
||||
// remove any existing cal-inline elements
|
||||
document.querySelectorAll("cal-inline").forEach((el) => el.remove());
|
||||
cal("inline", { elementOrSelector: "#fb-cal-embed", calLink: question.calUserName });
|
||||
}, [cal, question.calUserName]);
|
||||
|
||||
return (
|
||||
<div className="relative mt-4">
|
||||
<div id="fb-cal-embed" className={cn("h-96 overflow-auto rounded-lg border border-slate-200")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import CTAQuestion from "@/components/questions/CTAQuestion";
|
||||
import CalQuestion from "@/components/questions/CalQuestion";
|
||||
import ConsentQuestion from "@/components/questions/ConsentQuestion";
|
||||
import DateQuestion from "@/components/questions/DateQuestion";
|
||||
import FileUploadQuestion from "@/components/questions/FileUploadQuestion";
|
||||
@@ -165,5 +166,17 @@ export default function QuestionConditional({
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestion
|
||||
question={question}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
onBack={onBack}
|
||||
isFirstQuestion={isFirstQuestion}
|
||||
isLastQuestion={isLastQuestion}
|
||||
ttc={ttc}
|
||||
setTtc={setTtc}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
93
packages/surveys/src/components/questions/CalQuestion.tsx
Normal file
93
packages/surveys/src/components/questions/CalQuestion.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { BackButton } from "@/components/buttons/BackButton";
|
||||
import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import CalEmbed from "@/components/general/CalEmbed";
|
||||
import Headline from "@/components/general/Headline";
|
||||
import Subheader from "@/components/general/Subheader";
|
||||
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TResponseTtc } from "@formbricks/types/responses";
|
||||
import { TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
|
||||
interface CalQuestionProps {
|
||||
question: TSurveyCalQuestion;
|
||||
value: string | number | string[];
|
||||
onChange: (responseData: TResponseData) => void;
|
||||
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
|
||||
onBack: () => void;
|
||||
isFirstQuestion: boolean;
|
||||
isLastQuestion: boolean;
|
||||
ttc: TResponseTtc;
|
||||
setTtc: (ttc: TResponseTtc) => void;
|
||||
}
|
||||
|
||||
export default function CalQuestion({
|
||||
question,
|
||||
value,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onBack,
|
||||
isFirstQuestion,
|
||||
isLastQuestion,
|
||||
ttc,
|
||||
setTtc,
|
||||
}: CalQuestionProps) {
|
||||
const [startTime, setStartTime] = useState(performance.now());
|
||||
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const onSuccessfulBooking = useCallback(() => {
|
||||
onChange({ [question.id]: "booked" });
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
onSubmit({ [question.id]: "booked" }, updatedttc);
|
||||
}, [onChange, onSubmit, question.id, setTtc, startTime, ttc]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
setErrorMessage("Please book an appointment");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedttc = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
|
||||
setTtc(updatedttc);
|
||||
|
||||
onChange({ [question.id]: value });
|
||||
onSubmit({ [question.id]: value }, updatedttc);
|
||||
}}
|
||||
className="w-full">
|
||||
<Headline headline={question.headline} questionId={question.id} required={question.required} />
|
||||
|
||||
<Subheader subheader={question.subheader} questionId={question.id} />
|
||||
|
||||
<>
|
||||
{errorMessage && <span className="text-red-500">{errorMessage}</span>}
|
||||
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
|
||||
</>
|
||||
|
||||
<div className="mt-4 flex w-full justify-between">
|
||||
{!isFirstQuestion && (
|
||||
<BackButton
|
||||
backButtonLabel={question.backButtonLabel}
|
||||
onClick={() => {
|
||||
onBack();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div></div>
|
||||
{!question.required && (
|
||||
<SubmitButton
|
||||
buttonLabel={question.buttonLabel}
|
||||
isLastQuestion={isLastQuestion}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -114,7 +114,6 @@ export default function DateQuestion({
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (question.required && !value) {
|
||||
// alert("Please select a date");
|
||||
setErrorMessage("Please select a date.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum TSurveyQuestionType {
|
||||
Rating = "rating",
|
||||
Consent = "consent",
|
||||
PictureSelection = "pictureSelection",
|
||||
Cal = "cal",
|
||||
Date = "date",
|
||||
}
|
||||
|
||||
@@ -129,6 +130,7 @@ export const ZSurveyLogicCondition = z.enum([
|
||||
"includesOne",
|
||||
"uploaded",
|
||||
"notUploaded",
|
||||
"booked",
|
||||
]);
|
||||
|
||||
export type TSurveyLogicCondition = z.infer<typeof ZSurveyLogicCondition>;
|
||||
@@ -207,6 +209,11 @@ const ZSurveyPictureSelectionLogic = ZSurveyLogicBase.extend({
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyCalLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.enum(["booked", "skipped"]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyLogic = z.union([
|
||||
ZSurveyOpenTextLogic,
|
||||
ZSurveyConsentLogic,
|
||||
@@ -217,6 +224,7 @@ export const ZSurveyLogic = z.union([
|
||||
ZSurveyRatingLogic,
|
||||
ZSurveyPictureSelectionLogic,
|
||||
ZSurveyFileUploadLogic,
|
||||
ZSurveyCalLogic,
|
||||
]);
|
||||
|
||||
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
|
||||
@@ -236,16 +244,6 @@ const ZSurveyQuestionBase = z.object({
|
||||
isDraft: z.boolean().optional(),
|
||||
});
|
||||
|
||||
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 ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url", "number", "phone"]);
|
||||
export type TSurveyOpenTextQuestionInputType = z.infer<typeof ZSurveyOpenTextQuestionInputType>;
|
||||
|
||||
@@ -347,6 +345,24 @@ export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
|
||||
|
||||
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,
|
||||
@@ -358,6 +374,7 @@ export const ZSurveyQuestion = z.union([
|
||||
ZSurveyPictureSelectionQuestion,
|
||||
ZSurveyDateQuestion,
|
||||
ZSurveyFileUploadQuestion,
|
||||
ZSurveyCalQuestion,
|
||||
]);
|
||||
|
||||
export type TSurveyQuestion = z.infer<typeof ZSurveyQuestion>;
|
||||
@@ -454,6 +471,7 @@ export const ZSurveyTSurveyQuestionType = z.union([
|
||||
z.literal("rating"),
|
||||
z.literal("consent"),
|
||||
z.literal("pictureSelection"),
|
||||
z.literal("cal"),
|
||||
z.literal("date"),
|
||||
]);
|
||||
|
||||
|
||||
@@ -333,6 +333,10 @@ export default function SingleResponseCard({
|
||||
</div>
|
||||
) : question.type === TSurveyQuestionType.Date ? (
|
||||
<DateResponse date={response.data[question.id] as string} />
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<p className="ph-no-capture my-1 font-semibold capitalize text-slate-700">
|
||||
{response.data[question.id]}
|
||||
</p>
|
||||
) : (
|
||||
<p className="ph-no-capture my-1 font-semibold text-slate-700">
|
||||
{response.data[question.id]}
|
||||
|
||||
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
@@ -712,6 +712,9 @@ importers:
|
||||
|
||||
packages/surveys:
|
||||
devDependencies:
|
||||
'@calcom/embed-snippet':
|
||||
specifier: 1.1.2
|
||||
version: 1.1.2
|
||||
'@formbricks/lib':
|
||||
specifier: workspace:*
|
||||
version: link:../lib
|
||||
@@ -3031,7 +3034,6 @@ packages:
|
||||
|
||||
/@calcom/embed-core@1.3.2:
|
||||
resolution: {integrity: sha512-qxVfWpmPcYN5hTnwoKTP9QAlhEAHy4TFh+Xu+IoCnJma/uI2BjqsUWJ0BXsmm0m8sTFthaBkGiFomS1LeMYO+Q==}
|
||||
dev: false
|
||||
|
||||
/@calcom/embed-react@1.3.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-NbqkLd6J+VUy8jILY4GI1HEXRv3pZk1wnmL3CRrouB2y7pU5sV1qPjkgy+hwCRlYJJGiAIrspXkI/czMsfHxRQ==}
|
||||
@@ -3049,7 +3051,6 @@ packages:
|
||||
resolution: {integrity: sha512-UKz4BRyxWLPfCIr7FfZP2Aa8w3ZMXcfwc3frCjNfWphJvJjaCLi0nAUBXFx6ooIPhVkbzvelnkllbqigZRZPiA==}
|
||||
dependencies:
|
||||
'@calcom/embed-core': 1.3.2
|
||||
dev: false
|
||||
|
||||
/@commander-js/extra-typings@9.4.1(commander@9.4.1):
|
||||
resolution: {integrity: sha512-v0BqORYamk1koxDon6femDGLWSL7P78vYTyOU5nFaALnmNALL+ktgdHvWbxzzBBJIKS7kv3XvM/DqNwiLcgFTA==}
|
||||
|
||||
Reference in New Issue
Block a user