From 11e3862d59cdeb78423cfda371cf29bce499cb2b Mon Sep 17 00:00:00 2001 From: Faraaz Biyabani Date: Thu, 20 Mar 2025 16:24:47 +0530 Subject: [PATCH 1/2] fix: add instance button loading state Adding a reference instance is an async operation on visual builder side. Use a loading state to disable the button, so that the add instance message cannot be sent when it is already sent and a response is being awaited --- .../components/addInstanceButton.tsx | 68 +++++++++++++++---- .../generators/generateAddInstanceButtons.tsx | 19 +++++- .../utils/multipleElementAddButton.ts | 39 +++++------ src/visualBuilder/visualBuilder.style.ts | 9 ++- 4 files changed, 95 insertions(+), 40 deletions(-) diff --git a/src/visualBuilder/components/addInstanceButton.tsx b/src/visualBuilder/components/addInstanceButton.tsx index 036644fe..650341a3 100644 --- a/src/visualBuilder/components/addInstanceButton.tsx +++ b/src/visualBuilder/components/addInstanceButton.tsx @@ -3,44 +3,82 @@ import classNames from "classnames"; import { visualBuilderStyles } from "../visualBuilder.style"; import { PlusIcon } from "./icons"; import { ISchemaFieldMap } from "../utils/types/index.types"; +import { CslpData } from "../../cslp/types/cslp.types"; +import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; +import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; +import { Signal } from "@preact/signals"; interface AddInstanceButtonProps { value: any; onClick: (event: MouseEvent) => void; label?: string | undefined; fieldSchema: ISchemaFieldMap | undefined; + fieldMetadata: CslpData; + index: number; + loading: Signal; } function AddInstanceButtonComponent( props: AddInstanceButtonProps ): JSX.Element { const fieldSchema = props.fieldSchema; - const disabled = - fieldSchema && "max_instance" in fieldSchema && fieldSchema.max_instance - ? props.value.length >= fieldSchema.max_instance - : false; + const fieldMetadata = props.fieldMetadata; + const index = props.index; + const loading = props.loading; + + const onClick = async (event: MouseEvent) => { + loading.value = true; + try { + await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.ADD_INSTANCE, + { + fieldMetadata, + index, + } + ); + } catch (error) { + console.error("Visual Builder: Failed to add instance", error); + } + loading.value = false; + props.onClick(event); + }; + + const buttonClassName = classNames( + "visual-builder__add-button", + visualBuilderStyles()["visual-builder__add-button"], + { + "visual-builder__add-button--with-label": props.label, + }, + { + [visualBuilderStyles()["visual-builder__add-button--loading"]]: + loading.value, + }, + visualBuilderStyles()["visual-builder__tooltip"] + ); + + const maxInstances = + fieldSchema && fieldSchema.data_type !== "block" + ? fieldSchema.max_instance + : undefined; + const isMaxInstances = maxInstances + ? props.value.length >= maxInstances + : false; + const disabled = loading.value || isMaxInstances; return ( + ); + }), + }; +}); describe("generateAddInstanceButton", () => { - test("should generate a button", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + test("should generate and return a button", () => { const button = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, value: "", - onClick: () => {}, + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, + onClick: vi.fn(), + // @ts-expect-error mocking preact signal + loading: { value: false }, + index: 0, + label: "Add Instance", }); expect(button).toBeInstanceOf(HTMLButtonElement); }); - test("should call the callback when clicked", () => { - const callback = vi.fn(); - const button = generateAddInstanceButton({ + test("should call the AddInstanceButtonComponent with the correct props", () => { + generateAddInstanceButton({ + fieldSchema: singleLineFieldSchema, + value: "", + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, + onClick: vi.fn(), + // @ts-expect-error mocking preact signal + loading: { value: false }, + index: 0, + label: "Add Instance", + }); + const args = AddInstanceButtonComponent.mock.calls[0][0]; + expect(args).toStrictEqual({ fieldSchema: singleLineFieldSchema, value: "", - onClick: callback, + fieldMetadata: { hello: "world" }, + onClick: expect.any(Function), + loading: { value: false }, + index: 0, + label: "Add Instance", }); - button.click(); - expect(callback).toHaveBeenCalledTimes(1); }); }); diff --git a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts index 82043d23..fd3e44ae 100644 --- a/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts +++ b/src/visualBuilder/utils/__test__/multipleElementAddButton.test.ts @@ -13,6 +13,7 @@ import getChildrenDirection from "../getChildrenDirection"; import visualBuilderPostMessage from "../visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../types/postMessage.types"; import { singleLineFieldSchema } from "../../../__test__/data/fields"; +import { signal } from "@preact/signals"; Object.defineProperty(globalThis, "crypto", { value: { @@ -46,27 +47,16 @@ vi.mock("../visualBuilderPostMessage", async () => { }; }); -describe("generateAddInstanceButton", () => { - test("should generate a button", () => { - const button = generateAddInstanceButton({ - fieldSchema: singleLineFieldSchema, - value: "", - onClick: () => {}, - }); - expect(button.tagName).toBe("BUTTON"); - }); - - test("should run the callback when the button is clicked", () => { - const mockCallback = vi.fn(); - const button = generateAddInstanceButton({ - fieldSchema: singleLineFieldSchema, - value: "", - onClick: mockCallback, - }); - - button.click(); - expect(mockCallback).toHaveBeenCalled(); - }); +vi.mock("@preact/signals", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as unknown as Record), + signal: (initialValue: any) => { + return { + value: initialValue, + }; + }, + }; }); // TODO: rewrite this @@ -577,13 +567,19 @@ describe("removeAddInstanceButtons", () => { previousButton = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, value: "", onClick: vi.fn(), + loading: signal(false), }); nextButton = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, value: "", + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, onClick: vi.fn(), + loading: signal(false), }); overlayWrapper = document.createElement("div"); eventTarget = document.createElement("div"); @@ -672,7 +668,10 @@ describe("removeAddInstanceButtons", () => { const button = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, value: "", - onClick: () => {}, + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, + onClick: vi.fn(), + loading: signal(false), }); visualBuilderContainer.appendChild(button); } @@ -704,7 +703,10 @@ describe("removeAddInstanceButtons", () => { const button = generateAddInstanceButton({ fieldSchema: singleLineFieldSchema, value: "", - onClick: () => {}, + // @ts-expect-error mock field metadata + fieldMetadata: { hello: "world" }, + onClick: vi.fn(), + loading: signal(false), }); visualBuilderContainer.appendChild(button); }