From 097c14ca648a511779e9a284a114f79d0d206bcd Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Sat, 26 Oct 2024 04:08:48 +0530 Subject: [PATCH] feat: add workflow vars and search for event types (#17343) * Udit:goat - thank you for this * improve searchbar * remove unused code * add workflow vars * change requested * requested changes * changes * type-fix?? * type-fix?? * ---- --- .../views/event-types-listing-view.tsx | 59 +++++++++++++++---- apps/web/public/static/locales/en/common.json | 6 ++ .../workflows/api/scheduleEmailReminders.ts | 7 +++ .../ee/workflows/api/scheduleSMSReminders.ts | 7 +++ .../features/ee/workflows/lib/constants.ts | 11 +++- .../lib/reminders/emailReminderManager.ts | 3 + .../lib/reminders/smsReminderManager.ts | 3 + .../lib/reminders/templates/customTemplate.ts | 14 ++++- .../eventtypes/components/DuplicateDialog.tsx | 4 ++ packages/lib/hooks/useCreateEventType.ts | 6 +- .../viewer/eventTypes/getByViewer.schema.ts | 1 + .../getEventTypesFromGroup.handler.ts | 4 +- 12 files changed, 108 insertions(+), 17 deletions(-) diff --git a/apps/web/modules/event-types/views/event-types-listing-view.tsx b/apps/web/modules/event-types/views/event-types-listing-view.tsx index 647965d38c..0e93916164 100644 --- a/apps/web/modules/event-types/views/event-types-listing-view.tsx +++ b/apps/web/modules/event-types/views/event-types-listing-view.tsx @@ -20,17 +20,16 @@ import Shell from "@calcom/features/shell/Shell"; import { parseEventTypeColor } from "@calcom/lib"; import { APP_NAME } from "@calcom/lib/constants"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; +import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { useGetTheme } from "@calcom/lib/hooks/useTheme"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; -import type { User } from "@calcom/prisma/client"; import type { MembershipRole } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc, TRPCClientError } from "@calcom/trpc/react"; -import type { UserProfile } from "@calcom/types/UserProfile"; import { Alert, Badge, @@ -49,10 +48,12 @@ import { EmptyScreen, HeadSeo, HorizontalTabs, + Icon, Label, showToast, Skeleton, Switch, + TextField, Tooltip, ArrowButton, UserAvatarGroup, @@ -73,13 +74,6 @@ type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eve type EventTypeGroup = EventTypeGroups[number]; type EventType = EventTypeGroup["eventTypes"][number]; -type DeNormalizedEventType = Omit & { - users: (Pick & { - nonProfileUsername: string | null; - profile: UserProfile; - })[]; -}; - const LIMIT = 10; interface InfiniteEventTypeListProps { @@ -89,6 +83,7 @@ interface InfiniteEventTypeListProps { pages: { nextCursor: number | undefined; eventTypes: InfiniteEventType[] }[] | undefined; lockedByOrg?: boolean; isPending?: boolean; + debouncedSearchTerm?: string; } interface InfiniteTeamsTabProps { @@ -103,9 +98,13 @@ const InfiniteTeamsTab: FC = (props) => { const { activeEventTypeGroup } = props; const { t } = useLocale(); + const [searchTerm, setSearchTerm] = useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 500); + const query = trpc.viewer.eventTypes.getEventTypesFromGroup.useInfiniteQuery( { limit: LIMIT, + searchQuery: debouncedSearchTerm, group: { teamId: activeEventTypeGroup?.teamId, parentId: activeEventTypeGroup?.parentId }, }, { @@ -124,6 +123,19 @@ const InfiniteTeamsTab: FC = (props) => { return (
+ } + addOnClassname="!border-muted" + containerClassName="max-w-64 focus:!ring-offset-0 mb-4" + type="search" + value={searchTerm} + autoComplete="false" + onChange={(e) => { + setSearchTerm(e.target.value); + }} + placeholder={t("search")} + /> {!!activeEventTypeGroup && ( = (props) => { bookerUrl={activeEventTypeGroup.bookerUrl} readOnly={activeEventTypeGroup.metadata.readOnly} isPending={query.isPending} + debouncedSearchTerm={debouncedSearchTerm} /> )}
@@ -238,6 +251,7 @@ export const InfiniteEventTypeList = ({ bookerUrl, lockedByOrg, isPending, + debouncedSearchTerm, }: InfiniteEventTypeListProps): JSX.Element => { const { t } = useLocale(); const router = useRouter(); @@ -265,6 +279,7 @@ export const InfiniteEventTypeList = ({ await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ limit: LIMIT, + searchQuery: debouncedSearchTerm, group: { teamId: group?.teamId, parentId: group?.parentId }, }); @@ -283,7 +298,11 @@ export const InfiniteEventTypeList = ({ onError: async (err, _, context) => { if (context?.previousValue) { utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( - { limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } }, + { + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }, context.previousValue ); } @@ -316,12 +335,17 @@ export const InfiniteEventTypeList = ({ await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ limit: LIMIT, + searchQuery: debouncedSearchTerm, group: { teamId: group?.teamId, parentId: group?.parentId }, }); if (previousValue) { utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( - { limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } }, + { + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }, (data) => { return { pageParams: data?.pageParams ?? [], @@ -367,12 +391,17 @@ export const InfiniteEventTypeList = ({ await utils.viewer.eventTypes.getEventTypesFromGroup.cancel(); const previousValue = utils.viewer.eventTypes.getEventTypesFromGroup.getInfiniteData({ limit: LIMIT, + searchQuery: debouncedSearchTerm, group: { teamId: group?.teamId, parentId: group?.parentId }, }); if (previousValue) { await utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( - { limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } }, + { + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }, (data) => { if (!data) { return { @@ -396,7 +425,11 @@ export const InfiniteEventTypeList = ({ onError: (err, _, context) => { if (context?.previousValue) { utils.viewer.eventTypes.getEventTypesFromGroup.setInfiniteData( - { limit: LIMIT, group: { teamId: group?.teamId, parentId: group?.parentId } }, + { + limit: LIMIT, + searchQuery: debouncedSearchTerm, + group: { teamId: group?.teamId, parentId: group?.parentId }, + }, context.previousValue ); } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c70dbcdf20..2342da842a 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2703,5 +2703,11 @@ "reassigned_by": "Reassigned by", "row": "row", "rows": "rows", + "attendee_timezone_variable": "Attendee Timezone", + "event_start_time_in_attendee_timezone_variable": "Event Start Time In Attendee Timezone", + "event_end_time_in_attendee_timezone_variable": "Event End Time In Attendee Timezone", + "attendee_timezone_info": "Attendee Timezone", + "event_start_time_in_attendee_timezone_info": "Event Start Time In Attendee Timezone", + "event_end_time_in_attendee_timezone_info": "Event End Time In Attendee Timezone", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 44777eced3..898c5e9dd9 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -205,6 +205,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`, ratingUrl: `${bookerUrl}/booking/${reminder.booking.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${reminder.booking.uid}?noShow=true`, + attendeeTimezone: reminder.booking.attendees[0].timeZone, + eventTimeInAttendeeTimezone: dayjs(reminder.booking.startTime).tz( + reminder.booking.attendees[0].timeZone + ), + eventEndTimeInAttendeeTimezone: dayjs(reminder.booking?.endTime).tz( + reminder.booking.attendees[0].timeZone + ), }; const emailLocale = locale || "en"; const emailSubject = customTemplate( diff --git a/packages/features/ee/workflows/api/scheduleSMSReminders.ts b/packages/features/ee/workflows/api/scheduleSMSReminders.ts index a6d7f46740..5d0c952cae 100644 --- a/packages/features/ee/workflows/api/scheduleSMSReminders.ts +++ b/packages/features/ee/workflows/api/scheduleSMSReminders.ts @@ -134,6 +134,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { meetingUrl: bookingMetadataSchema.parse(reminder.booking?.metadata || {})?.videoCallUrl, cancelLink: `${bookerUrl}/booking/${reminder.booking.uid}?cancel=true`, rescheduleLink: `${bookerUrl}/reschedule/${reminder.booking.uid}`, + attendeeTimezone: reminder.booking.attendees[0].timeZone, + eventTimeInAttendeeTimezone: dayjs(reminder.booking.startTime).tz( + reminder.booking.attendees[0].timeZone + ), + eventEndTimeInAttendeeTimezone: dayjs(reminder.booking?.endTime).tz( + reminder.booking.attendees[0].timeZone + ), }; const customMessage = customTemplate( reminder.workflowStep.reminderBody || "", diff --git a/packages/features/ee/workflows/lib/constants.ts b/packages/features/ee/workflows/lib/constants.ts index 1445a1de6b..b57020d7bf 100644 --- a/packages/features/ee/workflows/lib/constants.ts +++ b/packages/features/ee/workflows/lib/constants.ts @@ -64,6 +64,15 @@ export const DYNAMIC_TEXT_VARIABLES = [ "reschedule_url", "rating_url", "no_show_url", + "attendee_timezone", + "event_start_time_in_attendee_timezone", + "event_end_time_in_attendee_timezone", ]; -export const FORMATTED_DYNAMIC_TEXT_VARIABLES = ["event_date_", "event_time_", "event_end_time_"]; +export const FORMATTED_DYNAMIC_TEXT_VARIABLES = [ + "event_date_", + "event_time_", + "event_end_time_", + "event_start_time_in_attendee_timezone_", + "event_end_time_in_attendee_timezone_", +]; diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 3f4162ca8f..4096308b71 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -163,6 +163,9 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => rescheduleLink: `${bookerUrl}/reschedule/${evt.uid}`, ratingUrl: `${bookerUrl}/booking/${evt.uid}?rating`, noShowUrl: `${bookerUrl}/booking/${evt.uid}?noShow=true`, + attendeeTimezone: evt.attendees[0].timeZone, + eventTimeInAttendeeTimezone: dayjs(startTime).tz(evt.attendees[0].timeZone), + eventEndTimeInAttendeeTimezone: dayjs(endTime).tz(evt.attendees[0].timeZone), }; const locale = diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 6d1b3111bc..677ee9fa3e 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -160,6 +160,9 @@ export const scheduleSMSReminder = async (args: ScheduleTextReminderArgs) => { meetingUrl: bookingMetadataSchema.parse(evt.metadata || {})?.videoCallUrl, cancelLink: `${evt.bookerUrl ?? WEBSITE_URL}/booking/${evt.uid}?cancel=true`, rescheduleLink: `${evt.bookerUrl ?? WEBSITE_URL}/reschedule/${evt.uid}`, + attendeeTimezone: evt.attendees[0].timeZone, + eventTimeInAttendeeTimezone: dayjs(evt.startTime).tz(evt.attendees[0].timeZone), + eventEndTimeInAttendeeTimezone: dayjs(evt.endTime).tz(evt.attendees[0].timeZone), }; const customMessage = customTemplate(smsMessage, variables, locale, evt.organizer.timeFormat); smsMessage = customMessage.text; diff --git a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts index e63a0a2b5f..0f234d08a8 100644 --- a/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts +++ b/packages/features/ee/workflows/lib/reminders/templates/customTemplate.ts @@ -23,6 +23,9 @@ export type VariablesType = { rescheduleLink?: string; ratingUrl?: string; noShowUrl?: string; + attendeeTimezone?: string; + eventTimeInAttendeeTimezone?: Dayjs; + eventEndTimeInAttendeeTimezone?: Dayjs; }; const customTemplate = ( @@ -83,7 +86,16 @@ const customTemplate = ( .replaceAll("{RESCHEDULE_URL}", rescheduleLink) .replaceAll("{MEETING_URL}", variables.meetingUrl || "") .replaceAll("{RATING_URL}", variables.ratingUrl || "") - .replaceAll("{NO_SHOW_URL}", variables.noShowUrl || ""); + .replaceAll("{NO_SHOW_URL}", variables.noShowUrl || "") + .replaceAll("{ATTENDEE_TIMEZONE}", variables.attendeeTimezone || "") + .replaceAll( + "{EVENT_START_TIME_IN_ATTENDEE_TIMEZONE}", + variables.eventTimeInAttendeeTimezone?.format(currentTimeFormat) || "" + ) + .replaceAll( + "{EVENT_END_TIME_IN_ATTENDEE_TIMEZONE}", + variables.eventEndTimeInAttendeeTimezone?.format(currentTimeFormat) || "" + ); const customInputvariables = dynamicText.match(/\{(.+?)}/g)?.map((variable) => { return variable.replace("{", "").replace("}", ""); diff --git a/packages/features/eventtypes/components/DuplicateDialog.tsx b/packages/features/eventtypes/components/DuplicateDialog.tsx index 418b3e467e..65958e7e27 100644 --- a/packages/features/eventtypes/components/DuplicateDialog.tsx +++ b/packages/features/eventtypes/components/DuplicateDialog.tsx @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; +import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; @@ -46,6 +47,8 @@ const DuplicateDialog = () => { const { data: { pageSlug, slug, ...defaultValues }, } = useTypedQuery(querySchema); + const [searchTerm, setSearchTerm] = useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 500); // react hook form const form = useForm({ @@ -77,6 +80,7 @@ const DuplicateDialog = () => { await utils.viewer.eventTypes.getUserEventGroups.invalidate(); await utils.viewer.eventTypes.getEventTypesFromGroup.invalidate({ limit: 10, + searchQuery: debouncedSearchTerm, group: { teamId: eventType?.teamId, parentId: eventType?.parentId }, }); diff --git a/packages/lib/hooks/useCreateEventType.ts b/packages/lib/hooks/useCreateEventType.ts index e7ea9f0282..53b84d1d41 100644 --- a/packages/lib/hooks/useCreateEventType.ts +++ b/packages/lib/hooks/useCreateEventType.ts @@ -1,9 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; import type { EventType } from "@prisma/client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod"; +import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { SchedulingType } from "@calcom/prisma/enums"; @@ -42,6 +43,8 @@ export const useCreateEventType = ( const utils = trpc.useUtils(); const { t } = useLocale(); const { form, isManagedEventType } = useCreateEventTypeForm(); + const [searchTerm, setSearchTerm] = useState(""); + const debouncedSearchTerm = useDebounce(searchTerm, 500); const createMutation = trpc.viewer.eventTypes.create.useMutation({ onSuccess: async ({ eventType }) => { @@ -49,6 +52,7 @@ export const useCreateEventType = ( await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({ group: { teamId: eventType.teamId, parentId: eventType.parentId }, + searchQuery: debouncedSearchTerm, limit: 10, }); diff --git a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts index fb48f9a0d5..a3b0b16b50 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getByViewer.schema.ts @@ -24,6 +24,7 @@ export const ZGetEventTypesFromGroupSchema = z.object({ cursor: z.number().nullish(), limit: z.number().default(10), group: z.object({ teamId: z.number().nullish(), parentId: z.number().nullish() }), + searchQuery: z.string().optional(), }); export type TGetEventTypesFromGroupSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts index 7b14fad209..107474b9c2 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/getEventTypesFromGroup.handler.ts @@ -30,7 +30,7 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) }); const userProfile = ctx.user.profile; - const { group, limit, cursor, filters } = input; + const { group, limit, cursor, filters, searchQuery } = input; const { teamId, parentId } = group; const isFilterSet = (filters && hasFilter(filters)) || !!teamId; @@ -52,6 +52,7 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) where: { teamId: null, schedulingType: null, + ...(searchQuery ? { title: { contains: searchQuery, mode: "insensitive" } } : {}), }, orderBy: [ { @@ -83,6 +84,7 @@ export const getEventTypesFromGroup = async ({ ctx, input }: GetByViewerOptions) schedulingType: { in: filters.schedulingTypes }, } : null), + ...(searchQuery ? { title: { contains: searchQuery, mode: "insensitive" } } : {}), }, orderBy: [ {