Merge branch 'main' into integromat-app

This commit is contained in:
aar2dee2 2023-08-29 19:55:26 +05:30
commit cc252433d8
58 changed files with 677 additions and 437 deletions

View File

@ -13,16 +13,18 @@ const schemaMembershipRequiredParams = z.object({
teamId: z.number(), teamId: z.number(),
}); });
export const membershipCreateBodySchema = Membership.partial({ export const membershipCreateBodySchema = Membership.omit({ id: true })
accepted: true, .partial({
role: true, accepted: true,
disableImpersonation: true, role: true,
}).transform((v) => ({ disableImpersonation: true,
accepted: false, })
role: MembershipRole.MEMBER, .transform((v) => ({
disableImpersonation: false, accepted: false,
...v, role: MembershipRole.MEMBER,
})); disableImpersonation: false,
...v,
}));
export const membershipEditBodySchema = Membership.omit({ export const membershipEditBodySchema = Membership.omit({
/** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */ /** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */

View File

@ -382,24 +382,22 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
}} }}
/> />
{selectedLocation && LocationOptions} {selectedLocation && LocationOptions}
<DialogFooter> <DialogFooter className="mt-4">
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse"> <Button
<Button onClick={() => {
onClick={() => { setShowLocationModal(false);
setShowLocationModal(false); setSelectedLocation?.(undefined);
setSelectedLocation?.(undefined); setEditingLocationType?.("");
setEditingLocationType?.(""); locationFormMethods.unregister(["locationType", "locationLink"]);
locationFormMethods.unregister(["locationType", "locationLink"]); }}
}} type="button"
type="button" color="secondary">
color="secondary"> {t("cancel")}
{t("cancel")} </Button>
</Button>
<Button data-testid="update-location" type="submit"> <Button data-testid="update-location" type="submit">
{t("update")} {t("update")}
</Button> </Button>
</div>
</DialogFooter> </DialogFooter>
</Form> </Form>
</div> </div>

View File

@ -41,7 +41,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
return ( return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}> <Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
<DialogContent> <DialogContent enableOverflow>
<div className="flex flex-row space-x-3"> <div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]"> <div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<Clock className="m-auto h-6 w-6" /> <Clock className="m-auto h-6 w-6" />

View File

@ -57,12 +57,10 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
}), }),
}); });
const formMethods = useForm<{ type formSchemaType = z.infer<typeof formSchema>;
username: string;
email_address: string; const formMethods = useForm<formSchemaType>({
full_name: string; mode: "onChange",
password: string;
}>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
}); });
@ -70,7 +68,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
props.onError(); props.onError();
}; };
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => { const onSubmit = formMethods.handleSubmit(async (data) => {
props.onSubmit(); props.onSubmit();
const response = await fetch("/api/auth/setup", { const response = await fetch("/api/auth/setup", {
method: "POST", method: "POST",
@ -130,11 +128,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
className={classNames("my-0", longWebsiteUrl && "rounded-t-none")} className={classNames("my-0", longWebsiteUrl && "rounded-t-none")}
onBlur={onBlur} onBlur={onBlur}
name="username" name="username"
onChange={async (e) => { onChange={(e) => onChange(e.target.value)}
onChange(e.target.value);
formMethods.setValue("username", e.target.value);
await formMethods.trigger("username");
}}
/> />
</> </>
)} )}
@ -148,11 +142,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<TextField <TextField
value={value || ""} value={value || ""}
onBlur={onBlur} onBlur={onBlur}
onChange={async (e) => { onChange={(e) => onChange(e.target.value)}
onChange(e.target.value);
formMethods.setValue("full_name", e.target.value);
await formMethods.trigger("full_name");
}}
color={formMethods.formState.errors.full_name ? "warn" : ""} color={formMethods.formState.errors.full_name ? "warn" : ""}
type="text" type="text"
name="full_name" name="full_name"
@ -172,11 +162,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<EmailField <EmailField
value={value || ""} value={value || ""}
onBlur={onBlur} onBlur={onBlur}
onChange={async (e) => { onChange={(e) => onChange(e.target.value)}
onChange(e.target.value);
formMethods.setValue("email_address", e.target.value);
await formMethods.trigger("email_address");
}}
className="my-0" className="my-0"
name="email_address" name="email_address"
/> />
@ -191,11 +177,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on
<PasswordField <PasswordField
value={value || ""} value={value || ""}
onBlur={onBlur} onBlur={onBlur}
onChange={async (e) => { onChange={(e) => onChange(e.target.value)}
onChange(e.target.value);
formMethods.setValue("password", e.target.value);
await formMethods.trigger("password");
}}
hintErrors={["caplow", "admin_min", "num"]} hintErrors={["caplow", "admin_min", "num"]}
name="password" name="password"
className="my-0" className="my-0"

View File

@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "3.2.5", "version": "3.2.6",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",

View File

@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername"; import { checkPremiumUsername } from "@calcom/ee/common/lib/checkPremiumUsername";
@ -11,18 +10,9 @@ import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername"; import { validateUsernameInTeam, validateUsername } from "@calcom/lib/validateUsername";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums"; import { IdentityProvider } from "@calcom/prisma/enums";
import { signupSchema } from "@calcom/prisma/zod-utils";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const signupSchema = z.object({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
token: z.string().optional(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") { if (req.method !== "POST") {
return res.status(405).end(); return res.status(405).end();

View File

@ -34,13 +34,17 @@ const triggerWebhook = async ({
booking: { booking: {
userId: number | undefined; userId: number | undefined;
eventTypeId: number | null; eventTypeId: number | null;
eventTypeParentId: number | null | undefined;
teamId?: number | null; teamId?: number | null;
}; };
}) => { }) => {
const eventTrigger: WebhookTriggerEvents = "RECORDING_READY"; const eventTrigger: WebhookTriggerEvents = "RECORDING_READY";
// Send Webhook call if hooked to BOOKING.RECORDING_READY // Send Webhook call if hooked to BOOKING.RECORDING_READY
const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId);
const subscriberOptions = { const subscriberOptions = {
userId: booking.userId, userId: triggerForUser ? booking.userId : null,
eventTypeId: booking.eventTypeId, eventTypeId: booking.eventTypeId,
triggerEvent: eventTrigger, triggerEvent: eventTrigger,
teamId: booking.teamId, teamId: booking.teamId,
@ -183,6 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
booking: { booking: {
userId: booking?.user?.id, userId: booking?.user?.id,
eventTypeId: booking.eventTypeId, eventTypeId: booking.eventTypeId,
eventTypeParentId: booking.eventType?.parentId,
teamId, teamId,
}, },
}); });

View File

@ -17,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui"; import { Alert, Button, EmailField, HeadSeo, PasswordField, TextField } from "@calcom/ui";
@ -25,14 +26,7 @@ import PageWrapper from "@components/PageWrapper";
import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants"; import { IS_GOOGLE_LOGIN_ENABLED } from "../server/lib/constants";
import { ssrInit } from "../server/lib/ssr"; import { ssrInit } from "../server/lib/ssr";
const signupSchema = z.object({ const signupSchema = apiSignupSchema.extend({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().min(7),
language: z.string().optional(),
token: z.string().optional(),
apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
}); });
@ -46,6 +40,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const flags = useFlagMap(); const flags = useFlagMap();
const methods = useForm<FormValues>({ const methods = useForm<FormValues>({
mode: "onChange",
resolver: zodResolver(signupSchema), resolver: zodResolver(signupSchema),
defaultValues: prepopulateFormValues, defaultValues: prepopulateFormValues,
}); });

View File

@ -158,9 +158,10 @@ test.describe("pro user", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); await expect(page.locator("[data-testid=success-page]")).toBeVisible();
additionalGuests.forEach(async (email) => { const promises = additionalGuests.map(async (email) => {
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email); await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
}); });
await Promise.all(promises);
}); });
test("Time slots should be reserved when selected", async ({ context, page }) => { test("Time slots should be reserved when selected", async ({ context, page }) => {

View File

@ -2030,5 +2030,6 @@
"value": "Value", "value": "Value",
"your_organization_updated_sucessfully": "Your organization updated successfully", "your_organization_updated_sucessfully": "Your organization updated successfully",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations", "seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"include_calendar_event": "Include calendar event",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }

View File

@ -5,15 +5,21 @@ export type GetAppData = (key: string) => unknown;
export type SetAppData = (key: string, value: unknown) => void; export type SetAppData = (key: string, value: unknown) => void;
type LockedIcon = JSX.Element | false | undefined; type LockedIcon = JSX.Element | false | undefined;
type Disabled = boolean | undefined; type Disabled = boolean | undefined;
// eslint-disable-next-line @typescript-eslint/no-empty-function
const EventTypeAppContext = React.createContext<[GetAppData, SetAppData, LockedIcon, Disabled]>([
() => ({}),
() => ({}),
undefined,
undefined,
]);
export type SetAppDataGeneric<TAppData extends ZodType> = < type AppContext = {
getAppData: GetAppData;
setAppData: SetAppData;
LockedIcon?: LockedIcon;
disabled?: Disabled;
};
// eslint-disable-next-line @typescript-eslint/no-empty-function
const EventTypeAppContext = React.createContext<AppContext>({
getAppData: () => ({}),
setAppData: () => ({}),
});
type SetAppDataGeneric<TAppData extends ZodType> = <
TKey extends keyof z.infer<TAppData>, TKey extends keyof z.infer<TAppData>,
TValue extends z.infer<TAppData>[TKey] TValue extends z.infer<TAppData>[TKey]
>( >(
@ -21,7 +27,7 @@ export type SetAppDataGeneric<TAppData extends ZodType> = <
value: TValue value: TValue
) => void; ) => void;
export type GetAppDataGeneric<TAppData extends ZodType> = <TKey extends keyof z.infer<TAppData>>( type GetAppDataGeneric<TAppData extends ZodType> = <TKey extends keyof z.infer<TAppData>>(
key: TKey key: TKey
) => z.infer<TAppData>[TKey]; ) => z.infer<TAppData>[TKey];
@ -29,7 +35,12 @@ export const useAppContextWithSchema = <TAppData extends ZodType>() => {
type GetAppData = GetAppDataGeneric<TAppData>; type GetAppData = GetAppDataGeneric<TAppData>;
type SetAppData = SetAppDataGeneric<TAppData>; type SetAppData = SetAppDataGeneric<TAppData>;
// TODO: Not able to do it without type assertion here // TODO: Not able to do it without type assertion here
const context = React.useContext(EventTypeAppContext) as [GetAppData, SetAppData, LockedIcon, Disabled]; const context = React.useContext(EventTypeAppContext) as {
getAppData: GetAppData;
setAppData: SetAppData;
LockedIcon: LockedIcon;
disabled: Disabled;
};
return context; return context;
}; };
export default EventTypeAppContext; export default EventTypeAppContext;

View File

@ -1,12 +1,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import Link from "next/link"; import Link from "next/link";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import { classNames } from "@calcom/lib"; import { classNames } from "@calcom/lib";
import type { RouterOutputs } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react";
import { Switch, Badge, Avatar } from "@calcom/ui"; import { Switch, Badge, Avatar } from "@calcom/ui";
import type { SetAppDataGeneric } from "../EventTypeAppContext";
import type { eventTypeAppCardZod } from "../eventTypeAppCardZod";
import type { CredentialOwner } from "../types"; import type { CredentialOwner } from "../types";
import OmniInstallAppButton from "./OmniInstallAppButton"; import OmniInstallAppButton from "./OmniInstallAppButton";
@ -16,24 +15,20 @@ export default function AppCard({
switchOnClick, switchOnClick,
switchChecked, switchChecked,
children, children,
setAppData,
returnTo, returnTo,
teamId, teamId,
disableSwitch,
LockedIcon,
}: { }: {
app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner }; app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner };
description?: React.ReactNode; description?: React.ReactNode;
switchChecked?: boolean; switchChecked?: boolean;
switchOnClick?: (e: boolean) => void; switchOnClick?: (e: boolean) => void;
children?: React.ReactNode; children?: React.ReactNode;
setAppData: SetAppDataGeneric<typeof eventTypeAppCardZod>;
returnTo?: string; returnTo?: string;
teamId?: number; teamId?: number;
disableSwitch?: boolean;
LockedIcon?: React.ReactNode; LockedIcon?: React.ReactNode;
}) { }) {
const [animationRef] = useAutoAnimate<HTMLDivElement>(); const [animationRef] = useAutoAnimate<HTMLDivElement>();
const { setAppData, LockedIcon, disabled } = useAppContextWithSchema();
return ( return (
<div <div
@ -92,7 +87,7 @@ export default function AppCard({
{app?.isInstalled || app.credentialOwner ? ( {app?.isInstalled || app.credentialOwner ? (
<div className="ml-auto flex items-center"> <div className="ml-auto flex items-center">
<Switch <Switch
disabled={!app.enabled || disableSwitch} disabled={!app.enabled || disabled}
onCheckedChange={(enabled) => { onCheckedChange={(enabled) => {
if (switchOnClick) { if (switchOnClick) {
switchOnClick(enabled); switchOnClick(enabled);

View File

@ -19,7 +19,7 @@ export const EventTypeAppCard = (props: {
const { app, getAppData, setAppData, LockedIcon, disabled } = props; const { app, getAppData, setAppData, LockedIcon, disabled } = props;
return ( return (
<ErrorBoundary message={`There is some problem with ${app.name} App`}> <ErrorBoundary message={`There is some problem with ${app.name} App`}>
<EventTypeAppContext.Provider value={[getAppData, setAppData, LockedIcon, disabled]}> <EventTypeAppContext.Provider value={{ getAppData, setAppData, LockedIcon, disabled }}>
<DynamicComponent <DynamicComponent
slug={app.slug === "stripe" ? "stripepayment" : app.slug} slug={app.slug === "stripe" ? "stripepayment" : app.slug}
componentMap={EventTypeAddonMap} componentMap={EventTypeAddonMap}

View File

@ -125,8 +125,13 @@ export async function deleteSubscription({
export async function listBookings(appApiKey: ApiKey) { export async function listBookings(appApiKey: ApiKey) {
try { try {
const where: Prisma.BookingWhereInput = {}; const where: Prisma.BookingWhereInput = {};
if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId }; if (appApiKey.teamId) {
else where.userId = appApiKey.userId; where.eventType = {
OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }],
};
} else {
where.userId = appApiKey.userId;
}
const bookings = await prisma.booking.findMany({ const bookings = await prisma.booking.findMany({
take: 3, take: 3,
where: { where: {
@ -206,6 +211,7 @@ export async function scheduleTrigger(
) { ) {
try { try {
//schedule job to call subscriber url at the end of meeting //schedule job to call subscriber url at the end of meeting
// FIXME: in-process scheduling - job will vanish on server crash / restart
const job = schedule.scheduleJob( const job = schedule.scheduleJob(
`${subscriber.appId}_${subscriber.id}`, `${subscriber.appId}_${subscriber.id}`,
booking.endTime, booking.endTime,
@ -253,38 +259,39 @@ export async function cancelScheduledJobs(
appId?: string | null, appId?: string | null,
isReschedule?: boolean isReschedule?: boolean
) { ) {
try { if (!booking.scheduledJobs) return;
let scheduledJobs = booking.scheduledJobs || [];
if (booking.scheduledJobs) { let scheduledJobs = booking.scheduledJobs || [];
booking.scheduledJobs.forEach(async (scheduledJob) => { const promises = booking.scheduledJobs.map(async (scheduledJob) => {
if (appId) { if (appId) {
if (scheduledJob.startsWith(appId)) { if (scheduledJob.startsWith(appId)) {
if (schedule.scheduledJobs[scheduledJob]) { if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel(); schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
}
} else {
//if no specific appId given, delete all scheduled jobs of booking
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = [];
} }
scheduledJobs = scheduledJobs?.filter((job) => scheduledJob !== job) || [];
}
} else {
//if no specific appId given, delete all scheduled jobs of booking
if (schedule.scheduledJobs[scheduledJob]) {
schedule.scheduledJobs[scheduledJob].cancel();
}
scheduledJobs = [];
}
if (!isReschedule) { if (!isReschedule) {
await prisma.booking.update({ await prisma.booking.update({
where: { where: {
uid: booking.uid, uid: booking.uid,
}, },
data: { data: {
scheduledJobs: scheduledJobs, scheduledJobs: scheduledJobs,
}, },
});
}
}); });
} }
});
try {
await Promise.all(promises);
} catch (error) { } catch (error) {
console.error("Error cancelling scheduled jobs", error); console.error("Error cancelling scheduled jobs", error);
} }

View File

@ -5,7 +5,7 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import type { EventTypeAppCardApp } from "../types"; import type { EventTypeAppCardApp } from "../types";
function useIsAppEnabled(app: EventTypeAppCardApp) { function useIsAppEnabled(app: EventTypeAppCardApp) {
const [getAppData, setAppData] = useAppContextWithSchema(); const { getAppData, setAppData } = useAppContextWithSchema();
const [enabled, setEnabled] = useState(() => { const [enabled, setEnabled] = useState(() => {
if (!app.credentialOwner) { if (!app.credentialOwner) {
return getAppData("enabled"); return getAppData("enabled");

View File

@ -9,7 +9,7 @@ import { Select } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData } = useAppContextWithSchema<typeof appDataSchema>();
const [enabled, setEnabled] = useState(getAppData("enabled")); const [enabled, setEnabled] = useState(getAppData("enabled"));
const [projects, setProjects] = useState(); const [projects, setProjects] = useState();
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>(); const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
@ -32,7 +32,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
switchOnClick={(e) => { switchOnClick={(e) => {
if (!e) { if (!e) {

View File

@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
updateEnabled(e); updateEnabled(e);
}} }}

View File

@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
updateEnabled(e); updateEnabled(e);
}} }}

View File

@ -8,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const thankYouPage = getAppData("thankYouPage"); const thankYouPage = getAppData("thankYouPage");
const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app); const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app);
@ -16,11 +16,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
description={t("confirmation_page_gif")} description={t("confirmation_page_gif")}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
setShowGifSelection(e); setShowGifSelection(e);
}} }}

View File

@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
updateEnabled(e); updateEnabled(e);
}} }}

View File

@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={updateEnabled} switchOnClick={updateEnabled}
switchChecked={enabled} switchChecked={enabled}
teamId={eventType.team?.id || undefined}> teamId={eventType.team?.id || undefined}>

View File

@ -16,7 +16,7 @@ type Option = { value: string; label: string };
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const { asPath } = useRouter(); const { asPath } = useRouter();
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price"); const price = getAppData("price");
const currency = getAppData("currency"); const currency = getAppData("currency");
const [selectedCurrency, setSelectedCurrency] = useState( const [selectedCurrency, setSelectedCurrency] = useState(
@ -38,7 +38,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
return ( return (
<AppCard <AppCard
returnTo={WEBAPP_URL + asPath} returnTo={WEBAPP_URL + asPath}
setAppData={setAppData}
app={app} app={app}
switchChecked={requirePayment} switchChecked={requirePayment}
switchOnClick={(enabled) => { switchOnClick={(enabled) => {

View File

@ -7,17 +7,14 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const plausibleUrl = getAppData("PLAUSIBLE_URL"); const plausibleUrl = getAppData("PLAUSIBLE_URL");
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
updateEnabled(e); updateEnabled(e);
}} }}

View File

@ -11,7 +11,7 @@ import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
const { t } = useLocale(); const { t } = useLocale();
const [_getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { disabled } = useAppContextWithSchema<typeof appDataSchema>();
const [additionalParameters, setAdditionalParameters] = useState(""); const [additionalParameters, setAdditionalParameters] = useState("");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
@ -37,10 +37,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
updateEnabled(e); updateEnabled(e);
}} }}

View File

@ -15,7 +15,7 @@ type Option = { value: string; label: string };
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const pathname = usePathname(); const pathname = usePathname();
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
const price = getAppData("price"); const price = getAppData("price");
const currency = getAppData("currency"); const currency = getAppData("currency");
const paymentOption = getAppData("paymentOption"); const paymentOption = getAppData("paymentOption");
@ -37,15 +37,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
.trim(); .trim();
return ( return (
<AppCard <AppCard
returnTo={WEBAPP_URL + pathname} returnTo={WEBAPP_URL + pathname + "?tabName=apps"}
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchChecked={requirePayment} switchChecked={requirePayment}
switchOnClick={(enabled) => { switchOnClick={(enabled) => {
setRequirePayment(enabled); setRequirePayment(enabled);
}}> }}
teamId={eventType.team?.id || undefined}>
<> <>
{recurringEventDefined && ( {recurringEventDefined && (
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} /> <Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />

View File

@ -8,13 +8,12 @@ import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId"); const trackingId = getAppData("trackingId");
const [enabled, setEnabled] = useState(getAppData("enabled")); const [enabled, setEnabled] = useState(getAppData("enabled"));
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
switchOnClick={(e) => { switchOnClick={(e) => {
if (!e) { if (!e) {

View File

@ -7,16 +7,13 @@ import { Sunrise, Sunset } from "@calcom/ui/components/icon";
import type { appDataSchema } from "../zod"; import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) { const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
const isSunrise = getAppData("isSunrise"); const isSunrise = getAppData("isSunrise");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
return ( return (
<AppCard <AppCard
setAppData={setAppData}
app={app} app={app}
disableSwitch={disabled}
LockedIcon={LockedIcon}
switchOnClick={(e) => { switchOnClick={(e) => {
if (!e) { if (!e) {
updateEnabled(false); updateEnabled(false);

View File

@ -121,18 +121,21 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter( const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
(ref) => !!credentialsMap.get(ref.type) (ref) => !!credentialsMap.get(ref.type)
); );
const promises = bookingRefsFiltered.map(async (bookingRef) => {
if (!bookingRef.uid) return;
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
});
try { try {
bookingRefsFiltered.forEach(async (bookingRef) => { await Promise.all(promises);
if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
}
});
} catch (error) { } catch (error) {
// FIXME: error logging - non-Error type errors are currently discarded
if (error instanceof Error) { if (error instanceof Error) {
logger.error(error.message); logger.error(error.message);
} }

View File

@ -121,17 +121,19 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => {
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter( const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
(ref) => !!credentialsMap.get(ref.type) (ref) => !!credentialsMap.get(ref.type)
); );
const promises = bookingRefsFiltered.map(async (bookingRef) => {
if (!bookingRef.uid) return;
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
});
try { try {
bookingRefsFiltered.forEach(async (bookingRef) => { await Promise.all(promises);
if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
}
});
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
logger.error(error.message); logger.error(error.message);

View File

@ -12,7 +12,7 @@ const libraries = [
}, },
]; ];
libraries.forEach(async (lib) => { const promises = libraries.map(async (lib) => {
await build({ await build({
build: { build: {
outDir: `./dist/${lib.fileName}`, outDir: `./dist/${lib.fileName}`,
@ -29,3 +29,4 @@ libraries.forEach(async (lib) => {
}, },
}); });
}); });
await Promise.all(promises);

View File

@ -10,15 +10,11 @@ export function isPasswordValid(password: string, breakdown?: boolean, strict?:
num = false, // At least one number num = false, // At least one number
min = false, // Eight characters, or fifteen in strict mode. min = false, // Eight characters, or fifteen in strict mode.
admin_min = false; admin_min = false;
if (password.length > 7 && (!strict || password.length > 14)) min = true; if (password.length >= 7 && (!strict || password.length > 14)) min = true;
if (strict && password.length > 14) admin_min = true; if (strict && password.length > 14) admin_min = true;
for (let i = 0; i < password.length; i++) { if (password.match(/\d/)) num = true;
if (!isNaN(parseInt(password[i]))) num = true; if (password.match(/[a-z]/)) low = true;
else { if (password.match(/[A-Z]/)) cap = true;
if (password[i] === password[i].toUpperCase()) cap = true;
if (password[i] === password[i].toLowerCase()) low = true;
}
}
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);

View File

@ -1,6 +1,5 @@
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
@ -24,13 +23,8 @@ export const DatePicker = () => {
return ( return (
<DatePickerComponent <DatePickerComponent
isLoading={schedule.isLoading} isLoading={schedule.isLoading}
onChange={(date: Dayjs) => { onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
setSelectedDate(date.format("YYYY-MM-DD")); onMonthChange={(date) => setMonth(date.format("YYYY-MM"))}
}}
onMonthChange={(date: Dayjs) => {
setMonth(date.format("YYYY-MM"));
setSelectedDate(date.format("YYYY-MM-DD"));
}}
includedDates={nonEmptyScheduleDays} includedDates={nonEmptyScheduleDays}
locale={i18n.language} locale={i18n.language}
browsingDate={month ? dayjs(month) : undefined} browsingDate={month ? dayjs(month) : undefined}

View File

@ -160,7 +160,8 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
updateQueryParam("date", selectedDate ?? ""); updateQueryParam("date", selectedDate ?? "");
// Setting month make sure small calendar in fullscreen layouts also updates. // Setting month make sure small calendar in fullscreen layouts also updates.
if (newSelection.month() !== currentSelection.month()) { // If selectedDate is null, prevents setting month to Invalid-Date
if (selectedDate && newSelection.month() !== currentSelection.month()) {
set({ month: newSelection.format("YYYY-MM") }); set({ month: newSelection.format("YYYY-MM") });
updateQueryParam("month", newSelection.format("YYYY-MM")); updateQueryParam("month", newSelection.format("YYYY-MM"));
} }
@ -193,7 +194,6 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
setMonth: (month: string | null) => { setMonth: (month: string | null) => {
set({ month, selectedTimeslot: null }); set({ month, selectedTimeslot: null });
updateQueryParam("month", month ?? ""); updateQueryParam("month", month ?? "");
get().setSelectedDate(null);
}, },
isTeamEvent: false, isTeamEvent: false,
seatedEventData: { seatedEventData: {

View File

@ -157,8 +157,10 @@ async function handler(req: CustomRequest) {
}, },
}); });
const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId);
const subscriberOptions = { const subscriberOptions = {
userId: bookingToDelete.userId, userId: triggerForUser ? bookingToDelete.userId : null,
eventTypeId: bookingToDelete.eventTypeId as number, eventTypeId: bookingToDelete.eventTypeId as number,
triggerEvent: eventTrigger, triggerEvent: eventTrigger,
teamId, teamId,
@ -428,9 +430,9 @@ async function handler(req: CustomRequest) {
bookingToDelete.recurringEventId && bookingToDelete.recurringEventId &&
allRemainingBookings allRemainingBookings
) { ) {
bookingToDelete.user.credentials const promises = bookingToDelete.user.credentials
.filter((credential) => credential.type.endsWith("_calendar")) .filter((credential) => credential.type.endsWith("_calendar"))
.forEach(async (credential) => { .map(async (credential) => {
const calendar = await getCalendar(credential); const calendar = await getCalendar(credential);
for (const updBooking of updatedBookings) { for (const updBooking of updatedBookings) {
const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar")); const bookingRef = updBooking.references.find((ref) => ref.type.includes("_calendar"));
@ -441,6 +443,13 @@ async function handler(req: CustomRequest) {
} }
} }
}); });
try {
await Promise.all(promises);
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
}
} else { } else {
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>); apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise<unknown>);
} }
@ -601,11 +610,13 @@ async function handler(req: CustomRequest) {
}); });
// delete scheduled jobs of cancelled bookings // delete scheduled jobs of cancelled bookings
// FIXME: async calls into ether
updatedBookings.forEach((booking) => { updatedBookings.forEach((booking) => {
cancelScheduledJobs(booking); cancelScheduledJobs(booking);
}); });
//Workflows - cancel all reminders for cancelled bookings //Workflows - cancel all reminders for cancelled bookings
// FIXME: async calls into ether
updatedBookings.forEach((booking) => { updatedBookings.forEach((booking) => {
booking.workflowReminders.forEach((reminder) => { booking.workflowReminders.forEach((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) { if (reminder.method === WorkflowMethods.EMAIL) {
@ -620,11 +631,14 @@ async function handler(req: CustomRequest) {
const prismaPromises: Promise<unknown>[] = [bookingReferenceDeletes]; const prismaPromises: Promise<unknown>[] = [bookingReferenceDeletes];
// @TODO: find a way in the future if a promise fails don't stop the rest of the promises
// Also if emails fails try to requeue them
try { try {
await Promise.all(prismaPromises.concat(apiDeletes)); const settled = await Promise.allSettled(prismaPromises.concat(apiDeletes));
const rejected = settled.filter(({ status }) => status === "rejected") as PromiseRejectedResult[];
if (rejected.length) {
throw new Error(`Reasons: ${rejected.map(({ reason }) => reason)}`);
}
// TODO: if emails fail try to requeue them
await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName }); await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName });
} catch (error) { } catch (error) {
console.error("Error deleting event", error); console.error("Error deleting event", error);

View File

@ -293,14 +293,16 @@ export async function handleConfirmation(args: {
}, },
}); });
const triggerForUser = !teamId || (teamId && booking.eventType?.parentId);
const subscribersBookingCreated = await getWebhooks({ const subscribersBookingCreated = await getWebhooks({
userId: booking.userId, userId: triggerForUser ? booking.userId : null,
eventTypeId: booking.eventTypeId, eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId, teamId,
}); });
const subscribersMeetingEnded = await getWebhooks({ const subscribersMeetingEnded = await getWebhooks({
userId: booking.userId, userId: triggerForUser ? booking.userId : null,
eventTypeId: booking.eventTypeId, eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED, triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId: booking.eventType?.teamId, teamId: booking.eventType?.teamId,

View File

@ -1126,8 +1126,10 @@ async function handler(
const teamId = await getTeamIdFromEventType({ eventType }); const teamId = await getTeamIdFromEventType({ eventType });
const triggerForUser = !teamId || (teamId && eventType.parentId);
const subscriberOptions: GetSubscriberOptions = { const subscriberOptions: GetSubscriberOptions = {
userId: organizerUser.id, userId: triggerForUser ? organizerUser.id : null,
eventTypeId, eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId, teamId,
@ -1140,7 +1142,7 @@ async function handler(
subscriberOptions.triggerEvent = eventTrigger; subscriberOptions.triggerEvent = eventTrigger;
const subscriberOptionsMeetingEnded = { const subscriberOptionsMeetingEnded = {
userId: organizerUser.id, userId: triggerForUser ? organizerUser.id : null,
eventTypeId, eventTypeId,
triggerEvent: WebhookTriggerEvents.MEETING_ENDED, triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
teamId, teamId,

View File

@ -16,7 +16,7 @@ export type DatePickerProps = {
/** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */ /** which day of the week to render the calendar. Usually Sunday (=0) or Monday (=1) - default: Sunday */
weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6; weekStart?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
/** Fires whenever a selected date is changed. */ /** Fires whenever a selected date is changed. */
onChange: (date: Dayjs) => void; onChange: (date: Dayjs | null) => void;
/** Fires when the month is changed. */ /** Fires when the month is changed. */
onMonthChange?: (date: Dayjs) => void; onMonthChange?: (date: Dayjs) => void;
/** which date or dates are currently selected (not tracked from here) */ /** which date or dates are currently selected (not tracked from here) */
@ -30,7 +30,7 @@ export type DatePickerProps = {
/** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */ /** Defaults to [], which dates are not bookable. Array of valid dates like: ["2022-04-23", "2022-04-24"] */
excludedDates?: string[]; excludedDates?: string[];
/** defaults to all, which dates are bookable (inverse of excludedDates) */ /** defaults to all, which dates are bookable (inverse of excludedDates) */
includedDates?: string[]; includedDates?: string[] | null;
/** allows adding classes to the container */ /** allows adding classes to the container */
className?: string; className?: string;
/** Shows a small loading spinner next to the month name */ /** Shows a small loading spinner next to the month name */
@ -121,7 +121,7 @@ const Days = ({
// Create placeholder elements for empty days in first week // Create placeholder elements for empty days in first week
const weekdayOfFirst = browsingDate.date(1).day(); const weekdayOfFirst = browsingDate.date(1).day();
const currentDate = minDate.utcOffset(browsingDate.utcOffset()); const currentDate = minDate.utcOffset(browsingDate.utcOffset());
const availableDates = (includedDates: string[] | undefined) => { const availableDates = (includedDates: string[] | undefined | null) => {
const dates = []; const dates = [];
const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate));
for ( for (
@ -148,6 +148,21 @@ const Days = ({
days.push(date); days.push(date);
} }
const daysToRenderForTheMonth = days.map((day) => {
if (!day) return { day: null, disabled: true };
return {
day: day,
disabled:
(includedDates && !includedDates.includes(yyyymmdd(day))) || excludedDates.includes(yyyymmdd(day)),
};
});
useHandleInitialDateSelection({
daysToRenderForTheMonth,
selected,
onChange: props.onChange,
});
const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow); const [selectedDatesAndTimes] = useBookerStore((state) => [state.selectedDatesAndTimes], shallow);
const isActive = (day: dayjs.Dayjs) => { const isActive = (day: dayjs.Dayjs) => {
@ -177,7 +192,7 @@ const Days = ({
return ( return (
<> <>
{days.map((day, idx) => ( {daysToRenderForTheMonth.map(({ day, disabled }, idx) => (
<div key={day === null ? `e-${idx}` : `day-${day.format()}`} className="relative w-full pt-[100%]"> <div key={day === null ? `e-${idx}` : `day-${day.format()}`} className="relative w-full pt-[100%]">
{day === null ? ( {day === null ? (
<div key={`e-${idx}`} /> <div key={`e-${idx}`} />
@ -194,10 +209,7 @@ const Days = ({
onClick={() => { onClick={() => {
props.onChange(day); props.onChange(day);
}} }}
disabled={ disabled={disabled}
(includedDates && !includedDates.includes(yyyymmdd(day))) ||
excludedDates.includes(yyyymmdd(day))
}
active={isActive(day)} active={isActive(day)}
/> />
)} )}
@ -293,4 +305,41 @@ const DatePicker = ({
); );
}; };
/**
* Takes care of selecting a valid date in the month if the selected date is not available in the month
*/
const useHandleInitialDateSelection = ({
daysToRenderForTheMonth,
selected,
onChange,
}: {
daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[];
selected: Dayjs | Dayjs[] | null | undefined;
onChange: (date: Dayjs | null) => void;
}) => {
// Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
if (selected instanceof Array) {
return;
}
const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
const isSelectedDateAvailable = selected
? daysToRenderForTheMonth.some(({ day, disabled }) => {
if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
})
: false;
if (firstAvailableDateOfTheMonth) {
// If selected date not available in the month, select the first available date of the month
if (!isSelectedDateAvailable) {
onChange(firstAvailableDateOfTheMonth);
}
} else {
// No date is available and if we were asked to select something inform that it couldn't be selected. This would actually help in not showing the timeslots section(with No Time Available) when no date in the month is available
if (selected) {
onChange(null);
}
}
};
export default DatePicker; export default DatePicker;

View File

@ -1,10 +1,16 @@
/* Schedule any workflow reminder that falls within 72 hours for email */ /* Schedule any workflow reminder that falls within 72 hours for email */
import type { Prisma } from "@prisma/client";
import client from "@sendgrid/client"; import client from "@sendgrid/client";
import sgMail from "@sendgrid/mail"; import sgMail from "@sendgrid/mail";
import { createEvent } from "ics";
import type { DateArray } from "ics";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import { RRule } from "rrule";
import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { parseRecurringEvent } from "@calcom/lib";
import { defaultHandler } from "@calcom/lib/server"; import { defaultHandler } from "@calcom/lib/server";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
@ -20,6 +26,65 @@ const senderEmail = process.env.SENDGRID_EMAIL as string;
sgMail.setApiKey(sendgridAPIKey); sgMail.setApiKey(sendgridAPIKey);
type Booking = Prisma.BookingGetPayload<{
include: {
eventType: true;
user: true;
attendees: true;
};
}>;
function getiCalEventAsString(booking: Booking) {
let recurrenceRule: string | undefined = undefined;
const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
if (recurringEvent?.count) {
recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", "");
}
const uid = uuidv4();
const icsEvent = createEvent({
uid,
startInputType: "utc",
start: dayjs(booking.startTime.toISOString() || "")
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
duration: {
minutes: dayjs(booking.endTime.toISOString() || "").diff(
dayjs(booking.startTime.toISOString() || ""),
"minute"
),
},
title: booking.eventType?.title || "",
description: booking.description || "",
location: booking.location || "",
organizer: {
email: booking.user?.email || "",
name: booking.user?.name || "",
},
attendees: [
{
name: booking.attendees[0].name,
email: booking.attendees[0].email,
partstat: "ACCEPTED",
role: "REQ-PARTICIPANT",
rsvp: true,
},
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
async function handler(req: NextApiRequest, res: NextApiResponse) { async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey; const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) { if (process.env.CRON_API_KEY !== apiKey) {
@ -258,6 +323,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
enable: sandboxMode, enable: sandboxMode,
}, },
}, },
attachments: reminder.workflowStep.includeCalendarEvent
? [
{
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
filename: "event.ics",
type: "text/calendar; method=REQUEST",
disposition: "attachment",
contentId: uuidv4(),
},
]
: undefined,
}); });
} }

View File

@ -113,6 +113,7 @@ export default function WorkflowDetailsPage(props: Props) {
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID, sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME, senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
numberVerificationPending: false, numberVerificationPending: false,
includeCalendarEvent: false,
}; };
steps?.push(step); steps?.push(step);
form.setValue("steps", steps); form.setValue("steps", steps);

View File

@ -861,6 +861,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""} {form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
</p> </p>
)} )}
{isEmailSubjectNeeded && (
<div className="mt-2">
<Controller
name={`steps.${step.stepNumber - 1}.includeCalendarEvent`}
control={form.control}
render={() => (
<CheckboxField
disabled={props.readOnly}
defaultChecked={
form.getValues(`steps.${step.stepNumber - 1}.includeCalendarEvent`) || false
}
description={t("include_calendar_event")}
onChange={(e) =>
form.setValue(
`steps.${step.stepNumber - 1}.includeCalendarEvent`,
e.target.checked
)
}
/>
)}
/>
</div>
)}
{!props.readOnly && ( {!props.readOnly && (
<div className="mt-3 "> <div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}> <button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>

View File

@ -1,8 +1,14 @@
import client from "@sendgrid/client"; import client from "@sendgrid/client";
import type { MailData } from "@sendgrid/helpers/classes/mail"; import type { MailData } from "@sendgrid/helpers/classes/mail";
import sgMail from "@sendgrid/mail"; import sgMail from "@sendgrid/mail";
import { createEvent } from "ics";
import type { ParticipationStatus } from "ics";
import type { DateArray } from "ics";
import { RRule } from "rrule";
import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { TimeUnit } from "@calcom/prisma/enums"; import type { TimeUnit } from "@calcom/prisma/enums";
@ -42,6 +48,47 @@ async function getBatchId() {
return batchIdResponse[1].batch_id as string; return batchIdResponse[1].batch_id as string;
} }
function getiCalEventAsString(evt: BookingInfo, status?: ParticipationStatus) {
const uid = uuidv4();
let recurrenceRule: string | undefined = undefined;
if (evt.eventType.recurringEvent?.count) {
recurrenceRule = new RRule(evt.eventType.recurringEvent).toString().replace("RRULE:", "");
}
const icsEvent = createEvent({
uid,
startInputType: "utc",
start: dayjs(evt.startTime)
.utc()
.toArray()
.slice(0, 6)
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
duration: { minutes: dayjs(evt.endTime).diff(dayjs(evt.startTime), "minute") },
title: evt.title,
description: evt.additionalNotes || "",
location: evt.location || "",
organizer: { email: evt.organizer.email || "", name: evt.organizer.name },
attendees: [
{
name: preprocessNameFieldDataWithVariant("fullName", evt.attendees[0].name) as string,
email: evt.attendees[0].email,
partstat: status,
role: "REQ-PARTICIPANT",
rsvp: true,
},
],
method: "REQUEST",
...{ recurrenceRule },
status: "CONFIRMED",
});
if (icsEvent.error) {
throw icsEvent.error;
}
return icsEvent.value;
}
type ScheduleEmailReminderAction = Extract< type ScheduleEmailReminderAction = Extract<
WorkflowActions, WorkflowActions,
"EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS" "EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS"
@ -62,7 +109,8 @@ export const scheduleEmailReminder = async (
template: WorkflowTemplates, template: WorkflowTemplates,
sender: string, sender: string,
hideBranding?: boolean, hideBranding?: boolean,
seatReferenceUid?: string seatReferenceUid?: string,
includeCalendarEvent?: boolean
) => { ) => {
if (action === WorkflowActions.EMAIL_ADDRESS) return; if (action === WorkflowActions.EMAIL_ADDRESS) return;
const { startTime, endTime } = evt; const { startTime, endTime } = evt;
@ -186,11 +234,19 @@ export const scheduleEmailReminder = async (
const batchId = await getBatchId(); const batchId = await getBatchId();
function sendEmail(data: Partial<MailData>) { function sendEmail(data: Partial<MailData>, triggerEvent?: WorkflowTriggerEvents) {
if (!process.env.SENDGRID_API_KEY) { if (!process.env.SENDGRID_API_KEY) {
console.info("No sendgrid API key provided, skipping email"); console.info("No sendgrid API key provided, skipping email");
return Promise.resolve(); return Promise.resolve();
} }
const status: ParticipationStatus =
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT
? "COMPLETED"
: triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED
? "DECLINED"
: "ACCEPTED";
return sgMail.send({ return sgMail.send({
to: data.to, to: data.to,
from: { from: {
@ -206,6 +262,17 @@ export const scheduleEmailReminder = async (
enable: sandboxMode, enable: sandboxMode,
}, },
}, },
attachments: includeCalendarEvent
? [
{
content: Buffer.from(getiCalEventAsString(evt, status) || "").toString("base64"),
filename: "event.ics",
type: "text/calendar; method=REQUEST",
disposition: "attachment",
contentId: uuidv4(),
},
]
: undefined,
sendAt: data.sendAt, sendAt: data.sendAt,
}); });
} }
@ -218,7 +285,7 @@ export const scheduleEmailReminder = async (
try { try {
if (!sendTo) throw new Error("No email addresses provided"); if (!sendTo) throw new Error("No email addresses provided");
const addressees = Array.isArray(sendTo) ? sendTo : [sendTo]; const addressees = Array.isArray(sendTo) ? sendTo : [sendTo];
const promises = addressees.map((email) => sendEmail({ to: email })); const promises = addressees.map((email) => sendEmail({ to: email }, triggerEvent));
// TODO: Maybe don't await for this? // TODO: Maybe don't await for this?
await Promise.all(promises); await Promise.all(promises);
} catch (error) { } catch (error) {
@ -237,10 +304,13 @@ export const scheduleEmailReminder = async (
) { ) {
try { try {
// If sendEmail failed then workflowReminer will not be created, failing E2E tests // If sendEmail failed then workflowReminer will not be created, failing E2E tests
await sendEmail({ await sendEmail(
to: sendTo, {
sendAt: scheduledDate.unix(), to: sendTo,
}); sendAt: scheduledDate.unix(),
},
triggerEvent
);
await prisma.workflowReminder.create({ await prisma.workflowReminder.create({
data: { data: {
bookingUid: uid, bookingUid: uid,

View File

@ -106,7 +106,8 @@ const processWorkflowStep = async (
step.template, step.template,
step.sender || SENDER_NAME, step.sender || SENDER_NAME,
hideBranding, hideBranding,
seatReferenceUid seatReferenceUid,
step.includeCalendarEvent
); );
} else if (isWhatsappAction(step.action)) { } else if (isWhatsappAction(step.action)) {
const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo; const sendTo = step.action === WorkflowActions.WHATSAPP_ATTENDEE ? smsReminderNumber : step.sendTo;

View File

@ -7,7 +7,7 @@ import type { TimeUnit } from "@calcom/prisma/enums";
import { WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums"; import { WorkflowTemplates, WorkflowActions, WorkflowMethods } from "@calcom/prisma/enums";
import { WorkflowTriggerEvents } from "@calcom/prisma/enums"; import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { CalEventResponses } from "@calcom/types/Calendar"; import type { CalEventResponses, RecurringEvent } from "@calcom/types/Calendar";
import { getSenderId } from "../alphanumericSenderIdSupport"; import { getSenderId } from "../alphanumericSenderIdSupport";
import * as twilio from "./smsProviders/twilioProvider"; import * as twilio from "./smsProviders/twilioProvider";
@ -44,6 +44,7 @@ export type BookingInfo = {
}; };
eventType: { eventType: {
slug?: string; slug?: string;
recurringEvent?: RecurringEvent | null;
}; };
startTime: string; startTime: string;
endTime: string; endTime: string;

View File

@ -64,6 +64,7 @@ const formSchema = z.object({
emailSubject: z.string().nullable(), emailSubject: z.string().nullable(),
template: z.nativeEnum(WorkflowTemplates), template: z.nativeEnum(WorkflowTemplates),
numberRequired: z.boolean().nullable(), numberRequired: z.boolean().nullable(),
includeCalendarEvent: z.boolean().nullable(),
sendTo: z sendTo: z
.string() .string()
.refine((val) => isValidPhoneNumber(val) || val.includes("@")) .refine((val) => isValidPhoneNumber(val) || val.includes("@"))

View File

@ -8,7 +8,6 @@ import type { ControlProps } from "react-select";
import { components } from "react-select"; import { components } from "react-select";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { AvailableTimes } from "@calcom/features/bookings"; import { AvailableTimes } from "@calcom/features/bookings";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
@ -229,10 +228,8 @@ const EmailEmbed = ({ eventType, username }: { eventType?: EventType; username:
<div className="text-default text-sm">{t("select_date")}</div> <div className="text-default text-sm">{t("select_date")}</div>
<DatePicker <DatePicker
isLoading={schedule.isLoading} isLoading={schedule.isLoading}
onChange={(date: Dayjs) => { onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
setSelectedDate(date.format("YYYY-MM-DD")); onMonthChange={(date) => {
}}
onMonthChange={(date: Dayjs) => {
setMonth(date.format("YYYY-MM")); setMonth(date.format("YYYY-MM"));
setSelectedDate(date.format("YYYY-MM-DD")); setSelectedDate(date.format("YYYY-MM-DD"));
}} }}

View File

@ -24,7 +24,10 @@ export const getFullName = (name: string | { firstName: string; lastName?: strin
if (typeof name === "string") { if (typeof name === "string") {
nameString = name; nameString = name;
} else { } else {
nameString = name.firstName + " " + name.lastName; nameString = name.firstName;
if (name.lastName) {
nameString = nameString + " " + name.lastName;
}
} }
return nameString; return nameString;
}; };

View File

@ -51,7 +51,11 @@ const DateOverrideForm = ({
const [selectedDates, setSelectedDates] = useState<Dayjs[]>(value ? [dayjs.utc(value[0].start)] : []); const [selectedDates, setSelectedDates] = useState<Dayjs[]>(value ? [dayjs.utc(value[0].start)] : []);
const onDateChange = (newDate: Dayjs) => { const onDateChange = (newDate: Dayjs | null) => {
// If no date is selected, do nothing
if (!newDate) {
return;
}
// If clicking on a selected date unselect it // If clicking on a selected date unselect it
if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) { if (selectedDates.some((date) => yyyymmdd(date) === yyyymmdd(newDate))) {
setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate))); setSelectedDates(selectedDates.filter((date) => yyyymmdd(date) !== yyyymmdd(newDate)));

View File

@ -3,7 +3,7 @@ import { useMemo } from "react";
import type { Slots } from "../use-schedule"; import type { Slots } from "../use-schedule";
export const getNonEmptyScheduleDays = (slots?: Slots) => { export const getNonEmptyScheduleDays = (slots?: Slots) => {
if (typeof slots === "undefined") return []; if (typeof slots === "undefined") return null;
return Object.keys(slots).filter((day) => slots[day].length > 0); return Object.keys(slots).filter((day) => slots[day].length > 0);
}; };

View File

@ -10,16 +10,15 @@ export type GetSubscriberOptions = {
}; };
const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => { const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient = defaultPrisma) => {
const userId = options.teamId ? 0 : options.userId ?? 0; const userId = options.userId ?? 0;
const eventTypeId = options.eventTypeId ?? 0; const eventTypeId = options.eventTypeId ?? 0;
const teamId = options.teamId ?? 0; const teamId = options.teamId ?? 0;
// if we have userId and teamId it is a managed event type and should trigger for team and user
const allWebhooks = await prisma.webhook.findMany({ const allWebhooks = await prisma.webhook.findMany({
where: { where: {
OR: [ OR: [
{ {
userId, userId,
teamId: null,
}, },
{ {
eventTypeId, eventTypeId,

View File

@ -30,7 +30,9 @@ export type WebhookDataType = CalendarEvent &
downloadLink?: string; downloadLink?: string;
}; };
function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: string }): string { function getZapierPayload(
data: CalendarEvent & EventTypeInfo & { status?: string; createdAt: string }
): string {
const attendees = data.attendees.map((attendee) => { const attendees = data.attendees.map((attendee) => {
return { return {
name: attendee.name, name: attendee.name,
@ -69,6 +71,7 @@ function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: strin
length: data.length, length: data.length,
}, },
attendees: attendees, attendees: attendees,
createdAt: data.createdAt,
}; };
return JSON.stringify(body); return JSON.stringify(body);
} }
@ -112,7 +115,7 @@ const sendPayload = async (
/* Zapier id is hardcoded in the DB, we send the raw data for this case */ /* Zapier id is hardcoded in the DB, we send the raw data for this case */
if (appId === "zapier") { if (appId === "zapier") {
body = getZapierPayload(data); body = getZapierPayload({ ...data, createdAt });
} else if (template) { } else if (template) {
body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType); body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType);
} else { } else {

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "WorkflowStep" ADD COLUMN "includeCalendarEvent" BOOLEAN NOT NULL DEFAULT false;

View File

@ -738,6 +738,7 @@ model WorkflowStep {
numberRequired Boolean? numberRequired Boolean?
sender String? sender String?
numberVerificationPending Boolean @default(true) numberVerificationPending Boolean @default(true)
includeCalendarEvent Boolean @default(false)
@@index([workflowId]) @@index([workflowId])
} }

View File

@ -14,6 +14,7 @@ import type {
import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema"; import type { FieldType as FormBuilderFieldType } from "@calcom/features/form-builder/schema";
import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema"; import { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema";
import { isSupportedTimeZone } from "@calcom/lib/date-fns"; import { isSupportedTimeZone } from "@calcom/lib/date-fns";
@ -602,6 +603,28 @@ export const emailSchemaRefinement = (value: string) => {
return emailRegex.test(value); return emailRegex.test(value);
}; };
export const signupSchema = z.object({
username: z.string().refine((value) => !value.includes("+"), {
message: "String should not contain a plus symbol (+).",
}),
email: z.string().email(),
password: z.string().superRefine((data, ctx) => {
const isStrict = false;
const result = isPasswordValid(data, true, isStrict);
Object.keys(result).map((key: string) => {
if (!result[key as keyof typeof result]) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [key],
message: key,
});
}
});
}),
language: z.string().optional(),
token: z.string().optional(),
});
export const ZVerifyCodeInputSchema = z.object({ export const ZVerifyCodeInputSchema = z.object({
email: z.string().email(), email: z.string().email(),
code: z.string(), code: z.string(),

View File

@ -102,168 +102,172 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" }); throw new TRPCError({ code: "FORBIDDEN", message: "User isn't owner of the current booking" });
} }
if (bookingToReschedule) { if (!bookingToReschedule) return;
let event: Partial<EventType> = {};
if (bookingToReschedule.eventTypeId) { let event: Partial<EventType> = {};
event = await prisma.eventType.findFirstOrThrow({ if (bookingToReschedule.eventTypeId) {
select: { event = await prisma.eventType.findFirstOrThrow({
title: true, select: {
users: true, title: true,
schedulingType: true, users: true,
recurringEvent: true, schedulingType: true,
}, recurringEvent: true,
where: { },
id: bookingToReschedule.eventTypeId,
},
});
}
await prisma.booking.update({
where: { where: {
id: bookingToReschedule.id, id: bookingToReschedule.eventTypeId,
},
data: {
rescheduled: true,
cancellationReason,
status: BookingStatus.CANCELLED,
updatedAt: dayjs().toISOString(),
}, },
}); });
// delete scheduled jobs of previous booking
cancelScheduledJobs(bookingToReschedule);
//cancel workflow reminders of previous booking
bookingToReschedule.workflowReminders.forEach((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
const [mainAttendee] = bookingToReschedule.attendees;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
const usersToPeopleType = (
users: PersonAttendeeCommonFields[],
selectedLanguage: TFunction
): Person[] => {
return users?.map((user) => {
return {
email: user.email || "",
name: user.name || "",
username: user?.username || "",
language: { translate: selectedLanguage, locale: user.locale || "en" },
timeZone: user?.timeZone,
};
});
};
const userTranslation = await getTranslation(user.locale ?? "en", "common");
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
const builder = new CalendarEventBuilder();
builder.init({
title: bookingToReschedule.title,
type: event && event.title ? event.title : bookingToReschedule.title,
startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(),
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
organizer: userAsPeopleType,
});
const director = new CalendarEventDirector();
director.setBuilder(builder);
director.setExistingBooking(bookingToReschedule);
cancellationReason && director.setCancellationReason(cancellationReason);
if (event) {
await director.buildForRescheduleEmail();
} else {
await director.buildWithoutEventTypeForRescheduleEmail();
}
// Handling calendar and videos cancellation
// This can set previous time as available, until virtual calendar is done
const credentials = await getUsersCredentials(user.id);
const credentialsMap = new Map();
credentials.forEach((credential) => {
credentialsMap.set(credential.type, credential);
});
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
credentialsMap.has(ref.type)
);
bookingRefsFiltered.forEach(async (bookingRef) => {
if (bookingRef.uid) {
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
}
});
// Send emails
await sendRequestRescheduleEmail(builder.calendarEvent, {
rescheduleLink: builder.rescheduleLink,
});
const evt: CalendarEvent = {
title: bookingToReschedule?.title,
type: event && event.title ? event.title : bookingToReschedule.title,
description: bookingToReschedule?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
...getCalEventResponses({
booking: bookingToReschedule,
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
}),
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
organizer: userAsPeopleType,
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
uid: bookingToReschedule?.uid,
location: bookingToReschedule?.location,
destinationCalendar:
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
};
// Send webhook
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: bookingToReschedule.eventType?.teamId ?? null },
parentId: bookingToReschedule?.eventType?.parentId ?? null,
},
});
// Send Webhook call if hooked to BOOKING.CANCELLED
const subscriberOptions = {
userId: bookingToReschedule.userId,
eventTypeId: bookingToReschedule.eventTypeId as number,
triggerEvent: eventTrigger,
teamId,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
}).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
})
);
await Promise.all(promises);
} }
await prisma.booking.update({
where: {
id: bookingToReschedule.id,
},
data: {
rescheduled: true,
cancellationReason,
status: BookingStatus.CANCELLED,
updatedAt: dayjs().toISOString(),
},
});
// delete scheduled jobs of previous booking
// FIXME: async fn off into the ether
cancelScheduledJobs(bookingToReschedule);
//cancel workflow reminders of previous booking
// FIXME: more async fns off into the ether
bookingToReschedule.workflowReminders.forEach((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
}
});
const [mainAttendee] = bookingToReschedule.attendees;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
const usersToPeopleType = (users: PersonAttendeeCommonFields[], selectedLanguage: TFunction): Person[] => {
return users?.map((user) => {
return {
email: user.email || "",
name: user.name || "",
username: user?.username || "",
language: { translate: selectedLanguage, locale: user.locale || "en" },
timeZone: user?.timeZone,
};
});
};
const userTranslation = await getTranslation(user.locale ?? "en", "common");
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
const builder = new CalendarEventBuilder();
builder.init({
title: bookingToReschedule.title,
type: event && event.title ? event.title : bookingToReschedule.title,
startTime: bookingToReschedule.startTime.toISOString(),
endTime: bookingToReschedule.endTime.toISOString(),
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
organizer: userAsPeopleType,
});
const director = new CalendarEventDirector();
director.setBuilder(builder);
director.setExistingBooking(bookingToReschedule);
cancellationReason && director.setCancellationReason(cancellationReason);
if (event) {
await director.buildForRescheduleEmail();
} else {
await director.buildWithoutEventTypeForRescheduleEmail();
}
// Handling calendar and videos cancellation
// This can set previous time as available, until virtual calendar is done
const credentials = await getUsersCredentials(user.id);
const credentialsMap = new Map();
credentials.forEach((credential) => {
credentialsMap.set(credential.type, credential);
});
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter((ref) =>
credentialsMap.has(ref.type)
);
// FIXME: error-handling
await Promise.allSettled(
bookingRefsFiltered.map(async (bookingRef) => {
if (!bookingRef.uid) return;
if (bookingRef.type.endsWith("_calendar")) {
const calendar = await getCalendar(credentialsMap.get(bookingRef.type));
return calendar?.deleteEvent(bookingRef.uid, builder.calendarEvent, bookingRef.externalCalendarId);
} else if (bookingRef.type.endsWith("_video")) {
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
}
})
);
// Send emails
await sendRequestRescheduleEmail(builder.calendarEvent, {
rescheduleLink: builder.rescheduleLink,
});
const evt: CalendarEvent = {
title: bookingToReschedule?.title,
type: event && event.title ? event.title : bookingToReschedule.title,
description: bookingToReschedule?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
...getCalEventResponses({
booking: bookingToReschedule,
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
}),
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
organizer: userAsPeopleType,
attendees: usersToPeopleType(
// username field doesn't exists on attendee but could be in the future
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
tAttendees
),
uid: bookingToReschedule?.uid,
location: bookingToReschedule?.location,
destinationCalendar: bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
};
// Send webhook
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
const teamId = await getTeamIdFromEventType({
eventType: {
team: { id: bookingToReschedule.eventType?.teamId ?? null },
parentId: bookingToReschedule?.eventType?.parentId ?? null,
},
});
const triggerForUser = !teamId || (teamId && bookingToReschedule.eventType?.parentId);
// Send Webhook call if hooked to BOOKING.CANCELLED
const subscriberOptions = {
userId: triggerForUser ? bookingToReschedule.userId : null,
eventTypeId: bookingToReschedule.eventTypeId as number,
triggerEvent: eventTrigger,
teamId,
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) =>
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
...evt,
smsReminderNumber: bookingToReschedule.smsReminderNumber || undefined,
}).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
})
);
await Promise.all(promises);
}; };

View File

@ -272,7 +272,7 @@ export const getByViewerHandler = async ({ ctx, input }: GetByViewerOptions) =>
...group.metadata, ...group.metadata,
teamId: group.teamId, teamId: group.teamId,
membershipRole: group.membershipRole, membershipRole: group.membershipRole,
image: `${bookerUrl}${group.teamId ? "/team" : ""}/${group.profile.slug}/avatar.png`, image: `${bookerUrl}/${group.profile.slug}/avatar.png`,
})), })),
}; };
}; };

View File

@ -264,13 +264,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}, },
}); });
steps.forEach(async (step) => { const promiseSteps = steps.map(async (step) => {
if ( if (
step.action !== WorkflowActions.SMS_ATTENDEE && step.action !== WorkflowActions.SMS_ATTENDEE &&
step.action !== WorkflowActions.WHATSAPP_ATTENDEE step.action !== WorkflowActions.WHATSAPP_ATTENDEE
) { ) {
//as we do not have attendees phone number (user is notified about that when setting this action) //as we do not have attendees phone number (user is notified about that when setting this action)
bookingsForReminders.forEach(async (booking) => { const promiseScheduleReminders = bookingsForReminders.map(async (booking) => {
const defaultLocale = "en"; const defaultLocale = "en";
const bookingInfo = { const bookingInfo = {
uid: booking.uid, uid: booking.uid,
@ -367,29 +367,28 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
); );
} }
}); });
await Promise.all(promiseScheduleReminders);
} }
}); });
await Promise.all(promiseSteps);
} }
//create all workflow - eventtypes relationships //create all workflow - eventtypes relationships
activeOnEventTypes.forEach(async (eventType) => { await ctx.prisma.workflowsOnEventTypes.createMany({
await ctx.prisma.workflowsOnEventTypes.createMany({ data: activeOnEventTypes.map((eventType) => ({
data: { workflowId: id,
workflowId: id, eventTypeId: eventType.id,
eventTypeId: eventType.id, })),
},
});
if (eventType.children.length) {
eventType.children.forEach(async (chEventType) => {
await ctx.prisma.workflowsOnEventTypes.createMany({
data: {
workflowId: id,
eventTypeId: chEventType.id,
},
});
});
}
}); });
await Promise.all(
activeOnEventTypes.map((eventType) =>
ctx.prisma.workflowsOnEventTypes.createMany({
data: eventType.children.map((chEventType) => ({
workflowId: id,
eventTypeId: chEventType.id,
})),
})
)
);
} }
userWorkflow.steps.map(async (oldStep) => { userWorkflow.steps.map(async (oldStep) => {
@ -455,6 +454,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
senderName: newStep.senderName, senderName: newStep.senderName,
}), }),
numberVerificationPending: false, numberVerificationPending: false,
includeCalendarEvent: newStep.includeCalendarEvent,
}, },
}); });
//cancel all reminders of step and create new ones (not for newEventTypes) //cancel all reminders of step and create new ones (not for newEventTypes)
@ -465,11 +465,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}); });
//cancel all workflow reminders from steps that were edited //cancel all workflow reminders from steps that were edited
remindersToUpdate.forEach(async (reminder) => { // FIXME: async calls into ether
remindersToUpdate.forEach((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) { if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId); deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) { } else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId); deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.WHATSAPP) {
deleteScheduledWhatsappReminder(reminder.id, reminder.referenceId);
} }
}); });
const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => { const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => {
@ -497,7 +500,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
user: true, user: true,
}, },
}); });
bookingsOfEventTypes.forEach(async (booking) => { const promiseScheduleReminders = bookingsOfEventTypes.map(async (booking) => {
const defaultLocale = "en"; const defaultLocale = "en";
const bookingInfo = { const bookingInfo = {
uid: booking.uid, uid: booking.uid,
@ -594,6 +597,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
); );
} }
}); });
await Promise.all(promiseScheduleReminders);
} }
} }
}); });
@ -617,7 +621,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
return activeEventType; return activeEventType;
} }
}); });
addedSteps.forEach(async (step) => { const promiseAddedSteps = addedSteps.map(async (step) => {
if (step) { if (step) {
const { senderName, ...newStep } = step; const { senderName, ...newStep } = step;
newStep.sender = getSender({ newStep.sender = getSender({
@ -749,6 +753,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
} }
} }
}); });
await Promise.all(promiseAddedSteps);
} }
//update trigger, name, time, timeUnit //update trigger, name, time, timeUnit

View File

@ -24,6 +24,7 @@ export const ZUpdateInputSchema = z.object({
numberRequired: z.boolean().nullable(), numberRequired: z.boolean().nullable(),
sender: z.string().optional().nullable(), sender: z.string().optional().nullable(),
senderName: z.string().optional().nullable(), senderName: z.string().optional().nullable(),
includeCalendarEvent: z.boolean(),
}) })
.array(), .array(),
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS), trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),

View File

@ -3,17 +3,21 @@ import { useFormContext } from "react-hook-form";
import { Check, Circle, Info, X } from "../../icon"; import { Check, Circle, Info, X } from "../../icon";
export function HintsOrErrors<T extends FieldValues = FieldValues>(props: { type hintsOrErrorsProps = {
hintErrors?: string[]; hintErrors?: string[];
fieldName: string; fieldName: string;
t: (key: string) => string; t: (key: string) => string;
}) { };
export function HintsOrErrors<T extends FieldValues = FieldValues>({
hintErrors,
fieldName,
t,
}: hintsOrErrorsProps) {
const methods = useFormContext() as ReturnType<typeof useFormContext> | null; const methods = useFormContext() as ReturnType<typeof useFormContext> | null;
/* If there's no methods it means we're using these components outside a React Hook Form context */ /* If there's no methods it means we're using these components outside a React Hook Form context */
if (!methods) return null; if (!methods) return null;
const { formState } = methods; const { formState } = methods;
const { hintErrors, fieldName, t } = props;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
const fieldErrors: FieldErrors<T> | undefined = formState.errors[fieldName]; const fieldErrors: FieldErrors<T> | undefined = formState.errors[fieldName];