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??

* ----
This commit is contained in:
Syed Ali Shahbaz
2024-10-25 22:38:48 +00:00
committed by GitHub
parent b9128a7a0b
commit 097c14ca64
12 changed files with 108 additions and 17 deletions
@@ -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<EventType, "userIds"> & {
users: (Pick<User, "id" | "name" | "username" | "avatarUrl"> & {
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<InfiniteTeamsTabProps> = (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<InfiniteTeamsTabProps> = (props) => {
return (
<div>
<TextField
className="max-w-64 bg-subtle !border-muted mb-4 mr-auto rounded-md !pl-0 focus:!ring-offset-0"
addOnLeading={<Icon name="search" className="text-subtle h-4 w-4" />}
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 && (
<InfiniteEventTypeList
pages={query?.data?.pages}
@@ -131,6 +143,7 @@ const InfiniteTeamsTab: FC<InfiniteTeamsTabProps> = (props) => {
bookerUrl={activeEventTypeGroup.bookerUrl}
readOnly={activeEventTypeGroup.metadata.readOnly}
isPending={query.isPending}
debouncedSearchTerm={debouncedSearchTerm}
/>
)}
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
@@ -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
);
}
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -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(
@@ -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 || "",
@@ -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_",
];
@@ -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 =
@@ -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;
@@ -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("}", "");
@@ -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 },
});
+5 -1
View File
@@ -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,
});
@@ -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<typeof ZGetEventTypesFromGroupSchema>;
@@ -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: [
{