Compare commits
15 Commits
main
...
CALCOM-577
Author | SHA1 | Date |
---|---|---|
gitstart-calcom | 1477807769 | |
gitstart-calcom | 7c0b23fe08 | |
Joe Au-Yeung | 3506dc0e6d | |
Peer Richelsen | b8f6f2a7d8 | |
Peer Richelsen | b0de588e09 | |
gitstart-calcom | 6756aa0da1 | |
Joe Au-Yeung | 720d4f2833 | |
GitStart-Cal.com | 634a025dcf | |
gitstart-calcom | c39fe9e6e1 | |
Keith Williams | f238a534c3 | |
GitStart-Cal.com | 501624002f | |
GitStart-Cal.com | c26b60911d | |
GitStart-Cal.com | 840535e3c2 | |
GitStart-Cal.com | 89bcce75d9 | |
gitstart-calcom | b912eea5c8 |
|
@ -33,10 +33,13 @@ import {
|
|||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Select,
|
||||
} from "@calcom/ui";
|
||||
import { Copy, Edit } from "@calcom/ui/components/icon";
|
||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
||||
|
||||
import InfoBadge from "@components/ui/InfoBadge";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
|
||||
|
@ -58,7 +61,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const [useAddToCalendarEmail, setUseAddToCalendarEmail] = useState(
|
||||
eventType.metadata.useAddToCalendarEmail ? true : false
|
||||
);
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||
|
@ -123,6 +128,24 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
|
||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||
|
||||
const emailMap = new Map();
|
||||
const calendarEmails =
|
||||
connectedCalendarsQuery.data?.connectedCalendars
|
||||
.map((selectedCalendar) => ({
|
||||
label: selectedCalendar.primary?.email || "",
|
||||
value: selectedCalendar.primary?.email || "",
|
||||
}))
|
||||
.filter((email) => {
|
||||
const mapValue = emailMap.get(email.value);
|
||||
if (!mapValue) {
|
||||
emailMap.set(email.label, email.value);
|
||||
return true;
|
||||
}
|
||||
}) ?? [];
|
||||
|
||||
const options = [...(user ? [{ label: user.email, value: user.email }] : []), ...calendarEmails];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-8">
|
||||
{/**
|
||||
|
@ -179,11 +202,64 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!!connectedCalendarsQuery.data?.connectedCalendars.length && (
|
||||
<Controller
|
||||
name="organizerEmail"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.metadata.organizerEmail}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
title={t("display_email_as_organizer")}
|
||||
revertToggle
|
||||
checked={useAddToCalendarEmail}
|
||||
LockedIcon={
|
||||
useAddToCalendarEmail && (
|
||||
<InfoBadge
|
||||
className="mt-0"
|
||||
content={t("automatically_determine_email_address_tooltip")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onCheckedChange={() => {
|
||||
setUseAddToCalendarEmail(!useAddToCalendarEmail);
|
||||
|
||||
if (!useAddToCalendarEmail) {
|
||||
formMethods.setValue("metadata.useAddToCalendarEmail", true);
|
||||
} else {
|
||||
formMethods.setValue("metadata.useAddToCalendarEmail", false);
|
||||
}
|
||||
}}>
|
||||
<div className="w-11/12 lg:-ml-2">
|
||||
<Select
|
||||
value={calendarEmails.find((cal) => cal.value === value)}
|
||||
required={!useAddToCalendarEmail}
|
||||
defaultValue={options.filter(
|
||||
(email) => email.value === eventType.metadata.organizerEmail ?? user?.email
|
||||
)}
|
||||
options={[...(user ? [{ label: user.email, value: user.email }] : []), ...calendarEmails]}
|
||||
className="mb-2 mt-1 block w-full min-w-0 flex-1 text-sm"
|
||||
onChange={(option) => {
|
||||
formMethods.setValue("metadata.organizerEmail", option?.value);
|
||||
onChange(option?.value);
|
||||
}}
|
||||
/>
|
||||
<div className="text-gray mt-2 flex items-center text-sm text-gray-700">
|
||||
{t("use_email_address_on_calendar_invite")}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<hr className="border-subtle [&:has(+div:empty)]:hidden" />
|
||||
<div>
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} />
|
||||
</div>
|
||||
<hr className="border-subtle" />
|
||||
<hr className="border-subtle mt-0" />
|
||||
<FormBuilder
|
||||
title={t("booking_questions_title")}
|
||||
description={t("booking_questions_description")}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
import { classNames } from "@calcom/lib";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
import { Info } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function InfoBadge({ content }: { content: string }) {
|
||||
type Props = {
|
||||
className?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export default function InfoBadge({ content, className }: Props) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip side="top" content={content}>
|
||||
<span title={content}>
|
||||
<Info className="text-subtle relative left-1 right-1 top-px mt-px h-4 w-4" />
|
||||
<Info
|
||||
className={classNames("text-subtle relative left-1 right-1 top-px mt-px h-4 w-4", className)}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
|||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { BookingStatus, ReminderType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -61,6 +62,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
select: {
|
||||
recurringEvent: true,
|
||||
bookingFields: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
responses: true,
|
||||
|
@ -119,7 +121,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
endTime: booking.endTime.toISOString(),
|
||||
organizer: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
email: EventTypeMetaDataSchema.parse(booking.eventType?.metadata)?.organizerEmail || user.email,
|
||||
name,
|
||||
timeZone: user.timeZone,
|
||||
language: { translate: tOrganizer, locale: user.locale ?? "en" },
|
||||
|
|
|
@ -444,7 +444,10 @@ export default function Success(props: SuccessProps) {
|
|||
</span>
|
||||
<Badge variant="blue">{t("Host")}</Badge>
|
||||
</div>
|
||||
<p className="text-default">{bookingInfo.user.email}</p>
|
||||
<p className="text-default">
|
||||
{EventTypeMetaDataSchema.parse(bookingInfo.eventType?.metadata)
|
||||
?.organizerEmail || bookingInfo.user?.email}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
|
@ -1059,6 +1062,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||
eventName: true,
|
||||
slug: true,
|
||||
timeZone: true,
|
||||
metadata: true,
|
||||
},
|
||||
},
|
||||
seatsReferences: {
|
||||
|
|
|
@ -134,6 +134,7 @@ export type FormValues = {
|
|||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
organizerEmail?: string;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
|
@ -172,6 +173,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
|
@ -476,9 +478,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
organizerEmail,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
let emailMeta;
|
||||
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
|
@ -503,10 +509,25 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, ...rest } = input;
|
||||
|
||||
// when no email is selected as organizer email, use email in 'Add to Calendar' (destinationCalendar)
|
||||
// otherwise, use email in default calendar
|
||||
if (metadata?.useAddToCalendarEmail) {
|
||||
if (input.destinationCalendar) {
|
||||
emailMeta = {
|
||||
organizerEmail: input.destinationCalendar.externalId,
|
||||
isDefaultCalendarEmail: false,
|
||||
};
|
||||
} else {
|
||||
emailMeta = {
|
||||
organizerEmail: connectedCalendarsQuery.data?.destinationCalendar.primaryEmail,
|
||||
isDefaultCalendarEmail: true, // to keep track of when the default calendar is updated
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
updateMutation.mutate({
|
||||
...rest,
|
||||
...input,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
|
@ -521,7 +542,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
metadata: { ...metadata, ...emailMeta },
|
||||
customInputs,
|
||||
});
|
||||
}}>
|
||||
|
|
|
@ -2055,5 +2055,9 @@
|
|||
"edit_users_availability":"Edit user's availability: {{username}}",
|
||||
"resend_invitation": "Resend invitation",
|
||||
"invitation_resent": "The invitation was resent.",
|
||||
"automatically_determine_email_address": "Display \"Add to calendar\" email as the organizer",
|
||||
"automatically_determine_email_address_tooltip": "If enabled, we'll use either the email \n address for the calendar you've chosen, \n or your primary email address on file.",
|
||||
"display_email_as_organizer": "Display \"Add to calendar\" email as the organizer",
|
||||
"use_email_address_on_calendar_invite": "We’ll use this email address on the calendar invite",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Alert, Button, EmptyScreen, Form, showToast } from "@calcom/ui";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
@ -160,6 +161,10 @@ export const BookEventFormChild = ({
|
|||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const { data: connectedCalendars, isLoading: connectedCalendarLoading } =
|
||||
trpc.viewer.connectedCalendars.useQuery();
|
||||
const updateEventTypeMutation = trpc.viewer.eventTypes.update.useMutation();
|
||||
const defaultEmail = connectedCalendars?.destinationCalendar?.primaryEmail;
|
||||
type BookingFormValues = {
|
||||
locationType?: EventLocationType["type"];
|
||||
responses: z.infer<typeof bookingFormSchema>["responses"] | null;
|
||||
|
@ -334,6 +339,16 @@ export const BookEventFormChild = ({
|
|||
),
|
||||
};
|
||||
|
||||
const eventMeta = EventTypeMetaDataSchema.parse(eventType?.metadata);
|
||||
|
||||
// if email in use is default email and not same with current default email, update organizerEmail
|
||||
if (eventMeta?.isDefaultCalendarEmail && eventMeta?.organizerEmail !== defaultEmail) {
|
||||
updateEventTypeMutation.mutate({
|
||||
metadata: { ...eventMeta, organizerEmail: defaultEmail },
|
||||
id: eventQuery?.data.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (eventQuery.data?.recurringEvent?.freq && recurringEventCount) {
|
||||
createRecurringBookingMutation.mutate(
|
||||
mapRecurringBookingToMutationInput(bookingInput, recurringEventCount)
|
||||
|
@ -398,7 +413,11 @@ export const BookEventFormChild = ({
|
|||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
loading={createBookingMutation.isLoading || createRecurringBookingMutation.isLoading}
|
||||
loading={
|
||||
createBookingMutation.isLoading ||
|
||||
createRecurringBookingMutation.isLoading ||
|
||||
connectedCalendarLoading
|
||||
}
|
||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}>
|
||||
{rescheduleUid
|
||||
? t("reschedule")
|
||||
|
|
|
@ -1068,7 +1068,10 @@ async function handler(
|
|||
organizer: {
|
||||
id: organizerUser.id,
|
||||
name: organizerUser.name || "Nameless",
|
||||
email: organizerUser.email || "Email-less",
|
||||
email:
|
||||
EventTypeMetaDataSchema.parse(eventType.metadata)?.organizerEmail ||
|
||||
organizerUser.email ||
|
||||
"Email-less",
|
||||
username: organizerUser.username || undefined,
|
||||
timeZone: organizerUser.timeZone,
|
||||
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
|
||||
|
|
|
@ -72,6 +72,9 @@ export const RequiresConfirmationThresholdUnits: z.ZodType<UnitTypeLongPlural> =
|
|||
|
||||
export const EventTypeMetaDataSchema = z
|
||||
.object({
|
||||
organizerEmail: z.string().optional(),
|
||||
isDefaultCalendarEmail: z.boolean().optional(),
|
||||
useAddToCalendarEmail: z.boolean().optional(),
|
||||
smartContractAddress: z.string().optional(),
|
||||
blockchainId: z.number().optional(),
|
||||
multipleDuration: z.number().array().optional(),
|
||||
|
|
|
@ -14,6 +14,7 @@ type Props = {
|
|||
onCheckedChange?: (checked: boolean) => void;
|
||||
"data-testid"?: string;
|
||||
tooltip?: string;
|
||||
revertToggle?: boolean;
|
||||
};
|
||||
|
||||
function SettingsToggle({
|
||||
|
@ -25,6 +26,7 @@ function SettingsToggle({
|
|||
children,
|
||||
disabled,
|
||||
tooltip,
|
||||
revertToggle,
|
||||
...rest
|
||||
}: Props) {
|
||||
const [animateRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
@ -44,16 +46,20 @@ function SettingsToggle({
|
|||
/>
|
||||
|
||||
<div>
|
||||
<Label className="text-emphasis text-sm font-semibold leading-none">
|
||||
{title}
|
||||
{LockedIcon}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-emphasis text-sm font-semibold leading-none">{title}</Label>
|
||||
<span>{LockedIcon}</span>
|
||||
</div>
|
||||
{description && <p className="text-default -mt-1.5 text-sm leading-normal">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
<div className="lg:ml-14" ref={animateRef}>
|
||||
{checked && <div className="mt-4">{children}</div>}
|
||||
{(revertToggle && !checked) || (!revertToggle && checked) ? (
|
||||
<div className="mt-4">{children}</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
|
|
Loading…
Reference in New Issue