From 57a64d7940d9e5de723f44c6b498f50ee3be6277 Mon Sep 17 00:00:00 2001 From: Pradumn Kumar <47187878+Pradumn27@users.noreply.github.com> Date: Tue, 11 Jul 2023 16:42:45 +0530 Subject: [PATCH] Survey Completed message shown to the enduser can now be customized (#464) * feat: added surveyClosedMessage field to database and also added it's types * feat: added the custom closed message to the frontend * fix: fixes build issue * fix: refactored the code to toggle surveyClosedMessage and redirectUrl * pnpm format * recreate prisma migration --------- Co-authored-by: Matthias Nannt --- .../[surveyId]/edit/ResponseOptionsCard.tsx | 116 ++++++++++++++++-- apps/web/app/s/[surveyId]/SurveyInactive.tsx | 27 +++- apps/web/app/s/[surveyId]/SurveyPage.tsx | 7 +- .../api/v1/client/surveys/[surveyId]/index.ts | 2 + .../duplicate/[targetEnvironmentId].ts | 2 + .../surveys/[surveyId]/duplicate/index.ts | 2 + .../surveys/[surveyId]/index.ts | 5 + packages/database/jsonTypes.ts | 3 +- .../migration.sql | 2 + packages/database/schema.prisma | 45 +++---- packages/database/zod-utils.ts | 2 +- packages/types/surveys.ts | 6 + packages/types/v1/surveys.ts | 10 ++ 13 files changed, 192 insertions(+), 37 deletions(-) create mode 100644 packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx index 8fd08ca7af..52d1737980 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/edit/ResponseOptionsCard.tsx @@ -18,20 +18,20 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res const [surveyCloseOnDateToggle, setSurveyCloseOnDateToggle] = useState(false); const [redirectUrl, setRedirectUrl] = useState(""); + const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false); + const [surveyClosedMessage, setSurveyClosedMessage] = useState({ + heading: "Survey Completed", + subheading: "This free & open-source survey has been closed", + }); const [closeOnDate, setCloseOnDate] = useState(); const handleRedirectCheckMark = () => { + setRedirectToggle((prev) => !prev); + if (redirectToggle && localSurvey.redirectUrl) { - setRedirectToggle(false); setRedirectUrl(null); setLocalSurvey({ ...localSurvey, redirectUrl: null }); - return; } - if (redirectToggle) { - setRedirectToggle(false); - return; - } - setRedirectToggle(true); }; const handleSurveyCloseOnDateToggle = () => { @@ -54,6 +54,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res setLocalSurvey({ ...localSurvey, redirectUrl: link }); }; + const handleCloseSurveyMessageToggle = () => { + setSurveyClosedMessageToggle((prev) => !prev); + + if (surveyClosedMessageToggle && localSurvey.surveyClosedMessage) { + setLocalSurvey({ ...localSurvey, surveyClosedMessage: null }); + } + }; + const handleCloseOnDateChange = (date: Date) => { const equivalentDate = date?.getDate(); date?.setUTCHours(0, 0, 0, 0); @@ -63,11 +71,36 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res setLocalSurvey({ ...localSurvey, closeOnDate: date ?? null }); }; + const handleClosedSurveyMessageChange = ({ + heading, + subheading, + }: { + heading?: string; + subheading?: string; + }) => { + const message = { + heading: heading ?? surveyClosedMessage.heading, + subheading: subheading ?? surveyClosedMessage.subheading, + }; + + setSurveyClosedMessage(message); + setLocalSurvey({ ...localSurvey, surveyClosedMessage: message }); + }; + useEffect(() => { if (localSurvey.redirectUrl) { setRedirectUrl(localSurvey.redirectUrl); setRedirectToggle(true); } + + if (!!localSurvey.surveyClosedMessage) { + setSurveyClosedMessage({ + heading: localSurvey.surveyClosedMessage.heading ?? surveyClosedMessage.heading, + subheading: localSurvey.surveyClosedMessage.subheading ?? surveyClosedMessage.subheading, + }); + setSurveyClosedMessageToggle(true); + } + if (localSurvey.closeOnDate) { setCloseOnDate(localSurvey.closeOnDate); setSurveyCloseOnDateToggle(true); @@ -144,6 +177,75 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res )} + {localSurvey.type === "link" && ( + <> +
+ + +
+ {redirectToggle && ( +
+ handleRedirectUrlChange(e.target.value)} + /> +
+ )} +
+ + +
+ {surveyClosedMessageToggle && ( +
+
+ +
+ handleClosedSurveyMessageChange({ heading: e.target.value })} + /> +
+
+
+ +
+ handleClosedSurveyMessageChange({ subheading: e.target.value })} + /> +
+
+
+ )} + + )}
diff --git a/apps/web/app/s/[surveyId]/SurveyInactive.tsx b/apps/web/app/s/[surveyId]/SurveyInactive.tsx index 1f807740eb..853d2b0860 100644 --- a/apps/web/app/s/[surveyId]/SurveyInactive.tsx +++ b/apps/web/app/s/[surveyId]/SurveyInactive.tsx @@ -5,7 +5,13 @@ import Image from "next/image"; import Link from "next/link"; import { Button } from "@formbricks/ui"; -const SurveyInactive = ({ status }) => { +const SurveyInactive = ({ + status, + surveyClosedMessage, +}: { + status: string; + surveyClosedMessage?: { heading: string; subheading: string }; +}) => { const icons = { "not found": , paused: , @@ -23,11 +29,20 @@ const SurveyInactive = ({ status }) => {
{icons[status]} -

Survey {status}.

-

{descriptions[status]}

- +

+ {status === "completed" && surveyClosedMessage ? surveyClosedMessage.heading : `Survey ${status}.`} +

+

+ {" "} + {status === "completed" && surveyClosedMessage + ? surveyClosedMessage.subheading + : descriptions[status]} +

+ {!(status === "completed" && surveyClosedMessage) && ( + + )}
diff --git a/apps/web/app/s/[surveyId]/SurveyPage.tsx b/apps/web/app/s/[surveyId]/SurveyPage.tsx index a4db190d57..b0b3cbd12e 100644 --- a/apps/web/app/s/[surveyId]/SurveyPage.tsx +++ b/apps/web/app/s/[surveyId]/SurveyPage.tsx @@ -26,7 +26,12 @@ export default function SurveyPage({ surveyId }: SurveyPageProps) { } if (isErrorSurvey && isErrorSurvey.status === 403) { - return ; + return ( + + ); } return ( diff --git a/apps/web/pages/api/v1/client/surveys/[surveyId]/index.ts b/apps/web/pages/api/v1/client/surveys/[surveyId]/index.ts index 5a09024b34..01269d2ac7 100644 --- a/apps/web/pages/api/v1/client/surveys/[surveyId]/index.ts +++ b/apps/web/pages/api/v1/client/surveys/[surveyId]/index.ts @@ -28,6 +28,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) environmentId: true, status: true, redirectUrl: true, + surveyClosedMessage: true, }, }); @@ -57,6 +58,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) reason: survey.status, brandColor: product?.brandColor, formbricksSignature: product?.formbricksSignature, + surveyClosedMessage: survey?.surveyClosedMessage, }); } diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/[targetEnvironmentId].ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/[targetEnvironmentId].ts index c7b4138ea6..5855418bf0 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/[targetEnvironmentId].ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/[targetEnvironmentId].ts @@ -1,5 +1,6 @@ import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; +import { Prisma as prismaClient } from "@prisma/client/"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -144,6 +145,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) id: targetEnvironmentId, }, }, + surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull, }, }); diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/index.ts index eb5e6ccd2b..a548ebcfaf 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/duplicate/index.ts @@ -1,5 +1,6 @@ import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; +import { Prisma as prismaClient } from "@prisma/client/"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -64,6 +65,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) id: environmentId, }, }, + surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull, }, }); diff --git a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/index.ts b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/index.ts index d79a375de8..9310d51264 100644 --- a/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/index.ts +++ b/apps/web/pages/api/v1/environments/[environmentId]/surveys/[surveyId]/index.ts @@ -1,6 +1,7 @@ import type { AttributeFilter } from "@formbricks/types/surveys"; import { hasEnvironmentAccess } from "@/lib/api/apiHelper"; import { prisma } from "@formbricks/database"; +import { Prisma as prismaClient } from "@prisma/client/"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handle(req: NextApiRequest, res: NextApiResponse) { @@ -87,6 +88,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) if (body.type === "link") { delete body.triggers; delete body.recontactDays; + // converts JSON field with null value to JsonNull as JSON fields can't be set to null since prisma 3.0 + if (!body.surveyClosedMessage) { + body.surveyClosedMessage = prismaClient.JsonNull; + } } if (body.triggers) { diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts index 1ec9b86da4..31b32b637f 100644 --- a/packages/database/jsonTypes.ts +++ b/packages/database/jsonTypes.ts @@ -1,6 +1,6 @@ import { TEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses"; import { TResponsePersonAttributes, TResponseData } from "@formbricks/types/v1/responses"; -import { TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys"; +import { TSurveyClosedMessage, TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys"; import { TUserNotificationSettings } from "@formbricks/types/v1/users"; declare global { @@ -12,6 +12,7 @@ declare global { export type ResponsePersonAttributes = TResponsePersonAttributes; export type SurveyQuestions = TSurveyQuestions; export type SurveyThankYouCard = TSurveyThankYouCard; + export type SurveyClosedMessage = TSurveyClosedMessage; export type UserNotificationSettings = TUserNotificationSettings; } } diff --git a/packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql b/packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql new file mode 100644 index 0000000000..d8dd0ce345 --- /dev/null +++ b/packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Survey" ADD COLUMN "surveyClosedMessage" JSONB; diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index f7e1a72902..741dbdd5b5 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -210,33 +210,36 @@ enum displayOptions { } model Survey { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @updatedAt @map(name: "updated_at") - name String - redirectUrl String? - type SurveyType @default(web) - environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) - environmentId String - status SurveyStatus @default(draft) + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @updatedAt @map(name: "updated_at") + name String + redirectUrl String? + type SurveyType @default(web) + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade) + environmentId String + status SurveyStatus @default(draft) /// @zod.custom(imports.ZSurveyQuestions) /// @zod.custom(imports.ZSurveyQuestions) /// [SurveyQuestions] - questions Json @default("[]") + questions Json @default("[]") /// @zod.custom(imports.ZSurveyThankYouCard) /// @zod.custom(imports.ZSurveyThankYouCard) /// [SurveyThankYouCard] - thankYouCard Json @default("{\"enabled\": false}") - responses Response[] - displayOption displayOptions @default(displayOnce) - recontactDays Int? - triggers SurveyTrigger[] - attributeFilters SurveyAttributeFilter[] - displays Display[] - autoClose Int? - delay Int @default(0) - autoComplete Int? - closeOnDate DateTime? + thankYouCard Json @default("{\"enabled\": false}") + responses Response[] + displayOption displayOptions @default(displayOnce) + recontactDays Int? + triggers SurveyTrigger[] + attributeFilters SurveyAttributeFilter[] + displays Display[] + autoClose Int? + delay Int @default(0) + autoComplete Int? + closeOnDate DateTime? + /// @zod.custom(imports.ZSurveyClosedMessage) + /// [SurveyClosedMessage] + surveyClosedMessage Json? } model Event { diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 1e17d99712..8764ab46d5 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -6,6 +6,6 @@ export { ZEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses"; export { ZResponseData, ZResponsePersonAttributes } from "@formbricks/types/v1/responses"; export const ZResponseMeta = z.record(z.union([z.string(), z.number()])); -export { ZSurveyQuestions, ZSurveyThankYouCard } from "@formbricks/types/v1/surveys"; +export { ZSurveyQuestions, ZSurveyThankYouCard, ZSurveyClosedMessage } from "@formbricks/types/v1/surveys"; export { ZUserNotificationSettings } from "@formbricks/types/v1/users"; diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts index 316d0250a7..826da7138b 100644 --- a/packages/types/surveys.ts +++ b/packages/types/surveys.ts @@ -6,6 +6,11 @@ export interface ThankYouCard { subheader?: string; } +export interface SurveyClosedMessage { + heading?: string; + subheading?: string; +} + export interface Survey { id: string; createdAt: string; @@ -26,6 +31,7 @@ export interface Survey { autoClose: number | null; delay: number; autoComplete: number | null; + surveyClosedMessage: SurveyClosedMessage | null; closeOnDate: Date | null; } diff --git a/packages/types/v1/surveys.ts b/packages/types/v1/surveys.ts index 52ddc9db3c..6de5d85dce 100644 --- a/packages/types/v1/surveys.ts +++ b/packages/types/v1/surveys.ts @@ -8,8 +8,17 @@ export const ZSurveyThankYouCard = z.object({ subheader: z.optional(z.string()), }); +export const ZSurveyClosedMessage = z + .object({ + heading: z.optional(z.string()), + subheading: z.optional(z.string()), + }) + .optional(); + export type TSurveyThankYouCard = z.infer; +export type TSurveyClosedMessage = z.infer; + export const ZSurveyChoice = z.object({ id: z.string(), label: z.string(), @@ -215,6 +224,7 @@ export const ZSurveyWithAnalytics = ZSurvey.extend({ numDisplays: z.number(), responseRate: z.number(), }), + surveyClosedMessage: ZSurveyClosedMessage, }); export type TSurveyWithAnalytics = z.infer;