From 45ad054c0dc1e7e7fb66ad9a0ce8fbda7dc227d0 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Sat, 2 Mar 2024 16:13:43 +0530 Subject: [PATCH 01/12] Add UI to select members to receive emails in routing form --- .../web/components/eventtype/EventTeamTab.tsx | 10 +++-- apps/web/public/static/locales/en/common.json | 1 + .../routing-forms/components/SingleForm.tsx | 41 +++++++++++++++++-- .../viewer/teams/listMembers.handler.ts | 2 + 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/apps/web/components/eventtype/EventTeamTab.tsx b/apps/web/components/eventtype/EventTeamTab.tsx index 3f960308937258..0e6a73444f19e9 100644 --- a/apps/web/components/eventtype/EventTeamTab.tsx +++ b/apps/web/components/eventtype/EventTeamTab.tsx @@ -303,7 +303,7 @@ const FixedHosts = ({ ); }; -const AddMembersWithSwitch = ({ +export const AddMembersWithSwitch = ({ teamMembers, value, onChange, @@ -312,6 +312,8 @@ const AddMembersWithSwitch = ({ automaticAddAllEnabled, onActive, isFixed, + placeholder = "", + containerClassName = "", }: { value: Host[]; onChange: (hosts: Host[]) => void; @@ -321,13 +323,15 @@ const AddMembersWithSwitch = ({ automaticAddAllEnabled: boolean; onActive: () => void; isFixed: boolean; + placeholder?: string; + containerClassName?: string; }) => { const { t } = useLocale(); const { setValue } = useFormContext(); return (
-
+
{automaticAddAllEnabled ? (
) : ( <> diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 039908543e21a6..d3852468b6c74e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2266,5 +2266,6 @@ "field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect", "field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})", "account_already_linked": "Account is already linked", + "select_members": "Select members", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index d46f0e109ce259..6cf5d2454b7752 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -2,7 +2,7 @@ import type { App_RoutingForms_Form, Team } from "@prisma/client"; import Link from "next/link"; import { useEffect, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; -import { Controller, useFormContext } from "react-hook-form"; +import { useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import { ShellMain } from "@calcom/features/shell/Shell"; @@ -22,7 +22,6 @@ import { DropdownMenuSeparator, Form, Meta, - SettingsToggle, showToast, TextAreaField, TextField, @@ -37,6 +36,7 @@ import { Trash, MessageCircle, } from "@calcom/ui/components/icon"; +import { AddMembersWithSwitch } from "@calcom/web/components/eventtype/EventTeamTab"; import { RoutingPages } from "../lib/RoutingPages"; import { isFallbackRoute } from "../lib/isFallbackRoute"; @@ -238,11 +238,14 @@ type SingleFormComponentProps = { function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { const utils = trpc.useContext(); const { t } = useLocale(); + const { data: teamMembers } = trpc.viewer.teams.listMembers.useQuery({}); const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); const [response, setResponse] = useState({}); const [decidedAction, setDecidedAction] = useState(null); const [skipFirstUpdate, setSkipFirstUpdate] = useState(true); + const [selectedMembers, setSelectedMembers] = useState([]); + const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(false); function testRouting() { const action = processRoute({ form, response }); @@ -339,7 +342,37 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { />
- ({ + value: member.id.toString(), + label: member.name, + avatar: member.avatarUrl, + email: member.email, + isFixed: true, + }))} + value={selectedMembers} + onChange={(value: any) => { + console.log("onChange", value); + }} + assignAllTeamMembers={assignAllTeamMembers} + setAssignAllTeamMembers={setAssignAllTeamMembers} + automaticAddAllEnabled={true} + isFixed={true} + onActive={() => + hookForm.setValue( + "settings.sendUpdatesTo", + (teamMembers || []).map((teamMember) => ({ + isFixed: true, + userId: teamMember.id, + priority: 2, + })), + { shouldDirty: true } + ) + } + placeholder={t("select_members")} + containerClassName="!px-0 !pb-0 !pt-0" + /> + {/* { @@ -352,7 +385,7 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { /> ); }} - /> + /> */}
{form.routers.length ? ( diff --git a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts index 1c12a4ec4dcfb0..87f18de3ae5fb0 100644 --- a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts @@ -36,6 +36,8 @@ export const listMembersHandler = async ({ ctx, input }: ListMembersOptions) => id: true, name: true, username: true, + email: true, + avatarUrl: true, }, }, accepted: true, From e49a9b10546c2c1c757ae3853231c6b8ecf9c1c0 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Sun, 3 Mar 2024 18:24:18 +0530 Subject: [PATCH 02/12] Add backend changes to support frontend to select team members to get routing form updates to --- .../routing-forms/components/SingleForm.tsx | 123 +++++++++++------- .../trpc/formMutation.handler.ts | 22 +++- .../routing-forms/trpc/response.handler.ts | 24 +++- .../app-store/routing-forms/trpc/utils.ts | 18 ++- packages/prisma/zod-utils.ts | 1 + 5 files changed, 136 insertions(+), 52 deletions(-) diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 6cf5d2454b7752..f84daa594f7e39 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -2,7 +2,7 @@ import type { App_RoutingForms_Form, Team } from "@prisma/client"; import Link from "next/link"; import { useEffect, useState } from "react"; import type { UseFormReturn } from "react-hook-form"; -import { useFormContext } from "react-hook-form"; +import { Controller, useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import { ShellMain } from "@calcom/features/shell/Shell"; @@ -22,6 +22,7 @@ import { DropdownMenuSeparator, Form, Meta, + SettingsToggle, showToast, TextAreaField, TextField, @@ -37,6 +38,7 @@ import { MessageCircle, } from "@calcom/ui/components/icon"; import { AddMembersWithSwitch } from "@calcom/web/components/eventtype/EventTeamTab"; +import type { Host } from "@calcom/web/pages/event-types/[type]"; import { RoutingPages } from "../lib/RoutingPages"; import { isFallbackRoute } from "../lib/isFallbackRoute"; @@ -244,7 +246,7 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { const [response, setResponse] = useState({}); const [decidedAction, setDecidedAction] = useState(null); const [skipFirstUpdate, setSkipFirstUpdate] = useState(true); - const [selectedMembers, setSelectedMembers] = useState([]); + const [selectedMembers, setSelectedMembers] = useState([]); const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(false); function testRouting() { @@ -279,6 +281,26 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [form]); + useEffect(() => { + if (form.teamId && form.settings?.sendUpdatesTo?.length && teamMembers?.length) { + let sendToAll = true; + teamMembers.forEach((member) => { + if (!form.settings?.sendUpdatesTo?.includes(member.id)) { + sendToAll = false; + return; + } + }); + setAssignAllTeamMembers(sendToAll); + setSelectedMembers( + form.settings.sendUpdatesTo.map((userId) => ({ + isFixed: true, + userId: userId, + priority: 1, + })) + ); + } + }, [form.teamId, form.settings?.sendUpdatesTo?.length, teamMembers?.length]); + const mutation = trpc.viewer.appRoutingForms.formMutation.useMutation({ onSuccess() { showToast(t("form_updated_successfully"), "success"); @@ -342,50 +364,59 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { />
- ({ - value: member.id.toString(), - label: member.name, - avatar: member.avatarUrl, - email: member.email, - isFixed: true, - }))} - value={selectedMembers} - onChange={(value: any) => { - console.log("onChange", value); - }} - assignAllTeamMembers={assignAllTeamMembers} - setAssignAllTeamMembers={setAssignAllTeamMembers} - automaticAddAllEnabled={true} - isFixed={true} - onActive={() => - hookForm.setValue( - "settings.sendUpdatesTo", - (teamMembers || []).map((teamMember) => ({ - isFixed: true, - userId: teamMember.id, - priority: 2, - })), - { shouldDirty: true } - ) - } - placeholder={t("select_members")} - containerClassName="!px-0 !pb-0 !pt-0" - /> - {/* { - return ( - onChange(val)} - /> - ); - }} - /> */} + {form.teamId ? ( + ({ + value: member.id.toString(), + label: member.name || "", + avatar: member.avatarUrl || "", + email: member.email, + isFixed: true, + }))} + value={selectedMembers} + onChange={(value) => { + setSelectedMembers(value); + hookForm.setValue( + "settings.sendUpdatesTo", + value.map((teamMember) => teamMember.userId), + { shouldDirty: true } + ); + hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true }); + }} + assignAllTeamMembers={assignAllTeamMembers} + setAssignAllTeamMembers={setAssignAllTeamMembers} + automaticAddAllEnabled={true} + isFixed={true} + onActive={() => { + hookForm.setValue( + "settings.sendUpdatesTo", + (teamMembers || []).map((teamMember) => teamMember.id), + { shouldDirty: true } + ); + hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true }); + }} + placeholder={t("select_members")} + containerClassName="!px-0 !pb-0 !pt-0" + /> + ) : ( + { + return ( + { + onChange(val); + hookForm.unregister("settings.sendUpdatesTo"); + }} + /> + ); + }} + /> + )}
{form.routers.length ? ( diff --git a/packages/app-store/routing-forms/trpc/formMutation.handler.ts b/packages/app-store/routing-forms/trpc/formMutation.handler.ts index 951a1076389c5e..1ae0b6a7d2541c 100644 --- a/packages/app-store/routing-forms/trpc/formMutation.handler.ts +++ b/packages/app-store/routing-forms/trpc/formMutation.handler.ts @@ -25,8 +25,9 @@ interface FormMutationHandlerOptions { } export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOptions) => { const { user, prisma } = ctx; - const { name, id, description, settings, disabled, addFallback, duplicateFrom, shouldConnect } = input; + const { name, id, description, disabled, addFallback, duplicateFrom, shouldConnect } = input; let teamId = input.teamId; + const settings = input.settings; if (!(await isFormCreateEditAllowed({ userId: user.id, formId: id, targetTeamId: teamId }))) { throw new TRPCError({ code: "FORBIDDEN", @@ -90,6 +91,25 @@ export const formMutationHandler = async ({ ctx, input }: FormMutationHandlerOpt } } + // Validate the users passed + if (teamId && settings?.sendUpdatesTo?.length) { + const sendUpdatesTo = await prisma.membership.findMany({ + where: { + teamId, + userId: { + in: settings.sendUpdatesTo, + }, + }, + select: { + userId: true, + }, + }); + settings.sendUpdatesTo = sendUpdatesTo.map((member) => member.userId); + // If its not a team, the user is sending the value, we will just ignore it + } else if (!teamId && settings?.sendUpdatesTo) { + delete settings.sendUpdatesTo; + } + return await prisma.app_RoutingForms_Form.upsert({ where: { id: id, diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index 488e29da7e34b6..15318613acf3a2 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -46,7 +46,7 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => }); } - const serializableFormWithFields = { + let serializableFormWithFields = { ...serializableForm, fields: serializableForm.fields, }; @@ -93,6 +93,28 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => data: input, }); + if (form.teamId && form.settings?.sendUpdatesTo?.length) { + const userEmails = await prisma.membership.findMany({ + where: { + teamId: form.teamId, + userId: { + in: form.settings.sendUpdatesTo, + }, + }, + include: { + user: { + select: { + email: true, + }, + }, + }, + }); + serializableFormWithFields = { + ...serializableFormWithFields, + userEmails: userEmails.map((userEmail) => userEmail.user.email), + }; + } + await onFormSubmission(serializableFormWithFields, dbFormResponse.response as Response); return dbFormResponse; } catch (e) { diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index e349ceb5923544..c062f61d0723d1 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -10,7 +10,10 @@ import type { OrderedResponses } from "../types/types"; import type { Response, SerializableForm } from "../types/types"; export async function onFormSubmission( - form: Ensure & { user: Pick }, "fields">, + form: Ensure< + SerializableForm & { user: Pick; userEmails?: string[] }, + "fields" + >, response: Response ) { const fieldResponsesByName: Record< @@ -71,19 +74,26 @@ export async function onFormSubmission( logger.debug( `Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}` ); - await sendResponseEmail(form, orderedResponses, form.user.email); + await sendResponseEmail(form, orderedResponses, [form.user.email]); + } else if (form.userEmails?.length) { + logger.debug( + `Preparing to send Form Response email for Form:${ + form.id + } to users with ids: ${form.settings.sendUpdatesTo.join(",")}` + ); + await sendResponseEmail(form, orderedResponses, form.userEmails); } } export const sendResponseEmail = async ( form: Pick, orderedResponses: OrderedResponses, - ownerEmail: string + toAddresses: string[] ) => { try { if (typeof window === "undefined") { const { default: ResponseEmail } = await import("../emails/templates/response-email"); - const email = new ResponseEmail({ form: form, toAddresses: [ownerEmail], orderedResponses }); + const email = new ResponseEmail({ form: form, toAddresses, orderedResponses }); await email.sendEmail(); } } catch (e) { diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 6847e20a9173f0..8ed846364d8c19 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -440,6 +440,7 @@ export const successRedirectUrl = z export const RoutingFormSettings = z .object({ emailOwnerOnSubmission: z.boolean(), + sendUpdatesTo: z.array(z.number()).optional(), }) .nullable(); From 23880dd1f0af0399d9ec5860ee4f88445cdd8650 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Mon, 4 Mar 2024 12:25:33 +0530 Subject: [PATCH 03/12] Fix type error --- .../routing-forms/trpc/response.handler.ts | 19 +++++++++++-------- .../app-store/routing-forms/trpc/utils.ts | 10 ++++------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index 15318613acf3a2..be13c45adf08df 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -2,6 +2,7 @@ import { Prisma } from "@prisma/client"; import { z } from "zod"; import type { PrismaClient } from "@calcom/prisma"; +import { RoutingFormSettings } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@calcom/trpc/server"; import { getSerializableForm } from "../lib/getSerializableForm"; @@ -46,7 +47,7 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => }); } - let serializableFormWithFields = { + const serializableFormWithFields = { ...serializableForm, fields: serializableForm.fields, }; @@ -93,12 +94,14 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => data: input, }); - if (form.teamId && form.settings?.sendUpdatesTo?.length) { + const settings = RoutingFormSettings.parse(form.settings); + let userWithEmails: string[] = []; + if (form.teamId && settings?.sendUpdatesTo?.length) { const userEmails = await prisma.membership.findMany({ where: { teamId: form.teamId, userId: { - in: form.settings.sendUpdatesTo, + in: settings.sendUpdatesTo, }, }, include: { @@ -109,13 +112,13 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => }, }, }); - serializableFormWithFields = { - ...serializableFormWithFields, - userEmails: userEmails.map((userEmail) => userEmail.user.email), - }; + userWithEmails = userEmails.map((userEmail) => userEmail.user.email); } - await onFormSubmission(serializableFormWithFields, dbFormResponse.response as Response); + await onFormSubmission( + { ...serializableFormWithFields, userWithEmails }, + dbFormResponse.response as Response + ); return dbFormResponse; } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) { diff --git a/packages/app-store/routing-forms/trpc/utils.ts b/packages/app-store/routing-forms/trpc/utils.ts index c062f61d0723d1..4cc3a4d93dc01a 100644 --- a/packages/app-store/routing-forms/trpc/utils.ts +++ b/packages/app-store/routing-forms/trpc/utils.ts @@ -11,7 +11,7 @@ import type { Response, SerializableForm } from "../types/types"; export async function onFormSubmission( form: Ensure< - SerializableForm & { user: Pick; userEmails?: string[] }, + SerializableForm & { user: Pick; userWithEmails?: string[] }, "fields" >, response: Response @@ -75,13 +75,11 @@ export async function onFormSubmission( `Preparing to send Form Response email for Form:${form.id} to form owner: ${form.user.email}` ); await sendResponseEmail(form, orderedResponses, [form.user.email]); - } else if (form.userEmails?.length) { + } else if (form.userWithEmails?.length) { logger.debug( - `Preparing to send Form Response email for Form:${ - form.id - } to users with ids: ${form.settings.sendUpdatesTo.join(",")}` + `Preparing to send Form Response email for Form:${form.id} to users: ${form.userWithEmails.join(",")}` ); - await sendResponseEmail(form, orderedResponses, form.userEmails); + await sendResponseEmail(form, orderedResponses, form.userWithEmails); } } From 6b3981654070045aab6f13a02bf20115d444bcb5 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Mon, 4 Mar 2024 14:01:18 +0530 Subject: [PATCH 04/12] Fix all the members of all the teams the user is part of shown instead of only the current team where the routing form is created --- packages/app-store/routing-forms/components/SingleForm.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index f84daa594f7e39..680f04fe0cc046 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -240,7 +240,9 @@ type SingleFormComponentProps = { function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { const utils = trpc.useContext(); const { t } = useLocale(); - const { data: teamMembers } = trpc.viewer.teams.listMembers.useQuery({}); + const { data: teamMembers } = form.teamId + ? trpc.viewer.teams.listMembers.useQuery({ teamIds: [form.teamId] }) + : null; const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); const [response, setResponse] = useState({}); From 9b852c84fbe0b041ef067fc1bc550a05bee2e682 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Tue, 5 Mar 2024 08:55:30 +0530 Subject: [PATCH 05/12] Fix type error --- packages/app-store/routing-forms/components/SingleForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 680f04fe0cc046..ef0401a7402abd 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -242,7 +242,7 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { const { t } = useLocale(); const { data: teamMembers } = form.teamId ? trpc.viewer.teams.listMembers.useQuery({ teamIds: [form.teamId] }) - : null; + : { data: [] }; const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); const [response, setResponse] = useState({}); From b68a4ab51ca58db6170c4c6d0f3da8baab99cac3 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Tue, 12 Mar 2024 11:51:58 +0530 Subject: [PATCH 06/12] Refactor components to a shared folder --- .../web/components/eventtype/EventTeamTab.tsx | 185 +----------------- .../routing-forms/components/SingleForm.tsx | 4 +- .../components/AddMembersWithSwitch.tsx | 149 ++++++++++++++ .../components/AssignAllTeamMembers.tsx | 45 +++++ .../components/CheckedTeamSelect.tsx | 3 +- packages/features/eventtypes/lib/types.ts | 6 + 6 files changed, 209 insertions(+), 183 deletions(-) create mode 100644 packages/features/eventtypes/components/AddMembersWithSwitch.tsx create mode 100644 packages/features/eventtypes/components/AssignAllTeamMembers.tsx diff --git a/apps/web/components/eventtype/EventTeamTab.tsx b/apps/web/components/eventtype/EventTeamTab.tsx index 26fbec75ef373f..f76603cf3a5d11 100644 --- a/apps/web/components/eventtype/EventTeamTab.tsx +++ b/apps/web/components/eventtype/EventTeamTab.tsx @@ -6,36 +6,16 @@ import type { ComponentProps, Dispatch, SetStateAction } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; import type { Options } from "react-select"; -import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect"; -import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect"; +import AddMembersWithSwitch, { + mapUserToValue, +} from "@calcom/features/eventtypes/components/AddMembersWithSwitch"; +import AssignAllTeamMembers from "@calcom/features/eventtypes/components/AssignAllTeamMembers"; import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect"; -import type { FormValues } from "@calcom/features/eventtypes/lib/types"; +import type { FormValues, TeamMember } from "@calcom/features/eventtypes/lib/types"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { SchedulingType } from "@calcom/prisma/enums"; import { Label, Select, SettingsToggle } from "@calcom/ui"; -interface IUserToValue { - id: number | null; - name: string | null; - username: string | null; - avatar: string; - email: string; -} - -type TeamMember = { - value: string; - label: string; - avatar: string; - email: string; -}; - -const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({ - value: `${id || ""}`, - label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`, - avatar, - email, -}); - export const mapMemberToChildrenOption = ( member: EventTypeSetupProps["teamMembers"][number], slug: string, @@ -59,16 +39,6 @@ export const mapMemberToChildrenOption = ( }; }; -const sortByLabel = (a: ReturnType, b: ReturnType) => { - if (a.label < b.label) { - return -1; - } - if (a.label > b.label) { - return 1; - } - return 0; -}; - const ChildrenEventTypesList = ({ options = [], value, @@ -105,94 +75,6 @@ const ChildrenEventTypesList = ({ ); }; -const AssignAllTeamMembers = ({ - assignAllTeamMembers, - setAssignAllTeamMembers, - onActive, - onInactive, -}: { - assignAllTeamMembers: boolean; - setAssignAllTeamMembers: Dispatch>; - onActive: () => void; - onInactive?: () => void; -}) => { - const { t } = useLocale(); - const { setValue } = useFormContext(); - - return ( - - name="assignAllTeamMembers" - render={() => ( - { - setValue("assignAllTeamMembers", active, { shouldDirty: true }); - setAssignAllTeamMembers(active); - if (active) { - onActive(); - } else if (!!onInactive) { - onInactive(); - } - }} - /> - )} - /> - ); -}; - -const CheckedHostField = ({ - labelText, - placeholder, - options = [], - isFixed, - value, - onChange, - helperText, - ...rest -}: { - labelText?: string; - placeholder: string; - isFixed: boolean; - value: Host[]; - onChange?: (options: Host[]) => void; - options?: Options; - helperText?: React.ReactNode | string; -} & Omit>, "onChange" | "value">) => { - return ( -
-
- {labelText ? : <>} - !!value.find((host) => host.userId.toString() === option.value)} - onChange={(options) => { - onChange && - onChange( - options.map((option) => ({ - isFixed, - userId: parseInt(option.value, 10), - priority: option.priority ?? 2, - })) - ); - }} - value={(value || []) - .filter(({ isFixed: _isFixed }) => isFixed === _isFixed) - .map((host) => { - const option = options.find((member) => member.value === host.userId.toString()); - return option ? { ...option, priority: host.priority ?? 2, isFixed } : options[0]; - }) - .filter(Boolean)} - controlShouldRenderValue={false} - options={options} - placeholder={placeholder} - {...rest} - /> -
-
- ); -}; - const FixedHostHelper = ( Add anyone who needs to attend the event. @@ -304,63 +186,6 @@ const FixedHosts = ({ ); }; -export const AddMembersWithSwitch = ({ - teamMembers, - value, - onChange, - assignAllTeamMembers, - setAssignAllTeamMembers, - automaticAddAllEnabled, - onActive, - isFixed, - placeholder = "", - containerClassName = "", -}: { - value: Host[]; - onChange: (hosts: Host[]) => void; - teamMembers: TeamMember[]; - assignAllTeamMembers: boolean; - setAssignAllTeamMembers: Dispatch>; - automaticAddAllEnabled: boolean; - onActive: () => void; - isFixed: boolean; - placeholder?: string; - containerClassName?: string; -}) => { - const { t } = useLocale(); - const { setValue } = useFormContext(); - - return ( -
-
- {automaticAddAllEnabled ? ( -
- setValue("hosts", [], { shouldDirty: true })} - /> -
- ) : ( - <> - )} - {!assignAllTeamMembers || !automaticAddAllEnabled ? ( - - ) : ( - <> - )} -
-
- ); -}; - const RoundRobinHosts = ({ teamMembers, value, diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 923a8610986336..5777373f56bcbe 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -5,6 +5,8 @@ import type { UseFormReturn } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import AddMembersWithSwitch from "@calcom/features/eventtypes/components/AddMembersWithSwitch"; +import type { Host } from "@calcom/features/eventtypes/lib/types"; import { ShellMain } from "@calcom/features/shell/Shell"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -38,8 +40,6 @@ import { Trash, MessageCircle, } from "@calcom/ui/components/icon"; -import { AddMembersWithSwitch } from "@calcom/web/components/eventtype/EventTeamTab"; -import type { Host } from "@calcom/web/pages/event-types/[type]"; import { getAbsoluteEventTypeRedirectUrl } from "../getEventTypeRedirectUrl"; import { RoutingPages } from "../lib/RoutingPages"; diff --git a/packages/features/eventtypes/components/AddMembersWithSwitch.tsx b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx new file mode 100644 index 00000000000000..74707dee0e05cb --- /dev/null +++ b/packages/features/eventtypes/components/AddMembersWithSwitch.tsx @@ -0,0 +1,149 @@ +import type { ComponentProps, Dispatch, SetStateAction } from "react"; +import { useFormContext } from "react-hook-form"; +import type { Options } from "react-select"; + +import type { FormValues, Host, TeamMember } from "@calcom/features/eventtypes/lib/types"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Label } from "@calcom/ui"; + +import AssignAllTeamMembers from "./AssignAllTeamMembers"; +import CheckedTeamSelect from "./CheckedTeamSelect"; +import type { CheckedSelectOption } from "./CheckedTeamSelect"; + +interface IUserToValue { + id: number | null; + name: string | null; + username: string | null; + avatar: string; + email: string; +} + +export const mapUserToValue = ( + { id, name, username, avatar, email }: IUserToValue, + pendingString: string +) => ({ + value: `${id || ""}`, + label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`, + avatar, + email, +}); + +const sortByLabel = (a: ReturnType, b: ReturnType) => { + if (a.label < b.label) { + return -1; + } + if (a.label > b.label) { + return 1; + } + return 0; +}; + +const CheckedHostField = ({ + labelText, + placeholder, + options = [], + isFixed, + value, + onChange, + helperText, + ...rest +}: { + labelText?: string; + placeholder: string; + isFixed: boolean; + value: Host[]; + onChange?: (options: Host[]) => void; + options?: Options; + helperText?: React.ReactNode | string; +} & Omit>, "onChange" | "value">) => { + return ( +
+
+ {labelText ? : <>} + !!value.find((host) => host.userId.toString() === option.value)} + onChange={(options) => { + onChange && + onChange( + options.map((option) => ({ + isFixed, + userId: parseInt(option.value, 10), + priority: option.priority ?? 2, + })) + ); + }} + value={(value || []) + .filter(({ isFixed: _isFixed }) => isFixed === _isFixed) + .map((host) => { + const option = options.find((member) => member.value === host.userId.toString()); + return option ? { ...option, priority: host.priority ?? 2, isFixed } : options[0]; + }) + .filter(Boolean)} + controlShouldRenderValue={false} + options={options} + placeholder={placeholder} + {...rest} + /> +
+
+ ); +}; + +const AddMembersWithSwitch = ({ + teamMembers, + value, + onChange, + assignAllTeamMembers, + setAssignAllTeamMembers, + automaticAddAllEnabled, + onActive, + isFixed, + placeholder = "", + containerClassName = "", +}: { + value: Host[]; + onChange: (hosts: Host[]) => void; + teamMembers: TeamMember[]; + assignAllTeamMembers: boolean; + setAssignAllTeamMembers: Dispatch>; + automaticAddAllEnabled: boolean; + onActive: () => void; + isFixed: boolean; + placeholder?: string; + containerClassName?: string; +}) => { + const { t } = useLocale(); + const { setValue } = useFormContext(); + + return ( +
+
+ {automaticAddAllEnabled ? ( +
+ setValue("hosts", [], { shouldDirty: true })} + /> +
+ ) : ( + <> + )} + {!assignAllTeamMembers || !automaticAddAllEnabled ? ( + + ) : ( + <> + )} +
+
+ ); +}; + +export default AddMembersWithSwitch; diff --git a/packages/features/eventtypes/components/AssignAllTeamMembers.tsx b/packages/features/eventtypes/components/AssignAllTeamMembers.tsx new file mode 100644 index 00000000000000..8ba190b001371a --- /dev/null +++ b/packages/features/eventtypes/components/AssignAllTeamMembers.tsx @@ -0,0 +1,45 @@ +import type { Dispatch, SetStateAction } from "react"; +import { Controller, useFormContext } from "react-hook-form"; + +import type { FormValues } from "@calcom/features/eventtypes/lib/types"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { SettingsToggle } from "@calcom/ui"; + +const AssignAllTeamMembers = ({ + assignAllTeamMembers, + setAssignAllTeamMembers, + onActive, + onInactive, +}: { + assignAllTeamMembers: boolean; + setAssignAllTeamMembers: Dispatch>; + onActive: () => void; + onInactive?: () => void; +}) => { + const { t } = useLocale(); + const { setValue } = useFormContext(); + + return ( + + name="assignAllTeamMembers" + render={() => ( + { + setValue("assignAllTeamMembers", active, { shouldDirty: true }); + setAssignAllTeamMembers(active); + if (active) { + onActive(); + } else if (!!onInactive) { + onInactive(); + } + }} + /> + )} + /> + ); +}; + +export default AssignAllTeamMembers; diff --git a/packages/features/eventtypes/components/CheckedTeamSelect.tsx b/packages/features/eventtypes/components/CheckedTeamSelect.tsx index f1bdebb9434030..3ca43e2432f70b 100644 --- a/packages/features/eventtypes/components/CheckedTeamSelect.tsx +++ b/packages/features/eventtypes/components/CheckedTeamSelect.tsx @@ -4,6 +4,7 @@ import type { Dispatch, SetStateAction } from "react"; import { useFormContext } from "react-hook-form"; import type { Props } from "react-select"; +import type { Host } from "@calcom/features/eventtypes/lib/types"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { @@ -18,7 +19,7 @@ import { Tooltip, } from "@calcom/ui"; import { X } from "@calcom/ui/components/icon"; -import type { FormValues, Host } from "@calcom/web/pages/event-types/[type]"; +import type { FormValues } from "@calcom/web/pages/event-types/[type]"; export type CheckedSelectOption = { avatar: string; diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index 6e5f77f1d54a2b..042baa59ca8739 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -20,6 +20,12 @@ export type AvailabilityOption = { export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"]; export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"]; export type Host = { isFixed: boolean; userId: number; priority: number }; +export type TeamMember = { + value: string; + label: string; + avatar: string; + email: string; +}; export type FormValues = { id: number; From a6d75dc725dc9704f496cca1f70ce72ec8efc333 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:08:33 -0400 Subject: [PATCH 07/12] fix type error --- packages/features/eventtypes/components/CheckedTeamSelect.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/features/eventtypes/components/CheckedTeamSelect.tsx b/packages/features/eventtypes/components/CheckedTeamSelect.tsx index 3ca43e2432f70b..cd2bf463bad77f 100644 --- a/packages/features/eventtypes/components/CheckedTeamSelect.tsx +++ b/packages/features/eventtypes/components/CheckedTeamSelect.tsx @@ -4,7 +4,7 @@ import type { Dispatch, SetStateAction } from "react"; import { useFormContext } from "react-hook-form"; import type { Props } from "react-select"; -import type { Host } from "@calcom/features/eventtypes/lib/types"; +import type { Host, FormValues } from "@calcom/features/eventtypes/lib/types"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { @@ -19,7 +19,6 @@ import { Tooltip, } from "@calcom/ui"; import { X } from "@calcom/ui/components/icon"; -import type { FormValues } from "@calcom/web/pages/event-types/[type]"; export type CheckedSelectOption = { avatar: string; From 849367891761d171db0cf140efed64c4380108ba Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Tue, 19 Mar 2024 18:23:40 +0530 Subject: [PATCH 08/12] Fix review comment: only include emails --- packages/app-store/routing-forms/trpc/response.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/routing-forms/trpc/response.handler.ts b/packages/app-store/routing-forms/trpc/response.handler.ts index be13c45adf08df..54721292360ada 100644 --- a/packages/app-store/routing-forms/trpc/response.handler.ts +++ b/packages/app-store/routing-forms/trpc/response.handler.ts @@ -104,7 +104,7 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) => in: settings.sendUpdatesTo, }, }, - include: { + select: { user: { select: { email: true, From 81e4d1b2178e56719f9fb6707226aeeb880f955c Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Wed, 20 Mar 2024 12:12:24 +0530 Subject: [PATCH 09/12] Remove listMembers trpc call in frontend and get the data in formQuery.handler --- .../routing-forms/components/SingleForm.tsx | 13 +++++------- .../routing-forms/lib/getSerializableForm.ts | 20 +++++++++++++++++++ .../viewer/teams/listMembers.handler.ts | 1 - 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 5777373f56bcbe..087d031dfe4ce2 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -245,9 +245,6 @@ type SingleFormComponentProps = { function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleFormComponentProps) { const utils = trpc.useContext(); const { t } = useLocale(); - const { data: teamMembers } = form.teamId - ? trpc.viewer.teams.listMembers.useQuery({ teamIds: [form.teamId] }) - : { data: [] }; const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); const [response, setResponse] = useState({}); @@ -301,9 +298,9 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF }, [form]); useEffect(() => { - if (form.teamId && form.settings?.sendUpdatesTo?.length && teamMembers?.length) { + if (form.teamId && form.settings?.sendUpdatesTo?.length && form.teamMembers?.length) { let sendToAll = true; - teamMembers.forEach((member) => { + form.teamMembers.forEach((member) => { if (!form.settings?.sendUpdatesTo?.includes(member.id)) { sendToAll = false; return; @@ -318,7 +315,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF })) ); } - }, [form.teamId, form.settings?.sendUpdatesTo?.length, teamMembers?.length]); + }, [form.teamId, form.settings?.sendUpdatesTo?.length, form.teamMembers?.length]); const mutation = trpc.viewer.appRoutingForms.formMutation.useMutation({ onSuccess() { @@ -385,7 +382,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
{form.teamId ? ( ({ + teamMembers={form.teamMembers.map((member) => ({ value: member.id.toString(), label: member.name || "", avatar: member.avatarUrl || "", @@ -409,7 +406,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF onActive={() => { hookForm.setValue( "settings.sendUpdatesTo", - (teamMembers || []).map((teamMember) => teamMember.id), + form.teamMembers.map((teamMember) => teamMember.id), { shouldDirty: true } ); hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true }); diff --git a/packages/app-store/routing-forms/lib/getSerializableForm.ts b/packages/app-store/routing-forms/lib/getSerializableForm.ts index 96bedf8cb41e13..15341ae142dd8d 100644 --- a/packages/app-store/routing-forms/lib/getSerializableForm.ts +++ b/packages/app-store/routing-forms/lib/getSerializableForm.ts @@ -58,6 +58,25 @@ export async function getSerializableForm({ description: f.description, })); const finalFields = fields; + let teamMembers = []; + if (form.teamId) { + teamMembers = await prisma.user.findMany({ + where: { + teams: { + some: { + teamId: form.teamId, + accepted: true, + }, + }, + }, + select: { + id: true, + name: true, + email: true, + avatarUrl: true, + }, + }); + } // Ideally we should't have needed to explicitly type it but due to some reason it's not working reliably with VSCode TypeCheck const serializableForm: SerializableForm = { @@ -67,6 +86,7 @@ export async function getSerializableForm({ routes, routers, connectedForms, + teamMembers, createdAt: form.createdAt.toString(), updatedAt: form.updatedAt.toString(), }; diff --git a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts index 87f18de3ae5fb0..2cbd5c7397104e 100644 --- a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts @@ -36,7 +36,6 @@ export const listMembersHandler = async ({ ctx, input }: ListMembersOptions) => id: true, name: true, username: true, - email: true, avatarUrl: true, }, }, From 319cf6d19b4a5a4e5a348f95b6d950dee90587c1 Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Wed, 20 Mar 2024 15:30:03 +0530 Subject: [PATCH 10/12] Address review comments --- apps/web/public/static/locales/en/common.json | 1 + .../routing-forms/components/SingleForm.tsx | 104 +++++++++--------- .../routing-forms/lib/getSerializableForm.ts | 4 +- .../app-store/routing-forms/types/types.d.ts | 7 ++ packages/prisma/zod-utils.ts | 1 + 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index a7fa99c7213e14..83d9c206efbd16 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1472,6 +1472,7 @@ "routing_forms_description": "Create forms to direct attendees to the correct destinations", "routing_forms_send_email_owner": "Send Email to Owner", "routing_forms_send_email_owner_description": "Sends an email to the owner when the form is submitted", + "routing_forms_send_email_to": "Send Email to", "add_new_form": "Add new form", "add_new_team_form": "Add new form to your team", "create_your_first_route": "Create your first route", diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 087d031dfe4ce2..439c502c3742e8 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -6,7 +6,6 @@ import { Controller, useFormContext } from "react-hook-form"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import AddMembersWithSwitch from "@calcom/features/eventtypes/components/AddMembersWithSwitch"; -import type { Host } from "@calcom/features/eventtypes/lib/types"; import { ShellMain } from "@calcom/features/shell/Shell"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -250,8 +249,6 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF const [response, setResponse] = useState({}); const [decidedAction, setDecidedAction] = useState(null); const [skipFirstUpdate, setSkipFirstUpdate] = useState(true); - const [selectedMembers, setSelectedMembers] = useState([]); - const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(false); const [eventTypeUrl, setEventTypeUrl] = useState(""); function testRouting() { @@ -297,25 +294,8 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF // eslint-disable-next-line react-hooks/exhaustive-deps }, [form]); - useEffect(() => { - if (form.teamId && form.settings?.sendUpdatesTo?.length && form.teamMembers?.length) { - let sendToAll = true; - form.teamMembers.forEach((member) => { - if (!form.settings?.sendUpdatesTo?.includes(member.id)) { - sendToAll = false; - return; - } - }); - setAssignAllTeamMembers(sendToAll); - setSelectedMembers( - form.settings.sendUpdatesTo.map((userId) => ({ - isFixed: true, - userId: userId, - priority: 1, - })) - ); - } - }, [form.teamId, form.settings?.sendUpdatesTo?.length, form.teamMembers?.length]); + const sendUpdatesTo = hookForm.watch("settings.sendUpdatesTo", []) as number[]; + const sendToAll = hookForm.watch("settings.sendToAll", false) as boolean; const mutation = trpc.viewer.appRoutingForms.formMutation.useMutation({ onSuccess() { @@ -381,39 +361,53 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
{form.teamId ? ( - ({ - value: member.id.toString(), - label: member.name || "", - avatar: member.avatarUrl || "", - email: member.email, - isFixed: true, - }))} - value={selectedMembers} - onChange={(value) => { - setSelectedMembers(value); - hookForm.setValue( - "settings.sendUpdatesTo", - value.map((teamMember) => teamMember.userId), - { shouldDirty: true } - ); - hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true }); - }} - assignAllTeamMembers={assignAllTeamMembers} - setAssignAllTeamMembers={setAssignAllTeamMembers} - automaticAddAllEnabled={true} - isFixed={true} - onActive={() => { - hookForm.setValue( - "settings.sendUpdatesTo", - form.teamMembers.map((teamMember) => teamMember.id), - { shouldDirty: true } - ); - hookForm.setValue("settings.emailOwnerOnSubmission", false, { shouldDirty: true }); - }} - placeholder={t("select_members")} - containerClassName="!px-0 !pb-0 !pt-0" - /> +
+ + {t("routing_forms_send_email_to")} + + ({ + value: member.id.toString(), + label: member.name || "", + avatar: member.avatarUrl || "", + email: member.email, + isFixed: true, + }))} + value={sendUpdatesTo.map((userId) => ({ + isFixed: true, + userId: userId, + priority: 1, + }))} + onChange={(value) => { + hookForm.setValue( + "settings.sendUpdatesTo", + value.map((teamMember) => teamMember.userId), + { shouldDirty: true } + ); + hookForm.setValue("settings.emailOwnerOnSubmission", false, { + shouldDirty: true, + }); + }} + assignAllTeamMembers={sendToAll} + setAssignAllTeamMembers={(value) => { + hookForm.setValue("settings.sendToAll", !!value, { shouldDirty: true }); + }} + automaticAddAllEnabled={true} + isFixed={true} + onActive={() => { + hookForm.setValue( + "settings.sendUpdatesTo", + form.teamMembers.map((teamMember) => teamMember.id), + { shouldDirty: true } + ); + hookForm.setValue("settings.emailOwnerOnSubmission", false, { + shouldDirty: true, + }); + }} + placeholder={t("select_members")} + containerClassName="!px-0 !pb-0 !pt-0" + /> +
) : ( ({ description: f.description, })); const finalFields = fields; - let teamMembers = []; + let teamMembers: SerializableFormTeamMembers[] = []; if (form.teamId) { teamMembers = await prisma.user.findMany({ where: { diff --git a/packages/app-store/routing-forms/types/types.d.ts b/packages/app-store/routing-forms/types/types.d.ts index 7cc639c1211c81..4b0aeef530807d 100644 --- a/packages/app-store/routing-forms/types/types.d.ts +++ b/packages/app-store/routing-forms/types/types.d.ts @@ -23,6 +23,12 @@ export type Fields = z.infer; export type Field = Fields[number]; export type Routes = z.infer; export type Route = Routes[0]; +export type SerializableFormTeamMembers = { + id: number; + name: string | null; + email: string; + avatarUrl: string | null; +}; export type SerializableForm = Omit< T, "fields" | "routes" | "createdAt" | "updatedAt" | "settings" @@ -34,6 +40,7 @@ export type SerializableForm = Omit< updatedAt: string; connectedForms: { name: string; description: string | null; id: string }[]; routers: { name: string; description: string | null; id: string }[]; + teamMembers: SerializableFormTeamMembers[]; }; export type LocalRoute = z.infer; diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index c3418658798658..48c7e273d3f9f2 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -444,6 +444,7 @@ export const RoutingFormSettings = z .object({ emailOwnerOnSubmission: z.boolean(), sendUpdatesTo: z.array(z.number()).optional(), + sendToAll: z.boolean().optional(), }) .nullable(); From 4d2cfb08482d82d5f88d1a6d2ee1b16356cc1d2e Mon Sep 17 00:00:00 2001 From: Anwar Sadath Date: Wed, 20 Mar 2024 15:33:32 +0530 Subject: [PATCH 11/12] Remove unrelated changes --- packages/trpc/server/routers/viewer/teams/listMembers.handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts index 2cbd5c7397104e..1c12a4ec4dcfb0 100644 --- a/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/listMembers.handler.ts @@ -36,7 +36,6 @@ export const listMembersHandler = async ({ ctx, input }: ListMembersOptions) => id: true, name: true, username: true, - avatarUrl: true, }, }, accepted: true, From 5206fd82dc43df7bc55c8170b63dccf55297380e Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 21 Mar 2024 10:30:26 -0400 Subject: [PATCH 12/12] Update SingleForm.tsx --- packages/app-store/routing-forms/components/SingleForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 439c502c3742e8..84b8513506ca37 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -362,7 +362,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
{form.teamId ? (
- + {t("routing_forms_send_email_to")}