chore: remove cron jobs and survey scheduling functionality (#6505)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Piyush Gupta
2025-09-11 12:27:11 +05:30
committed by GitHub
parent 0188aad97b
commit dd394f1d2c
110 changed files with 414 additions and 1096 deletions

View File

@@ -99,8 +99,6 @@ PASSWORD_RESET_DISABLED=1
# Organization Invite. Disable the ability for invited users to create an account.
# INVITE_DISABLED=1
# Docker cron jobs. Disable the supercronic cron jobs in the Docker image (useful for cluster setups).
# DOCKER_CRON_ENABLED=1
##########
# Other #

View File

@@ -114,9 +114,6 @@ RUN chown -R nextjs:nextjs ./node_modules/.prisma && chmod -R 755 ./node_modules
COPY --from=installer /prisma_version.txt .
RUN chown nextjs:nextjs ./prisma_version.txt && chmod 644 ./prisma_version.txt
COPY /docker/cronjobs /app/docker/cronjobs
RUN chmod -R 755 /app/docker/cronjobs
COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2
RUN chmod -R 755 ./node_modules/@paralleldrive/cuid2

View File

@@ -205,7 +205,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -242,7 +241,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,

View File

@@ -113,7 +113,6 @@ const mockSurveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -125,7 +124,6 @@ const mockSurveys: TSurvey[] = [
surveyClosedMessage: null,
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
autoComplete: null,
runOnDate: null,
} as unknown as TSurvey,
];

View File

@@ -224,7 +224,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -259,7 +258,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,

View File

@@ -125,7 +125,6 @@ const mockSurveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -137,7 +136,6 @@ const mockSurveys: TSurvey[] = [
surveyClosedMessage: null,
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
autoComplete: null,
runOnDate: null,
} as unknown as TSurvey,
];

View File

@@ -212,7 +212,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -249,7 +248,6 @@ const surveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,

View File

@@ -118,8 +118,6 @@ const mockSurveys: TSurvey[] = [
styling: null,
segment: null,
displayPercentage: null,
closeOnDate: null,
runOnDate: null,
} as unknown as TSurvey,
];
const mockSlackIntegration = {

View File

@@ -111,11 +111,9 @@ const mockSurvey = {
surveyClosedMessage: null,
welcomeCard: { enabled: false, headline: { default: "" } } as unknown as TSurvey["welcomeCard"],
segment: null,
closeOnDate: null,
delay: 0,
autoComplete: null,
recontactDays: null,
runOnDate: null,
displayPercentage: null,
createdBy: null,
} as unknown as TSurvey;

View File

@@ -43,14 +43,12 @@ const mockSurvey = {
languages: [],
segment: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayLimit: null,
displayOption: "displayOnce",
isBackButtonHidden: false,
pin: null,
recontactDays: null,
runOnDate: null,
showLanguageSwitch: false,
singleUse: null,
surveyClosedMessage: null,

View File

@@ -103,7 +103,6 @@ const mockSurvey = {
displayOption: "displayOnce",
recontactDays: 0,
autoClose: null,
closeOnDate: null,
delay: 0,
autoComplete: null,
surveyClosedMessage: null,

View File

@@ -77,7 +77,6 @@ const mockSurvey = {
updatedAt: new Date(),
environmentId: "env1",
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,

View File

@@ -53,8 +53,6 @@ describe("SuccessMessage", () => {
autoClose: null,
delay: 0,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
welcomeCard: {
enabled: false,
headline: { default: "" },

View File

@@ -181,12 +181,10 @@ const mockSurvey = {
styling: null,
surveyClosedMessage: null,
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
closeOnDate: null,
delay: 0,
displayPercentage: null,
recontactDays: null,
autoComplete: null,
runOnDate: null,
segment: null,
variables: [],
} as unknown as TSurvey;

View File

@@ -277,8 +277,6 @@ const mockSurvey: TSurvey = {
createdBy: null,
variables: [],
followUps: [],
runOnDate: null,
closeOnDate: null,
styling: null,
pin: null,
recaptcha: null,

View File

@@ -230,8 +230,6 @@ const mockSurvey: TSurvey = {
createdBy: null,
variables: [],
followUps: [],
runOnDate: null,
closeOnDate: null,
styling: null,
pin: null,
recaptcha: null,

View File

@@ -90,8 +90,6 @@ const mockSurvey: TSurvey = {
createdBy: null,
variables: [],
followUps: [],
runOnDate: null,
closeOnDate: null,
styling: null,
pin: null,
recaptcha: null,

View File

@@ -259,8 +259,6 @@ const mockSurvey = {
autoClose: null,
delay: 0,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
singleUse: { enabled: false, isEncrypted: false },
styling: null,
} as any;

View File

@@ -86,8 +86,6 @@ const mockSurvey = {
autoClose: null,
delay: 0,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
} as unknown as TSurvey;
const mockProject = {

View File

@@ -91,7 +91,6 @@ const mockBaseSurvey: TSurvey = {
segment: null,
recontactDays: null,
autoComplete: null,
closeOnDate: null,
createdAt: new Date(),
updatedAt: new Date(),
displayOption: "displayOnce",
@@ -104,7 +103,6 @@ const mockBaseSurvey: TSurvey = {
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
runOnDate: null,
showLanguageSwitch: false,
isBackButtonHidden: false,
followUps: [],

View File

@@ -60,7 +60,6 @@ describe("Utils Tests", () => {
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
autoComplete: null,
singleUse: null,

View File

@@ -156,11 +156,9 @@ const mockSurvey = {
triggers: [],
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
autoComplete: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
languages: [],
runOnDate: null,
singleUse: null,
surveyClosedMessage: null,
segment: null,

View File

@@ -97,7 +97,6 @@ const mockSurvey = {
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
autoComplete: null,
surveyClosedMessage: null,

View File

@@ -90,7 +90,6 @@ const baseSurvey: TSurvey = {
singleUse: null,
verifyEmail: null,
pin: null,
closeOnDate: null,
productOverwrites: null,
analytics: {
numCTA: 0,
@@ -105,7 +104,6 @@ const baseSurvey: TSurvey = {
},
createdBy: null,
autoComplete: null,
runOnDate: null,
endings: [],
};
@@ -123,29 +121,6 @@ describe("SurveyStatusDropdown", () => {
expect(screen.queryByTestId("select-container")).toBeNull();
});
test("disables select when status is scheduled", () => {
render(
<SurveyStatusDropdown environment={mockEnvironment} survey={{ ...baseSurvey, status: "scheduled" }} />
);
expect(screen.getByTestId("select-container")).toHaveAttribute("data-disabled", "true");
expect(screen.getByTestId("tooltip")).toBeInTheDocument();
expect(screen.getByTestId("tooltip-content")).toHaveTextContent(
"environments.surveys.survey_status_tooltip"
);
});
test("disables select when closeOnDate is in the past", () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 1);
render(
<SurveyStatusDropdown
environment={mockEnvironment}
survey={{ ...baseSurvey, status: "inProgress", closeOnDate: pastDate }}
/>
);
expect(screen.getByTestId("select-container")).toHaveAttribute("data-disabled", "true");
});
test("renders SurveyStatusIndicator for link survey", () => {
render(
<SurveyStatusDropdown

View File

@@ -9,7 +9,6 @@ import {
SelectValue,
} from "@/modules/ui/components/select";
import { SurveyStatusIndicator } from "@/modules/ui/components/survey-status-indicator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { useRouter } from "next/navigation";
import toast from "react-hot-toast";
@@ -30,11 +29,6 @@ export const SurveyStatusDropdown = ({
}: SurveyStatusDropdownProps) => {
const { t } = useTranslate();
const router = useRouter();
const isCloseOnDateEnabled = survey.closeOnDate !== null;
const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null;
const isStatusChangeDisabled =
(survey.status === "scheduled" || (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date())) ??
false;
const handleStatusChange = async (status: TSurvey["status"]) => {
const updateSurveyActionResponse = await updateSurveyAction({ ...survey, status });
@@ -68,53 +62,43 @@ export const SurveyStatusDropdown = ({
) : (
<Select
value={survey.status}
disabled={isStatusChangeDisabled}
onValueChange={(value: TSurvey["status"]) => {
handleStatusChange(value);
}}>
<TooltipProvider delayDuration={50}>
<Tooltip open={isStatusChangeDisabled ? undefined : false}>
<TooltipTrigger asChild>
<SelectTrigger className="w-[170px] bg-white md:w-[200px]">
<SelectValue>
<div className="flex items-center">
{(survey.type === "link" || environment.appSetupCompleted) && (
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-2 text-sm text-slate-700">
{survey.status === "scheduled" && t("common.scheduled")}
{survey.status === "inProgress" && t("common.in_progress")}
{survey.status === "paused" && t("common.paused")}
{survey.status === "completed" && t("common.completed")}
</span>
</div>
</SelectValue>
</SelectTrigger>
</TooltipTrigger>
<SelectContent className="bg-white">
<SelectItem className="group font-normal hover:text-slate-900" value="inProgress">
<div className="flex w-full items-center justify-center gap-4">
<SurveyStatusIndicator status={"inProgress"} />
{t("common.in_progress")}
</div>
</SelectItem>
<SelectItem className="group font-normal hover:text-slate-900" value="paused">
<div className="flex w-full items-center justify-center gap-2">
<SurveyStatusIndicator status={"paused"} />
{t("common.paused")}
</div>
</SelectItem>
<SelectItem className="group font-normal hover:text-slate-900" value="completed">
<div className="flex w-full items-center justify-center gap-2">
<SurveyStatusIndicator status={"completed"} />
{t("common.completed")}
</div>
</SelectItem>
</SelectContent>
<TooltipContent>{t("environments.surveys.survey_status_tooltip")}</TooltipContent>
</Tooltip>
</TooltipProvider>
<SelectTrigger className="w-[170px] bg-white md:w-[200px]">
<SelectValue>
<div className="flex items-center">
{(survey.type === "link" || environment.appSetupCompleted) && (
<SurveyStatusIndicator status={survey.status} />
)}
<span className="ml-2 text-sm text-slate-700">
{survey.status === "inProgress" && t("common.in_progress")}
{survey.status === "paused" && t("common.paused")}
{survey.status === "completed" && t("common.completed")}
</span>
</div>
</SelectValue>
</SelectTrigger>
<SelectContent className="bg-white">
<SelectItem className="group font-normal hover:text-slate-900" value="inProgress">
<div className="flex w-full items-center justify-center gap-4">
<SurveyStatusIndicator status={"inProgress"} />
{t("common.in_progress")}
</div>
</SelectItem>
<SelectItem className="group font-normal hover:text-slate-900" value="paused">
<div className="flex w-full items-center justify-center gap-2">
<SurveyStatusIndicator status={"paused"} />
{t("common.paused")}
</div>
</SelectItem>
<SelectItem className="group font-normal hover:text-slate-900" value="completed">
<div className="flex w-full items-center justify-center gap-2">
<SurveyStatusIndicator status={"completed"} />
{t("common.completed")}
</div>
</SelectItem>
</SelectContent>
</Select>
)}
</>

View File

@@ -65,8 +65,6 @@ const mockSurvey: TSurvey = {
followUps: [],
delay: 0,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
projectOverwrites: null,
styling: null,
showLanguageSwitch: null,

View File

@@ -49,8 +49,6 @@ describe("SurveyLayout", () => {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -148,8 +148,6 @@ export const mockSurvey: TSurvey = {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -143,7 +143,6 @@ const mockSurvey = {
segment: null,
recontactDays: null,
autoComplete: null,
closeOnDate: null,
createdAt: new Date(),
updatedAt: new Date(),
displayOption: "displayOnce",

View File

@@ -1,35 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { CRON_SECRET } from "@/lib/constants";
import { env } from "@/lib/env";
import { captureTelemetry } from "@/lib/telemetry";
import packageJson from "@/package.json";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
export const POST = async () => {
const headersList = await headers();
const apiKey = headersList.get("x-api-key");
if (!apiKey || apiKey !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
if (env.TELEMETRY_DISABLED === "1") {
return responses.successResponse({}, true);
}
const [surveyCount, responseCount, userCount] = await Promise.all([
prisma.survey.count(),
prisma.response.count(),
prisma.user.count(),
]);
captureTelemetry("ping", {
version: packageJson.version,
surveyCount,
responseCount,
userCount,
});
return responses.successResponse({}, true);
};

View File

@@ -1,71 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { CRON_SECRET } from "@/lib/constants";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
export const POST = async () => {
const headersList = await 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,
environmentId: 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,
environmentId: 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.`,
});
};

View File

@@ -80,7 +80,6 @@ const baseSurvey: TSurvey = {
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
@@ -100,7 +99,6 @@ const baseSurvey: TSurvey = {
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
runOnDate: null,
showLanguageSwitch: false,
isBackButtonHidden: false,
followUps: [],

View File

@@ -51,14 +51,12 @@ const baseSurvey: TSurvey = {
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
runOnDate: null,
showLanguageSwitch: false,
isBackButtonHidden: false,
followUps: [],
recaptcha: { enabled: false, threshold: 0.5 },
displayOption: "displayOnce",
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -107,13 +107,11 @@ const mockSurveys: TSurvey[] = [
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
runOnDate: null,
showLanguageSwitch: false,
questions: [],
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -60,7 +60,6 @@ const mockSurvey: TSurvey = {
displayOption: "displayOnce",
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
@@ -83,7 +82,6 @@ const mockSurvey: TSurvey = {
isSingleResponsePerEmailEnabled: false,
isVerifyEmailEnabled: false,
projectOverwrites: null,
runOnDate: null,
showLanguageSwitch: false,
};

View File

@@ -3682,9 +3682,7 @@ export const previewSurvey = (projectName: string, t: TFnType) => {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
recaptcha: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: 50,

View File

@@ -420,7 +420,6 @@ describe("endpoint-validator", () => {
expect(isAdminDomainRoute("/api/v2/management/surveys")).toBe(true);
expect(isAdminDomainRoute("/api/v1/integrations/webhook")).toBe(true);
expect(isAdminDomainRoute("/pipeline/jobs")).toBe(true);
expect(isAdminDomainRoute("/cron/tasks")).toBe(true);
expect(isAdminDomainRoute("/random/route")).toBe(true);
});
@@ -472,7 +471,6 @@ describe("endpoint-validator", () => {
expect(isRouteAllowedForDomain("/health", false)).toBe(true);
expect(isRouteAllowedForDomain("/storage/env123/public/file.jpg", false)).toBe(true);
expect(isRouteAllowedForDomain("/pipeline/jobs", false)).toBe(true);
expect(isRouteAllowedForDomain("/cron/tasks", false)).toBe(true);
expect(isRouteAllowedForDomain("/unknown/route", false)).toBe(true);
});

View File

@@ -72,7 +72,7 @@ export const isAdminDomainRoute = (url: string): boolean => {
return false;
}
// For non-public routes, allow them (includes known admin routes and unknown routes like pipeline, cron)
// For non-public routes, allow them (includes known admin routes and unknown routes like pipeline)
return true;
};

View File

@@ -16,7 +16,6 @@ export const env = createEnv({
BREVO_LIST_ID: z.string().optional(),
DATABASE_URL: z.string().url(),
DEBUG: z.enum(["1", "0"]).optional(),
DOCKER_CRON_ENABLED: z.enum(["1", "0"]).optional(),
AUTH_DEFAULT_TEAM_ID: z.string().optional(),
AUTH_SKIP_INVITE_FOR_SSO: z.enum(["1", "0"]).optional(),
E2E_TESTING: z.enum(["1", "0"]).optional(),
@@ -148,7 +147,6 @@ export const env = createEnv({
DEBUG: process.env.DEBUG,
AUTH_DEFAULT_TEAM_ID: process.env.AUTH_SSO_DEFAULT_TEAM_ID,
AUTH_SKIP_INVITE_FOR_SSO: process.env.AUTH_SKIP_INVITE_FOR_SSO,
DOCKER_CRON_ENABLED: process.env.DOCKER_CRON_ENABLED,
E2E_TESTING: process.env.E2E_TESTING,
EMAIL_AUTH_DISABLED: process.env.EMAIL_AUTH_DISABLED,
EMAIL_VERIFICATION_DISABLED: process.env.EMAIL_VERIFICATION_DISABLED,

View File

@@ -301,8 +301,6 @@ export const mockSurvey: TSurvey = {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -509,8 +509,6 @@ export const mockSurvey: TSurvey = {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -232,8 +232,6 @@ describe("Response Processing", () => {
autoClose: null,
autoComplete: null,
recontactDays: null,
runOnDate: null,
closeOnDate: null,
welcomeCard: {
enabled: false,
timeToFinish: false,

View File

@@ -198,8 +198,6 @@ const baseSurveyProperties = {
autoClose: 10,
delay: 0,
autoComplete: 7,
runOnDate: null,
closeOnDate: currentDate,
redirectUrl: "https://github.com/formbricks/formbricks",
recontactDays: 3,
displayLimit: 3,

View File

@@ -44,8 +44,6 @@ export const selectSurvey = {
recontactDays: true,
displayLimit: true,
autoClose: true,
runOnDate: true,
closeOnDate: true,
delay: true,
displayPercentage: true,
autoComplete: true,
@@ -519,19 +517,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
type,
};
// 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";
}
delete data.createdBy;
const prismaSurvey = await prisma.survey.update({
where: { id: surveyId },

View File

@@ -49,7 +49,6 @@ const survey: TSurvey = {
],
recontactDays: 1,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
displayLimit: null,

View File

@@ -13,7 +13,7 @@ export const ZGetSurveysFilter = z
startDate: z.coerce.date().optional(),
endDate: z.coerce.date().optional(),
surveyType: z.enum(["link", "app"]).optional(),
surveyStatus: z.enum(["draft", "scheduled", "inProgress", "paused", "completed"]).optional(),
surveyStatus: z.enum(["draft", "inProgress", "paused", "completed"]).optional(),
})
.refine(
(data) => {
@@ -43,8 +43,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
autoClose: true,
autoComplete: true,
delay: true,
runOnDate: true,
closeOnDate: true,
singleUse: true,
isVerifyEmailEnabled: true,
isSingleResponsePerEmailEnabled: true,
@@ -66,8 +64,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
displayLimit: true,
autoClose: true,
autoComplete: true,
runOnDate: true,
closeOnDate: true,
surveyClosedMessage: true,
styling: true,
projectOverwrites: true,

View File

@@ -60,10 +60,8 @@ describe("ResponseFeed", () => {
name: "",
subheading: "",
},
closeOnDate: null,
displayLimit: null,
autoComplete: null,
runOnDate: null,
productOverwrites: null,
styling: null,
pin: null,

View File

@@ -60,7 +60,6 @@ const mockSurveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -76,7 +75,6 @@ const mockSurveys: TSurvey[] = [
pin: null,
surveyClosedMessage: null,
autoComplete: null,
runOnDate: null,
createdBy: null,
} as unknown as TSurvey,
{
@@ -89,7 +87,6 @@ const mockSurveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -105,7 +102,6 @@ const mockSurveys: TSurvey[] = [
pin: null,
surveyClosedMessage: null,
autoComplete: null,
runOnDate: null,
createdBy: null,
} as unknown as TSurvey,
{
@@ -118,7 +114,6 @@ const mockSurveys: TSurvey[] = [
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayPercentage: null,
@@ -135,7 +130,6 @@ const mockSurveys: TSurvey[] = [
pin: null,
surveyClosedMessage: null,
autoComplete: null,
runOnDate: null,
createdBy: null,
} as unknown as TSurvey,
];

View File

@@ -48,7 +48,6 @@ const mockSurvey = {
displayOption: "displayOnce",
recontactDays: 7,
autoClose: null,
closeOnDate: null,
delay: 0,
displayPercentage: 100,
autoComplete: null,

View File

@@ -157,7 +157,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,
@@ -185,7 +184,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,
@@ -216,7 +214,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [
{
type: "endScreen",
@@ -250,7 +247,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [
{
type: "redirectToUrl",
@@ -281,7 +277,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,
@@ -314,7 +309,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,
@@ -347,7 +341,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,
@@ -379,7 +372,6 @@ describe("utils", () => {
type: "app",
triggers: [],
recontactDays: null,
closeOnDate: null,
endings: [],
delay: 0,
pin: null,

View File

@@ -109,7 +109,6 @@ const mockSurvey: TSurvey = {
pin: null,
displayPercentage: null,
segment: null,
closeOnDate: null,
createdBy: null,
} as unknown as TSurvey;

View File

@@ -32,8 +32,6 @@ const mockSurvey: TSurvey = {
createdBy: null,
segment: null,
displayPercentage: null,
closeOnDate: null,
runOnDate: null,
} as unknown as TSurvey;
describe("AddEndingCardButton", () => {

View File

@@ -32,12 +32,10 @@ describe("LogicEditorActions", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -106,12 +104,10 @@ describe("LogicEditorActions", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -198,12 +194,10 @@ describe("LogicEditorActions", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -273,12 +267,10 @@ describe("LogicEditorActions", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {

View File

@@ -136,7 +136,6 @@ const mockSurvey = {
productOverwrites: null,
singleUse: null,
verifyEmail: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
inlineTriggers: null,
@@ -146,7 +145,6 @@ const mockSurvey = {
redirectUrl: null,
createdBy: null,
autoComplete: null,
runOnDate: null,
displayProgressBar: true,
} as unknown as TSurvey;

View File

@@ -51,7 +51,6 @@ const baseSurvey = {
recontactDays: null,
displayOption: "displayOnce",
autoClose: null,
closeOnDate: null,
delay: 0,
autoComplete: null,
surveyClosedMessage: null,

View File

@@ -144,7 +144,6 @@ const baseSurvey = {
singleUse: null,
surveyClosedMessage: null,
verifyEmail: null,
closeOnDate: null,
projectOverwrites: null,
hiddenFields: { enabled: false },
} as unknown as TSurvey;

View File

@@ -242,8 +242,6 @@ const mockSurvey = {
hiddenFields: { enabled: true, fieldIds: [] },
createdAt: new Date(),
updatedAt: new Date(),
runOnDate: null,
closeOnDate: null,
} as unknown as TSurvey;
const mockProject = {

View File

@@ -46,12 +46,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -91,12 +89,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -136,12 +132,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: 1,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -191,12 +185,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displaySome",
recontactDays: null,
displayLimit: 1,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -248,12 +240,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -312,12 +302,10 @@ describe("RecontactOptionsCard", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: undefined, // Initial displayLimit is undefined
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {

View File

@@ -55,30 +55,6 @@ describe("ResponseOptionsCard", () => {
expect(collapsibleRoot).toHaveAttribute("data-state", "open");
});
test("should set runOnDateToggle to true when handleRunOnDateToggle is called and runOnDateToggle is false", async () => {
const localSurvey: TSurvey = {
id: "1",
name: "Test Survey",
type: "link",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
runOnDate: null,
} as unknown as TSurvey;
const setLocalSurvey = vi.fn();
render(
<ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} responseCount={0} />
);
const runOnDateToggle = screen.getByText("environments.surveys.edit.release_survey_on_date");
await userEvent.click(runOnDateToggle);
// Check if the switch element is checked after clicking
const runOnDateSwitch = screen.getByRole("switch", { name: /release_survey_on_date/i });
expect(runOnDateSwitch).toHaveAttribute("data-state", "checked");
});
test("should not correct the invalid autoComplete value when it is less than or equal to responseCount after blur", async () => {
const localSurvey: TSurvey = {
id: "1",

View File

@@ -3,7 +3,6 @@
import { cn } from "@/lib/cn";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Alert, AlertTitle } from "@/modules/ui/components/alert";
import { DatePicker } from "@/modules/ui/components/date-picker";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { Slider } from "@/modules/ui/components/slider";
@@ -31,8 +30,6 @@ export const ResponseOptionsCard = ({
const { t } = useTranslate();
const [open, setOpen] = useState(localSurvey.type === "link" ? true : false);
const autoComplete = localSurvey.autoComplete !== null;
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
const [closeOnDateToggle, setCloseOnDateToggle] = useState(false);
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
const [verifyEmailToggle, setVerifyEmailToggle] = useState(localSurvey.isVerifyEmailEnabled);
const [recaptchaToggle, setRecaptchaToggle] = useState(localSurvey.recaptcha?.enabled ?? false);
@@ -45,38 +42,12 @@ export const ResponseOptionsCard = ({
subheading: t("environments.surveys.edit.survey_completed_subheading"),
});
const [runOnDate, setRunOnDate] = useState<Date | null>(null);
const [closeOnDate, setCloseOnDate] = useState<Date | null>(null);
const [recaptchaThreshold, setRecaptchaThreshold] = useState<number>(localSurvey.recaptcha?.threshold ?? 0);
const isPinProtectionEnabled = localSurvey.pin !== null;
const [verifyProtectWithPinError, setVerifyProtectWithPinError] = useState<string | null>(null);
const handleRunOnDateToggle = () => {
if (runOnDateToggle) {
setRunOnDateToggle(false);
if (localSurvey.runOnDate) {
setRunOnDate(null);
setLocalSurvey({ ...localSurvey, runOnDate: null });
}
} else {
setRunOnDateToggle(true);
}
};
const handleCloseOnDateToggle = () => {
if (closeOnDateToggle) {
setCloseOnDateToggle(false);
if (localSurvey.closeOnDate) {
setCloseOnDate(null);
setLocalSurvey({ ...localSurvey, closeOnDate: null });
}
} else {
setCloseOnDateToggle(true);
}
};
const handleProtectSurveyWithPinToggle = () => {
setLocalSurvey((prevSurvey) => ({ ...prevSurvey, pin: isPinProtectionEnabled ? null : "1234" }));
};
@@ -126,18 +97,6 @@ export const ResponseOptionsCard = ({
});
};
const handleRunOnDateChange = (date: Date) => {
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setRunOnDate(utcDate);
setLocalSurvey({ ...localSurvey, runOnDate: utcDate ?? null });
};
const handleCloseOnDateChange = (date: Date) => {
const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0, 0));
setCloseOnDate(utcDate);
setLocalSurvey({ ...localSurvey, closeOnDate: utcDate ?? null });
};
const handleClosedSurveyMessageChange = ({
heading,
subheading,
@@ -146,7 +105,6 @@ export const ResponseOptionsCard = ({
subheading?: string;
}) => {
const message = {
enabled: closeOnDateToggle,
heading: heading ?? surveyClosedMessage.heading,
subheading: subheading ?? surveyClosedMessage.subheading,
};
@@ -167,16 +125,6 @@ export const ResponseOptionsCard = ({
});
setSurveyClosedMessageToggle(true);
}
if (localSurvey.runOnDate) {
setRunOnDate(localSurvey.runOnDate);
setRunOnDateToggle(true);
}
if (localSurvey.closeOnDate) {
setCloseOnDate(localSurvey.closeOnDate);
setCloseOnDateToggle(true);
}
}, [localSurvey, surveyClosedMessage.heading, surveyClosedMessage.subheading]);
const toggleAutocomplete = () => {
@@ -295,34 +243,6 @@ export const ResponseOptionsCard = ({
</p>
</label>
</AdvancedOptionToggle>
{/* Run Survey on Date */}
<AdvancedOptionToggle
htmlId="runOnDate"
isChecked={runOnDateToggle}
onToggle={handleRunOnDateToggle}
title={t("environments.surveys.edit.release_survey_on_date")}
description={t(
"environments.surveys.edit.automatically_release_the_survey_at_the_beginning_of_the_day_utc"
)}
childBorder={true}>
<div className="p-4">
<DatePicker date={runOnDate} updateSurveyDate={handleRunOnDateChange} />
</div>
</AdvancedOptionToggle>
{/* Close Survey on Date */}
<AdvancedOptionToggle
htmlId="closeOnDate"
isChecked={closeOnDateToggle}
onToggle={handleCloseOnDateToggle}
title={t("environments.surveys.edit.close_survey_on_date")}
description={t(
"environments.surveys.edit.automatically_closes_the_survey_at_the_beginning_of_the_day_utc"
)}
childBorder={true}>
<div className="p-4">
<DatePicker date={closeOnDate} updateSurveyDate={handleCloseOnDateChange} />
</div>
</AdvancedOptionToggle>
{/* recaptcha for spam protection */}
{isSpamProtectionAllowed && (

View File

@@ -135,7 +135,6 @@ const baseSurvey = {
updatedAt: new Date(),
createdBy: null,
variables: [],
closeOnDate: null,
endings: [],
hiddenFields: { enabled: false, fieldIds: [] },
} as unknown as TSurvey;

View File

@@ -97,7 +97,6 @@ const mockSurvey = {
environmentId: "env1",
variables: [],
welcomeCard: { enabled: false } as unknown as TSurvey["welcomeCard"],
closeOnDate: null,
segment: null,
createdBy: null,
} as unknown as TSurvey;

View File

@@ -3,7 +3,7 @@ import { updateSurveyAction } from "@/modules/survey/editor/actions";
import { SurveyMenuBar } from "@/modules/survey/editor/components/survey-menu-bar";
import { isSurveyValid } from "@/modules/survey/editor/lib/validation";
import { Project } from "@prisma/client";
import { cleanup, render, screen } from "@testing-library/react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { TSurvey, TSurveyOpenTextQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
@@ -71,6 +71,22 @@ vi.mock("@formbricks/i18n-utils/src/utils", () => ({
getLanguageLabel: vi.fn((code) => `Lang(${code})`),
}));
vi.mock("@formbricks/types/surveys/types", async () => {
const actual = await vi.importActual("@formbricks/types/surveys/types");
return {
...actual,
ZSurvey: {
safeParse: vi.fn(() => ({ success: true })),
},
ZSurveyEndScreenCard: {
parse: vi.fn((data) => data),
},
ZSurveyRedirectUrlCard: {
parse: vi.fn((data) => data),
},
};
});
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
@@ -91,7 +107,21 @@ vi.mock("lucide-react", async () => {
const mockRouter = {
back: vi.fn(),
push: vi.fn(),
refresh: vi.fn(),
};
// Mock Next.js router
vi.mock("next/navigation", () => ({
useRouter: () => mockRouter,
}));
// Mock Tolgee translate
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({
t: (key: string) => key, // Return the key as translation for testing
}),
}));
const mockSetLocalSurvey = vi.fn();
const mockSetActiveId = vi.fn();
const mockSetInvalidQuestions = vi.fn();
@@ -126,8 +156,6 @@ const baseSurvey = {
pin: null,
segment: null,
languages: [],
runOnDate: null,
closeOnDate: null,
welcomeCard: { enabled: false } as TSurvey["welcomeCard"],
} as unknown as TSurvey;
@@ -267,4 +295,129 @@ describe("SurveyMenuBar", () => {
expect(saveButton).not.toBeDisabled();
expect(saveCloseButton).not.toBeDisabled();
});
test("shows publish button for link surveys", () => {
// For link surveys, publish button shows without audience prompt
const linkSurvey = { ...baseSurvey, type: "link" as const };
render(<SurveyMenuBar {...defaultProps} localSurvey={linkSurvey} />);
expect(screen.getByText("environments.surveys.edit.publish")).toBeInTheDocument();
});
test("handles save with survey validation failure", async () => {
vi.mocked(isSurveyValid).mockReturnValue(false);
render(<SurveyMenuBar {...defaultProps} />);
const saveButton = screen.getByText("common.save").closest("button");
await userEvent.click(saveButton!);
expect(mockSetLocalSurvey).not.toHaveBeenCalled();
// Reset mock for other tests
vi.mocked(isSurveyValid).mockReturnValue(true);
});
test("handles save with update action error", async () => {
vi.mocked(updateSurveyAction).mockResolvedValue({
serverError: "Something went wrong",
});
render(<SurveyMenuBar {...defaultProps} />);
const saveButton = screen.getByText("common.save").closest("button");
await userEvent.click(saveButton!);
// Should not refresh router on error
expect(mockRouter.refresh).not.toHaveBeenCalled();
// Reset mock for other tests
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, updatedAt: new Date() } });
});
test("handles save with thrown exception", async () => {
vi.mocked(updateSurveyAction).mockRejectedValue(new Error("Network error"));
render(<SurveyMenuBar {...defaultProps} />);
const saveButton = screen.getByText("common.save").closest("button");
await userEvent.click(saveButton!);
expect(mockRouter.refresh).not.toHaveBeenCalled();
// Reset mock for other tests
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, updatedAt: new Date() } });
});
test("handles app survey without triggers", async () => {
const appSurveyNoTriggers = { ...baseSurvey, type: "app" as const, triggers: [] };
render(<SurveyMenuBar {...defaultProps} localSurvey={appSurveyNoTriggers} />);
const saveButton = screen.getByText("common.save").closest("button");
await userEvent.click(saveButton!);
// Should show error and not save when no triggers
expect(mockSetLocalSurvey).not.toHaveBeenCalled();
});
test("handles back without changes", async () => {
render(<SurveyMenuBar {...defaultProps} />);
const backButton = screen.getByText("common.back").closest("button");
await userEvent.click(backButton!);
expect(mockRouter.back).toHaveBeenCalled();
expect(screen.queryByTestId("alert-dialog")).not.toBeInTheDocument();
});
test("shows dialog buttons when changes exist", () => {
const changedSurvey = { ...baseSurvey, name: "Changed Name" };
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
const backButton = screen.getByText("common.back").closest("button");
fireEvent.click(backButton!);
// Dialog should be visible with both buttons
expect(screen.getByTestId("alert-dialog")).toBeInTheDocument();
expect(screen.getByText("common.discard")).toBeInTheDocument();
});
test("discards changes from dialog", async () => {
const changedSurvey = { ...baseSurvey, name: "Changed Name" };
render(<SurveyMenuBar {...defaultProps} localSurvey={changedSurvey} />);
const backButton = screen.getByText("common.back").closest("button");
await userEvent.click(backButton!);
const discardButton = screen.getByText("common.discard");
await userEvent.click(discardButton);
expect(mockRouter.back).toHaveBeenCalled();
});
test("renders save and close button for published surveys", () => {
const publishedSurvey = {
...baseSurvey,
status: "inProgress" as const,
triggers: ["trigger1"], // Has triggers so buttons are enabled
} as unknown as TSurvey;
render(<SurveyMenuBar {...defaultProps} localSurvey={publishedSurvey} />);
expect(screen.getByText("environments.surveys.edit.save_and_close")).toBeInTheDocument();
});
test("sets status to 'inProgress' when publishing survey", async () => {
vi.mocked(isSurveyValid).mockReturnValue(true);
vi.mocked(updateSurveyAction).mockResolvedValue({ data: { ...baseSurvey, status: "inProgress" } });
render(<SurveyMenuBar {...defaultProps} />);
const publishButton = screen.getByText("environments.surveys.edit.publish").closest("button");
await userEvent.click(publishButton!);
await new Promise((resolve) => setTimeout(resolve, 0));
// Verify updateSurveyAction was called with status "inProgress"
expect(updateSurveyAction).toHaveBeenCalledWith({
...baseSurvey,
status: "inProgress",
segment: null,
});
});
});

View File

@@ -286,7 +286,7 @@ export const SurveyMenuBar = ({
setIsSurveyPublishing(false);
return;
}
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
const status = "inProgress";
const segment = await handleSegmentUpdate();
clearSurveyLocalStorage();

View File

@@ -63,7 +63,6 @@ const mockSurvey = {
hiddenFields: { enabled: false },
segment: null,
projectOverwrites: null, // Start with no overwrites
closeOnDate: null,
createdBy: null,
} as unknown as TSurvey;

View File

@@ -64,12 +64,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -129,12 +127,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -186,12 +182,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -237,12 +231,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -284,12 +276,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [],
endings: [],
hiddenFields: {
@@ -358,12 +348,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [
{
id: "q1WithLogic",
@@ -428,12 +416,10 @@ describe("SurveyVariablesCardItem", () => {
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
questions: [
{
id: "q1",

View File

@@ -50,8 +50,6 @@ const mockSurvey = {
environmentId: "env-123",
createdBy: null,
segment: null,
closeOnDate: null,
runOnDate: null,
isVerifyEmailEnabled: false,
isSingleResponsePerEmailEnabled: false,
recaptcha: null,

View File

@@ -103,7 +103,6 @@ const mockSurveyAppBase = {
singleUse: null,
surveyClosedMessage: null,
segment: null,
closeOnDate: null,
createdAt: new Date(),
updatedAt: new Date(),
autoComplete: null,

View File

@@ -89,8 +89,6 @@ describe("Survey Editor Library Tests", () => {
hiddenFields: { enabled: false },
delay: 0,
autoComplete: null,
closeOnDate: null,
runOnDate: null,
projectOverwrites: null,
styling: null,
showLanguageSwitch: false,
@@ -431,45 +429,6 @@ describe("Survey Editor Library Tests", () => {
});
});
test("should handle scheduled status based on runOnDate", async () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const updatedSurvey: TSurvey = {
...mockSurvey,
status: "completed",
runOnDate: tomorrow,
};
await updateSurvey(updatedSurvey);
expect(prisma.survey.update).toHaveBeenCalledWith({
where: { id: "survey123" },
data: expect.objectContaining({
status: "scheduled", // Should be changed to scheduled because runOnDate is in the future
}),
select: expect.any(Object),
});
});
test("should remove scheduled status when runOnDate is not set", async () => {
const updatedSurvey: TSurvey = {
...mockSurvey,
status: "scheduled",
runOnDate: null,
};
await updateSurvey(updatedSurvey);
expect(prisma.survey.update).toHaveBeenCalledWith({
where: { id: "survey123" },
data: expect.objectContaining({
status: "inProgress", // Should be changed to inProgress because runOnDate is null
}),
select: expect.any(Object),
});
});
test("should throw ResourceNotFoundError when survey is not found", async () => {
vi.mocked(getSurvey).mockResolvedValueOnce(null as unknown as TSurvey);

View File

@@ -248,19 +248,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
type,
};
// 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";
}
delete data.createdBy;
const prismaSurvey = await prisma.survey.update({
where: { id: surveyId },

View File

@@ -214,12 +214,10 @@ const createMockSurvey = (): TSurvey =>
showResponseCount: false,
},
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
runOnDate: null,
thankYouCard: {
enabled: true,
title: { default: "Thank you" },

View File

@@ -486,11 +486,9 @@ describe("validation.isSurveyValid", () => {
triggers: [],
recontactDays: null,
autoClose: null,
closeOnDate: null,
delay: 0,
displayOption: "displayOnce",
displayLimit: null,
runOnDate: null,
thankYouCard: { enabled: true, title: { default: "Thank you" } }, // Minimal for type check
createdAt: new Date(),
updatedAt: new Date(),

View File

@@ -23,8 +23,6 @@ export const selectSurvey = {
recontactDays: true,
displayLimit: true,
autoClose: true,
runOnDate: true,
closeOnDate: true,
delay: true,
displayPercentage: true,
autoComplete: true,

View File

@@ -121,15 +121,6 @@ describe("SurveyInactive", () => {
expect(screen.getByTestId("mock-image")).toBeInTheDocument();
});
test("renders scheduled status correctly", async () => {
const Component = await SurveyInactive({ status: "scheduled" });
render(Component);
expect(screen.getByText("common.survey scheduled.")).toBeInTheDocument();
expect(screen.getByTestId("button")).toBeInTheDocument();
expect(screen.getByTestId("mock-image")).toBeInTheDocument();
});
test("shows branding when linkSurveyBranding is true", async () => {
const Component = await SurveyInactive({ status: "paused", project: { linkSurveyBranding: true } });
render(Component);

View File

@@ -12,7 +12,7 @@ export const SurveyInactive = async ({
surveyClosedMessage,
project,
}: {
status: "paused" | "completed" | "link invalid" | "scheduled" | "response submitted" | "link expired";
status: "paused" | "completed" | "link invalid" | "response submitted" | "link expired";
surveyClosedMessage?: TSurveyClosedMessage | null;
project?: Pick<Project, "linkSurveyBranding">;
}) => {

View File

@@ -90,8 +90,6 @@ describe("data", () => {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,
@@ -216,8 +214,6 @@ describe("data", () => {
recontactDays: null,
displayLimit: null,
autoClose: null,
runOnDate: null,
closeOnDate: null,
delay: 0,
displayPercentage: null,
autoComplete: null,

View File

@@ -37,8 +37,6 @@ export const getSurveyWithMetadata = reactCache(async (surveyId: string) => {
recontactDays: true,
displayLimit: true,
autoClose: true,
runOnDate: true,
closeOnDate: true,
delay: true,
displayPercentage: true,
autoComplete: true,

View File

@@ -35,8 +35,6 @@ export const SurveyCard = ({
switch (survey.status) {
case "inProgress":
return t("common.in_progress");
case "scheduled":
return t("common.scheduled");
case "completed":
return t("common.completed");
case "draft":
@@ -73,7 +71,6 @@ export const SurveyCard = ({
<div
className={cn(
"col-span-1 flex w-fit items-center gap-2 whitespace-nowrap rounded-full py-1 pl-1 pr-2 text-sm text-slate-800",
surveyStatusLabel === "Scheduled" && "bg-slate-200",
surveyStatusLabel === "In Progress" && "bg-emerald-50",
surveyStatusLabel === "Completed" && "bg-slate-200",
surveyStatusLabel === "Draft" && "bg-slate-100",

View File

@@ -29,7 +29,6 @@ const getCreatorOptions = (t: TFnType): TFilterOption[] => [
];
const getStatusOptions = (t: TFnType): TFilterOption[] => [
{ label: t("common.scheduled"), value: "scheduled" },
{ label: t("common.paused"), value: "paused" },
{ label: t("common.completed"), value: "completed" },
{ label: t("common.draft"), value: "draft" },
@@ -86,13 +85,7 @@ export const SurveyFilters = ({
};
const handleStatusChange = (value: string) => {
if (
value === "inProgress" ||
value === "paused" ||
value === "completed" ||
value === "draft" ||
value === "scheduled"
) {
if (value === "inProgress" || value === "paused" || value === "completed" || value === "draft") {
if (status.includes(value)) {
setSurveyFilters((prev) => ({ ...prev, status: prev.status.filter((v) => v !== value) }));
} else {

View File

@@ -25,8 +25,6 @@ export const getMinimalSurvey = (t: TFnType): TSurvey => ({
delay: 0, // No delay
displayPercentage: null,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
surveyClosedMessage: {
enabled: false,
},

View File

@@ -110,7 +110,6 @@ describe("QuestionToggleTable", () => {
},
styling: {},
autoComplete: false,
closeOnDate: null,
recaptcha: {
enabled: false,
},

View File

@@ -31,7 +31,6 @@ vi.mock("@tolgee/react", () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"common.gathering_responses": "Gathering responses",
"common.survey_scheduled": "Survey scheduled",
"common.survey_paused": "Survey paused",
"common.survey_completed": "Survey completed",
};
@@ -63,21 +62,6 @@ describe("SurveyStatusIndicator", () => {
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
});
test("renders scheduled status correctly without tooltip", () => {
const { container } = render(<SurveyStatusIndicator status="scheduled" />);
// Find the clock icon container
const clockIconContainer = container.querySelector(".rounded-full.bg-slate-300.p-1");
expect(clockIconContainer).toBeInTheDocument();
// Find the clock icon inside
const clockIcon = clockIconContainer?.querySelector("[data-testid='clock-icon']");
expect(clockIcon).toBeInTheDocument();
// Should not render tooltip components
expect(screen.queryByTestId("tooltip-provider")).not.toBeInTheDocument();
});
test("renders paused status correctly without tooltip", () => {
const { container } = render(<SurveyStatusIndicator status="paused" />);
@@ -137,16 +121,6 @@ describe("SurveyStatusIndicator", () => {
expect(tooltipContent).toHaveTextContent("Gathering responses");
});
test("renders scheduled status with tooltip correctly", () => {
const { container } = render(<SurveyStatusIndicator status="scheduled" tooltip={true} />);
expect(screen.getByTestId("tooltip-content")).toHaveTextContent("Survey scheduled");
// Use container query to find the first clock icon
const clockIcon = container.querySelector("[data-testid='clock-icon']");
expect(clockIcon).toBeInTheDocument();
});
test("renders paused status with tooltip correctly", () => {
const { container } = render(<SurveyStatusIndicator status="paused" tooltip={true} />);

View File

@@ -2,7 +2,7 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
import { useTranslate } from "@tolgee/react";
import { CheckIcon, ClockIcon, PauseIcon, PencilIcon } from "lucide-react";
import { CheckIcon, PauseIcon, PencilIcon } from "lucide-react";
import { TSurvey } from "@formbricks/types/surveys/types";
interface SurveyStatusIndicatorProps {
@@ -23,11 +23,6 @@ export const 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" />
@@ -54,13 +49,6 @@ export const 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">{t("common.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">{t("common.survey_paused")}</span>
@@ -90,11 +78,6 @@ export const 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" />

View File

@@ -46,13 +46,6 @@ run_with_timeout() {
fi
}
# Start cron jobs if enabled
if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then
echo "Starting cron jobs...";
supercronic -quiet /app/docker/cronjobs &
else
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0";
fi
echo "🗃️ Running database migrations..."
run_with_timeout 600 "database migration" sh -c '(cd packages/database && npm run db:migrate:deploy)'

View File

@@ -1,3 +0,0 @@
0 0 * * * curl $WEBAPP_URL/api/cron/survey-status -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"''

View File

@@ -72,9 +72,6 @@ x-environment: &environment
# Set the below to your Unsplash API Key for their Survey Backgrounds
# UNSPLASH_ACCESS_KEY:
# Set the below to 0 to disable cron jobs
# DOCKER_CRON_ENABLED: 1
# Set the below to your public domain (default is WEBAPP_URL)
# PUBLIC_URL:
@@ -183,8 +180,8 @@ x-environment: &environment
########################################## OPTIONAL (AUDIT LOGGING) ###########################################
# Set the below to 1 to enable audit logging.
# AUDIT_LOG_ENABLED: 1
# Set the below to 1 to enable audit logging.
# AUDIT_LOG_ENABLED: 1
# Set the below to get the ip address of the user from the request headers
# AUDIT_LOG_GET_USER_IP: 1

View File

@@ -268,9 +268,9 @@ EOT
encryption_key=$(openssl rand -hex 32) && sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $encryption_key/" docker-compose.yml
echo "🚗 ENCRYPTION_KEY updated successfully!"
cron_secret=$(openssl rand -hex 32) && sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $cron_secret/" docker-compose.yml
cron_secret=$(openssl rand -hex 32) && sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $cron_secret/" docker-compose.yml
echo "🚗 CRON_SECRET updated successfully!"
if [[ -n $mail_from ]]; then
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
sed -i "s|# MAIL_FROM_NAME:|MAIL_FROM_NAME: \"$mail_from_name\"|" docker-compose.yml

View File

@@ -6033,7 +6033,6 @@
{
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdAt": "2024-08-05T11:08:27.042Z",
"createdBy": "clfv1zvij0000ru0gunwpy43a",
"delay": 0,
@@ -6115,7 +6114,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segment": null,
"showLanguageSwitch": null,
"singleUse": {
@@ -6218,7 +6216,6 @@
"example": {
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdBy": null,
"delay": 0,
"displayLimit": null,
@@ -6298,7 +6295,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segmentId": null,
"showLanguageSwitch": null,
"singleUse": {
@@ -6336,7 +6332,6 @@
"data": {
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdAt": "2024-08-05T11:08:27.042Z",
"createdBy": "clfv1zvij0000ru0gunwpy43a",
"delay": 0,
@@ -6418,7 +6413,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segment": null,
"showLanguageSwitch": null,
"singleUse": {
@@ -6604,7 +6598,6 @@
"data": {
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdAt": "2024-08-05T11:08:27.042Z",
"createdBy": "clfv1zvij0000ru0gunwpy43a",
"delay": 0,
@@ -6686,7 +6679,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segment": null,
"showLanguageSwitch": null,
"singleUse": {
@@ -6859,7 +6851,6 @@
"data": {
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdAt": "2024-08-05T11:08:27.042Z",
"createdBy": "clfv1zvij0000ru0gunwpy43a",
"delay": 0,
@@ -6941,7 +6932,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segment": null,
"showLanguageSwitch": null,
"singleUse": {
@@ -7064,7 +7054,6 @@
"data": {
"autoClose": null,
"autoComplete": null,
"closeOnDate": null,
"createdAt": "2024-08-05T11:08:27.042Z",
"createdBy": "clfv1zvij0000ru0gunwpy43a",
"delay": 0,
@@ -7146,7 +7135,6 @@
],
"recontactDays": null,
"redirectUrl": null,
"runOnDate": null,
"segment": null,
"showLanguageSwitch": null,
"singleUse": {

View File

@@ -3982,7 +3982,6 @@ components:
type: string
enum:
- draft
- scheduled
- inProgress
- paused
- completed
@@ -4193,16 +4192,6 @@ components:
delay:
type: number
description: Delay before showing survey
runOnDate:
type:
- string
- "null"
description: Date to run the survey
closeOnDate:
type:
- string
- "null"
description: Date to close the survey
surveyClosedMessage:
type:
- object
@@ -4514,8 +4503,6 @@ components:
- autoClose
- autoComplete
- delay
- runOnDate
- closeOnDate
- surveyClosedMessage
- segmentId
- projectOverwrites

View File

@@ -6,9 +6,7 @@ icon: "github"
### GitHub Codespaces Setup
<Info>
This guide outlines how to set up Formbricks in a **GitHub Codespaces** environment.
</Info>
<Info>This guide outlines how to set up Formbricks in a **GitHub Codespaces** environment.</Info>
**Requirements:**
@@ -17,39 +15,42 @@ icon: "github"
**Steps:**
1. **Open your repository in GitHub Codespaces. If needed, clone the repository:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm (if not already configured):**
```bash
nvm install && nvm use
```
```bash
nvm install && nvm use
```
3. **Install the dependencies:**
```bash
pnpm install
```
```bash
pnpm install
```
4. **Create a `.env` file from the template:**
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
5. **Generate & set the required secrets:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Launch the development setup:**
```bash
pnpm go
```
```bash
pnpm go
```
Use the Codespaces port forwarding to access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
Make sure your Codespaces port configuration is set to allow access to the app.
</Tip>
<Tip>Make sure your Codespaces port configuration is set to allow access to the app.</Tip>

View File

@@ -6,9 +6,7 @@ icon: "code"
### Gitpod Setup
<Info>
This guide explains how to set up Formbricks in a **Gitpod** workspace.
</Info>
<Info>This guide explains how to set up Formbricks in a **Gitpod** workspace.</Info>
**Requirements:**
@@ -17,39 +15,42 @@ icon: "code"
**Steps:**
1. **Open the repository in Gitpod. The workspace typically clones the repo automatically. If not:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
```bash
nvm install && nvm use
```
3. **Install dependencies:**
```bash
pnpm install
```
```bash
pnpm install
```
4. **Create a `.env` file:**
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
5. **Generate & set secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Run the development setup:**
```bash
pnpm go
```
```bash
pnpm go
```
Access the running app via the forwarded port (typically [http://localhost:3000](http://localhost:3000) inside Gitpod).
<Tip>
Check your Gitpod settings to ensure Docker is enabled if required.
</Tip>
<Tip>Check your Gitpod settings to ensure Docker is enabled if required.</Tip>

View File

@@ -6,9 +6,7 @@ icon: "linux"
### Local Machine Setup - Linux
<Info>
This guide is recommended for advanced users setting up Formbricks on a **Linux** machine.
</Info>
<Info>This guide is recommended for advanced users setting up Formbricks on a **Linux** machine.</Info>
Here are the requirements for setting up Formbricks on Linux:
@@ -19,39 +17,42 @@ Here are the requirements for setting up Formbricks on Linux:
**Steps:**
1. **Clone the project & move into the directory:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
```bash
nvm install && nvm use
```
3. **Install NodeJS packages via pnpm:**
```bash
pnpm install
```
```bash
pnpm install
```
4. **Create a `.env` file based on `.env.example`:**
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
5. **Generate & set the secret values:**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Start the development setup:**
```bash
pnpm go
```
```bash
pnpm go
```
You can now access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
Create a new account on first login as no default account is available.
</Tip>
<Tip>Create a new account on first login as no default account is available.</Tip>

View File

@@ -6,9 +6,7 @@ icon: "apple"
### Local Machine Setup - Mac
<Info>
This guide is recommended for advanced users setting up Formbricks on a **Mac** machine.
</Info>
<Info>This guide is recommended for advanced users setting up Formbricks on a **Mac** machine.</Info>
**Requirements:**
@@ -19,39 +17,42 @@ icon: "apple"
**Steps:**
1. **Clone the project & change directory:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm:**
```bash
nvm install && nvm use
```
```bash
nvm install && nvm use
```
3. **Install NodeJS packages with pnpm:**
```bash
pnpm install
```
```bash
pnpm install
```
4. **Create a `.env` file from the example:**
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
5. **Generate & set secret values (using BSD sed syntax for macOS):**
```bash
sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env
sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env
sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
```
```bash
sed -i '' '/^ENCRYPTION_KEY=/s|.*|ENCRYPTION_KEY='$(openssl rand -hex 32)'|' .env
sed -i '' '/^NEXTAUTH_SECRET=/s|.*|NEXTAUTH_SECRET='$(openssl rand -hex 32)'|' .env
sed -i '' '/^CRON_SECRET=/s|.*|CRON_SECRET='$(openssl rand -hex 32)'|' .env
```
6. **Start the development setup:**
```bash
pnpm go
```
```bash
pnpm go
```
Visit [http://localhost:3000](http://localhost:3000) to access Formbricks.
<Tip>
Ensure you create a new account at first login.
</Tip>
<Tip>Ensure you create a new account at first login.</Tip>

View File

@@ -7,7 +7,8 @@ icon: "windows"
### Local Machine Setup - Windows
<Info>
This guide is intended for **Windows** users. For the best experience, use **WSL2** since pure Windows is not fully supported.
This guide is intended for **Windows** users. For the best experience, use **WSL2** since pure Windows is
not fully supported.
</Info>
**Requirements:**
@@ -19,39 +20,43 @@ icon: "windows"
**Steps (Using WSL2):**
1. **Open your WSL2 terminal and clone the project:**
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
2. **Setup NodeJS with nvm in WSL2:**
```bash
nvm install && nvm use
```
```bash
nvm install && nvm use
```
3. **Install packages using pnpm:**
```bash
pnpm install
```
```bash
pnpm install
```
4. **Create a `.env` file:**
```bash
cp .env.example .env
```
```bash
cp .env.example .env
```
5. **Generate & set secret values (Linux commands work in WSL2):**
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
sed -i '/^CRON_SECRET=/c\CRON_SECRET='$(openssl rand -hex 32) .env
```
6. **Start the development setup:**
```bash
pnpm go
```
```bash
pnpm go
```
Access Formbricks at [http://localhost:3000](http://localhost:3000).
<Tip>
If you run into conflicts, ensure any local services (like PostgreSQL) are stopped.
</Tip>
<Tip>If you run into conflicts, ensure any local services (like PostgreSQL) are stopped.</Tip>

View File

@@ -64,7 +64,6 @@ These variables are present inside your machine's docker-compose file. Restart t
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
| PROMETHEUS_ENABLED | Enables Prometheus metrics if set to 1. | optional | |
| PROMETHEUS_EXPORTER_PORT | Port for Prometheus metrics. | optional | 9090 |
| DOCKER_CRON_ENABLED | Controls whether cron jobs run in the Docker image. Set to 0 to disable (useful for cluster setups). | optional | 1 |
| DEFAULT_TEAM_ID | Default team ID for new users. | optional | |
| SENTRY_DSN | Set this to track errors and monitor performance in Sentry. | optional | |
| SENTRY_ENVIRONMENT | Set this to identify the environment in Sentry | optional | |

View File

@@ -155,19 +155,6 @@ When using S3 in a cluster setup, ensure that:
- The bucket has appropriate CORS settings configured
- IAM roles/users have sufficient permissions for read/write operations
## Disabling Docker Cron Jobs
When running Formbricks in a cluster setup, you should disable the built-in cron jobs in the Docker image to prevent them from running on multiple instances simultaneously. Instead, you should set up cron jobs in your orchestration system (like Kubernetes) to run on a single instance or as separate jobs.
To disable the Docker cron jobs, set the following environment variable:
```sh env
# Disable Docker cron jobs (0 = disabled, 1 = enabled)
DOCKER_CRON_ENABLED=0
```
This will prevent the cron jobs from starting in the Docker container while still allowing all other Formbricks functionality to work normally.
## Kubernetes Setup
Formbricks provides an official Helm chart for deploying the entire cluster stack on Kubernetes. The Helm chart is available in the [Formbricks GitHub repository](https://github.com/formbricks/formbricks/tree/main/helm-chart).

View File

@@ -64,21 +64,21 @@ Make sure Docker and Docker Compose are installed on your system. These are usua
sed -i '' "s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $(openssl rand -hex 32)/" docker-compose.yml
```
1. **Generate Cron Secret**
1. **Generate Cron Secret**
You require a Cron secret to secure API access for running cron jobs. Run one of the commands below based on your operating system:
You require a Cron secret to secure API access for running cron jobs. Run one of the commands below based on your operating system:
For Linux:
For Linux:
```bash
sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
```bash
sed -i "/CRON_SECRET:$/s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
For macOS:
For macOS:
```bash
sed -i '' "s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
```bash
sed -i '' "s/CRON_SECRET:.*/CRON_SECRET: $(openssl rand -hex 32)/" docker-compose.yml
```
1. **Start the Docker Setup**

View File

@@ -4,9 +4,7 @@ description: "How to set up Formbricks instance with a one-click script"
icon: "rocket"
---
<Note>
This only works with an Ubuntu machine, so ensure the underlying OS is verified beforehand!
</Note>
<Note>This only works with an Ubuntu machine, so ensure the underlying OS is verified beforehand!</Note>
If youre looking to quickly set up a production instance of Formbricks on an Ubuntu server, this guide is for you. Using a convenient shell script, you can install everything—including Docker, Postgres DB, and an SSL certificate—in just a few steps. The script takes care of all the dependencies and configuration for your server, making the process smooth and simple.
@@ -269,7 +267,7 @@ Y
🚙 Updating docker-compose.yml with your custom inputs...
🚗 NEXTAUTH_SECRET updated successfully!
🚗 ENCRYPTION_KEY updated successfully!
🚗 CRON_SECRET updated successfully!
🚗 CRON_SECRET updated successfully!
[+] Running 4/4
✔ Network formbricks_default Created 0.2s
@@ -343,4 +341,4 @@ If you encounter any issues, consider the following steps:
* **Check Formbricks Logs**: Run `cd formbricks && docker compose logs` to check the logs of the Formbricks stack.
If you have any questions or require help, feel free to reach out to us on [**GitHub Discussions**](https://github.com/formbricks/formbricks/discussions). 😃[
](https://formbricks.com/docs/developer-docs/rest-api)
](https://formbricks.com/docs/developer-docs/rest-api)

View File

@@ -96,7 +96,7 @@ Retrieve them using:
```sh
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.NEXTAUTH_SECRET}" | base64 --decode
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.ENCRYPTION_KEY}" | base64 --decode
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.CRON_SECRET}" | base64 --decode
kubectl get secret --namespace {{ .Release.Namespace }} {{ include "formbricks.name" . }}-app-secrets -o jsonpath="{.data.CRON_SECRET}" | base64 --decode
```
---

View File

@@ -113,13 +113,13 @@ If `namespaceOverride` is provided, it will be used; otherwise, it defaults to `
{{- end -}}
{{- end }}
{{- define "formbricks.cronSecret" -}}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }}
{{- if $secret }}
{{- index $secret.data "CRON_SECRET" | b64dec -}}
{{- else }}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- define "formbricks.cronSecret" -}}
{{- $secret := (lookup "v1" "Secret" .Release.Namespace (printf "%s-app-secrets" (include "formbricks.name" .))) }}
{{- if $secret }}
{{- index $secret.data "CRON_SECRET" | b64dec -}}
{{- else }}
{{- randAlphaNum 32 -}}
{{- end -}}
{{- end }}
{{- define "formbricks.encryptionKey" -}}

Some files were not shown because too many files have changed in this diff Show More