From c0f93052de785597a6819e64a5abea873533cfb6 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Wed, 23 Feb 2022 16:03:51 +0800 Subject: [PATCH 01/16] feat: Internal property to detect changes in a form -- Implement dirty check logic for a form -- Expose an form property, hasChanges for checking if the user has changed any values in the form -- Add isDirty derived property for the following widgets: AudioRecorderWidget, CameraWidget, CheckboxGroupWidget, CheckboxWidget, CurrencyInputWidget, DatePickerWidget2, FilePickerWidgetV2, InputWidgetV2, MultiSelectTreeWidget, MultiSelectWidgetV2, PhoneInputWidget, RadioGroupWidget, RichTextEditorWidget, SelectWidget, SingleSelectTreeWidget, SwitchGroupWidget, SwitchWidget --- .../utils/autocomplete/EntityDefinitions.ts | 1 + .../AudioRecorderWidget/widget/index.tsx | 4 ++- .../widgets/CameraWidget/component/index.tsx | 4 ++- .../src/widgets/CameraWidget/widget/index.tsx | 5 ++-- .../CheckboxGroupWidget/widget/index.tsx | 1 + .../widgets/CheckboxWidget/widget/index.tsx | 7 +++++ .../CurrencyInputWidget/widget/index.tsx | 5 ++++ .../DatePickerWidget2/widget/index.tsx | 1 + .../FilePickerWidgetV2/widget/index.tsx | 1 + .../src/widgets/FormWidget/widget/index.tsx | 15 ++++++++++ .../widgets/InputWidgetV2/widget/index.tsx | 10 +++++++ .../MultiSelectTreeWidget/widget/index.tsx | 1 + .../MultiSelectWidgetV2/widget/index.tsx | 1 + .../widgets/PhoneInputWidget/widget/index.tsx | 5 ++++ .../widgets/RadioGroupWidget/widget/index.tsx | 1 + .../RichTextEditorWidget/widget/index.tsx | 28 +++++++++++++++++++ .../src/widgets/SelectWidget/widget/index.tsx | 5 +--- .../SingleSelectTreeWidget/widget/index.tsx | 1 + .../SwitchGroupWidget/widget/index.tsx | 15 ++++++++-- .../src/widgets/SwitchWidget/widget/index.tsx | 1 + 20 files changed, 102 insertions(+), 10 deletions(-) diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index ab39393ad3..84f3ad0be1 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -330,6 +330,7 @@ export const entityDefinitions: Record = { "!url": "https://docs.appsmith.com/widget-reference/form", isVisible: isVisible, data: generateTypeDef(widget.data), + hasChanges: "bool", }), FORM_BUTTON_WIDGET: { "!doc": diff --git a/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx b/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx index bd62f65107..2dd7a5e0a5 100644 --- a/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx +++ b/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx @@ -121,7 +121,9 @@ class AudioRecorderWidget extends BaseWidget< } static getDerivedPropertiesMap(): DerivedPropertiesMap { - return {}; + return { + isDirty: `{{ !!this.blobURL }}`, + }; } handleRecordingStart = () => { diff --git a/app/client/src/widgets/CameraWidget/component/index.tsx b/app/client/src/widgets/CameraWidget/component/index.tsx index db6f2326a9..7e16ff24a5 100644 --- a/app/client/src/widgets/CameraWidget/component/index.tsx +++ b/app/client/src/widgets/CameraWidget/component/index.tsx @@ -904,7 +904,9 @@ function CameraComponent(props: CameraComponentProps) { }, [isAudioMuted, isVideoMuted]); useEffect(() => { - setIsReadyPlayerTimer(false); + // Clean up + resetMedia(); + if (mode === CameraModeTypes.CAMERA) { setMediaCaptureStatus(MediaCaptureStatusTypes.IMAGE_DEFAULT); return; diff --git a/app/client/src/widgets/CameraWidget/widget/index.tsx b/app/client/src/widgets/CameraWidget/widget/index.tsx index cb87cba644..fffcb04b9f 100644 --- a/app/client/src/widgets/CameraWidget/widget/index.tsx +++ b/app/client/src/widgets/CameraWidget/widget/index.tsx @@ -146,7 +146,9 @@ class CameraWidget extends BaseWidget { } static getDerivedPropertiesMap(): DerivedPropertiesMap { - return {}; + return { + isDirty: `{{ this.mode === "VIDEO" ? !!this.videoBlobURL : !!this.imageBlobURL }}`, + }; } static getDefaultPropertiesMap(): Record { @@ -160,7 +162,6 @@ class CameraWidget extends BaseWidget { imageDataURL: undefined, imageRawBinary: undefined, mediaCaptureStatus: MediaCaptureStatusTypes.IMAGE_DEFAULT, - timer: undefined, videoBlobURL: undefined, videoDataURL: undefined, videoRawBinary: undefined, diff --git a/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx b/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx index 71897d22c3..be57ab786a 100644 --- a/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx @@ -273,6 +273,7 @@ class CheckboxGroupWidget extends BaseWidget< static getDerivedPropertiesMap(): DerivedPropertiesMap { return { isValid: `{{ this.isRequired ? !!this.selectedValues.length : true }}`, + isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultSelectedValues, this.selectedValues); }}`, }; } diff --git a/app/client/src/widgets/CheckboxWidget/widget/index.tsx b/app/client/src/widgets/CheckboxWidget/widget/index.tsx index de7210b360..0832ca61bd 100644 --- a/app/client/src/widgets/CheckboxWidget/widget/index.tsx +++ b/app/client/src/widgets/CheckboxWidget/widget/index.tsx @@ -121,6 +121,7 @@ class CheckboxWidget extends BaseWidget { return { value: `{{!!this.isChecked}}`, isValid: `{{ this.isRequired ? !!this.isChecked : true }}`, + isDirty: `{{ this.isChecked !== this.defaultCheckedState }}`, }; } @@ -130,6 +131,12 @@ class CheckboxWidget extends BaseWidget { }; } + componentDidUpdate(prevProps: CheckboxWidgetProps) { + if (this.props.defaultCheckedState !== prevProps.defaultCheckedState) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { return ( { diff --git a/app/client/src/widgets/DatePickerWidget2/widget/index.tsx b/app/client/src/widgets/DatePickerWidget2/widget/index.tsx index fe8071969d..ba5eda1c77 100644 --- a/app/client/src/widgets/DatePickerWidget2/widget/index.tsx +++ b/app/client/src/widgets/DatePickerWidget2/widget/index.tsx @@ -299,6 +299,7 @@ class DatePickerWidget extends BaseWidget { isValid: `{{(()=>{${derivedProperties.isValidDate}})()}}`, selectedDate: `{{ this.value ? moment(this.value).toISOString() : "" }}`, formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : "" }}`, + isDirty: `{{ this.value !== this.defaultDate }}`, }; } diff --git a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx index 58670e262b..39d1d31f61 100644 --- a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx @@ -216,6 +216,7 @@ class FilePickerWidget extends BaseWidget< return { isValid: `{{ this.isRequired ? this.files.length > 0 : true }}`, files: `{{this.selectedFiles}}`, + isDirty: `{{ this.selectedFiles.length > 0 }}`, }; } diff --git a/app/client/src/widgets/FormWidget/widget/index.tsx b/app/client/src/widgets/FormWidget/widget/index.tsx index 996fa71c73..ab9b321530 100644 --- a/app/client/src/widgets/FormWidget/widget/index.tsx +++ b/app/client/src/widgets/FormWidget/widget/index.tsx @@ -27,11 +27,25 @@ class FormWidget extends ContainerWidget { componentDidMount() { super.componentDidMount(); this.updateFormData(); + this.checkFormValueChanges(); } componentDidUpdate(prevProps: ContainerWidgetProps) { super.componentDidUpdate(prevProps); this.updateFormData(); + this.checkFormValueChanges(); + } + + checkFormValueChanges() { + const containerWidget: ContainerWidgetProps = get( + this.props, + "children[0]", + ); + const childWidgets = containerWidget.children || []; + const hasChanges = childWidgets.some((child) => child.isDirty); + if (hasChanges !== this.props.hasChanges) { + this.props.updateWidgetMetaProperty("hasChanges", hasChanges); + } } updateFormData() { @@ -75,6 +89,7 @@ class FormWidget extends ContainerWidget { export interface FormWidgetProps extends ContainerComponentProps { name: string; data: Record; + hasChanges: boolean; } export default FormWidget; diff --git a/app/client/src/widgets/InputWidgetV2/widget/index.tsx b/app/client/src/widgets/InputWidgetV2/widget/index.tsx index 08add250a7..60d11f7f02 100644 --- a/app/client/src/widgets/InputWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/InputWidgetV2/widget/index.tsx @@ -338,6 +338,13 @@ class InputWidget extends BaseInputWidget { return super.getMetaPropertiesMap(); } + componentDidUpdate(prevPorps: InputWidgetProps) { + // If defaultText property has changed, reset isDirty to false + if (this.props.defaultText !== prevPorps.defaultText) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + handleFocusChange = (focusState: boolean) => { super.handleFocusChange(focusState); }; @@ -389,6 +396,9 @@ class InputWidget extends BaseInputWidget { if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + if (value === this.props.defaultText) { + this.props.updateWidgetMetaProperty("isDirty", false); + } }; getPageView() { diff --git a/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx index b4f1f0763d..6eae25dba7 100644 --- a/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx @@ -340,6 +340,7 @@ class MultiSelectTreeWidget extends BaseWidget< selectedOptionValues: '{{ this.selectedOptionValueArr.filter((o) => JSON.stringify(this.options).match(new RegExp(`"value":"${o}"`, "g")) )}}', isValid: `{{ this.isRequired ? this.selectedOptionValues?.length > 0 : true}}`, + isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultOptionValue, this.selectedOptionValues); }}`, }; } diff --git a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx index bc28795288..3b5616f07b 100644 --- a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx @@ -307,6 +307,7 @@ class MultiSelectWidget extends BaseWidget< selectedOptionLabels: `{{ this.selectedOptions ? this.selectedOptions.map((o) => o.label ) : [] }}`, selectedOptionValues: `{{ this.selectedOptions ? this.selectedOptions.map((o) => o.value ) : [] }}`, isValid: `{{this.isRequired ? !!this.selectedOptionValues && this.selectedOptionValues.length > 0 : true}}`, + isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.map((o) => o.value).every(element => array2.includes(element));} return true;})(this.defaultOptionValue, this.selectedOptionValues); }}`, }; } diff --git a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx index 6e4d3b73ca..3faf29bd64 100644 --- a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx @@ -205,6 +205,8 @@ class PhoneInputWidget extends BaseInputWidget< parseIncompletePhoneNumber(formattedValue), ); this.props.updateWidgetMetaProperty("text", formattedValue); + // If defaultText property has changed, reset isDirty to false + this.props.updateWidgetMetaProperty("isDirty", false); } } @@ -250,6 +252,9 @@ class PhoneInputWidget extends BaseInputWidget< if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } + if (value === this.props.defaultText) { + this.props.updateWidgetMetaProperty("isDirty", true); + } }; handleFocusChange = (focusState: boolean) => { diff --git a/app/client/src/widgets/RadioGroupWidget/widget/index.tsx b/app/client/src/widgets/RadioGroupWidget/widget/index.tsx index f58202fe49..eccd11e82e 100644 --- a/app/client/src/widgets/RadioGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/RadioGroupWidget/widget/index.tsx @@ -260,6 +260,7 @@ class RadioGroupWidget extends BaseWidget { "{{_.find(this.options, { value: this.selectedOptionValue })}}", isValid: `{{ this.isRequired ? !!this.selectedOptionValue : true }}`, value: `{{this.selectedOptionValue}}`, + isDirty: `{{ this.selectedOptionValue !== this.defaultOptionValue }}`, }; } diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index 0a124730fc..233c590fc8 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -130,6 +130,9 @@ class RichTextEditorWidget extends BaseWidget< static getMetaPropertiesMap(): Record { return { text: undefined, + defaultTextHtml: undefined, + defaultTextMarkdown: undefined, + isDefaultTextChanged: false, }; } @@ -143,10 +146,32 @@ class RichTextEditorWidget extends BaseWidget< return { value: `{{this.text}}`, isValid: `{{ this.isRequired ? this.text && this.text.length : true }}`, + isDirty: `{{ this.inputType === "html" ? this.text !== this.defaultTextHtml : this.text !== this.defaultTextMarkdown }}`, }; } + componentDidUpdate(prevProps: RichTextEditorWidgetProps): void { + if (this.props.defaultText !== prevProps.defaultText) { + this.props.updateWidgetMetaProperty("isDefaultTextChanged", true); + } + } + onValueChange = (text: string) => { + if ( + this.props.inputType === RTEFormats.HTML && + (!this.props.defaultTextHtml || this.props.isDefaultTextChanged) + ) { + this.props.updateWidgetMetaProperty("defaultTextHtml", text); + this.props.updateWidgetMetaProperty("isDefaultTextChanged", false); + } + if ( + this.props.inputType === RTEFormats.MARKDOWN && + (!this.props.defaultTextMarkdown || this.props.isDefaultTextChanged) + ) { + this.props.updateWidgetMetaProperty("defaultTextMarkdown", text); + this.props.updateWidgetMetaProperty("isDefaultTextChanged", false); + } + this.props.updateWidgetMetaProperty("text", text, { triggerPropertyName: "onTextChange", dynamicString: this.props.onTextChange, @@ -188,6 +213,9 @@ class RichTextEditorWidget extends BaseWidget< export interface RichTextEditorWidgetProps extends WidgetProps { defaultText?: string; + defaultTextHtml?: string; + defaultTextMarkdown?: string; + isDefaultTextChanged: boolean; text: string; inputType: string; placeholder?: string; diff --git a/app/client/src/widgets/SelectWidget/widget/index.tsx b/app/client/src/widgets/SelectWidget/widget/index.tsx index 0666f72f1a..43f03cbada 100644 --- a/app/client/src/widgets/SelectWidget/widget/index.tsx +++ b/app/client/src/widgets/SelectWidget/widget/index.tsx @@ -278,6 +278,7 @@ class SelectWidget extends BaseWidget { isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`, selectedOptionLabel: `{{ this.optionValue.label ?? this.optionValue.value }}`, selectedOptionValue: `{{ this.optionValue.value }}`, + isDirty: `{{ this.optionValue.value !== this.defaultValue.value }}`, }; } @@ -362,10 +363,6 @@ class SelectWidget extends BaseWidget { onOptionSelected = (selectedOption: DropdownOption) => { let isChanged = true; - if (!this.props.isDirty) { - this.props.updateWidgetMetaProperty("isDirty", true); - } - // Check if the value has changed. If no option // selected till now, there is a change if (this.props.selectedOptionValue) { diff --git a/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx index 95da6d2fe6..b5a8afad93 100644 --- a/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx +++ b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx @@ -304,6 +304,7 @@ class SingleSelectTreeWidget extends BaseWidget< selectedOptionValue: '{{ JSON.stringify(this.options).match(new RegExp(`"value":${Number.isFinite(this.selectedOption) ? this.selectedOption : `"${this.selectedOption}"` }`), "g") ? this.selectedOption : undefined }}', isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`, + isDirty: `{{ this.selectedOptionValue !== this.defaultOptionValue }}`, }; } diff --git a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx index a4500b6df8..3dcab49bce 100644 --- a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx @@ -189,6 +189,7 @@ class SwitchGroupWidget extends BaseWidget< selectedValue => this.options.map(option => option.value).includes(selectedValue) ) }}`, + isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultSelectedValues, this.selectedValuesArray); }}`, }; } @@ -196,6 +197,15 @@ class SwitchGroupWidget extends BaseWidget< return "SWITCH_GROUP_WIDGET"; } + componentDidUpdate(prevProps: SwitchGroupWidgetProps): void { + if (this.props.defaultSelectedValues !== prevProps.defaultSelectedValues) { + this.props.updateWidgetMetaProperty( + "selectedValuesArray", + this.props.defaultSelectedValues, + ); + } + } + getPageView() { const { alignment, @@ -205,7 +215,7 @@ class SwitchGroupWidget extends BaseWidget< isValid, options, parentRowSpace, - selectedValues, + selectedValuesArray, } = this.props; return ( @@ -217,7 +227,7 @@ class SwitchGroupWidget extends BaseWidget< options={options} required={isRequired} rowSpace={parentRowSpace} - selected={selectedValues} + selected={selectedValuesArray} valid={isValid} /> ); @@ -253,6 +263,7 @@ class SwitchGroupWidget extends BaseWidget< export interface SwitchGroupWidgetProps extends WidgetProps { options: OptionProps[]; defaultSelectedValues: string[]; + selectedValuesArray: string[]; isInline: boolean; isRequired?: boolean; isValid?: boolean; diff --git a/app/client/src/widgets/SwitchWidget/widget/index.tsx b/app/client/src/widgets/SwitchWidget/widget/index.tsx index 7a080e2c14..6350083f88 100644 --- a/app/client/src/widgets/SwitchWidget/widget/index.tsx +++ b/app/client/src/widgets/SwitchWidget/widget/index.tsx @@ -137,6 +137,7 @@ class SwitchWidget extends BaseWidget { static getDerivedPropertiesMap(): DerivedPropertiesMap { return { value: `{{!!this.isSwitchedOn}}`, + isDirty: `{{ this.isSwitchedOn !== this.defaultSwitchState }}`, }; } From dbb09dd4e031a719ff5387f57261da824a6009c0 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Wed, 2 Mar 2022 03:02:10 +0800 Subject: [PATCH 02/16] feat: Internal property to detect changes in a form -- Make isDirty to a meta property, followed by code refactoring -- Reset isDirty only if default value changes -- Recursively check if the form has changes, even considering nested case --- .../AudioRecorderWidget/widget/index.tsx | 10 +++-- .../src/widgets/CameraWidget/widget/index.tsx | 15 +++++-- .../CheckboxGroupWidget/widget/index.tsx | 21 +++++++++- .../widgets/CheckboxWidget/widget/index.tsx | 11 ++++- .../CurrencyInputWidget/widget/index.tsx | 14 +++++-- .../DatePickerWidget2/widget/index.tsx | 15 ++++++- .../FilePickerWidgetV2/widget/index.tsx | 6 ++- .../src/widgets/FormWidget/widget/index.tsx | 40 ++++++++++++++----- .../widgets/InputWidgetV2/widget/index.tsx | 8 ++-- .../MultiSelectTreeWidget/widget/index.tsx | 19 ++++++++- .../MultiSelectWidgetV2/widget/index.tsx | 11 ++++- .../widgets/PhoneInputWidget/widget/index.tsx | 8 ++-- .../widgets/RadioGroupWidget/widget/index.tsx | 17 +++++++- .../RichTextEditorWidget/widget/index.tsx | 8 ++++ .../src/widgets/SelectWidget/widget/index.tsx | 17 +++++++- .../SingleSelectTreeWidget/widget/index.tsx | 15 ++++++- .../SwitchGroupWidget/widget/index.tsx | 15 ++++++- .../src/widgets/SwitchWidget/widget/index.tsx | 16 +++++++- 18 files changed, 223 insertions(+), 43 deletions(-) diff --git a/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx b/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx index 2dd7a5e0a5..8b5c50ef57 100644 --- a/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx +++ b/app/client/src/widgets/AudioRecorderWidget/widget/index.tsx @@ -17,6 +17,7 @@ export interface AudioRecorderWidgetProps extends WidgetProps { onRecordingStart?: string; onRecordingComplete?: string; blobURL?: string; + isDirty: boolean; } class AudioRecorderWidget extends BaseWidget< @@ -117,16 +118,19 @@ class AudioRecorderWidget extends BaseWidget< blobURL: undefined, dataURL: undefined, rawBinary: undefined, + isDirty: false, }; } static getDerivedPropertiesMap(): DerivedPropertiesMap { - return { - isDirty: `{{ !!this.blobURL }}`, - }; + return {}; } handleRecordingStart = () => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + if (this.props.blobURL) { URL.revokeObjectURL(this.props.blobURL); } diff --git a/app/client/src/widgets/CameraWidget/widget/index.tsx b/app/client/src/widgets/CameraWidget/widget/index.tsx index fffcb04b9f..6fd50aed62 100644 --- a/app/client/src/widgets/CameraWidget/widget/index.tsx +++ b/app/client/src/widgets/CameraWidget/widget/index.tsx @@ -146,9 +146,7 @@ class CameraWidget extends BaseWidget { } static getDerivedPropertiesMap(): DerivedPropertiesMap { - return { - isDirty: `{{ this.mode === "VIDEO" ? !!this.videoBlobURL : !!this.imageBlobURL }}`, - }; + return {}; } static getDefaultPropertiesMap(): Record { @@ -165,6 +163,7 @@ class CameraWidget extends BaseWidget { videoBlobURL: undefined, videoDataURL: undefined, videoRawBinary: undefined, + isDirty: false, }; } @@ -216,6 +215,11 @@ class CameraWidget extends BaseWidget { this.props.updateWidgetMetaProperty("imageRawBinary", undefined); return; } + // Set isDirty to true when an image is caputured + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + const base64Data = image.split(",")[1]; const imageBlob = base64ToBlob(base64Data, "image/webp"); const blobURL = URL.createObjectURL(imageBlob); @@ -252,6 +256,10 @@ class CameraWidget extends BaseWidget { }; handleRecordingStart = () => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + if (this.props.onRecordingStart) { super.executeAction({ triggerPropertyName: "onRecordingStart", @@ -320,6 +328,7 @@ export interface CameraWidgetProps extends WidgetProps { onRecordingStop?: string; onVideoSave?: string; videoBlobURL?: string; + isDirty: boolean; } export default CameraWidget; diff --git a/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx b/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx index be57ab786a..5af25bb95d 100644 --- a/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/CheckboxGroupWidget/widget/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { compact } from "lodash"; +import { compact, xor } from "lodash"; import { ValidationResponse, @@ -267,13 +267,13 @@ class CheckboxGroupWidget extends BaseWidget< static getMetaPropertiesMap(): Record { return { selectedValues: undefined, + isDirty: false, }; } static getDerivedPropertiesMap(): DerivedPropertiesMap { return { isValid: `{{ this.isRequired ? !!this.selectedValues.length : true }}`, - isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultSelectedValues, this.selectedValues); }}`, }; } @@ -309,6 +309,14 @@ class CheckboxGroupWidget extends BaseWidget< }, }); } + // Reset isDirty to false whenever defaultSelectedValues changes + if ( + xor(this.props.defaultSelectedValues, prevProps.defaultSelectedValues) + .length > 0 && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } } getPageView() { @@ -347,6 +355,11 @@ class CheckboxGroupWidget extends BaseWidget< ); } + // Update isDirty to true whenever value changes + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("selectedValues", selectedValues, { triggerPropertyName: "onSelectionChange", dynamicString: this.props.onSelectionChange, @@ -371,6 +384,10 @@ class CheckboxGroupWidget extends BaseWidget< break; } + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("selectedValues", selectedValues, { triggerPropertyName: "onSelectionChange", dynamicString: this.props.onSelectionChange, diff --git a/app/client/src/widgets/CheckboxWidget/widget/index.tsx b/app/client/src/widgets/CheckboxWidget/widget/index.tsx index 0832ca61bd..cd5d6b2676 100644 --- a/app/client/src/widgets/CheckboxWidget/widget/index.tsx +++ b/app/client/src/widgets/CheckboxWidget/widget/index.tsx @@ -121,18 +121,21 @@ class CheckboxWidget extends BaseWidget { return { value: `{{!!this.isChecked}}`, isValid: `{{ this.isRequired ? !!this.isChecked : true }}`, - isDirty: `{{ this.isChecked !== this.defaultCheckedState }}`, }; } static getMetaPropertiesMap(): Record { return { isChecked: undefined, + isDirty: false, }; } componentDidUpdate(prevProps: CheckboxWidgetProps) { - if (this.props.defaultCheckedState !== prevProps.defaultCheckedState) { + if ( + this.props.defaultCheckedState !== prevProps.defaultCheckedState && + this.props.isDirty + ) { this.props.updateWidgetMetaProperty("isDirty", false); } } @@ -155,6 +158,10 @@ class CheckboxWidget extends BaseWidget { } onCheckChange = (isChecked: boolean) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("isChecked", isChecked, { triggerPropertyName: "onCheckChange", dynamicString: this.props.onCheckChange, diff --git a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx index d83c1cf861..7477528076 100644 --- a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx +++ b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx @@ -203,7 +203,12 @@ class CurrencyInputWidget extends BaseInputWidget< this.props.text === String(this.props.defaultText) ) { this.formatText(); - // If defaultText property has changed, reset isDirty to false + } + // If defaultText property has changed, reset isDirty to false + if ( + this.props.defaultText !== prevProps.defaultText && + this.props.isDirty + ) { this.props.updateWidgetMetaProperty("isDirty", false); } } @@ -250,9 +255,6 @@ class CurrencyInputWidget extends BaseInputWidget< if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } - if (value === String(this.props.defaultText)) { - this.props.updateWidgetMetaProperty("isDirty", false); - } }; handleFocusChange = (isFocused?: boolean) => { @@ -309,6 +311,10 @@ class CurrencyInputWidget extends BaseInputWidget< this.props.decimals, String(value), ); + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("text", String(formattedValue), { triggerPropertyName: "onTextChanged", dynamicString: this.props.onTextChanged, diff --git a/app/client/src/widgets/DatePickerWidget2/widget/index.tsx b/app/client/src/widgets/DatePickerWidget2/widget/index.tsx index ba5eda1c77..deda69f8b9 100644 --- a/app/client/src/widgets/DatePickerWidget2/widget/index.tsx +++ b/app/client/src/widgets/DatePickerWidget2/widget/index.tsx @@ -299,7 +299,6 @@ class DatePickerWidget extends BaseWidget { isValid: `{{(()=>{${derivedProperties.isValidDate}})()}}`, selectedDate: `{{ this.value ? moment(this.value).toISOString() : "" }}`, formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : "" }}`, - isDirty: `{{ this.value !== this.defaultDate }}`, }; } @@ -312,9 +311,19 @@ class DatePickerWidget extends BaseWidget { static getMetaPropertiesMap(): Record { return { value: undefined, + isDirty: false, }; } + componentDidUpdate(prevProps: DatePickerWidget2Props): void { + if ( + this.props.defaultDate !== prevProps.defaultDate && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { return ( { } onDateSelected = (selectedDate: string) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("value", selectedDate, { triggerPropertyName: "onDateSelected", dynamicString: this.props.onDateSelected, diff --git a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx index 39d1d31f61..bda82711d4 100644 --- a/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/FilePickerWidgetV2/widget/index.tsx @@ -216,7 +216,6 @@ class FilePickerWidget extends BaseWidget< return { isValid: `{{ this.isRequired ? this.files.length > 0 : true }}`, files: `{{this.selectedFiles}}`, - isDirty: `{{ this.selectedFiles.length > 0 }}`, }; } @@ -224,6 +223,7 @@ class FilePickerWidget extends BaseWidget< return { selectedFiles: [], uploadedFileData: {}, + isDirty: false, }; } @@ -384,6 +384,10 @@ class FilePickerWidget extends BaseWidget< }); Promise.all(fileReaderPromises).then((files) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty( "selectedFiles", dslFiles.concat(files), diff --git a/app/client/src/widgets/FormWidget/widget/index.tsx b/app/client/src/widgets/FormWidget/widget/index.tsx index ab9b321530..9f8c508f84 100644 --- a/app/client/src/widgets/FormWidget/widget/index.tsx +++ b/app/client/src/widgets/FormWidget/widget/index.tsx @@ -27,27 +27,47 @@ class FormWidget extends ContainerWidget { componentDidMount() { super.componentDidMount(); this.updateFormData(); - this.checkFormValueChanges(); + + // Check if the form is dirty + const hasChanges = this.checkFormValueChanges( + get(this.props, "children[0]"), + ); + + if (hasChanges !== this.props.hasChanges) { + this.props.updateWidgetMetaProperty("hasChanges", hasChanges); + } } componentDidUpdate(prevProps: ContainerWidgetProps) { super.componentDidUpdate(prevProps); this.updateFormData(); - this.checkFormValueChanges(); - } - - checkFormValueChanges() { - const containerWidget: ContainerWidgetProps = get( - this.props, - "children[0]", + // Check if the form is dirty + const hasChanges = this.checkFormValueChanges( + get(this.props, "children[0]"), ); - const childWidgets = containerWidget.children || []; - const hasChanges = childWidgets.some((child) => child.isDirty); + if (hasChanges !== this.props.hasChanges) { this.props.updateWidgetMetaProperty("hasChanges", hasChanges); } } + checkFormValueChanges( + containerWidget: ContainerWidgetProps, + ): boolean { + const childWidgets = containerWidget.children || []; + + const hasChanges = childWidgets.some((child) => child.isDirty); + if (!hasChanges) { + return childWidgets.some( + (child) => + child.children && + this.checkFormValueChanges(get(child, "children[0]")), + ); + } + + return hasChanges; + } + updateFormData() { const firstChild = get(this.props, "children[0]"); if (firstChild) { diff --git a/app/client/src/widgets/InputWidgetV2/widget/index.tsx b/app/client/src/widgets/InputWidgetV2/widget/index.tsx index d9c532f210..e3ca972cae 100644 --- a/app/client/src/widgets/InputWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/InputWidgetV2/widget/index.tsx @@ -338,7 +338,10 @@ class InputWidget extends BaseInputWidget { componentDidUpdate(prevPorps: InputWidgetProps) { // If defaultText property has changed, reset isDirty to false - if (this.props.defaultText !== prevPorps.defaultText) { + if ( + this.props.defaultText !== prevPorps.defaultText && + this.props.isDirty + ) { this.props.updateWidgetMetaProperty("isDirty", false); } } @@ -394,9 +397,6 @@ class InputWidget extends BaseInputWidget { if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } - if (value === this.props.defaultText) { - this.props.updateWidgetMetaProperty("isDirty", false); - } }; getPageView() { diff --git a/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx index 6eae25dba7..405839c113 100644 --- a/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx @@ -2,7 +2,7 @@ import React, { ReactNode } from "react"; import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; import { TextSize, WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -import { isArray, findIndex } from "lodash"; +import { isArray, findIndex, xor } from "lodash"; import { ValidationResponse, ValidationTypes, @@ -340,7 +340,6 @@ class MultiSelectTreeWidget extends BaseWidget< selectedOptionValues: '{{ this.selectedOptionValueArr.filter((o) => JSON.stringify(this.options).match(new RegExp(`"value":"${o}"`, "g")) )}}', isValid: `{{ this.isRequired ? this.selectedOptionValues?.length > 0 : true}}`, - isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultOptionValue, this.selectedOptionValues); }}`, }; } @@ -355,8 +354,20 @@ class MultiSelectTreeWidget extends BaseWidget< return { selectedOptionValueArr: undefined, selectedLabel: [], + isDirty: false, }; } + + componentDidUpdate(prevProps: MultiSelectTreeWidgetProps): void { + if ( + xor(this.props.defaultOptionValue, prevProps.defaultOptionValue).length > + 0 && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { const options = isArray(this.props.options) && @@ -405,6 +416,9 @@ class MultiSelectTreeWidget extends BaseWidget< } onOptionChange = (value?: DefaultValueType, labelList?: ReactNode[]) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } this.props.updateWidgetMetaProperty("selectedOptionValueArr", value); this.props.updateWidgetMetaProperty("selectedLabel", labelList, { triggerPropertyName: "onOptionChange", @@ -469,6 +483,7 @@ export interface MultiSelectTreeWidgetProps extends WidgetProps { labelTextColor?: string; labelTextSize?: TextSize; labelStyle?: string; + isDirty: boolean; } export default MultiSelectTreeWidget; diff --git a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx index b9ee721fde..a4ebaa6fe3 100644 --- a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx @@ -399,7 +399,6 @@ class MultiSelectWidget extends BaseWidget< selectedOptionLabels: `{{ this.selectedOptions ? this.selectedOptions.map((o) => _.isNil(o.label) ? o : o.label ) : [] }}`, selectedOptionValues: `{{ this.selectedOptions ? this.selectedOptions.map((o) => _.isNil(o.value) ? o : o.value ) : [] }}`, isValid: `{{this.isRequired ? !!this.selectedOptionValues && this.selectedOptionValues.length > 0 : true}}`, - isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.map((o) => o.value).every(element => array2.includes(element));} return true;})(this.defaultOptionValue, this.selectedOptionValues); }}`, }; } @@ -414,9 +413,19 @@ class MultiSelectWidget extends BaseWidget< return { selectedOptions: undefined, filterText: "", + isDirty: false, }; } + componentDidUpdate(prevProps: MultiSelectWidgetProps): void { + if ( + this.props.defaultOptionValue !== prevProps.defaultOptionValue && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { const options = isArray(this.props.options) ? this.props.options : []; const dropDownWidth = MinimumPopupRows * this.props.parentColumnSpace; diff --git a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx index 3faf29bd64..deaea538e0 100644 --- a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx @@ -205,8 +205,11 @@ class PhoneInputWidget extends BaseInputWidget< parseIncompletePhoneNumber(formattedValue), ); this.props.updateWidgetMetaProperty("text", formattedValue); + // If defaultText property has changed, reset isDirty to false - this.props.updateWidgetMetaProperty("isDirty", false); + if (this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", false); + } } } @@ -252,9 +255,6 @@ class PhoneInputWidget extends BaseInputWidget< if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } - if (value === this.props.defaultText) { - this.props.updateWidgetMetaProperty("isDirty", true); - } }; handleFocusChange = (focusState: boolean) => { diff --git a/app/client/src/widgets/RadioGroupWidget/widget/index.tsx b/app/client/src/widgets/RadioGroupWidget/widget/index.tsx index eccd11e82e..7d90148134 100644 --- a/app/client/src/widgets/RadioGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/RadioGroupWidget/widget/index.tsx @@ -260,7 +260,6 @@ class RadioGroupWidget extends BaseWidget { "{{_.find(this.options, { value: this.selectedOptionValue })}}", isValid: `{{ this.isRequired ? !!this.selectedOptionValue : true }}`, value: `{{this.selectedOptionValue}}`, - isDirty: `{{ this.selectedOptionValue !== this.defaultOptionValue }}`, }; } @@ -273,9 +272,19 @@ class RadioGroupWidget extends BaseWidget { static getMetaPropertiesMap(): Record { return { selectedOptionValue: undefined, + isDirty: false, }; } + componentDidUpdate(prevProps: RadioGroupWidgetProps): void { + if ( + this.props.defaultOptionValue !== prevProps.defaultOptionValue && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { return ( { } else { newVal = updatedValue; } + // Set isDirty to true when the selection changes + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("selectedOptionValue", newVal, { triggerPropertyName: "onSelectionChange", dynamicString: this.props.onSelectionChange, @@ -319,6 +333,7 @@ export interface RadioGroupWidgetProps extends WidgetProps { onSelectionChange: string; defaultOptionValue: string; isRequired?: boolean; + isDirty: boolean; } export default RadioGroupWidget; diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index 233c590fc8..f2cb006c38 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -133,6 +133,7 @@ class RichTextEditorWidget extends BaseWidget< defaultTextHtml: undefined, defaultTextMarkdown: undefined, isDefaultTextChanged: false, + isDirty: false, }; } @@ -152,11 +153,17 @@ class RichTextEditorWidget extends BaseWidget< componentDidUpdate(prevProps: RichTextEditorWidgetProps): void { if (this.props.defaultText !== prevProps.defaultText) { + if (this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", false); + } this.props.updateWidgetMetaProperty("isDefaultTextChanged", true); } } onValueChange = (text: string) => { + if (!(this.props.isDefaultTextChanged || this.props.isDirty)) { + this.props.updateWidgetMetaProperty("isDirty", true); + } if ( this.props.inputType === RTEFormats.HTML && (!this.props.defaultTextHtml || this.props.isDefaultTextChanged) @@ -224,6 +231,7 @@ export interface RichTextEditorWidgetProps extends WidgetProps { isVisible?: boolean; isRequired?: boolean; isToolbarHidden?: boolean; + isDirty: boolean; } export default RichTextEditorWidget; diff --git a/app/client/src/widgets/SelectWidget/widget/index.tsx b/app/client/src/widgets/SelectWidget/widget/index.tsx index c55ca8df31..459f4d6e88 100644 --- a/app/client/src/widgets/SelectWidget/widget/index.tsx +++ b/app/client/src/widgets/SelectWidget/widget/index.tsx @@ -334,6 +334,7 @@ class SelectWidget extends BaseWidget { value: undefined, label: undefined, filterText: "", + isDirty: false, }; } @@ -342,7 +343,6 @@ class SelectWidget extends BaseWidget { isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`, selectedOptionLabel: `{{(()=>{const label = _.isPlainObject(this.label) ? this.label?.label : this.label; return label; })()}}`, selectedOptionValue: `{{(()=>{const value = _.isPlainObject(this.value) ? this.value?.value : this.value; return value; })()}}`, - isDirty: `{{ this.value !== this.defaultValue }}`, }; } @@ -351,6 +351,17 @@ class SelectWidget extends BaseWidget { this.changeSelectedOption(); } + componentDidUpdate(prevProps: SelectWidgetProps): void { + const defaultOptionValue = + this.props.defaultOptionValue.value ?? this.props.defaultOptionValue; + const prevDefaultOptionValue = + prevProps.defaultOptionValue.value ?? prevProps.defaultOptionValue; + // Reset isDirty to false if defaultOptionValue changes + if (defaultOptionValue !== prevDefaultOptionValue && this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + isStringOrNumber = (value: any): value is string | number => isString(value) || isNumber(value); @@ -408,6 +419,10 @@ class SelectWidget extends BaseWidget { isChanged = !(this.props.selectedOptionValue === selectedOption.value); } if (isChanged) { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("label", selectedOption.label ?? ""); this.props.updateWidgetMetaProperty("value", selectedOption.value ?? "", { diff --git a/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx index b5a8afad93..8215ee0573 100644 --- a/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx +++ b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx @@ -304,7 +304,6 @@ class SingleSelectTreeWidget extends BaseWidget< selectedOptionValue: '{{ JSON.stringify(this.options).match(new RegExp(`"value":${Number.isFinite(this.selectedOption) ? this.selectedOption : `"${this.selectedOption}"` }`), "g") ? this.selectedOption : undefined }}', isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`, - isDirty: `{{ this.selectedOptionValue !== this.defaultOptionValue }}`, }; } @@ -320,9 +319,19 @@ class SingleSelectTreeWidget extends BaseWidget< selectedOption: undefined, selectedOptionValueArr: undefined, selectedLabel: [], + isDirty: false, }; } + componentDidUpdate(prevProps: SingleSelectTreeWidgetProps): void { + if ( + this.props.defaultOptionValue !== prevProps.defaultOptionValue && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + getPageView() { const options = isArray(this.props.options) && @@ -370,6 +379,10 @@ class SingleSelectTreeWidget extends BaseWidget< } onOptionChange = (value?: DefaultValueType, labelList?: ReactNode[]) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("selectedOption", value); this.props.updateWidgetMetaProperty("selectedLabel", labelList, { triggerPropertyName: "onOptionChange", diff --git a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx index 3dcab49bce..df1c32dec3 100644 --- a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Alignment } from "@blueprintjs/core"; +import { xor } from "lodash"; import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; import { DerivedPropertiesMap } from "utils/WidgetFactory"; @@ -189,7 +190,6 @@ class SwitchGroupWidget extends BaseWidget< selectedValue => this.options.map(option => option.value).includes(selectedValue) ) }}`, - isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.every(element => array2.includes(element));} return true;})(this.defaultSelectedValues, this.selectedValuesArray); }}`, }; } @@ -198,7 +198,14 @@ class SwitchGroupWidget extends BaseWidget< } componentDidUpdate(prevProps: SwitchGroupWidgetProps): void { - if (this.props.defaultSelectedValues !== prevProps.defaultSelectedValues) { + if ( + xor(this.props.defaultSelectedValues, prevProps.defaultSelectedValues) + .length > 0 + ) { + if (this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + this.props.updateWidgetMetaProperty( "selectedValuesArray", this.props.defaultSelectedValues, @@ -245,6 +252,10 @@ class SwitchGroupWidget extends BaseWidget< ); } + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty( "selectedValuesArray", selectedValuesArray, diff --git a/app/client/src/widgets/SwitchWidget/widget/index.tsx b/app/client/src/widgets/SwitchWidget/widget/index.tsx index 6350083f88..2d883e59c8 100644 --- a/app/client/src/widgets/SwitchWidget/widget/index.tsx +++ b/app/client/src/widgets/SwitchWidget/widget/index.tsx @@ -131,17 +131,30 @@ class SwitchWidget extends BaseWidget { static getMetaPropertiesMap(): Record { return { isSwitchedOn: undefined, + isDirty: false, }; } static getDerivedPropertiesMap(): DerivedPropertiesMap { return { value: `{{!!this.isSwitchedOn}}`, - isDirty: `{{ this.isSwitchedOn !== this.defaultSwitchState }}`, }; } + componentDidUpdate(prevProps: SwitchWidgetProps): void { + if ( + this.props.defaultSwitchState !== prevProps.defaultSwitchState && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } + } + onChange = (isSwitchedOn: boolean) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + this.props.updateWidgetMetaProperty("isSwitchedOn", isSwitchedOn, { triggerPropertyName: "onChange", dynamicString: this.props.onChange, @@ -157,6 +170,7 @@ export interface SwitchWidgetProps extends WidgetProps { defaultSwitchState: boolean; alignWidget: AlignWidget; label: string; + isDirty: boolean; } export default SwitchWidget; From 5254f8e7f3d7352420abd0259128f5c4650e0a96 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Thu, 3 Mar 2022 21:57:22 +0800 Subject: [PATCH 03/16] feat: Internal property to detect changes in a form -- Eliminate the redundant code from RichTextEditorWidget -- Change the way to detect default values changes with SelectWidget and MultiSelectWidgetV2 --- .../src/widgets/MultiSelectWidgetV2/widget/index.tsx | 10 +++++++--- .../src/widgets/RichTextEditorWidget/widget/index.tsx | 1 - app/client/src/widgets/SelectWidget/widget/index.tsx | 11 +++++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx index a4ebaa6fe3..6d7da7e496 100644 --- a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -import { isArray, isString, isNumber } from "lodash"; +import { isArray, isEqual, isString, isNumber, xorWith } from "lodash"; import { ValidationResponse, ValidationTypes, @@ -190,7 +190,7 @@ class MultiSelectWidget extends BaseWidget< fn: defaultOptionValueValidation, expected: { type: "Array of values", - example: `['option1', 'option2'] | [{ "label": "label1", "value": "value1" }]`, + example: ` "option1, option2" | ['option1', 'option2'] | [{ "label": "label1", "value": "value1" }]`, autocompleteDataType: AutocompleteDataType.ARRAY, }, }, @@ -419,7 +419,11 @@ class MultiSelectWidget extends BaseWidget< componentDidUpdate(prevProps: MultiSelectWidgetProps): void { if ( - this.props.defaultOptionValue !== prevProps.defaultOptionValue && + xorWith( + this.props.defaultOptionValue, + prevProps.defaultOptionValue, + isEqual, + ).length > 0 && this.props.isDirty ) { this.props.updateWidgetMetaProperty("isDirty", false); diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index 4a4e971361..e80c5216d6 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -147,7 +147,6 @@ class RichTextEditorWidget extends BaseWidget< return { value: `{{this.text}}`, isValid: `{{ this.isRequired ? this.text && this.text.length : true }}`, - isDirty: `{{ this.inputType === "html" ? this.text !== this.defaultTextHtml : this.text !== this.defaultTextMarkdown }}`, }; } diff --git a/app/client/src/widgets/SelectWidget/widget/index.tsx b/app/client/src/widgets/SelectWidget/widget/index.tsx index 459f4d6e88..4deed0aef7 100644 --- a/app/client/src/widgets/SelectWidget/widget/index.tsx +++ b/app/client/src/widgets/SelectWidget/widget/index.tsx @@ -11,7 +11,7 @@ import { import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import { MinimumPopupRows, GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; import { AutocompleteDataType } from "utils/autocomplete/TernServer"; -import { findIndex, isArray, isNumber, isString } from "lodash"; +import { findIndex, isArray, isEqual, isNumber, isString } from "lodash"; export function defaultOptionValueValidation( value: unknown, @@ -352,12 +352,11 @@ class SelectWidget extends BaseWidget { } componentDidUpdate(prevProps: SelectWidgetProps): void { - const defaultOptionValue = - this.props.defaultOptionValue.value ?? this.props.defaultOptionValue; - const prevDefaultOptionValue = - prevProps.defaultOptionValue.value ?? prevProps.defaultOptionValue; // Reset isDirty to false if defaultOptionValue changes - if (defaultOptionValue !== prevDefaultOptionValue && this.props.isDirty) { + if ( + !isEqual(this.props.defaultOptionValue, prevProps.defaultOptionValue) && + this.props.isDirty + ) { this.props.updateWidgetMetaProperty("isDirty", false); } } From 70706c7f8eaba10890f8e8b7b62d29db32b47996 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Mon, 7 Mar 2022 23:23:57 +0800 Subject: [PATCH 04/16] feat: Internal property to detect changes in a form -- Add Cypress tests FormWidget's hasChanges and all assoicated widgets' isDirty -- Add missing isDirty meta property for SwitchGroupWidget -- Rewrite isDirty check logic for RichTextEditorWidget --- app/client/cypress.json | 8 + app/client/cypress/fixtures/CameraDsl.json | 16 + .../fixtures/SwitchGroupWidgetDsl.json | 16 + .../cypress/fixtures/TreeSelectDsl.json | 16 + .../cypress/fixtures/formHasChangesDsl.json | 481 ++++++++++++++++++ app/client/cypress/fixtures/formdsl1.json | 16 + .../cypress/fixtures/multiSelectDsl.json | 19 + app/client/cypress/fixtures/newFormDsl.json | 19 + .../FormWidgets/AudioRecorder_spec.js | 40 ++ .../FormWidgets/Camera_spec.js | 8 +- .../FormWidgets/CheckBox_spec.js | 21 + .../FormWidgets/CheckboxGroup_spec.js | 24 + .../FormWidgets/CurrencyInput_spec.js | 24 + .../FormWidgets/DatePicker_2_spec.js | 43 ++ .../FormWidgets/FilePickerV2_spec.js | 30 ++ .../FormWidget_Nested_HasChanges_spec.js | 20 + .../FormWidgets/Inputv2_spec.js | 23 + .../FormWidgets/MultiSelect_spec.js | 18 + .../FormWidgets/Multi_Select_Tree_spec.js | 23 + .../FormWidgets/Phone_input_spec.js | 18 + .../FormWidgets/RadioGroup_spec.js | 30 ++ .../FormWidgets/RichTextEditor_spec.js | 18 + .../FormWidgets/Select_spec.js | 29 ++ .../FormWidgets/Single_Select_Tree_spec.js | 22 + .../FormWidgets/SwitchGroup_spec.js | 22 + .../FormWidgets/Switch_spec.js | 18 + app/client/cypress/support/commands.js | 1 + .../RichTextEditorWidget/widget/index.tsx | 24 +- .../SwitchGroupWidget/widget/index.tsx | 1 + 29 files changed, 1028 insertions(+), 20 deletions(-) create mode 100644 app/client/cypress/fixtures/formHasChangesDsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/AudioRecorder_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FilePickerV2_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RadioGroup_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js diff --git a/app/client/cypress.json b/app/client/cypress.json index f716569528..6d481a05cf 100644 --- a/app/client/cypress.json +++ b/app/client/cypress.json @@ -29,5 +29,13 @@ "retries": { "runMode": 2, "openMode": 0 + }, + "env": { + "USERNAME": "paul@appsmith.com", + "PASSWORD": "paul1234", + "TESTUSERNAME1": "paul@appsmith.com", + "TESTPASSWORD1": "paul1234", + "TESTUSERNAME2": "paul@appsmith.com", + "TESTPASSWORD2": "paul1234" } } \ No newline at end of file diff --git a/app/client/cypress/fixtures/CameraDsl.json b/app/client/cypress/fixtures/CameraDsl.json index 7cf44388e5..44bb9ab489 100644 --- a/app/client/cypress/fixtures/CameraDsl.json +++ b/app/client/cypress/fixtures/CameraDsl.json @@ -19,6 +19,22 @@ "dynamicBindingPathList": [], "leftColumn": 0, "children": [ + { + "isVisible": true, + "text": "Label", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 14.015625, + "parentRowSpace": 10, + "leftColumn": 1, + "rightColumn": 7, + "topRow": 1, + "bottomRow": 4, + "parentId": "bxekwxgc1i", + "widgetId": "9xcfqahpw2" + }, { "isMirrored": true, "widgetName": "Camera1", diff --git a/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json b/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json index acc93d0818..00bc7c4c28 100644 --- a/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json +++ b/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json @@ -59,6 +59,22 @@ "renderMode": "CANVAS", "isLoading": false, "isInline": true + }, + { + "isVisible": true, + "text": "Label", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 18.66171875, + "parentRowSpace": 10, + "leftColumn": 3, + "rightColumn": 10, + "topRow": 5, + "bottomRow": 9, + "parentId": "bxekwxgc1i", + "widgetId": "9xcfqahpw2" } ] } diff --git a/app/client/cypress/fixtures/TreeSelectDsl.json b/app/client/cypress/fixtures/TreeSelectDsl.json index 1a0dafdcf8..35ba148a30 100644 --- a/app/client/cypress/fixtures/TreeSelectDsl.json +++ b/app/client/cypress/fixtures/TreeSelectDsl.json @@ -122,6 +122,22 @@ "renderMode":"CANVAS", "isLoading":false, "allowClear":false + }, + { + "isVisible": true, + "text": "Label", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 15.974999999999998, + "parentRowSpace": 10, + "leftColumn": 3, + "rightColumn": 15, + "topRow": 10, + "bottomRow": 12, + "parentId": "bxekwxgc1i", + "widgetId": "9xcfqahpw2" } ] } diff --git a/app/client/cypress/fixtures/formHasChangesDsl.json b/app/client/cypress/fixtures/formHasChangesDsl.json new file mode 100644 index 0000000000..918195f33c --- /dev/null +++ b/app/client/cypress/fixtures/formHasChangesDsl.json @@ -0,0 +1,481 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 909, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1290, + "containerStyle": "none", + "snapRows": 125, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 52, + "minHeight": 690, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Form1", + "backgroundColor": "white", + "rightColumn": 62, + "isCanvas": true, + "displayName": "Form", + "iconSVG": "/static/media/icon.ea3e08d1.svg", + "widgetId": "ui8kenblnq", + "topRow": 5, + "bottomRow": 62, + "parentRowSpace": 10, + "isVisible": true, + "type": "FORM_WIDGET", + "hideCard": false, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false, + "animateLoading": true, + "parentColumnSpace": 14.015625, + "leftColumn": 2, + "children": [ + { + "widgetName": "Canvas1", + "rightColumn": 336.375, + "detachFromLayout": true, + "displayName": "Canvas", + "widgetId": "rgnqy2hpcb", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 570, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "hideCard": true, + "parentId": "ui8kenblnq", + "minHeight": 400, + "renderMode": "CANVAS", + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Text1", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b52.svg", + "topRow": 1, + "bottomRow": 5, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "dynamicTriggerPathList": [], + "leftColumn": 1.5, + "dynamicBindingPathList": [], + "shouldTruncate": false, + "truncateButtonColor": "#FFC13D", + "text": "TopLevelForm", + "key": "r71qxtucag", + "rightColumn": 25.5, + "textAlign": "LEFT", + "widgetId": "az2x2gnx8i", + "isVisible": true, + "fontStyle": "BOLD", + "textColor": "#231F20", + "shouldScroll": false, + "version": 1, + "parentId": "rgnqy2hpcb", + "renderMode": "CANVAS", + "isLoading": false, + "fontSize": "HEADING1" + }, + { + "boxShadow": "NONE", + "widgetName": "Container1", + "borderColor": "transparent", + "isCanvas": true, + "displayName": "Container", + "iconSVG": "/static/media/icon.1977dca3.svg", + "topRow": 7, + "bottomRow": 55, + "parentRowSpace": 10, + "type": "CONTAINER_WIDGET", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.8271484375, + "leftColumn": 1, + "children": [ + { + "widgetName": "Canvas2", + "rightColumn": 307.8515625, + "detachFromLayout": true, + "displayName": "Canvas", + "widgetId": "o16xur9huq", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 490, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "hideCard": true, + "parentId": "c3ib5l2wce", + "minHeight": 400, + "renderMode": "CANVAS", + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Form2", + "backgroundColor": "white", + "rightColumn": 64, + "isCanvas": true, + "displayName": "Form", + "iconSVG": "/static/media/icon.ea3e08d1.svg", + "widgetId": "r0vj63apg4", + "topRow": 1, + "bottomRow": 47, + "parentRowSpace": 10, + "isVisible": true, + "type": "FORM_WIDGET", + "hideCard": false, + "parentId": "o16xur9huq", + "renderMode": "CANVAS", + "isLoading": false, + "animateLoading": true, + "parentColumnSpace": 12.314224243164062, + "leftColumn": 0, + "children": [ + { + "widgetName": "Canvas3", + "rightColumn": 295.5413818359375, + "detachFromLayout": true, + "displayName": "Canvas", + "widgetId": "hyxstzafwr", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 460, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "hideCard": true, + "parentId": "r0vj63apg4", + "minHeight": 400, + "renderMode": "CANVAS", + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Text2", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b52.svg", + "topRow": 1, + "bottomRow": 5, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "dynamicTriggerPathList": [], + "leftColumn": 1.5, + "dynamicBindingPathList": [], + "shouldTruncate": false, + "truncateButtonColor": "#FFC13D", + "text": "NestedForm1", + "key": "r71qxtucag", + "rightColumn": 25.5, + "textAlign": "LEFT", + "widgetId": "9x1tljetyn", + "isVisible": true, + "fontStyle": "BOLD", + "textColor": "#231F20", + "shouldScroll": false, + "version": 1, + "parentId": "hyxstzafwr", + "renderMode": "CANVAS", + "isLoading": false, + "fontSize": "HEADING1" + }, + { + "boxShadow": "NONE", + "widgetName": "Container2", + "borderColor": "transparent", + "isCanvas": true, + "displayName": "Container", + "iconSVG": "/static/media/icon.1977dca3.svg", + "topRow": 6, + "bottomRow": 44, + "parentRowSpace": 10, + "type": "CONTAINER_WIDGET", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.001724243164062, + "leftColumn": 0, + "children": [ + { + "widgetName": "Canvas4", + "rightColumn": 288.0413818359375, + "detachFromLayout": true, + "displayName": "Canvas", + "widgetId": "bezg0vuivd", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 390, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "hideCard": true, + "parentId": "d3bbrwipbo", + "minHeight": 400, + "renderMode": "CANVAS", + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Form3", + "backgroundColor": "white", + "rightColumn": 62, + "isCanvas": true, + "displayName": "Form", + "iconSVG": "/static/media/icon.ea3e08d1.svg", + "widgetId": "e9qj527b6g", + "topRow": 0, + "bottomRow": 36, + "parentRowSpace": 10, + "isVisible": true, + "type": "FORM_WIDGET", + "hideCard": false, + "parentId": "bezg0vuivd", + "renderMode": "CANVAS", + "isLoading": false, + "animateLoading": true, + "parentColumnSpace": 12.001724243164062, + "leftColumn": 0, + "children": [ + { + "widgetName": "Canvas5", + "rightColumn": 288.0413818359375, + "detachFromLayout": true, + "displayName": "Canvas", + "widgetId": "ju0erkgavw", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 390, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "hideCard": true, + "parentId": "e9qj527b6g", + "minHeight": 400, + "renderMode": "CANVAS", + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "widgetName": "Text3", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b52.svg", + "topRow": 1, + "bottomRow": 5, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "dynamicTriggerPathList": [], + "leftColumn": 1.5, + "dynamicBindingPathList": [], + "shouldTruncate": false, + "truncateButtonColor": "#FFC13D", + "text": "NestedForm2", + "key": "r71qxtucag", + "rightColumn": 25.5, + "textAlign": "LEFT", + "widgetId": "kx3kje0esv", + "isVisible": true, + "fontStyle": "BOLD", + "textColor": "#231F20", + "shouldScroll": false, + "version": 1, + "parentId": "ju0erkgavw", + "renderMode": "CANVAS", + "isLoading": false, + "fontSize": "HEADING1" + }, + { + "resetFormOnClick": true, + "widgetName": "FormButton1", + "buttonColor": "#03B365", + "displayName": "FormButton", + "iconSVG": "/static/media/icon.c8f649ed.svg", + "topRow": 29, + "bottomRow": 33, + "type": "FORM_BUTTON_WIDGET", + "hideCard": true, + "animateLoading": true, + "leftColumn": 47, + "text": "Submit", + "key": "6rckarmxa8", + "rightColumn": 63, + "isDefaultClickDisabled": true, + "widgetId": "71jwl9rqyl", + "isVisible": true, + "recaptchaType": "V3", + "version": 1, + "parentId": "ju0erkgavw", + "renderMode": "CANVAS", + "isLoading": false, + "disabledWhenInvalid": true, + "buttonVariant": "PRIMARY" + }, + { + "resetFormOnClick": true, + "widgetName": "FormButton2", + "buttonColor": "#03B365", + "displayName": "FormButton", + "iconSVG": "/static/media/icon.c8f649ed.svg", + "topRow": 29, + "bottomRow": 33, + "type": "FORM_BUTTON_WIDGET", + "hideCard": true, + "animateLoading": true, + "leftColumn": 30, + "text": "Reset", + "key": "6rckarmxa8", + "rightColumn": 46, + "isDefaultClickDisabled": true, + "widgetId": "385vwj1sak", + "isVisible": true, + "recaptchaType": "V3", + "version": 1, + "parentId": "ju0erkgavw", + "renderMode": "CANVAS", + "isLoading": false, + "disabledWhenInvalid": false, + "buttonVariant": "SECONDARY" + }, + { + "widgetName": "Checkbox1", + "displayName": "Checkbox", + "iconSVG": "/static/media/icon.aaab032b.svg", + "topRow": 7, + "bottomRow": 11, + "parentRowSpace": 10, + "type": "CHECKBOX_WIDGET", + "alignWidget": "LEFT", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 11.011435985565186, + "leftColumn": 1, + "isDisabled": false, + "key": "kn37pa6063", + "isRequired": false, + "rightColumn": 21, + "widgetId": "xcs2djteir", + "isVisible": true, + "label": "Label", + "version": 1, + "parentId": "ju0erkgavw", + "renderMode": "CANVAS", + "isLoading": false, + "defaultCheckedState": true + } + ], + "key": "invgby4psp" + } + ], + "key": "1yxjunjfwb" + } + ], + "key": "invgby4psp" + } + ], + "borderWidth": "0", + "key": "tpj0tcwdhg", + "backgroundColor": "#FFFFFF", + "rightColumn": 64, + "widgetId": "d3bbrwipbo", + "containerStyle": "card", + "isVisible": true, + "version": 1, + "parentId": "hyxstzafwr", + "renderMode": "CANVAS", + "isLoading": false, + "borderRadius": "0" + } + ], + "key": "invgby4psp" + } + ], + "key": "1yxjunjfwb" + } + ], + "key": "invgby4psp" + } + ], + "borderWidth": "0", + "key": "tpj0tcwdhg", + "backgroundColor": "#FFFFFF", + "rightColumn": 64, + "widgetId": "c3ib5l2wce", + "containerStyle": "card", + "isVisible": true, + "version": 1, + "parentId": "rgnqy2hpcb", + "renderMode": "CANVAS", + "isLoading": false, + "borderRadius": "0" + }, + { + "widgetName": "Text4", + "displayName": "Text", + "iconSVG": "/static/media/icon.97c59b52.svg", + "topRow": 1, + "bottomRow": 5, + "parentRowSpace": 10, + "type": "TEXT_WIDGET", + "hideCard": false, + "animateLoading": true, + "parentColumnSpace": 12.8271484375, + "dynamicTriggerPathList": [], + "leftColumn": 47, + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "shouldTruncate": false, + "truncateButtonColor": "#FFC13D", + "text": "{{Form1.hasChanges}}", + "key": "r71qxtucag", + "rightColumn": 63, + "textAlign": "LEFT", + "widgetId": "6lwl9odtd7", + "isVisible": true, + "fontStyle": "BOLD", + "textColor": "#231F20", + "shouldScroll": false, + "version": 1, + "parentId": "rgnqy2hpcb", + "renderMode": "CANVAS", + "isLoading": false, + "fontSize": "PARAGRAPH" + } + ], + "key": "invgby4psp" + } + ], + "key": "1yxjunjfwb" + } + ] +} +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/formdsl1.json b/app/client/cypress/fixtures/formdsl1.json index 22d7d0acc4..513eeae362 100644 --- a/app/client/cypress/fixtures/formdsl1.json +++ b/app/client/cypress/fixtures/formdsl1.json @@ -105,6 +105,22 @@ "bottomRow": 7, "parentId": "7tkpo9s22m", "widgetId": "6h8j08u7ea" + }, + { + "isVisible": true, + "text": "Label", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.75, + "parentRowSpace": 38, + "leftColumn": 3, + "rightColumn": 5, + "topRow": 10, + "bottomRow": 12, + "parentId": "bxekwxgc1i", + "widgetId": "9xcfqahpw2" } ], "widgetId": "7tkpo9s22m", diff --git a/app/client/cypress/fixtures/multiSelectDsl.json b/app/client/cypress/fixtures/multiSelectDsl.json index a58625d801..598b0027f6 100644 --- a/app/client/cypress/fixtures/multiSelectDsl.json +++ b/app/client/cypress/fixtures/multiSelectDsl.json @@ -61,6 +61,25 @@ "leftColumn": 0, "dynamicBindingPathList": [], "children": [ + { + "widgetName": "Text1", + "rightColumn": 10, + "widgetId": "4d8d2eh4xg", + "dynamicPropertyPathList": [], + "topRow": 0, + "bottomRow": 3, + "parentRowSpace": 38, + "isVisible": true, + "type": "TEXT_WIDGET", + "dynamicBindingPathList": [], + "shouldScroll": true, + "parentId": "hjiybwqair", + "isLoading": false, + "parentColumnSpace": 71.75, + "leftColumn": 1, + "text": "Test text", + "textStyle": "HEADING" + }, { "isRequired": false, "widgetName": "Input1", diff --git a/app/client/cypress/fixtures/newFormDsl.json b/app/client/cypress/fixtures/newFormDsl.json index 5729138e1c..7af0b0cf49 100644 --- a/app/client/cypress/fixtures/newFormDsl.json +++ b/app/client/cypress/fixtures/newFormDsl.json @@ -52,6 +52,25 @@ "snapColumns": 16, "orientation": "VERTICAL", "children": [ + { + "widgetName": "Text1", + "rightColumn": 3, + "widgetId": "4d8d2eh4xg", + "dynamicPropertyPathList": [], + "topRow": 0, + "bottomRow": 1, + "parentRowSpace": 38, + "isVisible": true, + "type": "TEXT_WIDGET", + "dynamicBindingPathList": [], + "shouldScroll": true, + "parentId": "hjiybwqair", + "isLoading": false, + "parentColumnSpace": 71.75, + "leftColumn": 1, + "text": "Test text", + "textStyle": "HEADING" + }, { "isVisible": true, "inputType": "TEXT", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/AudioRecorder_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/AudioRecorder_spec.js new file mode 100644 index 0000000000..68d7829268 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/AudioRecorder_spec.js @@ -0,0 +1,40 @@ +const explorer = require("../../../../locators/explorerlocators.json"); + +const widgetName = "audiorecorderwidget"; + +describe("AudioRecorder Widget", () => { + it("Drag & drop AudioRecorder and Text widgets", () => { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 }); + cy.get(`.t--widget-${widgetName}`).should("exist"); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{AudioRecorder1.isDirty}}`, + ); + }); + + it("Check isDirty meta property", () => { + // Check if isDirty is false for the first time + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(`.t--widget-${widgetName} button`) + .first() + .click(); + cy.get(`.t--widget-${widgetName} .status`) + .should("have.text", "Press to start recording") + .should("exist"); + // Start recording and recorder for 3 seconds + cy.get(`.t--widget-${widgetName} button`) + .first() + .click(); + cy.wait(3000); + // Stop recording + cy.get(`.t--widget-${widgetName} button span.bp3-icon-symbol-square`) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Camera_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Camera_spec.js index 8e1042924f..1f37304c1f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Camera_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Camera_spec.js @@ -14,12 +14,16 @@ describe("Camera Widget", () => { cy.goToEditFromPublish(); }); - it("Property: onImageSave, show modal", () => { + it("Check isDirty, onImageSave", () => { const modalName = `modal`; const mainControlSelector = "//div[contains(@class, 't--widget-camerawidget')]//button"; cy.createModal(modalName); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", "{{Camera1.isDirty}}"); + // Initial value of isDirty should be false + cy.get(".t--widget-textwidget").should("contain", "false"); // Take photo cy.xpath(mainControlSelector) .eq(2) @@ -32,5 +36,7 @@ describe("Camera Widget", () => { // Assert: should trigger onImageSave action - modal popup cy.get(modalWidgetPage.modelTextField).should("have.text", modalName); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js index 166d31f5b5..2bfc34e429 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckBox_spec.js @@ -5,6 +5,7 @@ const publish = require("../../../../locators/publishWidgetspage.json"); const dsl = require("../../../../fixtures/newFormDsl.json"); const formWidgetDsl = require("../../../../fixtures/formWidgetdsl.json"); const pages = require("../../../../locators/Pages.json"); +const explorer = require("../../../../locators/explorerlocators.json"); describe("Checkbox Widget Functionality", function() { before(() => { @@ -72,6 +73,26 @@ describe("Checkbox Widget Functionality", function() { cy.get(publish.checkboxWidget + " " + "input").should("be.checked"); cy.get(publish.backToEditor).click(); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{checker.isDirty}}`); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(`${formWidgetsPage.checkboxWidget} label`) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultCheckedState property + cy.openPropertyPane("checkboxwidget"); + cy.get(".t--property-control-defaultselected label") + .last() + .click(); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js index 85812fe538..a8e9548b49 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js @@ -1,6 +1,7 @@ const commonlocators = require("../../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../../locators/FormWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); +const explorer = require("../../../../locators/explorerlocators.json"); const dsl = require("../../../../fixtures/checkboxgroupDsl.json"); describe("Checkbox Group Widget Functionality", function() { @@ -129,6 +130,29 @@ describe("Checkbox Group Widget Functionality", function() { ".t--draggable-checkboxgroupwidget div[data-cy^='checkbox-group-container']", ).should("have.css", "justify-content", "flex-start"); }); + + it("Check isDirty meta property", function() { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{CheckboxGroup1.isDirty}}`, + ); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(formWidgetsPage.labelCheckboxGroup) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultSelectedValues + cy.openPropertyPane("checkboxgroupwidget"); + cy.updateCodeInput(".t--property-control-defaultselectedvalues", "GREEN"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CurrencyInput_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CurrencyInput_spec.js index 6912a54e82..c826a07dd8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CurrencyInput_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CurrencyInput_spec.js @@ -111,4 +111,28 @@ describe("Currency widget - ", () => { cy.wait(300); cy.get(widgetInput).should("contain.value", ""); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{CurrencyInput1.isDirty}}`, + ); + // Init isDirty + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaulttext", "1"); + cy.closePropertyPane(); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(widgetInput).clear(); + cy.wait(300); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultText + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaulttext", "5"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js index 5c768dbed5..d23df97d9f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_2_spec.js @@ -140,6 +140,49 @@ describe("DatePicker Widget Property pane tests with js bindings", function() { .should("have.text", "May 4, 2021 6:25 AM"); }); + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{DatePicker1.isDirty}}`); + // Init isDirty + cy.openPropertyPane("datepickerwidget2"); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.testJsontext( + "defaultdate", + '{{moment("04/05/2021 05:25", "DD/MM/YYYY HH:mm").toISOString()}}', + ); + cy.closePropertyPane(); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget") + .first() + .should("contain", "false"); + // Interact with UI + cy.get(".t--draggable-datepickerwidget2 .bp3-input") + .clear({ + force: true, + }) + .type("04/05/2021 06:25"); + cy.wait("@updateLayout"); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget") + .first() + .should("contain", "true"); + // Change defaultDate + cy.openPropertyPane("datepickerwidget2"); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.testJsontext( + "defaultdate", + '{{moment("07/05/2021 05:25", "DD/MM/YYYY HH:mm").toISOString()}}', + ); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget") + .first() + .should("contain", "false"); + }); + it("Datepicker default date validation with js binding", function() { cy.PublishtheApp(); // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FilePickerV2_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FilePickerV2_spec.js new file mode 100644 index 0000000000..8642061780 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FilePickerV2_spec.js @@ -0,0 +1,30 @@ +const explorer = require("../../../../locators/explorerlocators.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); + +const widgetName = "filepickerwidgetv2"; + +describe("File picker widget v2", () => { + it("1. Drag & drop FilePicker/Text widgets", () => { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 }); + cy.get(`.t--widget-${widgetName}`).should("exist"); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{FilePicker1.isDirty}}`); + }); + + it("Check isDirty meta property", function() { + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Upload a new file + cy.get(`.t--widget-${widgetName}`).click(); + cy.get(commonlocators.filePickerInput) + .first() + .attachFile("testFile.mov"); + cy.get(commonlocators.filePickerUploadButton).click(); + //eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(500); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js new file mode 100644 index 0000000000..d499a9956f --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js @@ -0,0 +1,20 @@ +const dsl = require("../../../../fixtures/formHasChangesDsl.json"); + +describe("Form Widget", () => { + before(() => { + cy.addDsl(dsl); + }); + + it("Check hasChanges meta property", () => { + cy.wait(2000); + // Check if isDirty is false for the first time + cy.contains(".t--widget-textwidget", "false").should("exist"); + // Interact with UI + cy.get(`.t--widget-checkboxwidget label`) + .first() + .click(); + // Check if isDirty is set to true + cy.contains(".t--widget-textwidget", "false").should("not.exist"); + cy.contains(".t--widget-textwidget", "true").should("exist"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Inputv2_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Inputv2_spec.js index 2b38e4f9ee..8165af9cc1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Inputv2_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Inputv2_spec.js @@ -107,6 +107,29 @@ describe("Input widget V2 - ", () => { }); }); + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{Input1.isDirty}}`); + // Init isDirty + cy.openPropertyPane(widgetName); + cy.selectDropdownValue(".t--property-control-datatype", "Text"); + cy.updateCodeInput(".t--property-control-defaulttext", "a"); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(widgetInput).clear(); + cy.wait(300); + cy.get(widgetInput).type("b"); + cy.wait(300); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultText + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaulttext", "c"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); + function enterAndTest(text, expected) { cy.get(`.t--widget-${widgetName} input`).clear(); cy.wait(300); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js index 3d68652ee9..e30bcd091b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js @@ -90,6 +90,24 @@ describe("MultiSelect Widget Functionality", function() { .eq(1) .should("have.text", "Option 2"); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{MultiSelect2.isDirty}}`); + // Change defaultOptionValue + cy.openPropertyPane("multiselectwidgetv2"); + cy.updateCodeInput( + ".t--property-control-defaultvalue", + '[\n {\n "label": "Option 1",\n "value": "1"\n }\n]', + ); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(".rc-select-selector").click({ force: true }); + cy.dropdownMultiSelectDynamic("Option 2"); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js index 4f4521af16..96accd87be 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js @@ -2,6 +2,7 @@ const dsl = require("../../../../fixtures/TreeSelectDsl.json"); const formWidgetsPage = require("../../../../locators/FormWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); const commonlocators = require("../../../../locators/commonlocators.json"); +const explorer = require("../../../../locators/explorerlocators.json"); describe("MultiSelectTree Widget Functionality", function() { before(() => { @@ -41,6 +42,28 @@ describe("MultiSelectTree Widget Functionality", function() { ).should("be.visible"); cy.get(publish.backToEditor).click(); }); + + it("Check isDirty meta property", function() { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{MultiSelectTree1.isDirty}}`, + ); + // Change defaultValue + cy.openPropertyPane("multiselecttreewidget"); + cy.testJsontext("defaultvalue", "GREEN\n"); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(formWidgetsPage.treeSelectInput) + .first() + .click({ force: true }); + cy.treeMultiSelectDropdown("Red"); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js index 588afe9e86..5f4c293cb1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js @@ -94,4 +94,22 @@ describe("Phone input widget - ", () => { cy.wait(300); cy.get(widgetInput).should("contain.value", ""); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{PhoneInput1.isDirty}}`); + // Change defaultText + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaulttext", "1"); + cy.closePropertyPane(); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(widgetInput).clear(); + cy.wait(300); + cy.get(widgetInput).type("2"); + cy.wait(300); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RadioGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RadioGroup_spec.js new file mode 100644 index 0000000000..d6202324e2 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RadioGroup_spec.js @@ -0,0 +1,30 @@ +const explorer = require("../../../../locators/explorerlocators.json"); + +const widgetName = "radiogroupwidget"; + +describe("Radio Group Widget", () => { + it("Drag & drop Radio group & Text widgets", () => { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 }); + cy.get(`.t--widget-${widgetName}`).should("exist"); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{RadioGroup1.isDirty}}`); + }); + + it("Check isDirty meta property", function() { + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(".t--widget-radiogroupwidget .bp3-radio") + .last() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultOptionValue + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaultselectedvalue", "N"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js index 9635b69c6b..46b6f57df3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js @@ -128,6 +128,24 @@ describe("RichTextEditor Widget Functionality", function() { ); }); + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{RichTextEditor1.isDirty}}`, + ); + // Change defaultText + cy.openPropertyPane("richtexteditorwidget"); + cy.updateCodeInput(".t--property-control-defaulttext", "a"); + cy.closePropertyPane(); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.setTinyMceContent("rte-6h8j08u7ea", "abc"); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); + afterEach(() => { cy.goToEditFromPublish(); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js new file mode 100644 index 0000000000..4bba9edb7c --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js @@ -0,0 +1,29 @@ +const explorer = require("../../../../locators/explorerlocators.json"); + +const widgetName = "selectwidget"; + +describe("Select widget", () => { + it("1. DragDrop Select/Text widgets", () => { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 }); + cy.get(`.t--widget-${widgetName}`).should("exist"); + cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{Select1.isDirty}}`); + // Check if initial value of isDirty is false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get( + `.t--widget-${widgetName} .bp3-popover-target > div > .bp3-button`, + ).click(); + cy.get(`.bp3-popover-content ul.bp3-menu li`) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultOptionValue property + cy.updateCodeInput(".t--property-control-defaultvalue", "RED"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js index 355c946203..d532b8c10f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js @@ -41,6 +41,28 @@ describe("MultiSelectTree Widget Functionality", function() { ).should("be.visible"); cy.get(publish.backToEditor).click(); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{SingleSelectTree1.isDirty}}`, + ); + // Change defaultText + cy.openPropertyPane("singleselecttreewidget"); + cy.updateCodeInput(".t--property-control-defaultvalue", "GREEN"); + cy.closePropertyPane(); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get( + `${formWidgetsPage.singleselecttreeWidget} ${formWidgetsPage.treeSelectInput}`, + ) + .clear() + .type("BLUE{downArrow}{enter}"); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js index 497326be87..d8f7b1e665 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js @@ -100,4 +100,26 @@ describe("Switch Group Widget Functionality", function() { this.data.ModalName, ); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput( + ".t--property-control-text", + `{{switchgrouptest.isDirty}}`, + ); + // Change defaultSelectedValues + cy.openPropertyPane("switchgroupwidget"); + cy.updateCodeInput( + ".t--property-control-defaultselectedvalues", + `[\n"BLUE"\n]`, + ); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(formWidgetsPage.labelSwitchGroup) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js index 74294c26ee..9614963ce5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js @@ -86,6 +86,24 @@ describe("Switch Widget Functionality", function() { cy.get(publish.switchwidget + " " + ".bp3-align-left").should("not.exist"); cy.get(publish.backToEditor).click(); }); + + it("Check isDirty meta property", function() { + cy.openPropertyPane("textwidget"); + cy.updateCodeInput(".t--property-control-text", `{{Toggler.isDirty}}`); + // Change defaultSwitchState property + cy.openPropertyPane("switchwidget"); + cy.get(".t--property-control-defaultselected label") + .last() + .click(); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); + // Interact with UI + cy.get(`${formWidgetsPage.switchWidget} label`) + .first() + .click(); + // Check if isDirty is set to true + cy.get(".t--widget-textwidget").should("contain", "true"); + }); }); afterEach(() => { // put your clean up code if any diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 982e7b8c02..8995e113bb 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2922,6 +2922,7 @@ Cypress.Commands.add("setTinyMceContent", (tinyMceId, content) => { cy.window().then((win) => { const editor = win.tinymce.editors[tinyMceId]; editor.setContent(content); + editor.save(); }); }); diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index e80c5216d6..fde3917bd8 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -130,9 +130,7 @@ class RichTextEditorWidget extends BaseWidget< static getMetaPropertiesMap(): Record { return { text: undefined, - defaultTextHtml: undefined, - defaultTextMarkdown: undefined, - isDefaultTextChanged: false, + shouldReset: true, isDirty: false, }; } @@ -155,28 +153,16 @@ class RichTextEditorWidget extends BaseWidget< if (this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", false); } - this.props.updateWidgetMetaProperty("isDefaultTextChanged", true); + this.props.updateWidgetMetaProperty("shouldReset", true); } } onValueChange = (text: string) => { - if (!(this.props.isDefaultTextChanged || this.props.isDirty)) { + if (this.props.shouldReset) { + this.props.updateWidgetMetaProperty("shouldReset", false); + } else if (!this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", true); } - if ( - this.props.inputType === RTEFormats.HTML && - (!this.props.defaultTextHtml || this.props.isDefaultTextChanged) - ) { - this.props.updateWidgetMetaProperty("defaultTextHtml", text); - this.props.updateWidgetMetaProperty("isDefaultTextChanged", false); - } - if ( - this.props.inputType === RTEFormats.MARKDOWN && - (!this.props.defaultTextMarkdown || this.props.isDefaultTextChanged) - ) { - this.props.updateWidgetMetaProperty("defaultTextMarkdown", text); - this.props.updateWidgetMetaProperty("isDefaultTextChanged", false); - } this.props.updateWidgetMetaProperty("text", text, { triggerPropertyName: "onTextChange", diff --git a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx index df1c32dec3..ecd472d890 100644 --- a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx +++ b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx @@ -179,6 +179,7 @@ class SwitchGroupWidget extends BaseWidget< static getMetaPropertiesMap(): Record { return { selectedValuesArray: undefined, + isDirty: false, }; } From fd2acfc1da2f7d100bd9c7aecf38eb70596d9f19 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Mon, 7 Mar 2022 23:50:26 +0800 Subject: [PATCH 05/16] feat: Internal property to detect changes in a form -- Revert cypress.json --- app/client/cypress.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/client/cypress.json b/app/client/cypress.json index 8b582e894d..d19ed6f7a5 100644 --- a/app/client/cypress.json +++ b/app/client/cypress.json @@ -24,13 +24,5 @@ "retries": { "runMode": 2, "openMode": 0 - }, - "env": { - "USERNAME": "paul@appsmith.com", - "PASSWORD": "paul1234", - "TESTUSERNAME1": "paul@appsmith.com", - "TESTPASSWORD1": "paul1234", - "TESTUSERNAME2": "paul@appsmith.com", - "TESTPASSWORD2": "paul1234" } } \ No newline at end of file From 37a2b4c3c940394cc3dec96fc7772904d76d0b33 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Tue, 8 Mar 2022 02:07:17 +0800 Subject: [PATCH 06/16] feat: Internal property to detect changes in a form -- Fix failed test cases in CheckboxGroup_spec and RichTextEditor_spec --- .../FormWidgets/CheckboxGroup_spec.js | 12 +- .../FormWidgets/RichTextEditor_spec.js | 209 +++++++++--------- 2 files changed, 112 insertions(+), 109 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js index a8e9548b49..704e829098 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/CheckboxGroup_spec.js @@ -137,9 +137,12 @@ describe("Checkbox Group Widget Functionality", function() { cy.openPropertyPane("textwidget"); cy.updateCodeInput( ".t--property-control-text", - `{{CheckboxGroup1.isDirty}}`, + `{{checkboxgrouptest.isDirty}}`, ); - // Check if initial value of isDirty is false + // Change defaultSelectedValues + cy.openPropertyPane("checkboxgroupwidget"); + cy.updateCodeInput(".t--property-control-defaultselectedvalues", "GREEN"); + // Check if isDirty is reset to false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI cy.get(formWidgetsPage.labelCheckboxGroup) @@ -147,11 +150,6 @@ describe("Checkbox Group Widget Functionality", function() { .click(); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); - // Change defaultSelectedValues - cy.openPropertyPane("checkboxgroupwidget"); - cy.updateCodeInput(".t--property-control-defaultselectedvalues", "GREEN"); - // Check if isDirty is reset to false - cy.get(".t--widget-textwidget").should("contain", "false"); }); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js index 46b6f57df3..b467899f87 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js @@ -13,120 +13,120 @@ describe("RichTextEditor Widget Functionality", function() { cy.openPropertyPane("richtexteditorwidget"); }); - it("RichTextEditor-Edit Text area with HTML body functionality", function() { - //changing the Text Name - cy.widgetText( - this.data.RichTextEditorName, - formWidgetsPage.richTextEditorWidget, - formWidgetsPage.richTextEditorWidget + " " + commonlocators.widgetNameTag, - ); + // it("RichTextEditor-Edit Text area with HTML body functionality", function() { + // //changing the Text Name + // cy.widgetText( + // this.data.RichTextEditorName, + // formWidgetsPage.richTextEditorWidget, + // formWidgetsPage.richTextEditorWidget + " " + commonlocators.widgetNameTag, + // ); - //Edit the text area with Html - cy.testJsontext("defaulttext", this.data.HtmlText); + // //Edit the text area with Html + // cy.testJsontext("defaulttext", this.data.HtmlText); - //Validate Html - cy.validateHTMLText( - formWidgetsPage.richTextEditorWidget, - "h1", - "This is a Heading", - ); - cy.PublishtheApp(); - cy.validateHTMLText( - publishPage.richTextEditorWidget, - "h1", - "This is a Heading", - ); - }); + // //Validate Html + // cy.validateHTMLText( + // formWidgetsPage.richTextEditorWidget, + // "h1", + // "This is a Heading", + // ); + // cy.PublishtheApp(); + // cy.validateHTMLText( + // publishPage.richTextEditorWidget, + // "h1", + // "This is a Heading", + // ); + // }); - it("RichTextEditor-Enable Validation", function() { - //Uncheck the Disabled checkbox - cy.UncheckWidgetProperties(formWidgetsPage.disableJs); - cy.validateEnableWidget( - formWidgetsPage.richTextEditorWidget, - commonlocators.disabledBtn, - ); + // it("RichTextEditor-Enable Validation", function() { + // //Uncheck the Disabled checkbox + // cy.UncheckWidgetProperties(formWidgetsPage.disableJs); + // cy.validateEnableWidget( + // formWidgetsPage.richTextEditorWidget, + // commonlocators.disabledBtn, + // ); - cy.PublishtheApp(); - cy.validateEnableWidget( - publishPage.richTextEditorWidget, - commonlocators.disabledBtn, - ); - }); + // cy.PublishtheApp(); + // cy.validateEnableWidget( + // publishPage.richTextEditorWidget, + // commonlocators.disabledBtn, + // ); + // }); - it("RichTextEditor-Disable Validation", function() { - //Check the Disabled checkbox - cy.CheckWidgetProperties(formWidgetsPage.disableJs); - cy.validateDisableWidget( - formWidgetsPage.richTextEditorWidget, - commonlocators.disabledBtn, - ); + // it("RichTextEditor-Disable Validation", function() { + // //Check the Disabled checkbox + // cy.CheckWidgetProperties(formWidgetsPage.disableJs); + // cy.validateDisableWidget( + // formWidgetsPage.richTextEditorWidget, + // commonlocators.disabledBtn, + // ); - cy.PublishtheApp(); - cy.validateDisableWidget( - publishPage.richTextEditorWidget, - commonlocators.disabledBtn, - ); - }); + // cy.PublishtheApp(); + // cy.validateDisableWidget( + // publishPage.richTextEditorWidget, + // commonlocators.disabledBtn, + // ); + // }); - it("RichTextEditor-check Visible field validation", function() { - // Uncheck the visible checkbox - cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); - cy.PublishtheApp(); - cy.get(publishPage.richTextEditorWidget).should("not.exist"); - }); + // it("RichTextEditor-check Visible field validation", function() { + // // Uncheck the visible checkbox + // cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); + // cy.PublishtheApp(); + // cy.get(publishPage.richTextEditorWidget).should("not.exist"); + // }); - it("RichTextEditor-uncheck Visible field validation", function() { - // Check the visible checkbox - cy.CheckWidgetProperties(commonlocators.visibleCheckbox); - cy.PublishtheApp(); - cy.get(publishPage.richTextEditorWidget).should("be.visible"); - }); + // it("RichTextEditor-uncheck Visible field validation", function() { + // // Check the visible checkbox + // cy.CheckWidgetProperties(commonlocators.visibleCheckbox); + // cy.PublishtheApp(); + // cy.get(publishPage.richTextEditorWidget).should("be.visible"); + // }); - it("RichTextEditor-check Hide toolbar field validation", function() { - // Check the Hide toolbar checkbox - cy.CheckWidgetProperties(commonlocators.hideToolbarCheckbox); - cy.validateToolbarHidden( - formWidgetsPage.richTextEditorWidget, - commonlocators.rteToolbar, - ); - cy.PublishtheApp(); - cy.validateToolbarHidden( - publishPage.richTextEditorWidget, - commonlocators.rteToolbar, - ); - }); + // it("RichTextEditor-check Hide toolbar field validation", function() { + // // Check the Hide toolbar checkbox + // cy.CheckWidgetProperties(commonlocators.hideToolbarCheckbox); + // cy.validateToolbarHidden( + // formWidgetsPage.richTextEditorWidget, + // commonlocators.rteToolbar, + // ); + // cy.PublishtheApp(); + // cy.validateToolbarHidden( + // publishPage.richTextEditorWidget, + // commonlocators.rteToolbar, + // ); + // }); - it("RichTextEditor-uncheck Hide toolbar field validation", function() { - // Uncheck the Hide toolbar checkbox - cy.UncheckWidgetProperties(commonlocators.hideToolbarCheckbox); - cy.validateToolbarVisible( - formWidgetsPage.richTextEditorWidget, - commonlocators.rteToolbar, - ); - cy.PublishtheApp(); - cy.validateToolbarVisible( - publishPage.richTextEditorWidget, - commonlocators.rteToolbar, - ); - }); + // it("RichTextEditor-uncheck Hide toolbar field validation", function() { + // // Uncheck the Hide toolbar checkbox + // cy.UncheckWidgetProperties(commonlocators.hideToolbarCheckbox); + // cy.validateToolbarVisible( + // formWidgetsPage.richTextEditorWidget, + // commonlocators.rteToolbar, + // ); + // cy.PublishtheApp(); + // cy.validateToolbarVisible( + // publishPage.richTextEditorWidget, + // commonlocators.rteToolbar, + // ); + // }); - it("Reset RichTextEditor", function() { - cy.setTinyMceContent("rte-6h8j08u7ea", "

content

"); + // it("Reset RichTextEditor", function() { + // cy.setTinyMceContent("rte-6h8j08u7ea", "

content

"); - cy.validateHTMLText(formWidgetsPage.richTextEditorWidget, "h1", "content"); - cy.openPropertyPane("buttonwidget"); - cy.get(".t--property-control-onclick") - .find(".t--js-toggle") - .click({ force: true }); - cy.testJsontext("onclick", '{{resetWidget("RichtextEditor")}}'); - cy.get(".t--widget-buttonwidget .bp3-button").click({ force: true }); - cy.wait(500); - cy.validateHTMLText( - formWidgetsPage.richTextEditorWidget, - "h1", - "This is a Heading", - ); - }); + // cy.validateHTMLText(formWidgetsPage.richTextEditorWidget, "h1", "content"); + // cy.openPropertyPane("buttonwidget"); + // cy.get(".t--property-control-onclick") + // .find(".t--js-toggle") + // .click({ force: true }); + // cy.testJsontext("onclick", '{{resetWidget("RichtextEditor")}}'); + // cy.get(".t--widget-buttonwidget .bp3-button").click({ force: true }); + // cy.wait(500); + // cy.validateHTMLText( + // formWidgetsPage.richTextEditorWidget, + // "h1", + // "This is a Heading", + // ); + // }); it("Check isDirty meta property", function() { cy.openPropertyPane("textwidget"); @@ -138,6 +138,11 @@ describe("RichTextEditor Widget Functionality", function() { cy.openPropertyPane("richtexteditorwidget"); cy.updateCodeInput(".t--property-control-defaulttext", "a"); cy.closePropertyPane(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); // Check if isDirty is reset to false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI From 097ab33b4c6e62b356471b65b33a80648a0b1b55 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Wed, 16 Mar 2022 22:44:44 +0800 Subject: [PATCH 07/16] feat: Internal property to detect changes in a form -- Complement Cypress test case by adding reset cases -- Update isDirty reset logic for MultiSelectWidgetV2 --- .../FormWidget_Nested_HasChanges_spec.js | 1 - .../FormWidgets/MultiSelect_spec.js | 10 +- .../FormWidgets/Multi_Select_Tree_spec.js | 4 + .../FormWidgets/Phone_input_spec.js | 5 + .../FormWidgets/RichTextEditor_spec.js | 217 +++++++++--------- .../FormWidgets/Single_Select_Tree_spec.js | 17 +- .../FormWidgets/SwitchGroup_spec.js | 8 + .../FormWidgets/Switch_spec.js | 7 + app/client/cypress/support/commands.js | 1 - .../MultiSelectWidgetV2/widget/index.tsx | 38 ++- 10 files changed, 188 insertions(+), 120 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js index d499a9956f..535092b86e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_Nested_HasChanges_spec.js @@ -6,7 +6,6 @@ describe("Form Widget", () => { }); it("Check hasChanges meta property", () => { - cy.wait(2000); // Check if isDirty is false for the first time cy.contains(".t--widget-textwidget", "false").should("exist"); // Interact with UI diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js index e30bcd091b..1253f5dcbc 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/MultiSelect_spec.js @@ -94,19 +94,25 @@ describe("MultiSelect Widget Functionality", function() { it("Check isDirty meta property", function() { cy.openPropertyPane("textwidget"); cy.updateCodeInput(".t--property-control-text", `{{MultiSelect2.isDirty}}`); - // Change defaultOptionValue + // Init isDirty by changing defaultOptionValue cy.openPropertyPane("multiselectwidgetv2"); cy.updateCodeInput( ".t--property-control-defaultvalue", '[\n {\n "label": "Option 1",\n "value": "1"\n }\n]', ); - // Check if isDirty is set to false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI cy.get(".rc-select-selector").click({ force: true }); cy.dropdownMultiSelectDynamic("Option 2"); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Reset isDirty by changing defaultOptionValue + cy.updateCodeInput( + ".t--property-control-defaultvalue", + '[\n {\n "label": "Option 2",\n "value": "2"\n }\n]', + ); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js index b7437273a1..1c0a8b9dad 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js @@ -63,6 +63,10 @@ describe("MultiSelectTree Widget Functionality", function() { cy.treeMultiSelectDropdown("Red"); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Reset isDirty by changing defaultValue + cy.testJsontext("defaultvalue", "BLUE\n"); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js index aa07260196..650a14a4e9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Phone_input_spec.js @@ -120,5 +120,10 @@ describe("Phone input widget - ", () => { cy.wait(300); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Reset isDirty by changing defaultText + cy.openPropertyPane(widgetName); + cy.updateCodeInput(".t--property-control-defaulttext", "3"); + // Check if isDirty is set to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js index b467899f87..c1d6248768 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js @@ -13,120 +13,120 @@ describe("RichTextEditor Widget Functionality", function() { cy.openPropertyPane("richtexteditorwidget"); }); - // it("RichTextEditor-Edit Text area with HTML body functionality", function() { - // //changing the Text Name - // cy.widgetText( - // this.data.RichTextEditorName, - // formWidgetsPage.richTextEditorWidget, - // formWidgetsPage.richTextEditorWidget + " " + commonlocators.widgetNameTag, - // ); + it("RichTextEditor-Edit Text area with HTML body functionality", function() { + //changing the Text Name + cy.widgetText( + this.data.RichTextEditorName, + formWidgetsPage.richTextEditorWidget, + formWidgetsPage.richTextEditorWidget + " " + commonlocators.widgetNameTag, + ); - // //Edit the text area with Html - // cy.testJsontext("defaulttext", this.data.HtmlText); + //Edit the text area with Html + cy.testJsontext("defaulttext", this.data.HtmlText); - // //Validate Html - // cy.validateHTMLText( - // formWidgetsPage.richTextEditorWidget, - // "h1", - // "This is a Heading", - // ); - // cy.PublishtheApp(); - // cy.validateHTMLText( - // publishPage.richTextEditorWidget, - // "h1", - // "This is a Heading", - // ); - // }); + //Validate Html + cy.validateHTMLText( + formWidgetsPage.richTextEditorWidget, + "h1", + "This is a Heading", + ); + cy.PublishtheApp(); + cy.validateHTMLText( + publishPage.richTextEditorWidget, + "h1", + "This is a Heading", + ); + }); - // it("RichTextEditor-Enable Validation", function() { - // //Uncheck the Disabled checkbox - // cy.UncheckWidgetProperties(formWidgetsPage.disableJs); - // cy.validateEnableWidget( - // formWidgetsPage.richTextEditorWidget, - // commonlocators.disabledBtn, - // ); + it("RichTextEditor-Enable Validation", function() { + //Uncheck the Disabled checkbox + cy.UncheckWidgetProperties(formWidgetsPage.disableJs); + cy.validateEnableWidget( + formWidgetsPage.richTextEditorWidget, + commonlocators.disabledBtn, + ); - // cy.PublishtheApp(); - // cy.validateEnableWidget( - // publishPage.richTextEditorWidget, - // commonlocators.disabledBtn, - // ); - // }); + cy.PublishtheApp(); + cy.validateEnableWidget( + publishPage.richTextEditorWidget, + commonlocators.disabledBtn, + ); + }); - // it("RichTextEditor-Disable Validation", function() { - // //Check the Disabled checkbox - // cy.CheckWidgetProperties(formWidgetsPage.disableJs); - // cy.validateDisableWidget( - // formWidgetsPage.richTextEditorWidget, - // commonlocators.disabledBtn, - // ); + it("RichTextEditor-Disable Validation", function() { + //Check the Disabled checkbox + cy.CheckWidgetProperties(formWidgetsPage.disableJs); + cy.validateDisableWidget( + formWidgetsPage.richTextEditorWidget, + commonlocators.disabledBtn, + ); - // cy.PublishtheApp(); - // cy.validateDisableWidget( - // publishPage.richTextEditorWidget, - // commonlocators.disabledBtn, - // ); - // }); + cy.PublishtheApp(); + cy.validateDisableWidget( + publishPage.richTextEditorWidget, + commonlocators.disabledBtn, + ); + }); - // it("RichTextEditor-check Visible field validation", function() { - // // Uncheck the visible checkbox - // cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); - // cy.PublishtheApp(); - // cy.get(publishPage.richTextEditorWidget).should("not.exist"); - // }); + it("RichTextEditor-check Visible field validation", function() { + // Uncheck the visible checkbox + cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.richTextEditorWidget).should("not.exist"); + }); - // it("RichTextEditor-uncheck Visible field validation", function() { - // // Check the visible checkbox - // cy.CheckWidgetProperties(commonlocators.visibleCheckbox); - // cy.PublishtheApp(); - // cy.get(publishPage.richTextEditorWidget).should("be.visible"); - // }); + it("RichTextEditor-uncheck Visible field validation", function() { + // Check the visible checkbox + cy.CheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.richTextEditorWidget).should("be.visible"); + }); - // it("RichTextEditor-check Hide toolbar field validation", function() { - // // Check the Hide toolbar checkbox - // cy.CheckWidgetProperties(commonlocators.hideToolbarCheckbox); - // cy.validateToolbarHidden( - // formWidgetsPage.richTextEditorWidget, - // commonlocators.rteToolbar, - // ); - // cy.PublishtheApp(); - // cy.validateToolbarHidden( - // publishPage.richTextEditorWidget, - // commonlocators.rteToolbar, - // ); - // }); + it("RichTextEditor-check Hide toolbar field validation", function() { + // Check the Hide toolbar checkbox + cy.CheckWidgetProperties(commonlocators.hideToolbarCheckbox); + cy.validateToolbarHidden( + formWidgetsPage.richTextEditorWidget, + commonlocators.rteToolbar, + ); + cy.PublishtheApp(); + cy.validateToolbarHidden( + publishPage.richTextEditorWidget, + commonlocators.rteToolbar, + ); + }); - // it("RichTextEditor-uncheck Hide toolbar field validation", function() { - // // Uncheck the Hide toolbar checkbox - // cy.UncheckWidgetProperties(commonlocators.hideToolbarCheckbox); - // cy.validateToolbarVisible( - // formWidgetsPage.richTextEditorWidget, - // commonlocators.rteToolbar, - // ); - // cy.PublishtheApp(); - // cy.validateToolbarVisible( - // publishPage.richTextEditorWidget, - // commonlocators.rteToolbar, - // ); - // }); + it("RichTextEditor-uncheck Hide toolbar field validation", function() { + // Uncheck the Hide toolbar checkbox + cy.UncheckWidgetProperties(commonlocators.hideToolbarCheckbox); + cy.validateToolbarVisible( + formWidgetsPage.richTextEditorWidget, + commonlocators.rteToolbar, + ); + cy.PublishtheApp(); + cy.validateToolbarVisible( + publishPage.richTextEditorWidget, + commonlocators.rteToolbar, + ); + }); - // it("Reset RichTextEditor", function() { - // cy.setTinyMceContent("rte-6h8j08u7ea", "

content

"); + it("Reset RichTextEditor", function() { + cy.setTinyMceContent("rte-6h8j08u7ea", "

content

"); - // cy.validateHTMLText(formWidgetsPage.richTextEditorWidget, "h1", "content"); - // cy.openPropertyPane("buttonwidget"); - // cy.get(".t--property-control-onclick") - // .find(".t--js-toggle") - // .click({ force: true }); - // cy.testJsontext("onclick", '{{resetWidget("RichtextEditor")}}'); - // cy.get(".t--widget-buttonwidget .bp3-button").click({ force: true }); - // cy.wait(500); - // cy.validateHTMLText( - // formWidgetsPage.richTextEditorWidget, - // "h1", - // "This is a Heading", - // ); - // }); + cy.validateHTMLText(formWidgetsPage.richTextEditorWidget, "h1", "content"); + cy.openPropertyPane("buttonwidget"); + cy.get(".t--property-control-onclick") + .find(".t--js-toggle") + .click({ force: true }); + cy.testJsontext("onclick", '{{resetWidget("RichtextEditor")}}'); + cy.get(".t--widget-buttonwidget .bp3-button").click({ force: true }); + cy.wait(500); + cy.validateHTMLText( + formWidgetsPage.richTextEditorWidget, + "h1", + "This is a Heading", + ); + }); it("Check isDirty meta property", function() { cy.openPropertyPane("textwidget"); @@ -143,12 +143,23 @@ describe("RichTextEditor Widget Functionality", function() { "response.body.responseMeta.status", 200, ); - // Check if isDirty is reset to false + // Check if isDirty has been changed into false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI cy.setTinyMceContent("rte-6h8j08u7ea", "abc"); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultText + cy.openPropertyPane("richtexteditorwidget"); + cy.updateCodeInput(".t--property-control-defaulttext", "b"); + cy.closePropertyPane(); + cy.wait("@updateLayout").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js index a264c9836f..a4de78b0db 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js @@ -55,13 +55,20 @@ describe("Single Select Widget Functionality", function() { // Check if isDirty is reset to false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI - cy.get( - `${formWidgetsPage.singleselecttreeWidget} ${formWidgetsPage.treeSelectInput}`, - ) - .clear() - .type("BLUE{downArrow}{enter}"); + cy.get(formWidgetsPage.treeSelectInput) + .last() + .click({ force: true }); + cy.get(formWidgetsPage.treeSelectFilterInput) + .click() + .type("light"); + cy.treeSelectDropdown("Light Blue"); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultText + cy.openPropertyPane("singleselecttreewidget"); + cy.updateCodeInput(".t--property-control-defaultvalue", "RED"); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); afterEach(() => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js index d8f7b1e665..2dbffea652 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js @@ -121,5 +121,13 @@ describe("Switch Group Widget Functionality", function() { .click(); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultSelectedValues + cy.openPropertyPane("switchgroupwidget"); + cy.updateCodeInput( + ".t--property-control-defaultselectedvalues", + `[\n"GREEN"\n]`, + ); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js index 9614963ce5..e91b1bac2b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Switch_spec.js @@ -103,6 +103,13 @@ describe("Switch Widget Functionality", function() { .click(); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); + // Change defaultSwitchState property + cy.openPropertyPane("switchwidget"); + cy.get(".t--property-control-defaultselected label") + .last() + .click(); + // Check if isDirty is reset to false + cy.get(".t--widget-textwidget").should("contain", "false"); }); }); afterEach(() => { diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index b1dd5698df..72c25b03d3 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2908,7 +2908,6 @@ Cypress.Commands.add("setTinyMceContent", (tinyMceId, content) => { cy.window().then((win) => { const editor = win.tinymce.editors[tinyMceId]; editor.setContent(content); - editor.save(); }); }); diff --git a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx index 8faf76ad31..8946a59c7e 100644 --- a/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/MultiSelectWidgetV2/widget/index.tsx @@ -2,7 +2,14 @@ import React from "react"; import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; import { WidgetType } from "constants/WidgetConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; -import { isArray, isEqual, isString, isNumber, xorWith } from "lodash"; +import { + isArray, + isEqual, + isFinite, + isString, + isNumber, + xorWith, +} from "lodash"; import { ValidationResponse, ValidationTypes, @@ -418,14 +425,29 @@ class MultiSelectWidget extends BaseWidget< } componentDidUpdate(prevProps: MultiSelectWidgetProps): void { + // Check if defaultOptionValue is string + let isStringArray = false; if ( - xorWith( - this.props.defaultOptionValue, - prevProps.defaultOptionValue, - isEqual, - ).length > 0 && - this.props.isDirty + this.props.defaultOptionValue.some( + (value: any) => isString(value) || isFinite(value), + ) ) { + isStringArray = true; + } + + const hasChanges = isStringArray + ? xorWith( + this.props.defaultOptionValue as string[], + prevProps.defaultOptionValue as string[], + isEqual, + ).length > 0 + : xorWith( + this.props.defaultOptionValue as OptionValue[], + prevProps.defaultOptionValue as OptionValue[], + isEqual, + ).length > 0; + + if (hasChanges && this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", false); } } @@ -525,7 +547,7 @@ export interface MultiSelectWidgetProps extends WidgetProps { options?: DropdownOption[]; onOptionChange: string; onFilterChange: string; - defaultOptionValue: string | string[] | OptionValue[]; + defaultOptionValue: string[] | OptionValue[]; isRequired: boolean; isLoading: boolean; selectedOptions: LabelValueType[]; From dd49573d15ffb9da8fd23883fcfd1258936ad21e Mon Sep 17 00:00:00 2001 From: Paul Li Date: Thu, 17 Mar 2022 19:26:24 +0800 Subject: [PATCH 08/16] feat: 4182: Internal property to detect changes in a form -- Eliminate unnecessary props --- app/client/src/widgets/RichTextEditorWidget/widget/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index fde3917bd8..ccc999893c 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -205,9 +205,6 @@ class RichTextEditorWidget extends BaseWidget< export interface RichTextEditorWidgetProps extends WidgetProps { defaultText?: string; - defaultTextHtml?: string; - defaultTextMarkdown?: string; - isDefaultTextChanged: boolean; text: string; inputType: string; placeholder?: string; From 2b278ee7a10999ca9e2207c5ffca0c93621f5828 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Tue, 22 Mar 2022 18:09:52 +0800 Subject: [PATCH 09/16] feat: Internal property to detect changes in a form -- Merge componentDidUpdate methods --- .../src/widgets/InputWidgetV2/widget/index.tsx | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/client/src/widgets/InputWidgetV2/widget/index.tsx b/app/client/src/widgets/InputWidgetV2/widget/index.tsx index 6303a43542..8f4ebe1bdd 100644 --- a/app/client/src/widgets/InputWidgetV2/widget/index.tsx +++ b/app/client/src/widgets/InputWidgetV2/widget/index.tsx @@ -350,16 +350,6 @@ class InputWidget extends BaseInputWidget { }; } - componentDidUpdate(prevPorps: InputWidgetProps) { - // If defaultText property has changed, reset isDirty to false - if ( - this.props.defaultText !== prevPorps.defaultText && - this.props.isDirty - ) { - this.props.updateWidgetMetaProperty("isDirty", false); - } - } - handleFocusChange = (focusState: boolean) => { super.handleFocusChange(focusState); }; @@ -389,6 +379,13 @@ class InputWidget extends BaseInputWidget { getParsedText(this.props.inputText, this.props.inputType), ); } + // If defaultText property has changed, reset isDirty to false + if ( + this.props.defaultText !== prevProps.defaultText && + this.props.isDirty + ) { + this.props.updateWidgetMetaProperty("isDirty", false); + } }; onValueChange = (value: string) => { From 44685835ce8222976ceddc2399721d26343b5fa0 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Wed, 30 Mar 2022 23:15:44 +0800 Subject: [PATCH 10/16] feat: Internal property to detect changes in a form -- Reset isDirty if defaultText is changed in PhoneInputWidget -- Enable reset if defaultText is not empty in RichTextEditorWidget --- .../src/widgets/PhoneInputWidget/widget/index.tsx | 4 +++- .../widgets/RichTextEditorWidget/widget/index.tsx | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx index b3d6da2542..a73eaa565a 100644 --- a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx @@ -205,8 +205,10 @@ class PhoneInputWidget extends BaseInputWidget< parseIncompletePhoneNumber(formattedValue), ); this.props.updateWidgetMetaProperty("text", formattedValue); + } - // If defaultText property has changed, reset isDirty to false + // If defaultText property has changed, reset isDirty to false + if (this.props.defaultText !== prevProps.defaultText) { if (this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", false); } diff --git a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx index ccc999893c..57cfc7e5eb 100644 --- a/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx +++ b/app/client/src/widgets/RichTextEditorWidget/widget/index.tsx @@ -130,7 +130,7 @@ class RichTextEditorWidget extends BaseWidget< static getMetaPropertiesMap(): Record { return { text: undefined, - shouldReset: true, + shouldReset: false, isDirty: false, }; } @@ -148,12 +148,20 @@ class RichTextEditorWidget extends BaseWidget< }; } + componentDidMount(): void { + if (this.props.defaultText) { + this.props.updateWidgetMetaProperty("shouldReset", true); + } + } + componentDidUpdate(prevProps: RichTextEditorWidgetProps): void { if (this.props.defaultText !== prevProps.defaultText) { if (this.props.isDirty) { this.props.updateWidgetMetaProperty("isDirty", false); } - this.props.updateWidgetMetaProperty("shouldReset", true); + if (this.props.defaultText) { + this.props.updateWidgetMetaProperty("shouldReset", true); + } } } From 5bb926d6ed4492cafeeb0e2d596482ce8a2176a5 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Fri, 1 Apr 2022 04:07:22 +0800 Subject: [PATCH 11/16] feat: Internal property to detect changes in form -- Fix on the wrong widget name in RichTextEditor_spec.js -- Replace selectors so that Cypress can interact with UI in Select_spec.js --- .../FormWidgets/RichTextEditor_spec.js | 2 +- .../FormWidgets/Select_spec.js | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js index c1d6248768..c43a0b0a1a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js @@ -132,7 +132,7 @@ describe("RichTextEditor Widget Functionality", function() { cy.openPropertyPane("textwidget"); cy.updateCodeInput( ".t--property-control-text", - `{{RichTextEditor1.isDirty}}`, + `{{RichtextEditor.isDirty}}`, ); // Change defaultText cy.openPropertyPane("richtexteditorwidget"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js index 4bba9edb7c..d3e373dedb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Select_spec.js @@ -1,24 +1,29 @@ const explorer = require("../../../../locators/explorerlocators.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../../locators/FormWidgets.json"); +const widgetLocators = require("../../../../locators/Widgets.json"); const widgetName = "selectwidget"; describe("Select widget", () => { - it("1. DragDrop Select/Text widgets", () => { + it("1. Drag and drop Select/Text widgets", () => { cy.get(explorer.addWidget).click(); cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 }); cy.get(`.t--widget-${widgetName}`).should("exist"); cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 }); + }); + it("2. Check isDirty meta property", () => { cy.openPropertyPane("textwidget"); cy.updateCodeInput(".t--property-control-text", `{{Select1.isDirty}}`); // Check if initial value of isDirty is false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI - cy.get( - `.t--widget-${widgetName} .bp3-popover-target > div > .bp3-button`, - ).click(); - cy.get(`.bp3-popover-content ul.bp3-menu li`) - .first() - .click(); + cy.get(formWidgetsPage.selectWidget) + .find(widgetLocators.dropdownSingleSelect) + .click({ force: true }); + cy.get(commonlocators.singleSelectWidgetMenuItem) + .contains("Blue") + .click({ force: true }); // Check if isDirty is set to true cy.get(".t--widget-textwidget").should("contain", "true"); // Change defaultOptionValue property From 0a8b0dfe9a4230db7c004351fb1d19e15c302562 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Fri, 1 Apr 2022 15:42:05 +0800 Subject: [PATCH 12/16] feat: Internal property to detect changes in a form -- Fix on failed test cases because of newly added hasChanges meta property --- .../Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js | 3 ++- ...lorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js index 7dba62e0ec..7d0bb19a1d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js @@ -68,9 +68,10 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { cy.hoverAndClickParticularIndex(1); cy.selectAction("Show Bindings"); cy.get(apiwidget.propertyList).then(function($lis) { - expect($lis).to.have.length(2); + expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTest.data}}"); + expect($lis.eq(1)).to.contain("{{FormTest.hasChanges}}"); }); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js index 1694445d80..50d937ec12 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js @@ -46,9 +46,10 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { ee.expandCollapseEntity("FormTest"); ee.ActionContextMenuByEntityName("FormTestCopy", "Show Bindings"); cy.get(apiwidget.propertyList).then(function($lis) { - expect($lis).to.have.length(2); + expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTestCopy.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTestCopy.data}}"); + expect($lis.eq(1)).to.contain("{{FormTestCopy.hansChanges}}"); cy.contains("FormTestCopy"); cy.get($lis.eq(1)) .contains("{{FormTestCopy.data}}") From 2b00f1cdf9dc02e5521f63a3087aa8bfccc6ef64 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Fri, 1 Apr 2022 18:38:35 +0800 Subject: [PATCH 13/16] feat: Internal property to detect changes in a form -- Fix on typo add missing assertion for hasChanges on the spec files --- .../Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js | 3 ++- ...lorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js index 7d0bb19a1d..1a282211ed 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js @@ -35,9 +35,10 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { cy.hoverAndClickParticularIndex(1); cy.selectAction("Show Bindings"); cy.get(apiwidget.propertyList).then(function($lis) { - expect($lis).to.have.length(2); + expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTest.data}}"); + expect($lis.eq(2)).to.contain("{{FormTest.hasChanges}}"); }); cy.get(".t--entity-name") .contains("FormTest") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js index 50d937ec12..d1c14fc04e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js @@ -49,7 +49,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTestCopy.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTestCopy.data}}"); - expect($lis.eq(1)).to.contain("{{FormTestCopy.hansChanges}}"); + expect($lis.eq(1)).to.contain("{{FormTestCopy.hasChanges}}"); cy.contains("FormTestCopy"); cy.get($lis.eq(1)) .contains("{{FormTestCopy.data}}") From e25b5da73256dcca8a1ef16697c324852791f150 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Sat, 2 Apr 2022 00:11:54 +0800 Subject: [PATCH 14/16] feat: Internal property to detect changes in a form -- Fix on failed test cases in Entity_Explorer_Widgets_Copy_Delete_Undo_spec and Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec --- .../Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js | 2 +- ...plorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js index 1a282211ed..32cf172641 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js @@ -72,7 +72,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTest.data}}"); - expect($lis.eq(1)).to.contain("{{FormTest.hasChanges}}"); + expect($lis.eq(2)).to.contain("{{FormTest.hasChanges}}"); }); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js index d1c14fc04e..018238fa6a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Paste_Delete_Undo_Keyboard_Event_spec.js @@ -49,7 +49,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTestCopy.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTestCopy.data}}"); - expect($lis.eq(1)).to.contain("{{FormTestCopy.hasChanges}}"); + expect($lis.eq(2)).to.contain("{{FormTestCopy.hasChanges}}"); cy.contains("FormTestCopy"); cy.get($lis.eq(1)) .contains("{{FormTestCopy.data}}") From cc9db789b279df933ae40d676f86cf5c3928bfc9 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Sat, 2 Apr 2022 08:13:17 +0800 Subject: [PATCH 15/16] feat: Internal property to detect changes in a form -- Fix a Cypress test case in Entity_Explorer_DragAndDropWidget_spec.js --- .../ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js index c3fffd9d7b..ddf868ae8b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js @@ -59,9 +59,10 @@ describe("Entity explorer Drag and Drop widgets testcases", function() { cy.hoverAndClickParticularIndex(1); cy.selectAction("Show Bindings"); cy.get(apiwidget.propertyList).then(function($lis) { - expect($lis).to.have.length(2); + expect($lis).to.have.length(3); expect($lis.eq(0)).to.contain("{{FormTest.isVisible}}"); expect($lis.eq(1)).to.contain("{{FormTest.data}}"); + expect($lis.eq(2)).to.contain("{{FormTest.hasChanges}}"); }); }); }); From 842e330db3c38e5723a6d4e152839bb26a7d6b3a Mon Sep 17 00:00:00 2001 From: Paul Li Date: Sat, 2 Apr 2022 23:33:14 +0800 Subject: [PATCH 16/16] feat: Internal property to detect changes in a form -- Change updateCodeInput command to testJsontext --- .../FormWidgets/RichTextEditor_spec.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js index c43a0b0a1a..dddcbb27b2 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/RichTextEditor_spec.js @@ -136,13 +136,7 @@ describe("RichTextEditor Widget Functionality", function() { ); // Change defaultText cy.openPropertyPane("richtexteditorwidget"); - cy.updateCodeInput(".t--property-control-defaulttext", "a"); - cy.closePropertyPane(); - cy.wait("@updateLayout").should( - "have.nested.property", - "response.body.responseMeta.status", - 200, - ); + cy.testJsontext("defaulttext", "a"); // Check if isDirty has been changed into false cy.get(".t--widget-textwidget").should("contain", "false"); // Interact with UI @@ -151,13 +145,7 @@ describe("RichTextEditor Widget Functionality", function() { cy.get(".t--widget-textwidget").should("contain", "true"); // Change defaultText cy.openPropertyPane("richtexteditorwidget"); - cy.updateCodeInput(".t--property-control-defaulttext", "b"); - cy.closePropertyPane(); - cy.wait("@updateLayout").should( - "have.nested.property", - "response.body.responseMeta.status", - 200, - ); + cy.testJsontext("defaulttext", "b"); // Check if isDirty is reset to false cy.get(".t--widget-textwidget").should("contain", "false"); });