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:
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user