diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Input/Inputv2_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Input/Inputv2_spec.js index d3bf289515..c61151b3de 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Input/Inputv2_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Input/Inputv2_spec.js @@ -1,3 +1,4 @@ +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants"; import { agHelper } from "../../../../../support/Objects/ObjectsCore"; const widgetName = "inputwidgetv2"; @@ -385,7 +386,9 @@ describe( cy.get(widgetInput).clear(); cy.wait(300); // Input text and hit enter key - cy.get(widgetInput).type("test{enter}"); + cy.get(widgetInput).type("test{enter}", { + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); // Assert if the Text widget contains the whole value, test cy.get(".t--widget-textwidget").should("have.text", "test"); }); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js index 0728fa37cc..d666f68241 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_BasicClientSideData_spec.js @@ -1,6 +1,7 @@ const publishLocators = require("../../../../../locators/publishWidgetspage.json"); const commonlocators = require("../../../../../locators/commonlocators.json"); +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants"; import * as _ from "../../../../../support/Objects/ObjectsCore"; const widgetSelector = (name) => `[data-widgetname-cy="${name}"]`; @@ -76,7 +77,7 @@ describe( cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => { cy.wrap($inputWidget) .find("input") - .type(index + 1); + .type(index + 1, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE }); }); // Verify the typed value @@ -101,7 +102,7 @@ describe( cy.get(".t--draggable-inputwidgetv2").each(($inputWidget, index) => { cy.wrap($inputWidget) .find("input") - .type(index + 4); + .type(index + 4, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE }); }); // Verify the typed value diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_Nested_EventBindings_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_Nested_EventBindings_spec.js index 1518bd7c3f..dc10998d1c 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_Nested_EventBindings_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/ListV2/Listv2_Nested_EventBindings_spec.js @@ -1,3 +1,4 @@ +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../../../../src/constants/WidgetConstants"; const nestedListDSL = require("../../../../../fixtures/Listv2/nestedList.json"); const commonlocators = require("../../../../../locators/commonlocators.json"); @@ -22,10 +23,14 @@ describe( "{{showAlert(`${level_1.currentView.Text1.text} _ ${level_1.currentItem.id} _ ${level_1.currentIndex} _ ${level_1.currentView.Input1.text} _ ${currentView.Input2.text}`)}}", ); // Enter text in the parent list widget's text input - cy.get(widgetSelector("Input1")).find("input").type("outer input"); + cy.get(widgetSelector("Input1")) + .find("input") + .type("outer input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE }); // Enter text in the child list widget's text input in first row - cy.get(widgetSelector("Input2")).find("input").type("inner input"); + cy.get(widgetSelector("Input2")) + .find("input") + .type("inner input", { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE }); // click the button on inner list 1st row. cy.get(widgetSelector("Button3")).find("button").click({ force: true }); @@ -40,13 +45,17 @@ describe( cy.get(widgetSelector("Input1")) .find("input") .clear() - .type("outer input updated"); + .type("outer input updated", { + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); // Enter text in the child list widget's text input in first row cy.get(widgetSelector("Input2")) .find("input") .clear() - .type("inner input updated"); + .type("inner input updated", { + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); // click the button on inner list 1st row. cy.get(widgetSelector("Button3")).find("button").click({ force: true }); diff --git a/app/client/cypress/support/Pages/AggregateHelper.ts b/app/client/cypress/support/Pages/AggregateHelper.ts index 6ad77693d1..de301009b4 100644 --- a/app/client/cypress/support/Pages/AggregateHelper.ts +++ b/app/client/cypress/support/Pages/AggregateHelper.ts @@ -7,6 +7,7 @@ import { EntityItems } from "./AssertHelper"; import EditorNavigator from "./EditorNavigation"; import { EntityType } from "./EditorNavigation"; import ClickOptions = Cypress.ClickOptions; +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "../../../src/constants/WidgetConstants"; type ElementType = string | JQuery; @@ -945,10 +946,13 @@ export class AggregateHelper { .focus() .type("{backspace}".repeat(charCount), { timeout: 2, force: true }) .wait(50) - .type(totype); + .type(totype, { delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE }); else { if (charCount == -1) this.GetElement(selector).eq(index).clear(); - this.TypeText(selector, totype, index); + this.TypeText(selector, totype, { + index, + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); } } @@ -973,7 +977,10 @@ export class AggregateHelper { force = false, ) { this.ClearTextField(selector, force, index); - return this.TypeText(selector, totype, index); + return this.TypeText(selector, totype, { + index, + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); } public TypeText( @@ -1323,7 +1330,10 @@ export class AggregateHelper { toClear && this.ClearInputText(name); cy.xpath(this.locator._inputWidgetValueField(name, isInput)) .trigger("click") - .type(input, { parseSpecialCharSequences: false }); + .type(input, { + parseSpecialCharSequences: false, + delay: DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + }); } public ClearInputText(name: string, isInput = true) { diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index f463de9085..f978cf194f 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -279,3 +279,6 @@ export type PasteWidgetReduxAction = { groupWidgets: boolean; existingWidgets?: unknown; } & EitherMouseLocationORGridPosition; + +// Constant for debouncing the input change to avoid multiple Execute calls in reactive flow +export const DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE = 300; diff --git a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx index af75f5ab74..6a2a229e39 100644 --- a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx +++ b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx @@ -12,7 +12,7 @@ import { getCountryCodeFromCurrencyCode, } from "../component/CurrencyCodeDropdown"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; -import _ from "lodash"; +import _, { debounce } from "lodash"; import derivedProperties from "./parsedDerivedProperties"; import BaseInputWidget from "widgets/BaseInputWidget"; import type { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget"; @@ -42,7 +42,10 @@ import { DynamicHeight } from "utils/WidgetFeatures"; import { getDefaultCurrency } from "../component/CurrencyCodeDropdown"; import IconSVG from "../icon.svg"; import ThumbnailSVG from "../thumbnail.svg"; -import { WIDGET_TAGS } from "constants/WidgetConstants"; +import { + DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + WIDGET_TAGS, +} from "constants/WidgetConstants"; import { appsmithTelemetry } from "instrumentation"; export function defaultValueValidation( @@ -154,10 +157,19 @@ export function defaultValueValidation( }; } +interface CurrencyInputWidgetState extends WidgetState { + inputValue: string; +} + class CurrencyInputWidget extends BaseInputWidget< CurrencyInputWidgetProps, - WidgetState + CurrencyInputWidgetState > { + constructor(props: CurrencyInputWidgetProps) { + super(props); + this.state = { inputValue: props.text ?? "" }; + } + static type = "CURRENCY_INPUT_WIDGET"; static getConfig() { @@ -457,6 +469,12 @@ class CurrencyInputWidget extends BaseInputWidget< } componentDidUpdate(prevProps: CurrencyInputWidgetProps) { + if (prevProps.text !== this.props.text) { + this.setState({ inputValue: this.props.text ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if ( prevProps.text !== this.props.text && !this.props.isFocused && @@ -481,6 +499,10 @@ class CurrencyInputWidget extends BaseInputWidget< } } + componentWillUnmount() { + this.debouncedOnValueChange.cancel(); + } + formatText() { if (!!this.props.text && !this.isTextFormatted()) { try { @@ -506,6 +528,22 @@ class CurrencyInputWidget extends BaseInputWidget< } } + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { + // text is stored as what user has typed + this.props.updateWidgetMetaProperty("text", String(value), { + triggerPropertyName: "onTextChanged", + dynamicString: this.props.onTextChanged, + event: { + type: EventType.ON_TEXT_CHANGE, + }, + }); + + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + onValueChange = (value: string) => { let formattedValue = ""; const decimalSeperator = getLocaleDecimalSeperator(); @@ -524,18 +562,8 @@ class CurrencyInputWidget extends BaseInputWidget< }); } - // text is stored as what user has typed - this.props.updateWidgetMetaProperty("text", String(formattedValue), { - triggerPropertyName: "onTextChanged", - dynamicString: this.props.onTextChanged, - event: { - type: EventType.ON_TEXT_CHANGE, - }, - }); - - if (!this.props.isDirty) { - this.props.updateWidgetMetaProperty("isDirty", true); - } + this.setState({ inputValue: formattedValue }); + this.debouncedOnValueChange(formattedValue); }; isTextFormatted = () => { @@ -623,7 +651,7 @@ class CurrencyInputWidget extends BaseInputWidget< }; getWidgetView() { - const value = this.props.text ?? ""; + const value = this.state.inputValue ?? ""; const isInvalid = "isValid" in this.props && !this.props.isValid && !!this.props.isDirty; const currencyCode = this.props.currencyCode; diff --git a/app/client/src/widgets/InputWidget/widget/index.tsx b/app/client/src/widgets/InputWidget/widget/index.tsx index 8be7eaa8bc..4235a6118e 100644 --- a/app/client/src/widgets/InputWidget/widget/index.tsx +++ b/app/client/src/widgets/InputWidget/widget/index.tsx @@ -18,6 +18,7 @@ import type { ExecutionResult } from "constants/AppsmithActionConstants/ActionCo import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { TextSize } from "constants/WidgetConstants"; import { + DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, GridDefaults, RenderModes, WIDGET_TAGS, @@ -47,6 +48,7 @@ import { import type { InputType } from "../constants"; import { InputTypes } from "../constants"; import IconSVG from "../icon.svg"; +import { debounce } from "lodash"; export function defaultValueValidation( // TODO: Fix this the next time the file is edited @@ -141,14 +143,31 @@ export function defaultValueValidation( }; } -class InputWidget extends BaseWidget { +interface InputWidgetState extends WidgetState { + inputValue: string; +} + +class InputWidget extends BaseWidget { constructor(props: InputWidgetProps) { super(props); this.state = { text: props.text, + inputValue: props.text ?? "", }; } + componentDidUpdate(prevProps: InputWidgetProps) { + if (prevProps.text !== this.props.text) { + this.setState({ inputValue: this.props.text ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + } + + componentWillUnmount() { + this.debouncedOnValueChange.cancel(); + } + static type = "INPUT_WIDGET"; static getConfig() { @@ -877,7 +896,8 @@ class InputWidget extends BaseWidget { }; } - onValueChange = (value: string) => { + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { this.props.updateWidgetMetaProperty("text", value, { triggerPropertyName: "onTextChanged", dynamicString: this.props.onTextChanged, @@ -889,6 +909,11 @@ class InputWidget extends BaseWidget { if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (value: string) => { + this.setState({ inputValue: value }); + this.debouncedOnValueChange(value); }; onCurrencyTypeChange = (code?: string) => { @@ -967,12 +992,13 @@ class InputWidget extends BaseWidget { getFormattedText = () => { if (this.props.isFocused || this.props.inputType !== InputTypes.CURRENCY) { - return this.props.text !== undefined ? this.props.text : ""; + return this.state.inputValue !== undefined ? this.state.inputValue : ""; } - if (this.props.text === "" || this.props.text === undefined) return ""; + if (this.state.inputValue === "" || this.state.inputValue === undefined) + return ""; - const valueToFormat = String(this.props.text); + const valueToFormat = String(this.state.inputValue); const locale = getLocale(); const decimalSeparator = getDecimalSeparator(locale); diff --git a/app/client/src/widgets/InputWidgetV2/widget/index.tsx b/app/client/src/widgets/InputWidgetV2/widget/index.tsx index 0a041ec8d3..d44a720443 100644 --- a/app/client/src/widgets/InputWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/InputWidgetV2/widget/index.tsx @@ -18,7 +18,7 @@ import { INPUT_TEXT_MAX_CHAR_ERROR, } from "ee/constants/messages"; import type { SetterConfig, Stylesheet } from "entities/AppTheming"; -import { isNil, isNumber, merge, toString } from "lodash"; +import { debounce, isNil, isNumber, merge, toString } from "lodash"; import React from "react"; import { DynamicHeight } from "utils/WidgetFeatures"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; @@ -48,7 +48,10 @@ import type { PropertyUpdates, SnipingModeProperty, } from "WidgetProvider/constants"; -import { WIDGET_TAGS } from "constants/WidgetConstants"; +import { + DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + WIDGET_TAGS, +} from "constants/WidgetConstants"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; @@ -299,11 +302,16 @@ function InputTypeUpdateHook( return updates; } -class InputWidget extends BaseInputWidget { +interface InputWidgetState extends WidgetState { + inputValue: string; +} + +class InputWidget extends BaseInputWidget { constructor(props: InputWidgetProps) { super(props); this.state = { isFocused: false, + inputValue: props.inputText ?? "", }; } @@ -706,6 +714,12 @@ class InputWidget extends BaseInputWidget { }; componentDidUpdate = (prevProps: InputWidgetProps) => { + if (prevProps.inputText !== this.props.inputText) { + this.setState({ inputValue: this.props.inputText ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if ( prevProps.inputText !== this.props.inputText && this.props.inputText !== toString(this.props.text) @@ -732,7 +746,12 @@ class InputWidget extends BaseInputWidget { } }; - onValueChange = (value: string) => { + componentWillUnmount() { + this.debouncedOnValueChange.cancel(); + } + + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { /* * Ideally text property should be derived property. But widgets * with derived properties won't work as expected inside a List @@ -755,6 +774,11 @@ class InputWidget extends BaseInputWidget { if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (value: string) => { + this.setState({ inputValue: value }); + this.debouncedOnValueChange(value); }; static getSetterConfig(): SetterConfig { @@ -790,7 +814,7 @@ class InputWidget extends BaseInputWidget { }; getWidgetView() { - const value = this.props.inputText ?? ""; + const value = this.state.inputValue ?? ""; let isInvalid = false; if (this.props.isDirty) { diff --git a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx index a116fc7411..b92c0bb7c7 100644 --- a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx @@ -12,7 +12,7 @@ import { ISDCodeDropdownOptions, } from "../component/ISDCodeDropdown"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; -import _ from "lodash"; +import _, { debounce } from "lodash"; import BaseInputWidget from "widgets/BaseInputWidget"; import derivedProperties from "./parsedDerivedProperties"; import type { BaseInputWidgetProps } from "widgets/BaseInputWidget/widget"; @@ -37,7 +37,10 @@ import { DynamicHeight } from "utils/WidgetFeatures"; import { getDefaultISDCode } from "../component/ISDCodeDropdown"; import IconSVG from "../icon.svg"; import ThumbnailSVG from "../thumbnail.svg"; -import { WIDGET_TAGS } from "constants/WidgetConstants"; +import { + DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + WIDGET_TAGS, +} from "constants/WidgetConstants"; import { appsmithTelemetry } from "instrumentation"; export function defaultValueValidation( @@ -84,10 +87,21 @@ export function defaultValueValidation( }; } +interface PhoneWidgetState extends WidgetState { + inputValue: string; +} + class PhoneInputWidget extends BaseInputWidget< PhoneInputWidgetProps, - WidgetState + PhoneWidgetState > { + constructor(props: PhoneInputWidgetProps) { + super(props); + this.state = { + inputValue: props.text ?? "", + }; + } + static type = "PHONE_INPUT_WIDGET"; static getConfig() { @@ -356,6 +370,12 @@ class PhoneInputWidget extends BaseInputWidget< } componentDidUpdate(prevProps: PhoneInputWidgetProps) { + if (prevProps.text !== this.props.text) { + this.setState({ inputValue: this.props.text ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if (prevProps.dialCode !== this.props.dialCode) { this.onISDCodeChange(this.props.dialCode); } @@ -390,6 +410,10 @@ class PhoneInputWidget extends BaseInputWidget< } } + componentWillUnmount() { + this.debouncedOnValueChange.cancel(); + } + onISDCodeChange = (dialCode?: string) => { const countryCode = getCountryCode(dialCode); @@ -403,21 +427,13 @@ class PhoneInputWidget extends BaseInputWidget< } }; - onValueChange = (value: string) => { - let formattedValue; - - // Don't format, as value is typed, when user is deleting - if (value && value.length > this.props.text?.length) { - formattedValue = this.getFormattedPhoneNumber(value); - } else { - formattedValue = value; - } - + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { this.props.updateWidgetMetaProperty( "value", - parseIncompletePhoneNumber(formattedValue), + parseIncompletePhoneNumber(value), ); - this.props.updateWidgetMetaProperty("text", formattedValue, { + this.props.updateWidgetMetaProperty("text", value, { triggerPropertyName: "onTextChanged", dynamicString: this.props.onTextChanged, event: { @@ -428,6 +444,20 @@ class PhoneInputWidget extends BaseInputWidget< if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (value: string) => { + let formattedValue; + + // Don't format, as value is typed, when user is deleting + if (value && value.length > this.props.text?.length) { + formattedValue = this.getFormattedPhoneNumber(value); + } else { + formattedValue = value; + } + + this.setState({ inputValue: formattedValue }); + this.debouncedOnValueChange(formattedValue); }; handleFocusChange = (focusState: boolean) => { @@ -487,7 +517,7 @@ class PhoneInputWidget extends BaseInputWidget< } getWidgetView() { - const value = this.props.text ?? ""; + const value = this.state.inputValue; const isInvalid = "isValid" in this.props && !this.props.isValid && !!this.props.isDirty; const countryCode = this.props.countryCode; diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index fb989c3fac..0d6cd50525 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -34,7 +34,11 @@ import type { SnipingModeProperty, PropertyUpdates, } from "WidgetProvider/constants"; -import { WIDGET_TAGS } from "constants/WidgetConstants"; +import { + DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE, + WIDGET_TAGS, +} from "constants/WidgetConstants"; +import { debounce } from "lodash"; export enum RTEFormats { MARKDOWN = "markdown", @@ -48,10 +52,21 @@ const RichTextEditorComponent = lazy(async () => const converter = new showdown.Converter(); +interface RichTextEditorWidgetState extends WidgetState { + inputValue: string; +} + class RichTextEditorWidget extends BaseWidget< RichTextEditorWidgetProps, - WidgetState + RichTextEditorWidgetState > { + constructor(props: RichTextEditorWidgetProps) { + super(props); + this.state = { + inputValue: props.text ?? "", + }; + } + static type = "RICH_TEXT_EDITOR_WIDGET"; static getConfig() { @@ -491,6 +506,12 @@ class RichTextEditorWidget extends BaseWidget< } componentDidUpdate(prevProps: RichTextEditorWidgetProps): void { + if (prevProps.text !== this.props.text) { + this.setState({ inputValue: this.props.text ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if (this.props.defaultText !== prevProps.defaultText) { if (this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", false); @@ -498,7 +519,12 @@ class RichTextEditorWidget extends BaseWidget< } } - onValueChange = (text: string) => { + componentWillUnmount() { + this.debouncedOnValueChange.cancel(); + } + + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((text: string) => { if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } @@ -510,6 +536,11 @@ class RichTextEditorWidget extends BaseWidget< type: EventType.ON_TEXT_CHANGE, }, }); + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (text: string) => { + this.setState({ inputValue: text }); + this.debouncedOnValueChange(text); }; static getSetterConfig(): SetterConfig { @@ -532,7 +563,7 @@ class RichTextEditorWidget extends BaseWidget< } getWidgetView() { - let value = this.props.text ?? ""; + let value = this.state.inputValue; if (this.props.inputType === RTEFormats.MARKDOWN) { value = converter.makeHtml(value); diff --git a/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx index 768541792a..30d2159c9c 100644 --- a/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx @@ -1,4 +1,4 @@ -import _ from "lodash"; +import _, { debounce } from "lodash"; import React from "react"; import log from "loglevel"; import type { WidgetState } from "widgets/BaseWidget"; @@ -30,11 +30,20 @@ import { WDSBaseInputWidget } from "widgets/wds/WDSBaseInputWidget"; import { getCountryCodeFromCurrencyCode, validateInput } from "./helpers"; import type { KeyDownEvent } from "widgets/wds/WDSBaseInputWidget/component/types"; import { appsmithTelemetry } from "instrumentation"; +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants"; +interface WDSCurrencyInputWidgetState extends WidgetState { + inputValue: string; +} class WDSCurrencyInputWidget extends WDSBaseInputWidget< CurrencyInputWidgetProps, - WidgetState + WDSCurrencyInputWidgetState > { + constructor(props: CurrencyInputWidgetProps) { + super(props); + this.state = { inputValue: props.rawText ?? "" }; + } + static type = "WDS_CURRENCY_INPUT_WIDGET"; static getConfig() { @@ -152,6 +161,12 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< } componentDidUpdate(prevProps: CurrencyInputWidgetProps) { + if (prevProps.rawText !== this.props.rawText) { + this.setState({ inputValue: this.props.rawText ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if ( prevProps.text !== this.props.text && !this.props.isFocused && @@ -176,6 +191,27 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< } } + componentWillUnmount(): void { + this.debouncedOnValueChange.cancel(); + } + + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string, formattedValue: string) => { + this.props.updateWidgetMetaProperty("text", String(formattedValue)); + + this.props.updateWidgetMetaProperty("rawText", value, { + triggerPropertyName: "onTextChanged", + dynamicString: this.props.onTextChanged, + event: { + type: EventType.ON_TEXT_CHANGE, + }, + }); + + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + onValueChange = (value: string) => { let formattedValue = ""; const decimalSeperator = getLocaleDecimalSeperator(); @@ -194,19 +230,8 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< }); } - this.props.updateWidgetMetaProperty("text", String(formattedValue)); - - this.props.updateWidgetMetaProperty("rawText", value, { - triggerPropertyName: "onTextChanged", - dynamicString: this.props.onTextChanged, - event: { - type: EventType.ON_TEXT_CHANGE, - }, - }); - - if (!this.props.isDirty) { - this.props.updateWidgetMetaProperty("isDirty", true); - } + this.setState({ inputValue: formattedValue }); + this.debouncedOnValueChange(value, formattedValue); }; onFocusChange = (isFocused?: boolean) => { @@ -323,7 +348,7 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< } getWidgetView() { - const value = this.props.rawText ?? ""; + const value = this.state.inputValue; const validation = validateInput(this.props); return ( diff --git a/app/client/src/widgets/wds/WDSInputWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSInputWidget/widget/index.tsx index e41c0d9b9a..cd115caea4 100644 --- a/app/client/src/widgets/wds/WDSInputWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSInputWidget/widget/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { isNumber, merge, toString } from "lodash"; +import { debounce, isNumber, merge, toString } from "lodash"; import * as config from "../config"; import InputComponent from "../component"; import type { InputWidgetProps } from "./types"; @@ -14,8 +14,21 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import type { KeyDownEvent } from "widgets/wds/WDSBaseInputWidget/component/types"; import type { WidgetBaseConfiguration } from "WidgetProvider/constants"; import { INPUT_TYPES } from "widgets/wds/WDSBaseInputWidget/constants"; +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants"; + +interface WDSInputWidgetState extends WidgetState { + inputValue: string; +} + +class WDSInputWidget extends WDSBaseInputWidget< + InputWidgetProps, + WDSInputWidgetState +> { + constructor(props: InputWidgetProps) { + super(props); + this.state = { inputValue: props.rawText ?? "" }; + } -class WDSInputWidget extends WDSBaseInputWidget { static type = "WDS_INPUT_WIDGET"; static getConfig(): WidgetBaseConfiguration { @@ -155,6 +168,12 @@ class WDSInputWidget extends WDSBaseInputWidget { componentDidUpdate = (prevProps: InputWidgetProps) => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; + if (prevProps.rawText !== this.props.rawText) { + this.setState({ inputValue: this.props.rawText ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if ( prevProps.rawText !== this.props.rawText && this.props.rawText !== toString(this.props.text) @@ -183,7 +202,12 @@ class WDSInputWidget extends WDSBaseInputWidget { commitBatchMetaUpdates(); }; - onValueChange = (value: string) => { + componentWillUnmount(): void { + this.debouncedOnValueChange.cancel(); + } + + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { const { commitBatchMetaUpdates, pushBatchMetaUpdates } = this.props; // Ideally text property should be derived property. But widgets with @@ -205,6 +229,11 @@ class WDSInputWidget extends WDSBaseInputWidget { } commitBatchMetaUpdates(); + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (value: string) => { + this.setState({ inputValue: value }); + this.debouncedOnValueChange(value); }; resetWidgetText = () => { @@ -226,9 +255,9 @@ class WDSInputWidget extends WDSBaseInputWidget { }; getWidgetView() { - const { inputType, rawText } = this.props; + const { inputType } = this.props; - const value = rawText ?? ""; + const value = this.state.inputValue; const { errorMessage, validationStatus } = validateInput(this.props); return ( diff --git a/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx index f71b963331..50b1e0d73e 100644 --- a/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx @@ -21,11 +21,22 @@ import { PhoneInputComponent } from "../component"; import type { PhoneInputWidgetProps } from "./types"; import { getCountryCode, validateInput } from "./helpers"; import { appsmithTelemetry } from "instrumentation"; +import { debounce } from "lodash"; +import { DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE } from "constants/WidgetConstants"; + +interface WDSPhoneInputWidgetState extends WidgetState { + inputValue: string; +} class WDSPhoneInputWidget extends WDSBaseInputWidget< PhoneInputWidgetProps, - WidgetState + WDSPhoneInputWidgetState > { + constructor(props: PhoneInputWidgetProps) { + super(props); + this.state = { inputValue: props.text ?? "" }; + } + static type = "WDS_PHONE_INPUT_WIDGET"; static getConfig() { @@ -171,6 +182,12 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< } componentDidUpdate(prevProps: PhoneInputWidgetProps) { + if (prevProps.text !== this.props.text) { + this.setState({ inputValue: this.props.text ?? "" }); + // Cancel any pending debounced calls when value is updated externally + this.debouncedOnValueChange.cancel(); + } + if (prevProps.dialCode !== this.props.dialCode) { this.onISDCodeChange(this.props.dialCode); } @@ -205,6 +222,10 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< } } + componentWillUnmount(): void { + this.debouncedOnValueChange.cancel(); + } + onISDCodeChange = (dialCode?: string) => { const countryCode = getCountryCode(dialCode); @@ -218,21 +239,13 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< } }; - onValueChange = (value: string) => { - let formattedValue; - - // Don't format, as value is typed, when user is deleting - if (value && value.length > this.props.text?.length) { - formattedValue = this.getFormattedPhoneNumber(value); - } else { - formattedValue = value; - } - + // debouncing the input change to avoid multiple Execute calls in reactive flow + debouncedOnValueChange = debounce((value: string) => { this.props.updateWidgetMetaProperty( "rawText", - parseIncompletePhoneNumber(formattedValue), + parseIncompletePhoneNumber(value), ); - this.props.updateWidgetMetaProperty("text", formattedValue, { + this.props.updateWidgetMetaProperty("text", value, { triggerPropertyName: "onTextChanged", dynamicString: this.props.onTextChanged, event: { @@ -243,6 +256,20 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + }, DEBOUNCE_WAIT_TIME_ON_INPUT_CHANGE); + + onValueChange = (value: string) => { + let formattedValue; + + // Don't format, as value is typed, when user is deleting + if (value && value.length > this.props.text?.length) { + formattedValue = this.getFormattedPhoneNumber(value); + } else { + formattedValue = value; + } + + this.setState({ inputValue: formattedValue }); + this.debouncedOnValueChange(formattedValue); }; onFocusChange = (focusState: boolean) => { @@ -300,7 +327,7 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< }; getWidgetView() { - const rawText = this.props.text ?? ""; + const rawText = this.state.inputValue; const validation = validateInput(this.props);