Skip to content

Commit 11e3862

Browse files
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
1 parent 73e46ae commit 11e3862

4 files changed

Lines changed: 95 additions & 40 deletions

File tree

src/visualBuilder/components/addInstanceButton.tsx

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,82 @@ import classNames from "classnames";
33
import { visualBuilderStyles } from "../visualBuilder.style";
44
import { PlusIcon } from "./icons";
55
import { ISchemaFieldMap } from "../utils/types/index.types";
6+
import { CslpData } from "../../cslp/types/cslp.types";
7+
import visualBuilderPostMessage from "../utils/visualBuilderPostMessage";
8+
import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types";
9+
import { Signal } from "@preact/signals";
610

711
interface AddInstanceButtonProps {
812
value: any;
913
onClick: (event: MouseEvent) => void;
1014
label?: string | undefined;
1115
fieldSchema: ISchemaFieldMap | undefined;
16+
fieldMetadata: CslpData;
17+
index: number;
18+
loading: Signal<boolean>;
1219
}
1320

1421
function AddInstanceButtonComponent(
1522
props: AddInstanceButtonProps
1623
): JSX.Element {
1724
const fieldSchema = props.fieldSchema;
18-
const disabled =
19-
fieldSchema && "max_instance" in fieldSchema && fieldSchema.max_instance
20-
? props.value.length >= fieldSchema.max_instance
21-
: false;
25+
const fieldMetadata = props.fieldMetadata;
26+
const index = props.index;
27+
const loading = props.loading;
28+
29+
const onClick = async (event: MouseEvent) => {
30+
loading.value = true;
31+
try {
32+
await visualBuilderPostMessage?.send(
33+
VisualBuilderPostMessageEvents.ADD_INSTANCE,
34+
{
35+
fieldMetadata,
36+
index,
37+
}
38+
);
39+
} catch (error) {
40+
console.error("Visual Builder: Failed to add instance", error);
41+
}
42+
loading.value = false;
43+
props.onClick(event);
44+
};
45+
46+
const buttonClassName = classNames(
47+
"visual-builder__add-button",
48+
visualBuilderStyles()["visual-builder__add-button"],
49+
{
50+
"visual-builder__add-button--with-label": props.label,
51+
},
52+
{
53+
[visualBuilderStyles()["visual-builder__add-button--loading"]]:
54+
loading.value,
55+
},
56+
visualBuilderStyles()["visual-builder__tooltip"]
57+
);
58+
59+
const maxInstances =
60+
fieldSchema && fieldSchema.data_type !== "block"
61+
? fieldSchema.max_instance
62+
: undefined;
63+
const isMaxInstances = maxInstances
64+
? props.value.length >= maxInstances
65+
: false;
66+
const disabled = loading.value || isMaxInstances;
2267

2368
return (
2469
<button
25-
className={classNames(
26-
"visual-builder__add-button",
27-
visualBuilderStyles()["visual-builder__add-button"],
28-
{
29-
"visual-builder__add-button--with-label": props.label,
30-
},
31-
visualBuilderStyles()["visual-builder__tooltip"]
32-
)}
70+
className={buttonClassName}
3371
data-tooltip={"Add section"}
3472
data-testid="visual-builder-add-instance-button"
3573
disabled={disabled}
3674
title={
37-
disabled && fieldSchema && "max_instance" in fieldSchema
38-
? `Max ${fieldSchema.max_instance} instances allowed`
75+
maxInstances && isMaxInstances
76+
? `Max ${maxInstances} instances allowed`
3977
: undefined
4078
}
4179
onClick={(e) => {
4280
const event = e as unknown as MouseEvent;
43-
props.onClick(event);
81+
onClick(event);
4482
}}
4583
>
4684
<PlusIcon />

src/visualBuilder/generators/generateAddInstanceButtons.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,44 @@
1+
import React from "preact/compat";
12
import { render } from "preact";
23
import AddInstanceButtonComponent from "../components/addInstanceButton";
34
import { ISchemaFieldMap } from "../utils/types/index.types";
5+
import { CslpData } from "../../cslp/types/cslp.types";
6+
import { Signal } from "@preact/signals";
47

58
/**
6-
* Generates a button element, when clicked, triggers the provided callback function.
9+
* Generates a button element, when clicked, sends the add instance message and
10+
* then calls the provided callback function.
711
* @param onClickCallback - The function to be called when the button is clicked.
812
* @returns The generated button element.
913
*/
1014
export function generateAddInstanceButton({
11-
fieldSchema,
1215
value,
16+
fieldSchema,
17+
fieldMetadata,
18+
index,
19+
loading,
1320
onClick,
1421
label,
1522
}: {
23+
fieldSchema: ISchemaFieldMap | undefined;
1624
value: any;
25+
fieldMetadata: CslpData;
26+
index: number;
27+
loading: Signal<boolean>;
1728
onClick: (event: MouseEvent) => void;
1829
label?: string | undefined;
19-
fieldSchema: ISchemaFieldMap | undefined;
2030
}): HTMLButtonElement {
2131
const wrapper = document.createDocumentFragment();
2232

2333
render(
2434
<AddInstanceButtonComponent
35+
loading={loading}
36+
index={index}
2537
value={value}
2638
label={label}
2739
onClick={onClick}
2840
fieldSchema={fieldSchema}
41+
fieldMetadata={fieldMetadata}
2942
/>,
3043
wrapper
3144
);

src/visualBuilder/utils/multipleElementAddButton.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ import {
33
generateAddInstanceButton,
44
getAddInstanceButtons,
55
} from "../generators/generateAddInstanceButtons";
6-
import visualBuilderPostMessage from "./visualBuilderPostMessage";
7-
import { VisualBuilderPostMessageEvents } from "./types/postMessage.types";
86
import getChildrenDirection from "./getChildrenDirection";
97
import { hideOverlay } from "../generators/generateOverlay";
108
import { hideHoverOutline } from "../listeners/mouseHover";
119
import { ISchemaFieldMap } from "./types/index.types";
10+
import { signal } from "@preact/signals";
1211

1312
const WAIT_FOR_NEW_INSTANCE_TIMEOUT = 4000;
1413

@@ -106,32 +105,30 @@ export function handleAddButtonsForMultiple(
106105
});
107106
};
108107

108+
// this is a shared loading state between the
109+
// next and previous button for the duration
110+
// between the add-instance post message being
111+
// sent and receiving a response for it.
112+
const loading = signal(false);
113+
109114
const previousButton = generateAddInstanceButton({
110-
onClick: () => {
111-
visualBuilderPostMessage
112-
?.send(VisualBuilderPostMessageEvents.ADD_INSTANCE, {
113-
fieldMetadata: eventDetails.fieldMetadata,
114-
index: prevIndex,
115-
})
116-
.then(onMessageSent.bind(null, prevIndex));
117-
},
118-
label,
119115
fieldSchema,
120116
value: expectedFieldData,
117+
fieldMetadata: eventDetails.fieldMetadata,
118+
index: prevIndex,
119+
onClick: onMessageSent.bind(null, prevIndex),
120+
loading,
121+
label,
121122
});
122123

123124
const nextButton = generateAddInstanceButton({
124-
onClick: () => {
125-
visualBuilderPostMessage
126-
?.send(VisualBuilderPostMessageEvents.ADD_INSTANCE, {
127-
fieldMetadata: eventDetails.fieldMetadata,
128-
index: nextIndex,
129-
})
130-
.then(onMessageSent.bind(null, nextIndex));
131-
},
132-
label,
133125
fieldSchema,
134126
value: expectedFieldData,
127+
fieldMetadata: eventDetails.fieldMetadata,
128+
index: nextIndex,
129+
onClick: onMessageSent.bind(null, nextIndex),
130+
loading,
131+
label,
135132
});
136133

137134
if (!visualBuilderContainer.contains(previousButton)) {
@@ -216,7 +213,7 @@ export function removeAddInstanceButtons(
216213
}
217214

218215
/**
219-
* This function that observes the parent element and focuses the newly added instance.
216+
* This function observes the parent element and focuses the newly added instance.
220217
*
221218
* @param parentCslp The parent cslp value.
222219
* @param index The index of the new instance.

src/visualBuilder/visualBuilder.style.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ export function visualBuilderStyles() {
148148
overflow: hidden;
149149
text-overflow: ellipsis;
150150
`,
151+
"visual-builder__add-button--loading": css`
152+
cursor: wait;
153+
/* we have not-allowed on disabled, so we need this */
154+
&:disabled {
155+
cursor: wait;
156+
}
157+
`,
151158
"visual-builder__start-editing-btn": css`
152159
z-index: 1000;
153160
text-decoration: none;
@@ -666,7 +673,7 @@ export const VisualBuilderGlobalStyles = `
666673
[data-cslp] [contenteditable="true"] {
667674
outline: none;
668675
}
669-
676+
670677
@keyframes visual-builder__spinner {
671678
0% {
672679
transform: rotate(0deg);

0 commit comments

Comments
 (0)