diff --git a/apps/api/lib/validations/membership.ts b/apps/api/lib/validations/membership.ts index 24740eaa52..4f6a5cbd8d 100644 --- a/apps/api/lib/validations/membership.ts +++ b/apps/api/lib/validations/membership.ts @@ -13,16 +13,18 @@ const schemaMembershipRequiredParams = z.object({ teamId: z.number(), }); -export const membershipCreateBodySchema = Membership.partial({ - accepted: true, - role: true, - disableImpersonation: true, -}).transform((v) => ({ - accepted: false, - role: MembershipRole.MEMBER, - disableImpersonation: false, - ...v, -})); +export const membershipCreateBodySchema = Membership.omit({ id: true }) + .partial({ + accepted: true, + role: true, + disableImpersonation: true, + }) + .transform((v) => ({ + accepted: false, + role: MembershipRole.MEMBER, + disableImpersonation: false, + ...v, + })); export const membershipEditBodySchema = Membership.omit({ /** To avoid complication, let's avoid updating these, instead you can delete and create a new invite */ diff --git a/apps/web/components/dialog/EditLocationDialog.tsx b/apps/web/components/dialog/EditLocationDialog.tsx index b962d55971..c1be998af3 100644 --- a/apps/web/components/dialog/EditLocationDialog.tsx +++ b/apps/web/components/dialog/EditLocationDialog.tsx @@ -382,24 +382,22 @@ export const EditLocationDialog = (props: ISetLocationDialog) => { }} /> {selectedLocation && LocationOptions} - -
- + + - -
+
diff --git a/apps/web/components/dialog/RescheduleDialog.tsx b/apps/web/components/dialog/RescheduleDialog.tsx index 6109bb88be..973bf59cbf 100644 --- a/apps/web/components/dialog/RescheduleDialog.tsx +++ b/apps/web/components/dialog/RescheduleDialog.tsx @@ -41,7 +41,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => { return ( - +
diff --git a/apps/web/components/setup/AdminUser.tsx b/apps/web/components/setup/AdminUser.tsx index 9d50b1f87a..4dce0f9934 100644 --- a/apps/web/components/setup/AdminUser.tsx +++ b/apps/web/components/setup/AdminUser.tsx @@ -57,12 +57,10 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on }), }); - const formMethods = useForm<{ - username: string; - email_address: string; - full_name: string; - password: string; - }>({ + type formSchemaType = z.infer; + + const formMethods = useForm({ + mode: "onChange", resolver: zodResolver(formSchema), }); @@ -70,7 +68,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on props.onError(); }; - const onSubmit = formMethods.handleSubmit(async (data: z.infer) => { + const onSubmit = formMethods.handleSubmit(async (data) => { props.onSubmit(); const response = await fetch("/api/auth/setup", { method: "POST", @@ -130,11 +128,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on className={classNames("my-0", longWebsiteUrl && "rounded-t-none")} onBlur={onBlur} name="username" - onChange={async (e) => { - onChange(e.target.value); - formMethods.setValue("username", e.target.value); - await formMethods.trigger("username"); - }} + onChange={(e) => onChange(e.target.value)} /> )} @@ -148,11 +142,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("full_name", e.target.value); - await formMethods.trigger("full_name"); - }} + onChange={(e) => onChange(e.target.value)} color={formMethods.formState.errors.full_name ? "warn" : ""} type="text" name="full_name" @@ -172,11 +162,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("email_address", e.target.value); - await formMethods.trigger("email_address"); - }} + onChange={(e) => onChange(e.target.value)} className="my-0" name="email_address" /> @@ -191,11 +177,7 @@ export const AdminUser = (props: { onSubmit: () => void; onError: () => void; on { - onChange(e.target.value); - formMethods.setValue("password", e.target.value); - await formMethods.trigger("password"); - }} + onChange={(e) => onChange(e.target.value)} hintErrors={["caplow", "admin_min", "num"]} name="password" className="my-0" diff --git a/apps/web/package.json b/apps/web/package.json index ad933e85a3..fc39783df1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "3.2.5", + "version": "3.2.6", "private": true, "scripts": { "analyze": "ANALYZE=true next build", diff --git a/apps/web/pages/api/auth/signup.ts b/apps/web/pages/api/auth/signup.ts index f53421fb9c..f44c231fb3 100644 --- a/apps/web/pages/api/auth/signup.ts +++ b/apps/web/pages/api/auth/signup.ts @@ -1,5 +1,4 @@ import type { NextApiRequest, NextApiResponse } from "next"; -import { z } from "zod"; import dayjs from "@calcom/dayjs"; 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 prisma from "@calcom/prisma"; import { IdentityProvider } from "@calcom/prisma/enums"; +import { signupSchema } 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) { if (req.method !== "POST") { return res.status(405).end(); diff --git a/apps/web/pages/api/recorded-daily-video.ts b/apps/web/pages/api/recorded-daily-video.ts index 116203a4f5..0ce22581a8 100644 --- a/apps/web/pages/api/recorded-daily-video.ts +++ b/apps/web/pages/api/recorded-daily-video.ts @@ -34,13 +34,17 @@ const triggerWebhook = async ({ booking: { userId: number | undefined; eventTypeId: number | null; + eventTypeParentId: number | null | undefined; teamId?: number | null; }; }) => { const eventTrigger: WebhookTriggerEvents = "RECORDING_READY"; // Send Webhook call if hooked to BOOKING.RECORDING_READY + + const triggerForUser = !booking.teamId || (booking.teamId && booking.eventTypeParentId); + const subscriberOptions = { - userId: booking.userId, + userId: triggerForUser ? booking.userId : null, eventTypeId: booking.eventTypeId, triggerEvent: eventTrigger, teamId: booking.teamId, @@ -183,6 +187,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { booking: { userId: booking?.user?.id, eventTypeId: booking.eventTypeId, + eventTypeParentId: booking.eventType?.parentId, teamId, }, }); diff --git a/apps/web/pages/signup.tsx b/apps/web/pages/signup.tsx index ea0c0a0974..c41025bc99 100644 --- a/apps/web/pages/signup.tsx +++ b/apps/web/pages/signup.tsx @@ -17,6 +17,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; +import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; 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 { ssrInit } from "../server/lib/ssr"; -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(), +const signupSchema = apiSignupSchema.extend({ 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 flags = useFlagMap(); const methods = useForm({ + mode: "onChange", resolver: zodResolver(signupSchema), defaultValues: prepopulateFormValues, }); diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index 8a2814c443..3ca81bc2c7 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -158,9 +158,10 @@ test.describe("pro user", () => { 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 Promise.all(promises); }); test("Time slots should be reserved when selected", async ({ context, page }) => { diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index cfdfca697c..52c328b982 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2030,5 +2030,6 @@ "value": "Value", "your_organization_updated_sucessfully": "Your organization updated successfully", "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/EventTypeAppContext.tsx b/packages/app-store/EventTypeAppContext.tsx index 0ddaf7a821..584d6a2fa0 100644 --- a/packages/app-store/EventTypeAppContext.tsx +++ b/packages/app-store/EventTypeAppContext.tsx @@ -5,15 +5,21 @@ export type GetAppData = (key: string) => unknown; export type SetAppData = (key: string, value: unknown) => void; type LockedIcon = JSX.Element | false | 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 = < +type AppContext = { + getAppData: GetAppData; + setAppData: SetAppData; + LockedIcon?: LockedIcon; + disabled?: Disabled; +}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const EventTypeAppContext = React.createContext({ + getAppData: () => ({}), + setAppData: () => ({}), +}); + +type SetAppDataGeneric = < TKey extends keyof z.infer, TValue extends z.infer[TKey] >( @@ -21,7 +27,7 @@ export type SetAppDataGeneric = < value: TValue ) => void; -export type GetAppDataGeneric = >( +type GetAppDataGeneric = >( key: TKey ) => z.infer[TKey]; @@ -29,7 +35,12 @@ export const useAppContextWithSchema = () => { type GetAppData = GetAppDataGeneric; type SetAppData = SetAppDataGeneric; // 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; }; export default EventTypeAppContext; diff --git a/packages/app-store/_components/AppCard.tsx b/packages/app-store/_components/AppCard.tsx index d73ba73c37..5489c1ea95 100644 --- a/packages/app-store/_components/AppCard.tsx +++ b/packages/app-store/_components/AppCard.tsx @@ -1,12 +1,11 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import Link from "next/link"; +import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import { classNames } from "@calcom/lib"; import type { RouterOutputs } from "@calcom/trpc/react"; import { Switch, Badge, Avatar } from "@calcom/ui"; -import type { SetAppDataGeneric } from "../EventTypeAppContext"; -import type { eventTypeAppCardZod } from "../eventTypeAppCardZod"; import type { CredentialOwner } from "../types"; import OmniInstallAppButton from "./OmniInstallAppButton"; @@ -16,24 +15,20 @@ export default function AppCard({ switchOnClick, switchChecked, children, - setAppData, returnTo, teamId, - disableSwitch, - LockedIcon, }: { app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner }; description?: React.ReactNode; switchChecked?: boolean; switchOnClick?: (e: boolean) => void; children?: React.ReactNode; - setAppData: SetAppDataGeneric; returnTo?: string; teamId?: number; - disableSwitch?: boolean; LockedIcon?: React.ReactNode; }) { const [animationRef] = useAutoAnimate(); + const { setAppData, LockedIcon, disabled } = useAppContextWithSchema(); return (
{ if (switchOnClick) { switchOnClick(enabled); diff --git a/packages/app-store/_components/EventTypeAppCardInterface.tsx b/packages/app-store/_components/EventTypeAppCardInterface.tsx index e312297007..8f34ce9e11 100644 --- a/packages/app-store/_components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/_components/EventTypeAppCardInterface.tsx @@ -19,7 +19,7 @@ export const EventTypeAppCard = (props: { const { app, getAppData, setAppData, LockedIcon, disabled } = props; return ( - + { - if (appId) { - if (scheduledJob.startsWith(appId)) { - if (schedule.scheduledJobs[scheduledJob]) { - 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 = []; + let scheduledJobs = booking.scheduledJobs || []; + const promises = booking.scheduledJobs.map(async (scheduledJob) => { + if (appId) { + if (scheduledJob.startsWith(appId)) { + if (schedule.scheduledJobs[scheduledJob]) { + 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 = []; + } - if (!isReschedule) { - await prisma.booking.update({ - where: { - uid: booking.uid, - }, - data: { - scheduledJobs: scheduledJobs, - }, - }); - } + if (!isReschedule) { + await prisma.booking.update({ + where: { + uid: booking.uid, + }, + data: { + scheduledJobs: scheduledJobs, + }, }); } + }); + + try { + await Promise.all(promises); } catch (error) { console.error("Error cancelling scheduled jobs", error); } diff --git a/packages/app-store/_utils/useIsAppEnabled.ts b/packages/app-store/_utils/useIsAppEnabled.ts index 7a020d30ff..c400cd4031 100644 --- a/packages/app-store/_utils/useIsAppEnabled.ts +++ b/packages/app-store/_utils/useIsAppEnabled.ts @@ -5,7 +5,7 @@ import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext"; import type { EventTypeAppCardApp } from "../types"; function useIsAppEnabled(app: EventTypeAppCardApp) { - const [getAppData, setAppData] = useAppContextWithSchema(); + const { getAppData, setAppData } = useAppContextWithSchema(); const [enabled, setEnabled] = useState(() => { if (!app.credentialOwner) { return getAppData("enabled"); diff --git a/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx b/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx index d742941000..cc81f0237e 100644 --- a/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/basecamp3/components/EventTypeAppCardInterface.tsx @@ -9,7 +9,7 @@ import { Select } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) { - const [getAppData, setAppData] = useAppContextWithSchema(); + const { getAppData } = useAppContextWithSchema(); const [enabled, setEnabled] = useState(getAppData("enabled")); const [projects, setProjects] = useState(); const [selectedProject, setSelectedProject] = useState(); @@ -32,7 +32,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ return ( { if (!e) { diff --git a/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx b/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx index 44e3ea5c72..5320587871 100644 --- a/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/fathom/components/EventTypeAppCardInterface.tsx @@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { updateEnabled(e); }} diff --git a/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx b/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx index 44e3ea5c72..5320587871 100644 --- a/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/ga4/components/EventTypeAppCardInterface.tsx @@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { updateEnabled(e); }} diff --git a/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx b/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx index 970b7bc099..b285eeedef 100644 --- a/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/giphy/components/EventTypeAppCardInterface.tsx @@ -8,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const thankYouPage = getAppData("thankYouPage"); const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app); @@ -16,11 +16,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ return ( { setShowGifSelection(e); }} diff --git a/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx b/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx index 44e3ea5c72..5320587871 100644 --- a/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/gtm/components/EventTypeAppCardInterface.tsx @@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { updateEnabled(e); }} diff --git a/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx b/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx index d8d3940c84..668523d3ad 100644 --- a/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/metapixel/components/EventTypeAppCardInterface.tsx @@ -7,16 +7,13 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( diff --git a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx index 08a6eff4ce..c9b92f441b 100644 --- a/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/paypal/components/EventTypeAppCardInterface.tsx @@ -16,7 +16,7 @@ type Option = { value: string; label: string }; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const { asPath } = useRouter(); - const [getAppData, setAppData] = useAppContextWithSchema(); + const { getAppData, setAppData } = useAppContextWithSchema(); const price = getAppData("price"); const currency = getAppData("currency"); const [selectedCurrency, setSelectedCurrency] = useState( @@ -38,7 +38,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ return ( { diff --git a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx index 63abae7450..2b24f7c4fb 100644 --- a/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/plausible/components/EventTypeAppCardInterface.tsx @@ -7,17 +7,14 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const plausibleUrl = getAppData("PLAUSIBLE_URL"); const trackingId = getAppData("trackingId"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { updateEnabled(e); }} diff --git a/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx b/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx index fc492f20c1..ceb1d451fc 100644 --- a/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/qr_code/components/EventTypeAppCardInterface.tsx @@ -11,7 +11,7 @@ import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) { const { t } = useLocale(); - const [_getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { disabled } = useAppContextWithSchema(); const [additionalParameters, setAdditionalParameters] = useState(""); const { enabled, updateEnabled } = useIsAppEnabled(app); @@ -37,10 +37,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ return ( { updateEnabled(e); }} diff --git a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx index bf048bf48e..e49c827a81 100644 --- a/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/stripepayment/components/EventTypeAppCardInterface.tsx @@ -15,7 +15,7 @@ type Option = { value: string; label: string }; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { const pathname = usePathname(); - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData, disabled } = useAppContextWithSchema(); const price = getAppData("price"); const currency = getAppData("currency"); const paymentOption = getAppData("paymentOption"); @@ -37,15 +37,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ .trim(); return ( { setRequirePayment(enabled); - }}> + }} + teamId={eventType.team?.id || undefined}> <> {recurringEventDefined && ( diff --git a/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx b/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx index ae14000501..d8e07f9407 100644 --- a/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/templates/booking-pages-tag/components/EventTypeAppCardInterface.tsx @@ -8,13 +8,12 @@ import { TextField } from "@calcom/ui"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) { - const [getAppData, setAppData] = useAppContextWithSchema(); + const { getAppData, setAppData } = useAppContextWithSchema(); const trackingId = getAppData("trackingId"); const [enabled, setEnabled] = useState(getAppData("enabled")); return ( { if (!e) { diff --git a/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx b/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx index 85f4a6a504..c79e7ecff2 100644 --- a/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx +++ b/packages/app-store/templates/event-type-app-card/components/EventTypeAppCardInterface.tsx @@ -7,16 +7,13 @@ import { Sunrise, Sunset } from "@calcom/ui/components/icon"; import type { appDataSchema } from "../zod"; const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) { - const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema(); + const { getAppData, setAppData } = useAppContextWithSchema(); const isSunrise = getAppData("isSunrise"); const { enabled, updateEnabled } = useIsAppEnabled(app); return ( { if (!e) { updateEnabled(false); diff --git a/packages/app-store/vital/lib/reschedule.ts b/packages/app-store/vital/lib/reschedule.ts index fdfa3a3b15..53f91ccd32 100644 --- a/packages/app-store/vital/lib/reschedule.ts +++ b/packages/app-store/vital/lib/reschedule.ts @@ -121,18 +121,21 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter( (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 { - 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); - } else if (bookingRef.type.endsWith("_video")) { - return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid); - } - } - }); + await Promise.all(promises); } catch (error) { + // FIXME: error logging - non-Error type errors are currently discarded if (error instanceof Error) { logger.error(error.message); } diff --git a/packages/app-store/wipemycalother/lib/reschedule.ts b/packages/app-store/wipemycalother/lib/reschedule.ts index 2cab855fd1..a9134ffb53 100644 --- a/packages/app-store/wipemycalother/lib/reschedule.ts +++ b/packages/app-store/wipemycalother/lib/reschedule.ts @@ -121,17 +121,19 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter( (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 { - 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); - } else if (bookingRef.type.endsWith("_video")) { - return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid); - } - } - }); + await Promise.all(promises); } catch (error) { if (error instanceof Error) { logger.error(error.message); diff --git a/packages/atoms/build.mjs b/packages/atoms/build.mjs index bd5c138cf6..c5db37a1b7 100644 --- a/packages/atoms/build.mjs +++ b/packages/atoms/build.mjs @@ -12,7 +12,7 @@ const libraries = [ }, ]; -libraries.forEach(async (lib) => { +const promises = libraries.map(async (lib) => { await build({ build: { outDir: `./dist/${lib.fileName}`, @@ -29,3 +29,4 @@ libraries.forEach(async (lib) => { }, }); }); +await Promise.all(promises); diff --git a/packages/features/auth/lib/isPasswordValid.ts b/packages/features/auth/lib/isPasswordValid.ts index 95f431a5f7..1e56d84ba1 100644 --- a/packages/features/auth/lib/isPasswordValid.ts +++ b/packages/features/auth/lib/isPasswordValid.ts @@ -10,15 +10,11 @@ export function isPasswordValid(password: string, breakdown?: boolean, strict?: num = false, // At least one number min = false, // Eight characters, or fifteen in strict mode. 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; - for (let i = 0; i < password.length; i++) { - if (!isNaN(parseInt(password[i]))) num = true; - else { - if (password[i] === password[i].toUpperCase()) cap = true; - if (password[i] === password[i].toLowerCase()) low = true; - } - } + if (password.match(/\d/)) num = true; + if (password.match(/[a-z]/)) low = true; + if (password.match(/[A-Z]/)) cap = true; if (!breakdown) return cap && low && num && min && (strict ? admin_min : true); diff --git a/packages/features/bookings/Booker/components/DatePicker.tsx b/packages/features/bookings/Booker/components/DatePicker.tsx index 10ec92c909..d3970073db 100644 --- a/packages/features/bookings/Booker/components/DatePicker.tsx +++ b/packages/features/bookings/Booker/components/DatePicker.tsx @@ -1,6 +1,5 @@ import { shallow } from "zustand/shallow"; -import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; import { default as DatePickerComponent } from "@calcom/features/calendars/DatePicker"; import { useNonEmptyScheduleDays } from "@calcom/features/schedules"; @@ -24,13 +23,8 @@ export const DatePicker = () => { return ( { - setSelectedDate(date.format("YYYY-MM-DD")); - }} - onMonthChange={(date: Dayjs) => { - setMonth(date.format("YYYY-MM")); - setSelectedDate(date.format("YYYY-MM-DD")); - }} + onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)} + onMonthChange={(date) => setMonth(date.format("YYYY-MM"))} includedDates={nonEmptyScheduleDays} locale={i18n.language} browsingDate={month ? dayjs(month) : undefined} diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 17a2e8d86b..85c1cd5f31 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -160,7 +160,8 @@ export const useBookerStore = create((set, get) => ({ updateQueryParam("date", selectedDate ?? ""); // 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") }); updateQueryParam("month", newSelection.format("YYYY-MM")); } @@ -193,7 +194,6 @@ export const useBookerStore = create((set, get) => ({ setMonth: (month: string | null) => { set({ month, selectedTimeslot: null }); updateQueryParam("month", month ?? ""); - get().setSelectedDate(null); }, isTeamEvent: false, seatedEventData: { diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 80254098eb..4c57f42c8c 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -157,8 +157,10 @@ async function handler(req: CustomRequest) { }, }); + const triggerForUser = !teamId || (teamId && bookingToDelete.eventType?.parentId); + const subscriberOptions = { - userId: bookingToDelete.userId, + userId: triggerForUser ? bookingToDelete.userId : null, eventTypeId: bookingToDelete.eventTypeId as number, triggerEvent: eventTrigger, teamId, @@ -428,9 +430,9 @@ async function handler(req: CustomRequest) { bookingToDelete.recurringEventId && allRemainingBookings ) { - bookingToDelete.user.credentials + const promises = bookingToDelete.user.credentials .filter((credential) => credential.type.endsWith("_calendar")) - .forEach(async (credential) => { + .map(async (credential) => { const calendar = await getCalendar(credential); for (const updBooking of updatedBookings) { 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 { apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId) as Promise); } @@ -601,11 +610,13 @@ async function handler(req: CustomRequest) { }); // delete scheduled jobs of cancelled bookings + // FIXME: async calls into ether updatedBookings.forEach((booking) => { cancelScheduledJobs(booking); }); //Workflows - cancel all reminders for cancelled bookings + // FIXME: async calls into ether updatedBookings.forEach((booking) => { booking.workflowReminders.forEach((reminder) => { if (reminder.method === WorkflowMethods.EMAIL) { @@ -620,11 +631,14 @@ async function handler(req: CustomRequest) { const prismaPromises: Promise[] = [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 { - 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 }); } catch (error) { console.error("Error deleting event", error); diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 4bdd790f0b..2927709dfe 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -293,14 +293,16 @@ export async function handleConfirmation(args: { }, }); + const triggerForUser = !teamId || (teamId && booking.eventType?.parentId); + const subscribersBookingCreated = await getWebhooks({ - userId: booking.userId, + userId: triggerForUser ? booking.userId : null, eventTypeId: booking.eventTypeId, triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, teamId, }); const subscribersMeetingEnded = await getWebhooks({ - userId: booking.userId, + userId: triggerForUser ? booking.userId : null, eventTypeId: booking.eventTypeId, triggerEvent: WebhookTriggerEvents.MEETING_ENDED, teamId: booking.eventType?.teamId, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index d6dd49199c..1aa34e2d3e 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1126,8 +1126,10 @@ async function handler( const teamId = await getTeamIdFromEventType({ eventType }); + const triggerForUser = !teamId || (teamId && eventType.parentId); + const subscriberOptions: GetSubscriberOptions = { - userId: organizerUser.id, + userId: triggerForUser ? organizerUser.id : null, eventTypeId, triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, teamId, @@ -1140,7 +1142,7 @@ async function handler( subscriberOptions.triggerEvent = eventTrigger; const subscriberOptionsMeetingEnded = { - userId: organizerUser.id, + userId: triggerForUser ? organizerUser.id : null, eventTypeId, triggerEvent: WebhookTriggerEvents.MEETING_ENDED, teamId, diff --git a/packages/features/calendars/DatePicker.tsx b/packages/features/calendars/DatePicker.tsx index bb4485f5ea..e4bfda0d1a 100644 --- a/packages/features/calendars/DatePicker.tsx +++ b/packages/features/calendars/DatePicker.tsx @@ -16,7 +16,7 @@ export type DatePickerProps = { /** 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; /** Fires whenever a selected date is changed. */ - onChange: (date: Dayjs) => void; + onChange: (date: Dayjs | null) => void; /** Fires when the month is changed. */ onMonthChange?: (date: Dayjs) => void; /** 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"] */ excludedDates?: string[]; /** defaults to all, which dates are bookable (inverse of excludedDates) */ - includedDates?: string[]; + includedDates?: string[] | null; /** allows adding classes to the container */ className?: string; /** 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 const weekdayOfFirst = browsingDate.date(1).day(); const currentDate = minDate.utcOffset(browsingDate.utcOffset()); - const availableDates = (includedDates: string[] | undefined) => { + const availableDates = (includedDates: string[] | undefined | null) => { const dates = []; const lastDateOfMonth = browsingDate.date(daysInMonth(browsingDate)); for ( @@ -148,6 +148,21 @@ const Days = ({ 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 isActive = (day: dayjs.Dayjs) => { @@ -177,7 +192,7 @@ const Days = ({ return ( <> - {days.map((day, idx) => ( + {daysToRenderForTheMonth.map(({ day, disabled }, idx) => (
{day === null ? (
@@ -194,10 +209,7 @@ const Days = ({ onClick={() => { props.onChange(day); }} - disabled={ - (includedDates && !includedDates.includes(yyyymmdd(day))) || - excludedDates.includes(yyyymmdd(day)) - } + disabled={disabled} 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; diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index f6ac593d94..b63b1ca47c 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -1,10 +1,16 @@ /* Schedule any workflow reminder that falls within 72 hours for email */ +import type { Prisma } from "@prisma/client"; import client from "@sendgrid/client"; import sgMail from "@sendgrid/mail"; +import { createEvent } from "ics"; +import type { DateArray } from "ics"; import type { NextApiRequest, NextApiResponse } from "next"; +import { RRule } from "rrule"; +import { v4 as uuidv4 } from "uuid"; import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { parseRecurringEvent } from "@calcom/lib"; import { defaultHandler } from "@calcom/lib/server"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; @@ -20,6 +26,65 @@ const senderEmail = process.env.SENDGRID_EMAIL as string; 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) { const apiKey = req.headers.authorization || req.query.apiKey; if (process.env.CRON_API_KEY !== apiKey) { @@ -258,6 +323,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { 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, }); } diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx index 4760d25175..a5950cc49c 100644 --- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx +++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx @@ -113,6 +113,7 @@ export default function WorkflowDetailsPage(props: Props) { sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID, senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME, numberVerificationPending: false, + includeCalendarEvent: false, }; steps?.push(step); form.setValue("steps", steps); diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 228c890c9e..65f9e14ac5 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -861,6 +861,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { {form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}

)} + {isEmailSubjectNeeded && ( +
+ ( + + form.setValue( + `steps.${step.stepNumber - 1}.includeCalendarEvent`, + e.target.checked + ) + } + /> + )} + /> +
+ )} {!props.readOnly && (