perf: Lazy load libphonenumber-js only when validation is needed. (#10812)
This commit is contained in:
@@ -4,7 +4,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -42,36 +42,23 @@ type BookEventFormProps = {
|
||||
onCancel?: () => void;
|
||||
};
|
||||
|
||||
type DefaultValues = Record<string, unknown>;
|
||||
|
||||
export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const routerQuery = useRouterQuery();
|
||||
const session = useSession();
|
||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation({
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
const removeSelectedSlot = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation({
|
||||
trpc: { context: { skipBatch: true } },
|
||||
});
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useLocale();
|
||||
const { timezone } = useTimePreferences();
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
|
||||
const username = useBookerStore((state) => state.username);
|
||||
const formValues = useBookerStore((state) => state.formValues);
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const seatedEventData = useBookerStore((state) => state.seatedEventData);
|
||||
const verifiedEmail = useBookerStore((state) => state.verifiedEmail);
|
||||
const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail);
|
||||
const isRescheduling = !!rescheduleUid && !!bookingData;
|
||||
const event = useEvent();
|
||||
const eventType = event.data;
|
||||
const eventQuery = useEvent();
|
||||
const eventType = eventQuery.data;
|
||||
|
||||
const reserveSlot = () => {
|
||||
if (eventType?.id && timeslot && (duration || eventType?.length)) {
|
||||
@@ -102,107 +89,88 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [eventType?.id, timeslot]);
|
||||
|
||||
const defaultValues = useMemo(() => {
|
||||
if (Object.keys(formValues).length) return formValues;
|
||||
const { initialValues, key } = useInitialFormValues({
|
||||
eventType,
|
||||
rescheduleUid,
|
||||
isRescheduling,
|
||||
});
|
||||
|
||||
if (!eventType?.bookingFields) {
|
||||
return {};
|
||||
}
|
||||
const querySchema = getBookingResponsesPartialSchema({
|
||||
eventType: {
|
||||
bookingFields: eventType.bookingFields,
|
||||
},
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
});
|
||||
|
||||
const parsedQuery = querySchema.parse({
|
||||
...routerQuery,
|
||||
// `guest` because we need to support legacy URL with `guest` query param support
|
||||
// `guests` because the `name` of the corresponding bookingField is `guests`
|
||||
guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"),
|
||||
});
|
||||
|
||||
const defaultUserValues = {
|
||||
email: rescheduleUid
|
||||
? bookingData?.attendees[0].email
|
||||
: parsedQuery["email"] || session.data?.user?.email || "",
|
||||
name: rescheduleUid
|
||||
? bookingData?.attendees[0].name
|
||||
: parsedQuery["name"] || session.data?.user?.name || "",
|
||||
};
|
||||
|
||||
if (!isRescheduling) {
|
||||
const defaults = {
|
||||
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
|
||||
};
|
||||
|
||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||
return {
|
||||
...responses,
|
||||
[field.name]: parsedQuery[field.name] || undefined,
|
||||
};
|
||||
}, {});
|
||||
|
||||
defaults.responses = {
|
||||
...responses,
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email,
|
||||
};
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
if ((!rescheduleUid && !bookingData) || !bookingData.attendees.length) {
|
||||
return {};
|
||||
}
|
||||
const primaryAttendee = bookingData.attendees[0];
|
||||
if (!primaryAttendee) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
responses: {} as Partial<z.infer<typeof bookingFormSchema>["responses"]>,
|
||||
};
|
||||
|
||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||
return {
|
||||
...responses,
|
||||
[field.name]: bookingData.responses[field.name],
|
||||
};
|
||||
}, {});
|
||||
defaults.responses = {
|
||||
...responses,
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email,
|
||||
};
|
||||
return defaults;
|
||||
}, [eventType?.bookingFields, formValues, isRescheduling, bookingData, rescheduleUid]);
|
||||
return (
|
||||
<BookEventFormChild
|
||||
// initialValues would be null initially as the async schema parsing is happening. Let's show the form in first render without any prefill values
|
||||
// But ensure that when initialValues is available, the form is reset and rerendered with the prefill values
|
||||
key={key}
|
||||
onCancel={onCancel}
|
||||
initialValues={initialValues}
|
||||
isRescheduling={isRescheduling}
|
||||
eventQuery={eventQuery}
|
||||
rescheduleUid={rescheduleUid}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const BookEventFormChild = ({
|
||||
onCancel,
|
||||
initialValues,
|
||||
isRescheduling,
|
||||
eventQuery,
|
||||
rescheduleUid,
|
||||
}: BookEventFormProps & {
|
||||
initialValues: DefaultValues;
|
||||
isRescheduling: boolean;
|
||||
eventQuery: ReturnType<typeof useEvent>;
|
||||
rescheduleUid: string | null;
|
||||
}) => {
|
||||
const eventType = eventQuery.data;
|
||||
const bookingFormSchema = z
|
||||
.object({
|
||||
responses: event?.data
|
||||
responses: eventQuery?.data
|
||||
? getBookingResponsesSchema({
|
||||
eventType: event?.data,
|
||||
eventType: eventQuery?.data,
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
})
|
||||
: // Fallback until event is loaded.
|
||||
z.object({}),
|
||||
})
|
||||
.passthrough();
|
||||
const searchParams = useSearchParams();
|
||||
const routerQuery = useRouterQuery();
|
||||
const setFormValues = useBookerStore((state) => state.setFormValues);
|
||||
const seatedEventData = useBookerStore((state) => state.seatedEventData);
|
||||
const verifiedEmail = useBookerStore((state) => state.verifiedEmail);
|
||||
const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail);
|
||||
const bookingSuccessRedirect = useBookingSuccessRedirect();
|
||||
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useLocale();
|
||||
const { timezone } = useTimePreferences();
|
||||
const errorRef = useRef<HTMLDivElement>(null);
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const eventSlug = useBookerStore((state) => state.eventSlug);
|
||||
const duration = useBookerStore((state) => state.selectedDuration);
|
||||
const timeslot = useBookerStore((state) => state.selectedTimeslot);
|
||||
const recurringEventCount = useBookerStore((state) => state.recurringEventCount);
|
||||
const username = useBookerStore((state) => state.username);
|
||||
type BookingFormValues = {
|
||||
locationType?: EventLocationType["type"];
|
||||
responses: z.infer<typeof bookingFormSchema>["responses"];
|
||||
responses: z.infer<typeof bookingFormSchema>["responses"] | null;
|
||||
// Key is not really part of form values, but only used to have a key
|
||||
// to set generic error messages on. Needed until RHF has implemented root error keys.
|
||||
globalError: undefined;
|
||||
};
|
||||
|
||||
const bookingForm = useForm<BookingFormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
||||
defaultValues: initialValues,
|
||||
resolver: zodResolver(
|
||||
// Since this isn't set to strict we only validate the fields in the schema
|
||||
bookingFormSchema,
|
||||
{},
|
||||
{
|
||||
// bookingFormSchema is an async schema, so inform RHF to do async validation.
|
||||
mode: "async",
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
const createBookingMutation = useMutation(createBooking, {
|
||||
onSuccess: (responseData) => {
|
||||
const { uid, paymentUid } = responseData;
|
||||
@@ -287,7 +255,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
|
||||
// It shouldn't be possible that this method is fired without having event data,
|
||||
// but since in theory (looking at the types) it is possible, we still handle that case.
|
||||
if (!event?.data) {
|
||||
if (!eventQuery?.data) {
|
||||
bookingForm.setError("globalError", { message: t("error_booking_event") });
|
||||
return;
|
||||
}
|
||||
@@ -301,8 +269,8 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
setEmailVerificationModalVisible(true);
|
||||
};
|
||||
|
||||
if (event.isError) return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||
if (event.isLoading || !event.data) return <FormSkeleton />;
|
||||
if (eventQuery.isError) return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||
if (eventQuery.isLoading || !eventQuery.data) return <FormSkeleton />;
|
||||
if (!timeslot)
|
||||
return (
|
||||
<EmptyScreen
|
||||
@@ -319,26 +287,26 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
setFormValues({});
|
||||
bookingForm.clearErrors();
|
||||
|
||||
// It shouldn't be possible that this method is fired without having event data,
|
||||
// It shouldn't be possible that this method is fired without having eventQuery data,
|
||||
// but since in theory (looking at the types) it is possible, we still handle that case.
|
||||
if (!event?.data) {
|
||||
if (!eventQuery?.data) {
|
||||
bookingForm.setError("globalError", { message: t("error_booking_event") });
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensures that duration is an allowed value, if not it defaults to the
|
||||
// default event duration.
|
||||
// default eventQuery duration.
|
||||
const validDuration =
|
||||
duration &&
|
||||
event.data.metadata?.multipleDuration &&
|
||||
event.data.metadata?.multipleDuration.includes(duration)
|
||||
eventQuery.data.metadata?.multipleDuration &&
|
||||
eventQuery.data.metadata?.multipleDuration.includes(duration)
|
||||
? duration
|
||||
: event.data.length;
|
||||
: eventQuery.data.length;
|
||||
|
||||
const bookingInput = {
|
||||
values,
|
||||
duration: validDuration,
|
||||
event: event.data,
|
||||
event: eventQuery.data,
|
||||
date: timeslot,
|
||||
timeZone: timezone,
|
||||
language: i18n.language,
|
||||
@@ -356,7 +324,7 @@ export const BookEventForm = ({ onCancel }: BookEventFormProps) => {
|
||||
),
|
||||
};
|
||||
|
||||
if (event.data?.recurringEvent?.freq && recurringEventCount) {
|
||||
if (eventQuery.data?.recurringEvent?.freq && recurringEventCount) {
|
||||
createRecurringBookingMutation.mutate(
|
||||
mapRecurringBookingToMutationInput(bookingInput, recurringEventCount)
|
||||
);
|
||||
@@ -465,3 +433,102 @@ const getError = (
|
||||
"Unknown error"
|
||||
);
|
||||
};
|
||||
|
||||
function useInitialFormValues({
|
||||
eventType,
|
||||
rescheduleUid,
|
||||
isRescheduling,
|
||||
}: {
|
||||
eventType: ReturnType<typeof useEvent>["data"];
|
||||
rescheduleUid: string | null;
|
||||
isRescheduling: boolean;
|
||||
}) {
|
||||
const [initialValues, setDefaultValues] = useState<DefaultValues>({});
|
||||
const bookingData = useBookerStore((state) => state.bookingData);
|
||||
const formValues = useBookerStore((state) => state.formValues);
|
||||
const searchParams = useSearchParams();
|
||||
const routerQuery = useRouterQuery();
|
||||
const session = useSession();
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
if (Object.keys(formValues).length) return formValues;
|
||||
|
||||
if (!eventType?.bookingFields) {
|
||||
return {};
|
||||
}
|
||||
const querySchema = getBookingResponsesPartialSchema({
|
||||
eventType: {
|
||||
bookingFields: eventType.bookingFields,
|
||||
},
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
});
|
||||
|
||||
const parsedQuery = await querySchema.parseAsync({
|
||||
...routerQuery,
|
||||
// `guest` because we need to support legacy URL with `guest` query param support
|
||||
// `guests` because the `name` of the corresponding bookingField is `guests`
|
||||
guests: searchParams?.getAll("guests") || searchParams?.getAll("guest"),
|
||||
});
|
||||
|
||||
const defaultUserValues = {
|
||||
email: rescheduleUid
|
||||
? bookingData?.attendees[0].email
|
||||
: parsedQuery["email"] || session.data?.user?.email || "",
|
||||
name: rescheduleUid
|
||||
? bookingData?.attendees[0].name
|
||||
: parsedQuery["name"] || session.data?.user?.name || "",
|
||||
};
|
||||
|
||||
if (!isRescheduling) {
|
||||
const defaults = {
|
||||
responses: {} as Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>,
|
||||
};
|
||||
|
||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||
return {
|
||||
...responses,
|
||||
[field.name]: parsedQuery[field.name] || undefined,
|
||||
};
|
||||
}, {});
|
||||
|
||||
defaults.responses = {
|
||||
...responses,
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email,
|
||||
};
|
||||
|
||||
setDefaultValues(defaults);
|
||||
}
|
||||
|
||||
if ((!rescheduleUid && !bookingData) || !bookingData?.attendees.length) {
|
||||
return {};
|
||||
}
|
||||
const primaryAttendee = bookingData.attendees[0];
|
||||
if (!primaryAttendee) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
responses: {} as Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>,
|
||||
};
|
||||
|
||||
const responses = eventType.bookingFields.reduce((responses, field) => {
|
||||
return {
|
||||
...responses,
|
||||
[field.name]: bookingData.responses[field.name],
|
||||
};
|
||||
}, {});
|
||||
defaults.responses = {
|
||||
...responses,
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email,
|
||||
};
|
||||
setDefaultValues(defaults);
|
||||
})();
|
||||
}, [eventType?.bookingFields, formValues, isRescheduling, bookingData, rescheduleUid]);
|
||||
|
||||
// When initialValues is available(after doing async schema parsing) or session is available(so that we can prefill logged-in user email and name), we need to reset the form with the initialValues
|
||||
const key = `${Object.keys(initialValues).length}_${session ? 1 : 0}`;
|
||||
|
||||
return { initialValues, key };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import z from "zod";
|
||||
|
||||
import type { ALL_VIEWS } from "@calcom/features/form-builder/schema";
|
||||
@@ -106,18 +105,21 @@ function preprocess<T extends z.ZodType>({
|
||||
});
|
||||
return newResponses;
|
||||
},
|
||||
schema.superRefine((responses, ctx) => {
|
||||
schema.superRefine(async (responses, ctx) => {
|
||||
if (!eventType.bookingFields) {
|
||||
// if eventType has been deleted, we won't have bookingFields and thus we can't validate the responses.
|
||||
return;
|
||||
}
|
||||
eventType.bookingFields.forEach((bookingField) => {
|
||||
for (const bookingField of eventType.bookingFields) {
|
||||
const value = responses[bookingField.name];
|
||||
const stringSchema = z.string();
|
||||
const emailSchema = isPartialSchema ? z.string() : z.string().refine(emailSchemaRefinement);
|
||||
const phoneSchema = isPartialSchema
|
||||
? z.string()
|
||||
: z.string().refine((val) => isValidPhoneNumber(val));
|
||||
: z.string().refine(async (val) => {
|
||||
const { isValidPhoneNumber } = await import("libphonenumber-js");
|
||||
return isValidPhoneNumber(val);
|
||||
});
|
||||
// Tag the message with the input name so that the message can be shown at appropriate place
|
||||
const m = (message: string) => `{${bookingField.name}}${message}`;
|
||||
const views = bookingField.views;
|
||||
@@ -132,7 +134,7 @@ function preprocess<T extends z.ZodType>({
|
||||
const isRequired = hidden ? false : isFieldApplicableToCurrentView ? bookingField.required : false;
|
||||
|
||||
if ((isPartialSchema || !isRequired) && value === undefined) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isRequired && !isPartialSchema && !value)
|
||||
@@ -146,7 +148,7 @@ function preprocess<T extends z.ZodType>({
|
||||
message: m("email_validation_error"),
|
||||
});
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldTypeSchema = fieldTypesSchemaMap[bookingField.type as keyof typeof fieldTypesSchemaMap];
|
||||
@@ -159,7 +161,7 @@ function preprocess<T extends z.ZodType>({
|
||||
field: bookingField,
|
||||
isPartialSchema,
|
||||
});
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bookingField.type === "multiemail") {
|
||||
@@ -169,7 +171,7 @@ function preprocess<T extends z.ZodType>({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: m("email_validation_error"),
|
||||
});
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const emails = emailsParsed.data;
|
||||
@@ -179,21 +181,21 @@ function preprocess<T extends z.ZodType>({
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bookingField.type === "checkbox" || bookingField.type === "multiselect") {
|
||||
if (!stringSchema.array().safeParse(value).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid array of strings") });
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bookingField.type === "phone") {
|
||||
if (!phoneSchema.safeParse(value).success) {
|
||||
if (!(await phoneSchema.safeParseAsync(value)).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bookingField.type === "boolean") {
|
||||
@@ -201,7 +203,7 @@ function preprocess<T extends z.ZodType>({
|
||||
if (!schema.safeParse(value).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid Boolean") });
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bookingField.type === "radioInput") {
|
||||
@@ -227,7 +229,7 @@ function preprocess<T extends z.ZodType>({
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use fieldTypeConfig.propsType to validate for propsType=="text" or propsType=="select" as in those cases, the response would be a string.
|
||||
@@ -237,11 +239,11 @@ function preprocess<T extends z.ZodType>({
|
||||
if (!schema.safeParse(value).success) {
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("Invalid string") });
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Can't parse unknown booking field type: ${bookingField.type}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
if (isPartialSchema) {
|
||||
|
||||
@@ -464,7 +464,7 @@ async function getOriginalRescheduledBooking(uid: string, seatsEventType?: boole
|
||||
});
|
||||
}
|
||||
|
||||
function getBookingData({
|
||||
async function getBookingData({
|
||||
req,
|
||||
isNotAnApiCall,
|
||||
eventType,
|
||||
@@ -534,7 +534,7 @@ function getBookingData({
|
||||
}
|
||||
});
|
||||
|
||||
const reqBody = bookingDataSchema.parse(req.body);
|
||||
const reqBody = await bookingDataSchema.parseAsync(req.body);
|
||||
|
||||
// Work with Typescript to require reqBody.end
|
||||
type ReqBodyWithoutEnd = z.infer<typeof bookingDataSchema>;
|
||||
@@ -671,7 +671,7 @@ async function handler(
|
||||
smsReminderNumber,
|
||||
rescheduleReason,
|
||||
...reqBody
|
||||
} = getBookingData({
|
||||
} = await getBookingData({
|
||||
req,
|
||||
isNotAnApiCall,
|
||||
eventType,
|
||||
|
||||
@@ -81,7 +81,7 @@ type Component =
|
||||
TProps extends Omit<TextLikeComponentProps, "value" | "setValue"> & {
|
||||
variant: string | undefined;
|
||||
variants: z.infer<typeof variantsConfigSchema>["variants"];
|
||||
value: Record<string, string> | string;
|
||||
value: Record<string, string> | string | undefined;
|
||||
setValue: (value: string | Record<string, string>) => void;
|
||||
}
|
||||
>(
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { fieldSchema } from "./schema";
|
||||
|
||||
export const preprocessNameFieldDataWithVariant = (
|
||||
variantName: "fullName" | "firstAndLastName",
|
||||
value: string | Record<"firstName" | "lastName", string>
|
||||
value: string | Record<"firstName" | "lastName", string> | undefined
|
||||
) => {
|
||||
// We expect an object here, but if we get a string, then we will try to transform it into the appropriate object
|
||||
if (variantName === "firstAndLastName") {
|
||||
@@ -16,7 +16,7 @@ export const preprocessNameFieldDataWithVariant = (
|
||||
}
|
||||
};
|
||||
|
||||
export const getFullName = (name: string | { firstName: string; lastName?: string }) => {
|
||||
export const getFullName = (name: string | { firstName: string; lastName?: string } | undefined) => {
|
||||
if (!name) {
|
||||
return "";
|
||||
}
|
||||
@@ -29,8 +29,9 @@ export const getFullName = (name: string | { firstName: string; lastName?: strin
|
||||
return nameString;
|
||||
};
|
||||
|
||||
function getFirstAndLastName(value: string | Record<"firstName" | "lastName", string>) {
|
||||
function getFirstAndLastName(value: string | Record<"firstName" | "lastName", string> | undefined) {
|
||||
let newValue: Record<"firstName" | "lastName", string>;
|
||||
value = value || "";
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
// Support name={"firstName": "John", "lastName": "Johny Janardan"} for prefilling
|
||||
|
||||
Reference in New Issue
Block a user