Merge branch 'main' into integromat-app
This commit is contained in:
commit
cc252433d8
|
@ -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 */
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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")} />
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)}>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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("@"))
|
||||||
|
|
|
@ -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"));
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)));
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "WorkflowStep" ADD COLUMN "includeCalendarEvent" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -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])
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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`,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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];
|
||||||
|
|
Loading…
Reference in New Issue
Block a user