mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-04 09:29:42 -06:00
feat: add survey schedule option (#2386)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> Co-authored-by: Johannes <johannes@formbricks.com> Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
name: Cron - reportUsageToStripe
|
||||
name: Cron - Report usage to Stripe
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Cron - closeOnDate
|
||||
name: Cron - Survey status update
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL }}/api/cron/close_surveys \
|
||||
curl ${{ env.APP_URL }}/api/cron/survey-status \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
|
||||
4
.github/workflows/cron-weeklySummary.yml
vendored
4
.github/workflows/cron-weeklySummary.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Cron - weeklySummary
|
||||
name: Cron - Weekly summary
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
curl ${{ env.APP_URL }}/api/cron/weekly_summary \
|
||||
curl ${{ env.APP_URL }}/api/cron/weekly-summary \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
|
||||
|
||||
@@ -119,6 +119,7 @@ const SummaryHeader = ({
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<span className="ml-1 text-sm text-slate-700">
|
||||
{survey.status === "scheduled" && "Scheduled"}
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
{survey.status === "completed" && "Completed"}
|
||||
|
||||
@@ -10,18 +10,22 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { SurveyStatusIndicator } from "@formbricks/ui/SurveyStatusIndicator";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
interface SurveyStatusDropdownProps {
|
||||
environment: TEnvironment;
|
||||
updateLocalSurveyStatus?: (status: TSurvey["status"]) => void;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
export default function SurveyStatusDropdown({
|
||||
environment,
|
||||
updateLocalSurveyStatus,
|
||||
survey,
|
||||
}: {
|
||||
environment: TEnvironment;
|
||||
updateLocalSurveyStatus?: (status: "draft" | "inProgress" | "paused" | "completed" | "archived") => void;
|
||||
survey: TSurvey;
|
||||
}) {
|
||||
}: SurveyStatusDropdownProps) {
|
||||
const isCloseOnDateEnabled = survey.closeOnDate !== null;
|
||||
const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null;
|
||||
const isStatusChangeDisabled = (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date()) ?? false;
|
||||
const isStatusChangeDisabled =
|
||||
(survey.status === "scheduled" || (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date())) ??
|
||||
false;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -34,7 +38,7 @@ export default function SurveyStatusDropdown({
|
||||
value={survey.status}
|
||||
disabled={isStatusChangeDisabled}
|
||||
onValueChange={(value) => {
|
||||
const castedValue = value as "draft" | "inProgress" | "paused" | "completed";
|
||||
const castedValue = value as TSurvey["status"];
|
||||
updateSurveyAction({ ...survey, status: castedValue })
|
||||
.then(() => {
|
||||
toast.success(
|
||||
@@ -51,8 +55,7 @@ export default function SurveyStatusDropdown({
|
||||
toast.error(`Error: ${error.message}`);
|
||||
});
|
||||
|
||||
if (updateLocalSurveyStatus)
|
||||
updateLocalSurveyStatus(value as "draft" | "inProgress" | "paused" | "completed" | "archived");
|
||||
if (updateLocalSurveyStatus) updateLocalSurveyStatus(value as TSurvey["status"]);
|
||||
}}>
|
||||
<TooltipProvider delayDuration={50}>
|
||||
<Tooltip open={isStatusChangeDisabled ? undefined : false}>
|
||||
@@ -64,6 +67,7 @@ export default function SurveyStatusDropdown({
|
||||
<SurveyStatusIndicator status={survey.status} />
|
||||
)}
|
||||
<span className="ml-2 text-sm text-slate-700">
|
||||
{survey.status === "scheduled" && "Scheduled"}
|
||||
{survey.status === "inProgress" && "In-progress"}
|
||||
{survey.status === "paused" && "Paused"}
|
||||
{survey.status === "completed" && "Completed"}
|
||||
@@ -88,8 +92,8 @@ export default function SurveyStatusDropdown({
|
||||
</SelectContent>
|
||||
|
||||
<TooltipContent>
|
||||
To update the survey status, update the “Close
|
||||
<br /> survey on date” setting in the Response Options.
|
||||
To update the survey status, update the schedule and close setting in the survey response
|
||||
options.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { ArrowUpRight, CheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { KeyboardEventHandler, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
@@ -27,7 +28,8 @@ export default function ResponseOptionsCard({
|
||||
const [open, setOpen] = useState(localSurvey.type === "link" ? true : false);
|
||||
const autoComplete = localSurvey.autoComplete !== null;
|
||||
const [redirectToggle, setRedirectToggle] = useState(false);
|
||||
const [surveyCloseOnDateToggle, setSurveyCloseOnDateToggle] = useState(false);
|
||||
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
|
||||
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
|
||||
useState;
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>("");
|
||||
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
|
||||
@@ -48,7 +50,8 @@ export default function ResponseOptionsCard({
|
||||
name: "",
|
||||
subheading: "",
|
||||
});
|
||||
const [closeOnDate, setCloseOnDate] = useState<Date>();
|
||||
const [runOnDate, setRunOnDate] = useState<Date | null>(null);
|
||||
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
|
||||
|
||||
const isPinProtectionEnabled = localSurvey.pin !== null;
|
||||
|
||||
@@ -63,19 +66,28 @@ export default function ResponseOptionsCard({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSurveyCloseOnDateToggle = () => {
|
||||
if (surveyCloseOnDateToggle && localSurvey.closeOnDate) {
|
||||
setSurveyCloseOnDateToggle(false);
|
||||
setCloseOnDate(undefined);
|
||||
setLocalSurvey({ ...localSurvey, closeOnDate: null });
|
||||
return;
|
||||
const handleRunOnDateToggle = () => {
|
||||
if (runOnDateToggle) {
|
||||
setRunOnDateToggle(false);
|
||||
if (localSurvey.runOnDate) {
|
||||
setRunOnDate(null);
|
||||
setLocalSurvey({ ...localSurvey, runOnDate: null });
|
||||
}
|
||||
} else {
|
||||
setRunOnDateToggle(true);
|
||||
}
|
||||
};
|
||||
|
||||
if (surveyCloseOnDateToggle) {
|
||||
setSurveyCloseOnDateToggle(false);
|
||||
return;
|
||||
const handleCloseOnDateToggle = () => {
|
||||
if (closeOnDateToggle) {
|
||||
setCloseOnDateToggle(false);
|
||||
if (localSurvey.closeOnDate) {
|
||||
setCloseOnDate(null);
|
||||
setLocalSurvey({ ...localSurvey, closeOnDate: null });
|
||||
}
|
||||
} else {
|
||||
setCloseOnDateToggle(true);
|
||||
}
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
};
|
||||
|
||||
const handleProtectSurveyWithPinToggle = () => {
|
||||
@@ -126,6 +138,15 @@ export default function ResponseOptionsCard({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRunOnDateChange = (date: Date) => {
|
||||
const equivalentDate = date?.getDate();
|
||||
date?.setUTCHours(0, 0, 0, 0);
|
||||
date?.setDate(equivalentDate);
|
||||
|
||||
setRunOnDate(date);
|
||||
setLocalSurvey({ ...localSurvey, runOnDate: date ?? null });
|
||||
};
|
||||
|
||||
const handleCloseOnDateChange = (date: Date) => {
|
||||
const equivalentDate = date?.getDate();
|
||||
date?.setUTCHours(0, 0, 0, 0);
|
||||
@@ -143,7 +164,7 @@ export default function ResponseOptionsCard({
|
||||
subheading?: string;
|
||||
}) => {
|
||||
const message = {
|
||||
enabled: surveyCloseOnDateToggle,
|
||||
enabled: closeOnDateToggle,
|
||||
heading: heading ?? surveyClosedMessage.heading,
|
||||
subheading: subheading ?? surveyClosedMessage.subheading,
|
||||
};
|
||||
@@ -245,9 +266,14 @@ export default function ResponseOptionsCard({
|
||||
setVerifyEmailToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.runOnDate) {
|
||||
setRunOnDate(localSurvey.runOnDate);
|
||||
setRunOnDateToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.closeOnDate) {
|
||||
setCloseOnDate(localSurvey.closeOnDate);
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
setCloseOnDateToggle(true);
|
||||
}
|
||||
}, [
|
||||
localSurvey,
|
||||
@@ -318,7 +344,7 @@ export default function ResponseOptionsCard({
|
||||
description="Automatically close the survey after a certain number of responses."
|
||||
childBorder={true}>
|
||||
<label htmlFor="autoCompleteResponses" className="cursor-pointer bg-slate-50 p-4">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
<p className="text-sm text-slate-700">
|
||||
Automatically mark the survey as complete after
|
||||
<Input
|
||||
autoFocus
|
||||
@@ -334,18 +360,27 @@ export default function ResponseOptionsCard({
|
||||
</p>
|
||||
</label>
|
||||
</AdvancedOptionToggle>
|
||||
{/* Run Survey on Date */}
|
||||
<AdvancedOptionToggle
|
||||
htmlId="runOnDate"
|
||||
isChecked={runOnDateToggle}
|
||||
onToggle={handleRunOnDateToggle}
|
||||
title="Release survey on date"
|
||||
description="Automatically release the survey at the beginning of the day (UTC)."
|
||||
childBorder={true}>
|
||||
<div className="p-4">
|
||||
<DatePicker date={runOnDate} handleDateChange={handleRunOnDateChange} />
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
{/* Close Survey on Date */}
|
||||
<AdvancedOptionToggle
|
||||
htmlId="closeOnDate"
|
||||
isChecked={surveyCloseOnDateToggle}
|
||||
onToggle={handleSurveyCloseOnDateToggle}
|
||||
isChecked={closeOnDateToggle}
|
||||
onToggle={handleCloseOnDateToggle}
|
||||
title="Close survey on date"
|
||||
description="Automatically closes the survey at the beginning of the day (UTC)."
|
||||
childBorder={true}>
|
||||
<div className="flex cursor-pointer p-4">
|
||||
<p className="mr-2 mt-3 text-sm font-semibold text-slate-700">
|
||||
Automatically mark survey as complete on:
|
||||
</p>
|
||||
<div className="p-4">
|
||||
<DatePicker date={closeOnDate} handleDateChange={handleCloseOnDateChange} />
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
@@ -356,22 +391,17 @@ export default function ResponseOptionsCard({
|
||||
isChecked={redirectToggle}
|
||||
onToggle={handleRedirectCheckMark}
|
||||
title="Redirect on completion"
|
||||
description="Redirect user to specified link on survey completion"
|
||||
description="Redirect user to link destination when they completed the survey"
|
||||
childBorder={true}>
|
||||
<div className="w-full p-4">
|
||||
<div className="flex w-full cursor-pointer items-center">
|
||||
<p className="mr-2 w-[400px] text-sm font-semibold text-slate-700">
|
||||
Redirect respondents here:
|
||||
</p>
|
||||
<Input
|
||||
autoFocus
|
||||
className="w-full bg-white"
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
autoFocus
|
||||
className="w-full bg-white"
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
|
||||
@@ -427,7 +457,15 @@ export default function ResponseOptionsCard({
|
||||
Blocks survey if the survey URL has no Single Use Id (suId).
|
||||
</li>
|
||||
<li className="text-sm text-slate-600">
|
||||
Blocks survey if a submission with the Single Use Id (suId) in the URL exists already.
|
||||
Blocks survey if a submission with the Single Use Id (suId) exists already.
|
||||
</li>
|
||||
<li className="text-sm text-slate-600">
|
||||
<Link
|
||||
href="https://formbricks.com/docs/link-surveys/single-use-links"
|
||||
target="_blank"
|
||||
className="underline">
|
||||
Docs <ArrowUpRight className="inline" size={16} />
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<Label htmlFor="headline">‘Link Used’ Message</Label>
|
||||
@@ -479,7 +517,7 @@ export default function ResponseOptionsCard({
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<p className="text-md font-semibold">How it works</p>
|
||||
<Label htmlFor="howItWorks">How it works</Label>
|
||||
<p className="mb-4 mt-2 text-sm text-slate-500">
|
||||
Respondants will receive the survey link via email.
|
||||
</p>
|
||||
@@ -513,25 +551,25 @@ export default function ResponseOptionsCard({
|
||||
title="Protect survey with a PIN"
|
||||
description="Only users who have the PIN can access the survey."
|
||||
childBorder={true}>
|
||||
<div className="flex w-full items-center space-x-1 p-4 pb-4">
|
||||
<div className="w-full cursor-pointer items-center bg-slate-50">
|
||||
<Label htmlFor="headline">Add PIN</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
id="pin"
|
||||
isInvalid={Boolean(verifyProtectWithPinError)}
|
||||
className="mb-4 mt-2 bg-white"
|
||||
name="pin"
|
||||
placeholder="1234"
|
||||
onBlur={handleProtectSurveyPinBlurEvent}
|
||||
defaultValue={localSurvey.pin ? localSurvey.pin : undefined}
|
||||
onKeyDown={handleSurveyPinInputKeyDown}
|
||||
onChange={(e) => handleProtectSurveyPinChange(e.target.value)}
|
||||
/>
|
||||
{verifyProtectWithPinError && (
|
||||
<p className="text-sm text-red-700">{verifyProtectWithPinError}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<Label htmlFor="headline" className="sr-only">
|
||||
Add PIN:
|
||||
</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
id="pin"
|
||||
isInvalid={Boolean(verifyProtectWithPinError)}
|
||||
className="bg-white"
|
||||
name="pin"
|
||||
placeholder="Add a four digit PIN"
|
||||
onBlur={handleProtectSurveyPinBlurEvent}
|
||||
defaultValue={localSurvey.pin ? localSurvey.pin : undefined}
|
||||
onKeyDown={handleSurveyPinInputKeyDown}
|
||||
onChange={(e) => handleProtectSurveyPinChange(e.target.value)}
|
||||
/>
|
||||
{verifyProtectWithPinError && (
|
||||
<p className="pt-1 text-sm text-red-700">{verifyProtectWithPinError}</p>
|
||||
)}
|
||||
</div>
|
||||
</AdvancedOptionToggle>
|
||||
</>
|
||||
|
||||
@@ -370,7 +370,8 @@ export default function SurveyMenuBar({
|
||||
setIsSurveyPublishing(false);
|
||||
return;
|
||||
}
|
||||
await updateSurveyAction({ ...localSurvey, status: "inProgress" });
|
||||
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
|
||||
await updateSurveyAction({ ...localSurvey, status });
|
||||
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
|
||||
} catch (error) {
|
||||
toast.error("An error occured while publishing the survey.");
|
||||
|
||||
@@ -2623,6 +2623,7 @@ export const minimalSurvey: TSurvey = {
|
||||
delay: 0, // No delay
|
||||
displayPercentage: null,
|
||||
autoComplete: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
surveyClosedMessage: {
|
||||
enabled: false,
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { CRON_SECRET } from "@formbricks/lib/constants";
|
||||
|
||||
export async function POST() {
|
||||
const headersList = headers();
|
||||
const apiKey = headersList.get("x-api-key");
|
||||
|
||||
if (!apiKey || apiKey !== CRON_SECRET) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
const surveys = await prisma.survey.findMany({
|
||||
where: {
|
||||
status: "inProgress",
|
||||
closeOnDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!surveys.length) {
|
||||
return responses.successResponse({ message: "No surveys to close" });
|
||||
}
|
||||
|
||||
const mutationResp = await prisma.survey.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: surveys.map((survey) => survey.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: "completed",
|
||||
},
|
||||
});
|
||||
|
||||
return responses.successResponse({
|
||||
message: `Closed ${mutationResp.count} survey(s)`,
|
||||
});
|
||||
}
|
||||
70
apps/web/app/api/cron/survey-status/route.ts
Normal file
70
apps/web/app/api/cron/survey-status/route.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { CRON_SECRET } from "@formbricks/lib/constants";
|
||||
|
||||
export async function POST() {
|
||||
const headersList = headers();
|
||||
const apiKey = headersList.get("x-api-key");
|
||||
|
||||
if (!apiKey || apiKey !== CRON_SECRET) {
|
||||
return responses.notAuthenticatedResponse();
|
||||
}
|
||||
|
||||
// close surveys that are in progress and have a closeOnDate in the past
|
||||
const surveysToClose = await prisma.survey.findMany({
|
||||
where: {
|
||||
status: "inProgress",
|
||||
closeOnDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (surveysToClose.length) {
|
||||
await prisma.survey.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: surveysToClose.map((survey) => survey.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: "completed",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// run surveys that are scheduled and have a runOnDate in the past
|
||||
const scheduledSurveys = await prisma.survey.findMany({
|
||||
where: {
|
||||
status: "scheduled",
|
||||
runOnDate: {
|
||||
lte: new Date(),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (scheduledSurveys.length) {
|
||||
await prisma.survey.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: scheduledSurveys.map((survey) => survey.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: "inProgress",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return responses.successResponse({
|
||||
message: `Updated ${surveysToClose.length} surveys to completed and ${scheduledSurveys.length} surveys to inProgress.`,
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const SurveyInactive = ({
|
||||
status,
|
||||
surveyClosedMessage,
|
||||
}: {
|
||||
status: "paused" | "completed" | "link invalid";
|
||||
status: "paused" | "completed" | "link invalid" | "scheduled";
|
||||
surveyClosedMessage?: TSurveyClosedMessage | null;
|
||||
}) => {
|
||||
const icons = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"functions": {
|
||||
"app/api/cron/weekly_summary/*.ts": {
|
||||
"app/api/cron/**/*.ts": {
|
||||
"maxDuration": 30
|
||||
},
|
||||
"app/api/v1/client/**/*.ts": {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
0 0 * * * curl $WEBAPP_URL/api/cron/close_surveys -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
|
||||
0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly_summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
|
||||
0 0 * * * curl $WEBAPP_URL/api/cron/survey-status -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
|
||||
0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly-summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
|
||||
0 9 * * * curl $WEBAPP_URL/api/cron/ping -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "SurveyStatus" ADD VALUE 'scheduled';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "runOnDate" TIMESTAMP(3);
|
||||
@@ -178,6 +178,7 @@ model TagsOnResponses {
|
||||
|
||||
enum SurveyStatus {
|
||||
draft
|
||||
scheduled
|
||||
inProgress
|
||||
paused
|
||||
completed
|
||||
@@ -284,8 +285,9 @@ model Survey {
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
displays Display[]
|
||||
autoClose Int?
|
||||
delay Int @default(0)
|
||||
autoComplete Int?
|
||||
delay Int @default(0)
|
||||
runOnDate DateTime?
|
||||
closeOnDate DateTime?
|
||||
/// @zod.custom(imports.ZSurveyClosedMessage)
|
||||
/// [SurveyClosedMessage]
|
||||
|
||||
@@ -289,6 +289,7 @@ export const mockSurvey: TSurvey = {
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
|
||||
@@ -114,6 +114,7 @@ export const PREVIEW_SURVEY = {
|
||||
displayOption: "displayOnce",
|
||||
recontactDays: null,
|
||||
autoClose: null,
|
||||
runOnDate: null,
|
||||
closeOnDate: null,
|
||||
delay: 0,
|
||||
displayPercentage: null,
|
||||
|
||||
@@ -58,6 +58,7 @@ export const selectSurvey = {
|
||||
displayOption: true,
|
||||
recontactDays: true,
|
||||
autoClose: true,
|
||||
runOnDate: true,
|
||||
closeOnDate: true,
|
||||
delay: true,
|
||||
displayPercentage: true,
|
||||
@@ -469,11 +470,25 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
|
||||
}
|
||||
|
||||
surveyData.updatedAt = new Date();
|
||||
|
||||
data = {
|
||||
...surveyData,
|
||||
...data,
|
||||
};
|
||||
|
||||
// Remove scheduled status when runOnDate is not set
|
||||
if (data.status === "scheduled" && data.runOnDate === null) {
|
||||
data.status = "inProgress";
|
||||
}
|
||||
// Set scheduled status when runOnDate is set and in the future on completed surveys
|
||||
if (
|
||||
(data.status === "completed" || data.status === "paused" || data.status === "inProgress") &&
|
||||
data.runOnDate &&
|
||||
data.runOnDate > new Date()
|
||||
) {
|
||||
data.status = "scheduled";
|
||||
}
|
||||
|
||||
try {
|
||||
const prismaSurvey = await prisma.survey.update({
|
||||
where: { id: surveyId },
|
||||
|
||||
@@ -160,6 +160,7 @@ const baseSurveyProperties = {
|
||||
autoClose: 10,
|
||||
delay: 0,
|
||||
autoComplete: 7,
|
||||
runOnDate: null,
|
||||
closeOnDate: currentDate,
|
||||
redirectUrl: "http://github.com/formbricks/formbricks",
|
||||
recontactDays: 3,
|
||||
|
||||
@@ -11,6 +11,9 @@ export const formatSurveyDateFields = (survey: TSurvey): TSurvey => {
|
||||
if (typeof survey.updatedAt === "string") {
|
||||
survey.updatedAt = new Date(survey.updatedAt);
|
||||
}
|
||||
if (typeof survey.runOnDate === "string") {
|
||||
survey.runOnDate = new Date(survey.runOnDate);
|
||||
}
|
||||
if (typeof survey.closeOnDate === "string") {
|
||||
survey.closeOnDate = new Date(survey.closeOnDate);
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
|
||||
|
||||
export type TSurveyType = z.infer<typeof ZSurveyType>;
|
||||
|
||||
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
|
||||
const ZSurveyStatus = z.enum(["draft", "scheduled", "inProgress", "paused", "completed"]);
|
||||
|
||||
export type TSurveyStatus = z.infer<typeof ZSurveyStatus>;
|
||||
|
||||
@@ -473,6 +473,7 @@ export const ZSurvey = z.object({
|
||||
hiddenFields: ZSurveyHiddenFields,
|
||||
delay: z.number(),
|
||||
autoComplete: z.number().nullable(),
|
||||
runOnDate: z.date().nullable(),
|
||||
closeOnDate: z.date().nullable(),
|
||||
productOverwrites: ZSurveyProductOverwrites.nullable(),
|
||||
styling: ZSurveyStyling.nullable(),
|
||||
@@ -506,6 +507,7 @@ export const ZSurveyInput = z
|
||||
hiddenFields: ZSurveyHiddenFields.optional(),
|
||||
delay: z.number().optional(),
|
||||
autoComplete: z.number().nullish(),
|
||||
runOnDate: z.date().nullish(),
|
||||
closeOnDate: z.date().nullish(),
|
||||
styling: ZSurveyStyling.optional(),
|
||||
surveyClosedMessage: ZSurveyClosedMessage.nullish(),
|
||||
@@ -542,6 +544,7 @@ export type TSurvey = z.infer<typeof ZSurvey>;
|
||||
export type TSurveyDates = {
|
||||
createdAt: TSurvey["createdAt"];
|
||||
updatedAt: TSurvey["updatedAt"];
|
||||
runOnDate: TSurvey["runOnDate"];
|
||||
closeOnDate: TSurvey["closeOnDate"];
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function DatePicker({
|
||||
date,
|
||||
handleDateChange,
|
||||
}: {
|
||||
date?: Date;
|
||||
date?: Date | null;
|
||||
handleDateChange: (date?: Date) => void;
|
||||
}) {
|
||||
let formattedDate = date ? new Date(date) : undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, PauseIcon, PencilIcon } from "lucide-react";
|
||||
import { CheckIcon, ClockIcon, PauseIcon, PencilIcon } from "lucide-react";
|
||||
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
@@ -23,6 +23,11 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
|
||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
)}
|
||||
{status === "scheduled" && (
|
||||
<div className=" rounded-full bg-slate-300 p-1">
|
||||
<ClockIcon className="h-3 w-3 text-slate-600" />
|
||||
</div>
|
||||
)}
|
||||
{status === "paused" && (
|
||||
<div className=" rounded-full bg-slate-300 p-1">
|
||||
<PauseIcon className="h-3 w-3 text-slate-600" />
|
||||
@@ -49,6 +54,13 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
|
||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
</>
|
||||
) : status === "scheduled" ? (
|
||||
<>
|
||||
<span className="text-slate-800">Survey scheduled.</span>
|
||||
<div className=" rounded-full bg-slate-300 p-1">
|
||||
<ClockIcon className="h-3 w-3 text-slate-600" />
|
||||
</div>
|
||||
</>
|
||||
) : status === "paused" ? (
|
||||
<>
|
||||
<span className="text-slate-800">Survey paused.</span>
|
||||
@@ -78,6 +90,11 @@ export function SurveyStatusIndicator({ status, tooltip }: SurveyStatusIndicator
|
||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
)}
|
||||
{status === "scheduled" && (
|
||||
<div className="rounded-full bg-slate-300 p-1">
|
||||
<ClockIcon className="h-3 w-3 text-slate-600" />
|
||||
</div>
|
||||
)}
|
||||
{status === "paused" && (
|
||||
<div className="rounded-full bg-slate-300 p-1">
|
||||
<PauseIcon className="h-3 w-3 text-slate-600" />
|
||||
|
||||
@@ -34,7 +34,8 @@ export default function SurveyCard({
|
||||
const isSurveyCreationDeletionDisabled = isViewer;
|
||||
|
||||
const surveyStatusLabel = useMemo(() => {
|
||||
if (survey.status === "inProgress") return "Active";
|
||||
if (survey.status === "inProgress") return "In Progress";
|
||||
else if (survey.status === "scheduled") return "Scheduled";
|
||||
else if (survey.status === "completed") return "Completed";
|
||||
else if (survey.status === "draft") return "Draft";
|
||||
else if (survey.status === "paused") return "Paused";
|
||||
@@ -98,7 +99,8 @@ export default function SurveyCard({
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-xs text-slate-800",
|
||||
surveyStatusLabel === "Active" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Scheduled" && "bg-slate-200",
|
||||
surveyStatusLabel === "In Progress" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Completed" && "bg-slate-200",
|
||||
surveyStatusLabel === "Draft" && "bg-slate-100",
|
||||
surveyStatusLabel === "Paused" && "bg-slate-100"
|
||||
@@ -123,7 +125,8 @@ export default function SurveyCard({
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-fit items-center gap-2 rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
|
||||
surveyStatusLabel === "Active" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Scheduled" && "bg-slate-200",
|
||||
surveyStatusLabel === "In Progress" && "bg-emerald-50",
|
||||
surveyStatusLabel === "Completed" && "bg-slate-200",
|
||||
surveyStatusLabel === "Draft" && "bg-slate-100",
|
||||
surveyStatusLabel === "Paused" && "bg-slate-100"
|
||||
|
||||
@@ -35,6 +35,7 @@ interface FilterDropdownProps {
|
||||
|
||||
const statusOptions = [
|
||||
{ label: "In Progress", value: "inProgress" },
|
||||
{ label: "Scheduled", value: "scheduled" },
|
||||
{ label: "Paused", value: "paused" },
|
||||
{ label: "Completed", value: "completed" },
|
||||
{ label: "Draft", value: "draft" },
|
||||
|
||||
Reference in New Issue
Block a user