feat: Autofill feature (#15437)

* feat: Autofill feature

* Revert "feat: Autofill feature"

This reverts commit ac94257cbb516e140ed550ae804e725ca5d036ca.

* feat: store prev response in localStorage

* review fixes

* add tests

* remove unused import

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
This commit is contained in:
Amit Sharma
2024-11-18 20:04:02 +05:30
committed by GitHub
co-authored by Hariom Balhara unknown
parent 7b94822c8d
commit 8194ffcd07
6 changed files with 103 additions and 3 deletions
@@ -22,6 +22,7 @@ export interface IUseBookingForm {
guests: string[];
name: string | null;
};
lastBookingResponse?: Record<string, string>;
}
export type UseBookingFormReturnType = ReturnType<typeof useBookingForm>;
@@ -34,6 +35,7 @@ export const useBookingForm = ({
hasSession,
extraOptions,
prefillFormParams,
lastBookingResponse,
}: IUseBookingForm) => {
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const bookingData = useBookerStore((state) => state.bookingData);
@@ -71,6 +73,7 @@ export const useBookingForm = ({
hasSession,
extraOptions,
prefillFormParams,
lastBookingResponse,
});
const bookingForm = useForm<BookingFormValues>({
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import type { z } from "zod";
import { z } from "zod";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import type getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
@@ -21,6 +21,7 @@ type UseInitialFormValuesProps = {
guests: string[];
name: string | null;
};
lastBookingResponse?: Record<string, string>;
};
export function useInitialFormValues({
@@ -33,6 +34,7 @@ export function useInitialFormValues({
hasSession,
extraOptions,
prefillFormParams,
lastBookingResponse,
}: UseInitialFormValuesProps) {
const [initialValues, setDefaultValues] = useState<{
responses?: Partial<z.infer<ReturnType<typeof getBookingResponsesSchema>>>;
@@ -62,6 +64,7 @@ export function useInitialFormValues({
// `guests` because the `name` of the corresponding bookingField is `guests`
guests: prefillFormParams.guests,
});
const parsedLastBookingResponse = z.record(z.any()).nullish().parse(lastBookingResponse);
const defaultUserValues = {
email:
@@ -69,13 +72,13 @@ export function useInitialFormValues({
? bookingData?.attendees[0].email
: !!parsedQuery["email"]
? parsedQuery["email"]
: email ?? "",
: email ?? parsedLastBookingResponse?.email ?? "",
name:
rescheduleUid && bookingData && bookingData.attendees.length > 0
? bookingData?.attendees[0].name
: !!parsedQuery["name"]
? parsedQuery["name"]
: name ?? username ?? "",
: name ?? username ?? parsedLastBookingResponse?.name ?? "",
};
if (!isRescheduling) {
@@ -0,0 +1,65 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { localStorage } from "@calcom/lib/webstorage";
import { setLastBookingResponse, getLastBookingResponse } from "./lastBookingResponse";
vi.mock("@calcom/lib/webstorage", () => ({
localStorage: {
getItem: vi.fn(),
setItem: vi.fn(),
},
}));
describe("Last Booking Response", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("setLastBookingResponse", () => {
it("should not store anything if responses is null", () => {
setLastBookingResponse(null);
expect(localStorage.setItem).not.toHaveBeenCalled();
});
it("should store only allowed responses in localStorage", () => {
const responses = {
email: "test@example.com",
name: "John Doe",
age: 30,
};
setLastBookingResponse(responses);
expect(localStorage.setItem).toHaveBeenCalledWith(
"lastBookingResponse",
JSON.stringify({ email: "test@example.com", name: "John Doe" })
);
});
});
describe("getLastBookingResponse", () => {
it("should return an empty object if there is no last booking response", () => {
(localStorage.getItem as jest.Mock).mockReturnValue(null);
const result = getLastBookingResponse();
expect(result).toEqual({});
});
it("should return parsed response if valid JSON is present", () => {
const mockResponse = JSON.stringify({ email: "test@example.com", name: "John Doe" });
(localStorage.getItem as jest.Mock).mockReturnValue(mockResponse);
const result = getLastBookingResponse();
expect(result).toEqual({ email: "test@example.com", name: "John Doe" });
});
it("should return an empty object if JSON is invalid", () => {
const invalidJson = "{ email: 'test@example.com' ";
(localStorage.getItem as jest.Mock).mockReturnValue(invalidJson);
const result = getLastBookingResponse();
expect(result).toEqual({});
});
});
});
@@ -0,0 +1,22 @@
import logger from "@calcom/lib/logger";
import { localStorage } from "@calcom/lib/webstorage";
const responsesToStore = ["email", "name"];
export const setLastBookingResponse = (responses: Record<string, unknown> | null) => {
if (!responses) return;
const prevResponse = Object.fromEntries(
Object.entries(responses).filter(([key]) => responsesToStore.includes(key))
);
localStorage.setItem("lastBookingResponse", JSON.stringify(prevResponse));
};
export const getLastBookingResponse = () => {
const lastBookingResponse = localStorage.getItem("lastBookingResponse");
try {
return JSON.parse(lastBookingResponse ?? "{}");
} catch (err) {
logger.error("Error parsing lastBookingResponse: ", err);
return {};
}
};
@@ -19,6 +19,7 @@ import { useVerifyCode } from "@calcom/features/bookings/Booker/components/hooks
import { useVerifyEmail } from "@calcom/features/bookings/Booker/components/hooks/useVerifyEmail";
import { useBookerStore, useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event";
import { getLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse";
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
@@ -85,6 +86,8 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
};
}, [searchParams, firstNameQueryParam, lastNameQueryParam]);
const lastBookingResponse = getLastBookingResponse();
const bookerForm = useBookingForm({
event: event.data,
sessionEmail: session?.user.email,
@@ -93,6 +96,7 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => {
hasSession,
extraOptions: routerQuery,
prefillFormParams,
lastBookingResponse,
});
const calendars = useCalendars({ hasSession });
const verifyEmail = useVerifyEmail({
@@ -1,5 +1,6 @@
import type { UseBookingFormReturnType } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { setLastBookingResponse } from "@calcom/features/bookings/Booker/utils/lastBookingResponse";
import {
useTimePreferences,
mapBookingToMutationInput,
@@ -74,6 +75,8 @@ export const useHandleBookEvent = ({
? duration
: event.data.length;
setLastBookingResponse(values.responses);
const bookingInput = {
values,
duration: validDuration,