From b114e4797e2e1d9668f5e03c169a53da6294fc76 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 16 Apr 2024 17:01:10 +0200 Subject: [PATCH 001/106] chore: use CUSTOM_CACHE_DISABLED env instead of vercel specific env (#2462) --- apps/web/next.config.mjs | 3 ++- turbo.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 85ab9c3f06..ea62d08964 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -26,7 +26,8 @@ const nextConfig = { "app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"], }, }, - cacheHandler: process.env.VERCEL !== "1" ? require.resolve("./cache-handler.mjs") : undefined, + cacheHandler: + process.env.CUSTOM_CACHE_DISABLED !== "1" ? require.resolve("./cache-handler.mjs") : undefined, transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"], images: { remotePatterns: [ diff --git a/turbo.json b/turbo.json index 68d4adfae4..60f79e9d01 100644 --- a/turbo.json +++ b/turbo.json @@ -66,6 +66,7 @@ "DEFAULT_TEAM_ROLE", "ONBOARDING_DISABLED", "CRON_SECRET", + "CUSTOM_CACHE_DISABLED", "CUSTOMER_IO_API_KEY", "CUSTOMER_IO_SITE_ID", "DEBUG", From 4b1652655ba89e3a4b0bcb3c10501ff8ab7f1110 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Tue, 16 Apr 2024 18:14:40 +0200 Subject: [PATCH 002/106] chore: move cacheHandler to a conditional import (#2463) --- apps/web/next.config.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index ea62d08964..743158fb20 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -26,8 +26,6 @@ const nextConfig = { "app/api/packages": ["../../packages/js-core/dist/*", "../../packages/surveys/dist/*"], }, }, - cacheHandler: - process.env.CUSTOM_CACHE_DISABLED !== "1" ? require.resolve("./cache-handler.mjs") : undefined, transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"], images: { remotePatterns: [ @@ -142,6 +140,11 @@ const nextConfig = { }, }; +// set custom cache handler +if (process.env.CUSTOM_CACHE_DISABLED !== "1") { + nextConfig.cacheHandler = require.resolve("./cache-handler.mjs"); +} + // set actions allowed origins if (process.env.WEBAPP_URL) { nextConfig.experimental.serverActions = { From 1bd9b8a485af35f2b28939a33f60058170e490f1 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:44:37 +0530 Subject: [PATCH 003/106] fix: single select logic issue (#2458) Co-authored-by: Matti Nannt --- .../[surveyId]/edit/components/MultipleChoiceSingleForm.tsx | 4 +++- packages/types/surveys.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm.tsx index 960ec1c31f..61412159f8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/MultipleChoiceSingleForm.tsx @@ -97,7 +97,9 @@ export const MultipleChoiceSingleForm = ({ question.logic?.forEach((logic) => { let newL: string | string[] | undefined = logic.value; if (Array.isArray(logic.value)) { - newL = logic.value.map((value) => (value === oldLabel ? newLabel : value)); + newL = logic.value.map((value) => + value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : value + ); } else { newL = logic.value === getLocalizedValue(oldLabel, selectedLanguageCode) ? newLabel : logic.value; } diff --git a/packages/types/surveys.ts b/packages/types/surveys.ts index 217567afa3..5c7e3cabb1 100644 --- a/packages/types/surveys.ts +++ b/packages/types/surveys.ts @@ -177,8 +177,8 @@ export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({ }); export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({ - condition: z.enum(["submitted", "skipped", "equals", "notEquals"]).optional(), - value: z.string().optional(), + condition: z.enum(["submitted", "skipped", "equals", "notEquals", "includesOne"]).optional(), + value: z.union([z.array(z.string()), z.string()]).optional(), }); export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({ From 74b4be99a4ad87ae01ccdd12d3b9b1064f010193 Mon Sep 17 00:00:00 2001 From: Piyush Gupta <56182734+gupta-piyush19@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:49:18 +0530 Subject: [PATCH 004/106] fix: single survey card rerenders on tag add or delete (#2461) Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com> --- .../(analysis)/responses/components/ResponsePage.tsx | 4 +++- .../[surveyId]/(analysis)/summary/components/SummaryPage.tsx | 4 +++- .../surveys/[surveyId]/components/CustomFilter.tsx | 4 +++- apps/web/app/s/[surveyId]/lib/prefilling.ts | 1 - 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx index afbd5a9c04..935bc5b26a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx @@ -67,7 +67,9 @@ const ResponsePage = ({ const filters = useMemo( () => getFormattedFilters(survey, selectedFilter, dateRange), - [survey, selectedFilter, dateRange] + + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedFilter, dateRange] ); const searchParams = useSearchParams(); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx index e278a7d6d9..ea9deb2caa 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage.tsx @@ -79,7 +79,9 @@ const SummaryPage = ({ const filters = useMemo( () => getFormattedFilters(survey, selectedFilter, dateRange), - [survey, selectedFilter, dateRange] + + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedFilter, dateRange] ); useEffect(() => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx index 797ba6bc46..b3797f1ec9 100755 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/CustomFilter.tsx @@ -89,7 +89,9 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => { const filters = useMemo( () => getFormattedFilters(survey, selectedFilter, dateRange), - [survey, selectedFilter, dateRange] + + // eslint-disable-next-line react-hooks/exhaustive-deps + [selectedFilter, dateRange] ); const datePickerRef = useRef(null); diff --git a/apps/web/app/s/[surveyId]/lib/prefilling.ts b/apps/web/app/s/[surveyId]/lib/prefilling.ts index cf7546f442..6d9a297652 100644 --- a/apps/web/app/s/[surveyId]/lib/prefilling.ts +++ b/apps/web/app/s/[surveyId]/lib/prefilling.ts @@ -33,7 +33,6 @@ export function getPrefillResponseData( } catch (error) { console.error(error); } - // eslint-disable-next-line react-hooks/exhaustive-deps } export const checkValidity = (question: TSurveyQuestion, answer: any, language: string): boolean => { From 4fc8ee8181816f5c85cd835af57a5f9b8a9c1c8a Mon Sep 17 00:00:00 2001 From: Gideon Mohr Date: Wed, 17 Apr 2024 10:26:14 +0200 Subject: [PATCH 005/106] fix: Pass only supported properties of `account` to `CreateAccount` (#2445) Co-authored-by: Shubham Palriwala --- packages/lib/account/service.ts | 4 +++- packages/lib/account/utils.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 packages/lib/account/utils.ts diff --git a/packages/lib/account/service.ts b/packages/lib/account/service.ts index 34e601e282..1846da4e2b 100644 --- a/packages/lib/account/service.ts +++ b/packages/lib/account/service.ts @@ -5,13 +5,15 @@ import { TAccount, TAccountInput, ZAccountInput } from "@formbricks/types/accoun import { DatabaseError } from "@formbricks/types/errors"; import { validateInputs } from "../utils/validate"; +import { filterAccountInputData } from "./utils"; export const createAccount = async (accountData: TAccountInput): Promise => { validateInputs([accountData, ZAccountInput]); try { + const supportedAccountData = filterAccountInputData(accountData); const account = await prisma.account.create({ - data: accountData, + data: supportedAccountData, }); return account; } catch (error) { diff --git a/packages/lib/account/utils.ts b/packages/lib/account/utils.ts new file mode 100644 index 0000000000..9aa946ea65 --- /dev/null +++ b/packages/lib/account/utils.ts @@ -0,0 +1,11 @@ +import { TAccountInput, ZAccountInput } from "@formbricks/types/account"; + +export const filterAccountInputData = (account: any) => { + const supportedProps = Object.keys(ZAccountInput.shape); + return supportedProps.reduce((acc, prop) => { + if (account.hasOwnProperty(prop)) { + acc[prop] = account[prop]; + } + return acc; + }, {} as TAccountInput); +}; From e8e701c5670f7aa397ab30dddc15925776ea3231 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:11:33 +0530 Subject: [PATCH 006/106] fix: The play() request was interrupted by a call to pause() (#2465) --- .../edit/components/AnimatedSurveyBg.tsx | 34 +++++-------------- packages/ui/TabBar/index.tsx | 3 +- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx index 0466652784..7bc8db333a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/AnimatedSurveyBg.tsx @@ -1,3 +1,4 @@ +import { debounce } from "lodash"; import { useState } from "react"; interface AnimatedSurveyBgProps { @@ -7,7 +8,6 @@ interface AnimatedSurveyBgProps { export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyBgProps) => { const [animation, setAnimation] = useState(background); - const [hoveredVideo, setHoveredVideo] = useState(null); const animationFiles = { "/animated-bgs/Thumbnails/1_Thumb.mp4": "/animated-bgs/4K/1_4k.mp4", @@ -42,31 +42,16 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB "/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4", }; - const handleMouseEnter = (index: number) => { - setHoveredVideo(index); - playVideo(index); - }; - - const handleMouseLeave = (index: number) => { - setHoveredVideo(null); - pauseVideo(index); - }; - - // Function to play the video - const playVideo = (index: number) => { + const togglePlayback = (index: number, type: "play" | "pause") => { const video = document.getElementById(`video-${index}`) as HTMLVideoElement; - if (video) { - video.play(); + try { + type === "play" ? video.play() : video.pause(); + } catch (error) { + console.error(error); } }; - // Function to pause the video - const pauseVideo = (index: number) => { - const video = document.getElementById(`video-${index}`) as HTMLVideoElement; - if (video) { - video.pause(); - } - }; + const debouncedManagePlayback = debounce(togglePlayback, 150); const handleBg = (x: string) => { setAnimation(x); @@ -81,14 +66,13 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB return (
handleMouseEnter(index)} - onMouseLeave={() => handleMouseLeave(index)} + onMouseEnter={() => debouncedManagePlayback(index, "play")} + onMouseLeave={() => debouncedManagePlayback(index, "pause")} onClick={() => handleBg(value)} className="relative cursor-pointer overflow-hidden rounded-lg"> diff --git a/packages/ui/TabBar/index.tsx b/packages/ui/TabBar/index.tsx index 772398ab77..b0cfddd272 100644 --- a/packages/ui/TabBar/index.tsx +++ b/packages/ui/TabBar/index.tsx @@ -44,9 +44,8 @@ export const TabBar: React.FC = ({ return (
- + <> + + ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/components/BulkInviteTab.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/BulkInviteTab.tsx new file mode 100644 index 0000000000..479605e07b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/BulkInviteTab.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { UploadIcon, XIcon } from "lucide-react"; +import Link from "next/link"; +import Papa, { type ParseResult } from "papaparse"; +import { useRef, useState } from "react"; +import toast from "react-hot-toast"; + +import { ZInvitees } from "@formbricks/types/invites"; +import { Alert, AlertDescription } from "@formbricks/ui/Alert"; +import { Button } from "@formbricks/ui/Button"; + +import { MembershipRole } from "./AddMemberModal"; + +interface BulkInviteTabProps { + setOpen: (v: boolean) => void; + onSubmit: (data: { name: string; email: string; role: MembershipRole }[]) => void; + canDoRoleManagement: boolean; +} + +export const BulkInviteTab = ({ setOpen, onSubmit, canDoRoleManagement }: BulkInviteTabProps) => { + const fileInputRef = useRef(null); + const [csvFile, setCSVFile] = useState(); + + const onFileInputChange = (e: React.ChangeEvent) => { + if (!e.target.files?.length) { + return; + } + const file = e.target.files[0]; + setCSVFile(file); + }; + + const onImport = () => { + if (!csvFile) { + return; + } + Papa.parse(csvFile, { + skipEmptyLines: true, + comments: "Full Name,Email Address,Role", + complete: (results: ParseResult) => { + const members = results.data.map((csv) => { + const [name, email, role] = csv; + + return { + name: name.trim(), + email: email.trim(), + role: canDoRoleManagement ? (role.trim().toLowerCase() as MembershipRole) : MembershipRole.Admin, + }; + }); + try { + ZInvitees.parse(members); + onSubmit(members); + } catch (err) { + console.error(err.message); + toast.error("Please check the CSV file and make sure it is according to our format"); + } + setOpen(false); + }, + }); + }; + + const removeFile = (event: React.MouseEvent) => { + event.stopPropagation(); + setCSVFile(undefined); + // Reset the file input value to ensure it can detect the same file if re-selected + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( +
+
fileInputRef.current?.click()}> + {csvFile ? ( + + ) : ( + + )} + {csvFile ? csvFile.name : "Click here to upload"} + +
+
+ {!canDoRoleManagement && ( + + +

+ Warning: Please note that on the Free Plan, all team members are + automatically assigned the "Admin" role regardless of the role specified in the CSV + file. +

+
+
+ )} +
+
+
+ + + + +
+
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/components/EditMemberships/TeamActions.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/EditMemberships/TeamActions.tsx index d37c135218..20e8fd5304 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/members/components/EditMemberships/TeamActions.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/EditMemberships/TeamActions.tsx @@ -10,7 +10,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; -import { TMembershipRole } from "@formbricks/types/memberships"; +import { TInvitee } from "@formbricks/types/invites"; import { TTeam } from "@formbricks/types/teams"; import { Button } from "@formbricks/ui/Button"; import CreateTeamModal from "@formbricks/ui/CreateTeamModal"; @@ -57,9 +57,13 @@ export default function TeamActions({ } }; - const handleAddMember = async (data: { name: string; email: string; role: TMembershipRole }) => { + const handleAddMembers = async (data: TInvitee[]) => { try { - await inviteUserAction(team.id, data.email, data.name, data.role); + await Promise.all( + data.map(async ({ name, email, role }) => { + await inviteUserAction(team.id, email, name, role); + }) + ); toast.success("Member invited successfully"); } catch (err) { toast.error(`Error: ${err.message}`); @@ -102,7 +106,7 @@ export default function TeamActions({ ({ + } = useForm({ defaultValues: { name: team.name, }, @@ -47,7 +47,7 @@ export default function EditTeamName({ team, membershipRole }: TEditTeamNameProp const currentTeamName = teamName?.trim().toLowerCase() ?? ""; const previousTeamName = team?.name?.trim().toLowerCase() ?? ""; - const handleUpdateTeamName: SubmitHandler = async (data) => { + const handleUpdateTeamName: SubmitHandler = async (data) => { try { data.name = data.name.trim(); setIsUpdatingTeam(true); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/members/components/IndividualInviteTab.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/IndividualInviteTab.tsx new file mode 100644 index 0000000000..5b43ed94e6 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/settings/members/components/IndividualInviteTab.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useForm } from "react-hook-form"; + +import { AddMemberRole } from "@formbricks/ee/RoleManagement/components/AddMemberRole"; +import { Button } from "@formbricks/ui/Button"; +import { Input } from "@formbricks/ui/Input"; +import { Label } from "@formbricks/ui/Label"; +import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice"; + +import { MembershipRole } from "./AddMemberModal"; + +interface IndividualInviteTabProps { + setOpen: (v: boolean) => void; + onSubmit: (data: { name: string; email: string; role: MembershipRole }[]) => void; + canDoRoleManagement: boolean; + isFormbricksCloud: boolean; + environmentId: string; +} +export const IndividualInviteTab = ({ + setOpen, + onSubmit, + canDoRoleManagement, + isFormbricksCloud, + environmentId, +}: IndividualInviteTabProps) => { + const { register, getValues, handleSubmit, reset, control } = useForm<{ + name: string; + email: string; + role: MembershipRole; + }>(); + + const submitEventClass = async () => { + const data = getValues(); + data.role = data.role || MembershipRole.Admin; + onSubmit([data]); + setOpen(false); + reset(); + }; + return ( +
+
+
+
+ + value.trim() !== "" })} + /> +
+
+ + +
+
+ + {!canDoRoleManagement && + (isFormbricksCloud ? ( + + ) : ( + + ))} +
+
+
+
+
+ + +
+
+
+ ); +}; diff --git a/apps/web/package.json b/apps/web/package.json index 7dc15754c9..76a4171ecb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -49,6 +49,7 @@ "next": "14.2.1", "nodemailer": "^6.9.13", "otplib": "^12.0.1", + "papaparse": "^5.4.1", "posthog-js": "^1.125.0", "prismjs": "^1.29.0", "qrcode": "^1.5.3", @@ -71,6 +72,7 @@ "@types/bcryptjs": "^2.4.6", "@types/lodash": "^4.17.0", "@types/markdown-it": "^14.0.1", + "@types/papaparse": "^5.3.14", "@types/qrcode": "^1.5.5", "eslint-config-formbricks": "workspace:*" } diff --git a/apps/web/public/sample-csv/formbricks-team-members-template.csv b/apps/web/public/sample-csv/formbricks-team-members-template.csv new file mode 100644 index 0000000000..2d53dadf2a --- /dev/null +++ b/apps/web/public/sample-csv/formbricks-team-members-template.csv @@ -0,0 +1,5 @@ +Full Name,Email Address,Role +John Doe,john@example.com,Admin +Jane Doe,jane@example.com,Developer +Janice Doe,janice@example.com,Editor +Jhonny Doe,jhonny@example.com,Viewer \ No newline at end of file diff --git a/packages/types/integration/airtable.ts b/packages/types/integration/airtable.ts index 2b6cb849b7..c14f22a967 100644 --- a/packages/types/integration/airtable.ts +++ b/packages/types/integration/airtable.ts @@ -23,7 +23,7 @@ export type TIntegrationAirtableConfigData = z.infer; diff --git a/packages/types/integration/googleSheet.ts b/packages/types/integration/googleSheet.ts index 86d05f79f8..6393ef802f 100644 --- a/packages/types/integration/googleSheet.ts +++ b/packages/types/integration/googleSheet.ts @@ -24,7 +24,7 @@ export type TIntegrationGoogleSheetsConfigData = z.infer; diff --git a/packages/types/integration/notion.ts b/packages/types/integration/notion.ts index 9e41bdfc61..6776a8b125 100644 --- a/packages/types/integration/notion.ts +++ b/packages/types/integration/notion.ts @@ -17,7 +17,7 @@ export const ZIntegrationNotionCredential = z.object({ type: z.string(), object: z.string(), person: z.object({ - email: z.string(), + email: z.string().email(), }), avatar_url: z.string(), }) diff --git a/packages/types/invites.ts b/packages/types/invites.ts index 0172feaec1..7c9834f1cd 100644 --- a/packages/types/invites.ts +++ b/packages/types/invites.ts @@ -4,7 +4,7 @@ import { ZMembershipRole } from "./memberships"; export const ZInvite = z.object({ id: z.string(), - email: z.string(), + email: z.string().email(), name: z.string().nullish(), teamId: z.string(), creatorId: z.string(), @@ -17,12 +17,14 @@ export const ZInvite = z.object({ export type TInvite = z.infer; export const ZInvitee = z.object({ - email: z.string(), - name: z.string().nullable(), + email: z.string().email(), + name: z.string(), role: ZMembershipRole, }); export type TInvitee = z.infer; +export const ZInvitees = z.array(ZInvitee); + export const ZCurrentUser = z.object({ id: z.string(), name: z.string().nullable(), diff --git a/packages/types/memberships.ts b/packages/types/memberships.ts index d28595e8d7..c1259a88df 100644 --- a/packages/types/memberships.ts +++ b/packages/types/memberships.ts @@ -15,7 +15,7 @@ export type TMembership = z.infer; export const ZMember = z.object({ name: z.string().nullable(), - email: z.string(), + email: z.string().email(), userId: z.string(), accepted: z.boolean(), role: ZMembershipRole, diff --git a/packages/types/user.ts b/packages/types/user.ts index 8180ab32f8..f5af773270 100644 --- a/packages/types/user.ts +++ b/packages/types/user.ts @@ -24,7 +24,7 @@ export type TUserNotificationSettings = z.infer; export const ZUserUpdateInput = z.object({ name: z.string().nullish(), - email: z.string().optional(), + email: z.string().email().optional(), emailVerified: z.date().nullish(), onboardingCompleted: z.boolean().optional(), role: ZRole.optional(), @@ -54,7 +54,7 @@ export type TUserUpdateInput = z.infer; export const ZUserCreateInput = z.object({ name: z.string().optional(), - email: z.string(), + email: z.string().email(), emailVerified: z.date().optional(), onboardingCompleted: z.boolean().optional(), role: ZRole.optional(), diff --git a/packages/ui/ModalWithTabs/index.tsx b/packages/ui/ModalWithTabs/index.tsx index 199f3aea67..06e1b80bb8 100644 --- a/packages/ui/ModalWithTabs/index.tsx +++ b/packages/ui/ModalWithTabs/index.tsx @@ -12,12 +12,12 @@ interface ModalWithTabsProps { closeOnOutsideClick?: boolean; } -type TabProps = { +interface TabProps { title: string; children: React.ReactNode; -}; +} -export default function ModalWithTabs({ +export const ModalWithTabs = ({ open, setOpen, tabs, @@ -25,7 +25,7 @@ export default function ModalWithTabs({ label, description, closeOnOutsideClick, -}: ModalWithTabsProps) { +}: ModalWithTabsProps) => { const [activeTab, setActiveTab] = useState(0); const handleTabClick = (index: number) => { @@ -52,7 +52,7 @@ export default function ModalWithTabs({ -
+
{tabs.map((tab, index) => (
@@ -213,15 +212,3 @@ export const ThemeStylingPreviewSurvey = ({
); }; - -const ResetProgressButton = ({ resetQuestionProgress }: { resetQuestionProgress: () => void }) => { - return ( - - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx index b47d201fa5..04aa16e814 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs.tsx @@ -4,11 +4,11 @@ import { useMemo } from "react"; import { cn } from "@formbricks/lib/cn"; import { TSurveyEditorTabs } from "@formbricks/types/surveys"; -type Tab = { +interface Tab { id: TSurveyEditorTabs; label: string; icon: JSX.Element; -}; +} const tabs: Tab[] = [ { @@ -34,11 +34,11 @@ interface QuestionsAudienceTabsProps { isStylingTabVisible?: boolean; } -export default function QuestionsAudienceTabs({ +export const QuestionsAudienceTabs = ({ activeId, setActiveId, isStylingTabVisible, -}: QuestionsAudienceTabsProps) { +}: QuestionsAudienceTabsProps) => { const tabsComputed = useMemo(() => { if (isStylingTabVisible) { return tabs; @@ -68,4 +68,4 @@ export default function QuestionsAudienceTabs({ ); -} +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx index 7004a0af4f..665b421b5a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView.tsx @@ -37,7 +37,7 @@ interface QuestionsViewProps { isFormbricksCloud: boolean; } -export default function QuestionsView({ +export const QuestionsView = ({ activeQuestionId, setActiveQuestionId, localSurvey, @@ -49,7 +49,7 @@ export default function QuestionsView({ selectedLanguageCode, isMultiLanguageAllowed, isFormbricksCloud, -}: QuestionsViewProps) { +}: QuestionsViewProps) => { const internalQuestionIdMap = useMemo(() => { return localSurvey.questions.reduce((acc, question) => { acc[question.id] = createId(); @@ -362,4 +362,4 @@ export default function QuestionsView({ ); -} +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx index 931b29e0c8..847a34f495 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView.tsx @@ -27,7 +27,7 @@ interface SettingsViewProps { isFormbricksCloud: boolean; } -export default function SettingsView({ +export const SettingsView = ({ environment, localSurvey, setLocalSurvey, @@ -38,7 +38,7 @@ export default function SettingsView({ membershipRole, isUserTargetingAllowed = false, isFormbricksCloud, -}: SettingsViewProps) { +}: SettingsViewProps) => { return (
@@ -98,4 +98,4 @@ export default function SettingsView({ )}
); -} +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx index 3ffc6656df..d9cfc85dad 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyEditor.tsx @@ -2,7 +2,12 @@ import { refetchProduct } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions"; import { LoadingSkeleton } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/LoadingSkeleton"; +import { QuestionsAudienceTabs } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsStylingSettingsTabs"; +import { QuestionsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionsView"; +import { SettingsView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SettingsView"; import { StylingView } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/StylingView"; +import { SurveyMenuBar } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar"; +import { PreviewSurvey } from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurvey"; import { useCallback, useEffect, useRef, useState } from "react"; import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions"; @@ -16,12 +21,6 @@ import { TProduct } from "@formbricks/types/product"; import { TSegment } from "@formbricks/types/segment"; import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys"; -import PreviewSurvey from "../../../components/PreviewSurvey"; -import QuestionsAudienceTabs from "./QuestionsStylingSettingsTabs"; -import QuestionsView from "./QuestionsView"; -import SettingsView from "./SettingsView"; -import SurveyMenuBar from "./SurveyMenuBar"; - interface SurveyEditorProps { survey: TSurvey; product: TProduct; @@ -232,8 +231,7 @@ export default function SurveyEditor({