Compare commits

...

15 Commits

Author SHA1 Message Date
gitstart-calcom 1477807769 resolve conflicts 2023-09-19 07:05:30 +00:00
gitstart-calcom 7c0b23fe08 Requested changes 2023-09-13 01:20:46 +00:00
Joe Au-Yeung 3506dc0e6d
Merge branch 'main' into CALCOM-5774-2 2023-09-01 10:24:24 -04:00
Peer Richelsen b8f6f2a7d8
Merge branch 'main' into CALCOM-5774-2 2023-08-31 20:58:39 +02:00
Peer Richelsen b0de588e09
Merge branch 'main' into CALCOM-5774-2 2023-08-31 20:57:29 +02:00
gitstart-calcom 6756aa0da1 Requested changes 2023-08-30 11:58:27 +00:00
Joe Au-Yeung 720d4f2833
Merge branch 'main' into CALCOM-5774-2 2023-08-23 10:01:29 -04:00
GitStart-Cal.com 634a025dcf
Merge branch 'main' into CALCOM-5774-2 2023-08-22 08:07:19 -03:00
gitstart-calcom c39fe9e6e1 resolve conflicts 2023-08-22 11:00:24 +00:00
Keith Williams f238a534c3
Merge branch 'main' into CALCOM-5774-2 2023-08-14 14:38:35 +02:00
GitStart-Cal.com 501624002f
Merge branch 'main' into CALCOM-5774-2 2023-08-03 09:03:56 -03:00
GitStart-Cal.com c26b60911d
Merge branch 'main' into CALCOM-5774-2 2023-07-31 07:15:44 +01:00
GitStart-Cal.com 840535e3c2
Merge branch 'main' into CALCOM-5774-2 2023-07-28 07:27:29 +01:00
GitStart-Cal.com 89bcce75d9
Merge branch 'main' into CALCOM-5774-2 2023-07-27 06:38:35 +01:00
gitstart-calcom b912eea5c8 Update CALCOM-5774 with the requested changes 2023-07-24 21:20:27 +00:00
10 changed files with 163 additions and 17 deletions

View File

@ -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")}

View File

@ -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>
</>

View File

@ -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" },

View File

@ -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: {

View File

@ -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,
});
}}>

View File

@ -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": "Well use this email address on the calendar invite",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}

View File

@ -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")

View File

@ -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" },

View File

@ -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(),

View File

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