Merge branch 'main' into integromat-app
This commit is contained in:
commit
cc252433d8
|
@ -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 */
|
||||
|
|
|
@ -382,24 +382,22 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||
}}
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<DialogFooter>
|
||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
setEditingLocationType?.("");
|
||||
locationFormMethods.unregister(["locationType", "locationLink"]);
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
setEditingLocationType?.("");
|
||||
locationFormMethods.unregister(["locationType", "locationLink"]);
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button data-testid="update-location" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button data-testid="update-location" type="submit">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -41,7 +41,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent>
|
||||
<DialogContent enableOverflow>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Clock className="m-auto h-6 w-6" />
|
||||
|
|
|
@ -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<typeof formSchema>;
|
||||
|
||||
const formMethods = useForm<formSchemaType>({
|
||||
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<typeof formSchema>) => {
|
||||
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
|
|||
<TextField
|
||||
value={value || ""}
|
||||
onBlur={onBlur}
|
||||
onChange={async (e) => {
|
||||
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
|
|||
<EmailField
|
||||
value={value || ""}
|
||||
onBlur={onBlur}
|
||||
onChange={async (e) => {
|
||||
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
|
|||
<PasswordField
|
||||
value={value || ""}
|
||||
onBlur={onBlur}
|
||||
onChange={async (e) => {
|
||||
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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.2.5",
|
||||
"version": "3.2.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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<FormValues>({
|
||||
mode: "onChange",
|
||||
resolver: zodResolver(signupSchema),
|
||||
defaultValues: prepopulateFormValues,
|
||||
});
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
|
|
@ -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<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>,
|
||||
TValue extends z.infer<TAppData>[TKey]
|
||||
>(
|
||||
|
@ -21,7 +27,7 @@ export type SetAppDataGeneric<TAppData extends ZodType> = <
|
|||
value: TValue
|
||||
) => 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
|
||||
) => z.infer<TAppData>[TKey];
|
||||
|
||||
|
@ -29,7 +35,12 @@ export const useAppContextWithSchema = <TAppData extends ZodType>() => {
|
|||
type GetAppData = GetAppDataGeneric<TAppData>;
|
||||
type SetAppData = SetAppDataGeneric<TAppData>;
|
||||
// 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;
|
||||
|
|
|
@ -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<typeof eventTypeAppCardZod>;
|
||||
returnTo?: string;
|
||||
teamId?: number;
|
||||
disableSwitch?: boolean;
|
||||
LockedIcon?: React.ReactNode;
|
||||
}) {
|
||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { setAppData, LockedIcon, disabled } = useAppContextWithSchema();
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -92,7 +87,7 @@ export default function AppCard({
|
|||
{app?.isInstalled || app.credentialOwner ? (
|
||||
<div className="ml-auto flex items-center">
|
||||
<Switch
|
||||
disabled={!app.enabled || disableSwitch}
|
||||
disabled={!app.enabled || disabled}
|
||||
onCheckedChange={(enabled) => {
|
||||
if (switchOnClick) {
|
||||
switchOnClick(enabled);
|
||||
|
|
|
@ -19,7 +19,7 @@ export const EventTypeAppCard = (props: {
|
|||
const { app, getAppData, setAppData, LockedIcon, disabled } = props;
|
||||
return (
|
||||
<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
|
||||
slug={app.slug === "stripe" ? "stripepayment" : app.slug}
|
||||
componentMap={EventTypeAddonMap}
|
||||
|
|
|
@ -125,8 +125,13 @@ export async function deleteSubscription({
|
|||
export async function listBookings(appApiKey: ApiKey) {
|
||||
try {
|
||||
const where: Prisma.BookingWhereInput = {};
|
||||
if (appApiKey.teamId) where.eventType = { teamId: appApiKey.teamId };
|
||||
else where.userId = appApiKey.userId;
|
||||
if (appApiKey.teamId) {
|
||||
where.eventType = {
|
||||
OR: [{ teamId: appApiKey.teamId }, { parent: { teamId: appApiKey.teamId } }],
|
||||
};
|
||||
} else {
|
||||
where.userId = appApiKey.userId;
|
||||
}
|
||||
const bookings = await prisma.booking.findMany({
|
||||
take: 3,
|
||||
where: {
|
||||
|
@ -206,6 +211,7 @@ export async function scheduleTrigger(
|
|||
) {
|
||||
try {
|
||||
//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(
|
||||
`${subscriber.appId}_${subscriber.id}`,
|
||||
booking.endTime,
|
||||
|
@ -253,38 +259,39 @@ export async function cancelScheduledJobs(
|
|||
appId?: string | null,
|
||||
isReschedule?: boolean
|
||||
) {
|
||||
try {
|
||||
let scheduledJobs = booking.scheduledJobs || [];
|
||||
if (!booking.scheduledJobs) return;
|
||||
|
||||
if (booking.scheduledJobs) {
|
||||
booking.scheduledJobs.forEach(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 = [];
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Select } from "@calcom/ui";
|
|||
import type { appDataSchema } from "../zod";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { getAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
||||
const [projects, setProjects] = useState();
|
||||
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
|
||||
|
@ -32,7 +32,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
switchOnClick={(e) => {
|
||||
if (!e) {
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const thankYouPage = getAppData("thankYouPage");
|
||||
const { enabled: showGifSelection, updateEnabled: setShowGifSelection } = useIsAppEnabled(app);
|
||||
|
||||
|
@ -16,11 +16,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
description={t("confirmation_page_gif")}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
setShowGifSelection(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={updateEnabled}
|
||||
switchChecked={enabled}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
|
|
|
@ -16,7 +16,7 @@ type Option = { value: string; label: string };
|
|||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const { asPath } = useRouter();
|
||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
const currency = getAppData("currency");
|
||||
const [selectedCurrency, setSelectedCurrency] = useState(
|
||||
|
@ -38,7 +38,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
return (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + asPath}
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
switchChecked={requirePayment}
|
||||
switchOnClick={(enabled) => {
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const plausibleUrl = getAppData("PLAUSIBLE_URL");
|
||||
const trackingId = getAppData("trackingId");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const [additionalParameters, setAdditionalParameters] = useState("");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
|
@ -37,10 +37,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const price = getAppData("price");
|
||||
const currency = getAppData("currency");
|
||||
const paymentOption = getAppData("paymentOption");
|
||||
|
@ -37,15 +37,13 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||
.trim();
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={WEBAPP_URL + pathname}
|
||||
setAppData={setAppData}
|
||||
returnTo={WEBAPP_URL + pathname + "?tabName=apps"}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchChecked={requirePayment}
|
||||
switchOnClick={(enabled) => {
|
||||
setRequirePayment(enabled);
|
||||
}}>
|
||||
}}
|
||||
teamId={eventType.team?.id || undefined}>
|
||||
<>
|
||||
{recurringEventDefined && (
|
||||
<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";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const trackingId = getAppData("trackingId");
|
||||
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
switchOnClick={(e) => {
|
||||
if (!e) {
|
||||
|
|
|
@ -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<typeof appDataSchema>();
|
||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||
const isSunrise = getAppData("isSunrise");
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
setAppData={setAppData}
|
||||
app={app}
|
||||
disableSwitch={disabled}
|
||||
LockedIcon={LockedIcon}
|
||||
switchOnClick={(e) => {
|
||||
if (!e) {
|
||||
updateEnabled(false);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<DatePickerComponent
|
||||
isLoading={schedule.isLoading}
|
||||
onChange={(date: Dayjs) => {
|
||||
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}
|
||||
|
|
|
@ -160,7 +160,8 @@ export const useBookerStore = create<BookerStore>((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<BookerStore>((set, get) => ({
|
|||
setMonth: (month: string | null) => {
|
||||
set({ month, selectedTimeslot: null });
|
||||
updateQueryParam("month", month ?? "");
|
||||
get().setSelectedDate(null);
|
||||
},
|
||||
isTeamEvent: false,
|
||||
seatedEventData: {
|
||||
|
|
|
@ -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<unknown>);
|
||||
}
|
||||
|
@ -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<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 {
|
||||
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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => (
|
||||
<div key={day === null ? `e-${idx}` : `day-${day.format()}`} className="relative w-full pt-[100%]">
|
||||
{day === null ? (
|
||||
<div key={`e-${idx}`} />
|
||||
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -861,6 +861,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
|
||||
</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 && (
|
||||
<div className="mt-3 ">
|
||||
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import client from "@sendgrid/client";
|
||||
import type { MailData } from "@sendgrid/helpers/classes/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 { preprocessNameFieldDataWithVariant } from "@calcom/features/form-builder/utils";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
|
@ -42,6 +48,47 @@ async function getBatchId() {
|
|||
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<
|
||||
WorkflowActions,
|
||||
"EMAIL_HOST" | "EMAIL_ATTENDEE" | "EMAIL_ADDRESS"
|
||||
|
@ -62,7 +109,8 @@ export const scheduleEmailReminder = async (
|
|||
template: WorkflowTemplates,
|
||||
sender: string,
|
||||
hideBranding?: boolean,
|
||||
seatReferenceUid?: string
|
||||
seatReferenceUid?: string,
|
||||
includeCalendarEvent?: boolean
|
||||
) => {
|
||||
if (action === WorkflowActions.EMAIL_ADDRESS) return;
|
||||
const { startTime, endTime } = evt;
|
||||
|
@ -186,11 +234,19 @@ export const scheduleEmailReminder = async (
|
|||
|
||||
const batchId = await getBatchId();
|
||||
|
||||
function sendEmail(data: Partial<MailData>) {
|
||||
function sendEmail(data: Partial<MailData>, triggerEvent?: WorkflowTriggerEvents) {
|
||||
if (!process.env.SENDGRID_API_KEY) {
|
||||
console.info("No sendgrid API key provided, skipping email");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const status: ParticipationStatus =
|
||||
triggerEvent === WorkflowTriggerEvents.AFTER_EVENT
|
||||
? "COMPLETED"
|
||||
: triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED
|
||||
? "DECLINED"
|
||||
: "ACCEPTED";
|
||||
|
||||
return sgMail.send({
|
||||
to: data.to,
|
||||
from: {
|
||||
|
@ -206,6 +262,17 @@ export const scheduleEmailReminder = async (
|
|||
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,
|
||||
});
|
||||
}
|
||||
|
@ -218,7 +285,7 @@ export const scheduleEmailReminder = async (
|
|||
try {
|
||||
if (!sendTo) throw new Error("No email addresses provided");
|
||||
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?
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
|
@ -237,10 +304,13 @@ export const scheduleEmailReminder = async (
|
|||
) {
|
||||
try {
|
||||
// If sendEmail failed then workflowReminer will not be created, failing E2E tests
|
||||
await sendEmail({
|
||||
to: sendTo,
|
||||
sendAt: scheduledDate.unix(),
|
||||
});
|
||||
await sendEmail(
|
||||
{
|
||||
to: sendTo,
|
||||
sendAt: scheduledDate.unix(),
|
||||
},
|
||||
triggerEvent
|
||||
);
|
||||
await prisma.workflowReminder.create({
|
||||
data: {
|
||||
bookingUid: uid,
|
||||
|
|
|
@ -106,7 +106,8 @@ const processWorkflowStep = async (
|
|||
step.template,
|
||||
step.sender || SENDER_NAME,
|
||||
hideBranding,
|
||||
seatReferenceUid
|
||||
seatReferenceUid,
|
||||
step.includeCalendarEvent
|
||||
);
|
||||
} else if (isWhatsappAction(step.action)) {
|
||||
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 { WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
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 * as twilio from "./smsProviders/twilioProvider";
|
||||
|
@ -44,6 +44,7 @@ export type BookingInfo = {
|
|||
};
|
||||
eventType: {
|
||||
slug?: string;
|
||||
recurringEvent?: RecurringEvent | null;
|
||||
};
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
|
|
|
@ -64,6 +64,7 @@ const formSchema = z.object({
|
|||
emailSubject: z.string().nullable(),
|
||||
template: z.nativeEnum(WorkflowTemplates),
|
||||
numberRequired: z.boolean().nullable(),
|
||||
includeCalendarEvent: z.boolean().nullable(),
|
||||
sendTo: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val) || val.includes("@"))
|
||||
|
|
|
@ -8,7 +8,6 @@ import type { ControlProps } from "react-select";
|
|||
import { components } from "react-select";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { AvailableTimes } from "@calcom/features/bookings";
|
||||
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>
|
||||
<DatePicker
|
||||
isLoading={schedule.isLoading}
|
||||
onChange={(date: Dayjs) => {
|
||||
setSelectedDate(date.format("YYYY-MM-DD"));
|
||||
}}
|
||||
onMonthChange={(date: Dayjs) => {
|
||||
onChange={(date) => setSelectedDate(date ? date.format("YYYY-MM-DD") : date)}
|
||||
onMonthChange={(date) => {
|
||||
setMonth(date.format("YYYY-MM"));
|
||||
setSelectedDate(date.format("YYYY-MM-DD"));
|
||||
}}
|
||||
|
|
|
@ -24,7 +24,10 @@ export const getFullName = (name: string | { firstName: string; lastName?: strin
|
|||
if (typeof name === "string") {
|
||||
nameString = name;
|
||||
} else {
|
||||
nameString = name.firstName + " " + name.lastName;
|
||||
nameString = name.firstName;
|
||||
if (name.lastName) {
|
||||
nameString = nameString + " " + name.lastName;
|
||||
}
|
||||
}
|
||||
return nameString;
|
||||
};
|
||||
|
|
|
@ -51,7 +51,11 @@ const DateOverrideForm = ({
|
|||
|
||||
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 (selectedDates.some((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";
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
|
|
@ -10,16 +10,15 @@ export type GetSubscriberOptions = {
|
|||
};
|
||||
|
||||
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 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({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
teamId: null,
|
||||
},
|
||||
{
|
||||
eventTypeId,
|
||||
|
|
|
@ -30,7 +30,9 @@ export type WebhookDataType = CalendarEvent &
|
|||
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) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
|
@ -69,6 +71,7 @@ function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: strin
|
|||
length: data.length,
|
||||
},
|
||||
attendees: attendees,
|
||||
createdAt: data.createdAt,
|
||||
};
|
||||
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 */
|
||||
if (appId === "zapier") {
|
||||
body = getZapierPayload(data);
|
||||
body = getZapierPayload({ ...data, createdAt });
|
||||
} else if (template) {
|
||||
body = applyTemplate(template, { ...data, triggerEvent, createdAt }, contentType);
|
||||
} 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?
|
||||
sender String?
|
||||
numberVerificationPending Boolean @default(true)
|
||||
includeCalendarEvent Boolean @default(false)
|
||||
|
||||
@@index([workflowId])
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import type {
|
|||
|
||||
import { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
||||
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 { fieldsSchema as formBuilderFieldsSchema } from "@calcom/features/form-builder/schema";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/date-fns";
|
||||
|
@ -602,6 +603,28 @@ export const emailSchemaRefinement = (value: string) => {
|
|||
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({
|
||||
email: z.string().email(),
|
||||
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" });
|
||||
}
|
||||
|
||||
if (bookingToReschedule) {
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
}
|
||||
await prisma.booking.update({
|
||||
if (!bookingToReschedule) return;
|
||||
|
||||
let event: Partial<EventType> = {};
|
||||
if (bookingToReschedule.eventTypeId) {
|
||||
event = await prisma.eventType.findFirstOrThrow({
|
||||
select: {
|
||||
title: true,
|
||||
users: true,
|
||||
schedulingType: true,
|
||||
recurringEvent: true,
|
||||
},
|
||||
where: {
|
||||
id: bookingToReschedule.id,
|
||||
},
|
||||
data: {
|
||||
rescheduled: true,
|
||||
cancellationReason,
|
||||
status: BookingStatus.CANCELLED,
|
||||
updatedAt: dayjs().toISOString(),
|
||||
id: bookingToReschedule.eventTypeId,
|
||||
},
|
||||
});
|
||||
|
||||
// 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,
|
||||
teamId: group.teamId,
|
||||
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 (
|
||||
step.action !== WorkflowActions.SMS_ATTENDEE &&
|
||||
step.action !== WorkflowActions.WHATSAPP_ATTENDEE
|
||||
) {
|
||||
//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 bookingInfo = {
|
||||
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
|
||||
activeOnEventTypes.forEach(async (eventType) => {
|
||||
await ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: {
|
||||
workflowId: 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 ctx.prisma.workflowsOnEventTypes.createMany({
|
||||
data: activeOnEventTypes.map((eventType) => ({
|
||||
workflowId: id,
|
||||
eventTypeId: eventType.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) => {
|
||||
|
@ -455,6 +454,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
senderName: newStep.senderName,
|
||||
}),
|
||||
numberVerificationPending: false,
|
||||
includeCalendarEvent: newStep.includeCalendarEvent,
|
||||
},
|
||||
});
|
||||
//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
|
||||
remindersToUpdate.forEach(async (reminder) => {
|
||||
// FIXME: async calls into ether
|
||||
remindersToUpdate.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 eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => {
|
||||
|
@ -497,7 +500,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
user: true,
|
||||
},
|
||||
});
|
||||
bookingsOfEventTypes.forEach(async (booking) => {
|
||||
const promiseScheduleReminders = bookingsOfEventTypes.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
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;
|
||||
}
|
||||
});
|
||||
addedSteps.forEach(async (step) => {
|
||||
const promiseAddedSteps = addedSteps.map(async (step) => {
|
||||
if (step) {
|
||||
const { senderName, ...newStep } = step;
|
||||
newStep.sender = getSender({
|
||||
|
@ -749,6 +753,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseAddedSteps);
|
||||
}
|
||||
|
||||
//update trigger, name, time, timeUnit
|
||||
|
|
|
@ -24,6 +24,7 @@ export const ZUpdateInputSchema = z.object({
|
|||
numberRequired: z.boolean().nullable(),
|
||||
sender: z.string().optional().nullable(),
|
||||
senderName: z.string().optional().nullable(),
|
||||
includeCalendarEvent: z.boolean(),
|
||||
})
|
||||
.array(),
|
||||
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
|
||||
|
|
|
@ -3,17 +3,21 @@ import { useFormContext } from "react-hook-form";
|
|||
|
||||
import { Check, Circle, Info, X } from "../../icon";
|
||||
|
||||
export function HintsOrErrors<T extends FieldValues = FieldValues>(props: {
|
||||
type hintsOrErrorsProps = {
|
||||
hintErrors?: string[];
|
||||
fieldName: string;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
};
|
||||
|
||||
export function HintsOrErrors<T extends FieldValues = FieldValues>({
|
||||
hintErrors,
|
||||
fieldName,
|
||||
t,
|
||||
}: hintsOrErrorsProps) {
|
||||
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 (!methods) return null;
|
||||
const { formState } = methods;
|
||||
const { hintErrors, fieldName, t } = props;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const fieldErrors: FieldErrors<T> | undefined = formState.errors[fieldName];
|
||||
|
|
Loading…
Reference in New Issue
Block a user