From c3960a7a4d5377cfd143a451ba2db67edd6edf3a Mon Sep 17 00:00:00 2001 From: bhavin Date: Fri, 7 May 2021 18:38:43 +0530 Subject: [PATCH 01/20] fix :extended maxDate and minDate limits to 100 years from now. --- .../src/components/propertyControls/DatePickerControl.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/propertyControls/DatePickerControl.tsx b/app/client/src/components/propertyControls/DatePickerControl.tsx index 96aa68f0d3..0151e46e44 100644 --- a/app/client/src/components/propertyControls/DatePickerControl.tsx +++ b/app/client/src/components/propertyControls/DatePickerControl.tsx @@ -46,11 +46,11 @@ class DatePickerControl extends BaseControl< year = this.now.get("year"); maxDate: Date = this.now .clone() - .set({ month: 11, date: 31, year: this.year + 20 }) + .set({ month: 11, date: 31, year: this.year + 100 }) .toDate(); minDate: Date = this.now .clone() - .set({ month: 0, date: 1, year: this.year - 20 }) + .set({ month: 0, date: 1, year: this.year - 100 }) .toDate(); constructor(props: DatePickerControlProps) { From 285f7faf411d4031b24f3d46d0ec0ec59b440633 Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Wed, 12 May 2021 11:03:34 +0100 Subject: [PATCH 02/20] fix: add color picker to form component --- app/client/src/widgets/FormWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/widgets/FormWidget.tsx b/app/client/src/widgets/FormWidget.tsx index 40af23180a..585320c45f 100644 --- a/app/client/src/widgets/FormWidget.tsx +++ b/app/client/src/widgets/FormWidget.tsx @@ -19,7 +19,7 @@ class FormWidget extends ContainerWidget { label: "Background Color", helpText: "Use a html color name, HEX, RGB or RGBA value", placeholderText: "#FFFFFF / Gray / rgb(255, 99, 71)", - controlType: "INPUT_TEXT", + controlType: "COLOR_PICKER", isBindProperty: true, isTriggerProperty: false, validation: VALIDATION_TYPES.TEXT, From 0015d72122fd868219c755975e239bfea170903e Mon Sep 17 00:00:00 2001 From: bhavin Date: Wed, 12 May 2021 21:01:17 +0530 Subject: [PATCH 03/20] updated max year for datepicker to +100, same as property pane datepicker --- .../components/designSystems/blueprint/DatePickerComponent2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx index 94fa2764fd..749480237e 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx @@ -107,7 +107,7 @@ class DatePickerComponent extends React.Component< ? new Date(this.props.maxDate) : now .clone() - .set({ month: 11, date: 31, year: year + 20 }) + .set({ month: 11, date: 31, year: year + 100 }) .toDate(); const isValid = this.state.selectedDate ? this.isValidDate(new Date(this.state.selectedDate)) From 083efdcdbee807c695edf5b2323937f5232f20c1 Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Thu, 13 May 2021 13:31:46 +0100 Subject: [PATCH 04/20] fix: Form widget random color test --- .../Entity_Explorer_DragAndDropWidget_spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 a2d387bd31..534e39f531 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 @@ -1,5 +1,6 @@ const testdata = require("../../../../fixtures/testdata.json"); const apiwidget = require("../../../../locators/apiWidgetslocator.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); const explorer = require("../../../../locators/explorerlocators.json"); const commonlocators = require("../../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../../locators/FormWidgets.json"); @@ -29,10 +30,13 @@ describe("Entity explorer Drag and Drop widgets testcases", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ From e7cd7148fa2bbcdbd97b671079a3a723073df1b7 Mon Sep 17 00:00:00 2001 From: Tolulope Adetula <31691737+Tooluloope@users.noreply.github.com> Date: Thu, 13 May 2021 14:08:07 +0100 Subject: [PATCH 05/20] update: fix test --- .../ClientSideTests/FormWidgets/FormWidget_spec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js index 50bef3f6c9..70cef20567 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js @@ -3,6 +3,7 @@ const formWidgetsPage = require("../../../../locators/FormWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); const dsl = require("../../../../fixtures/formdsl.json"); const pages = require("../../../../locators/Pages.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); describe("Form Widget Functionality", function() { before(() => { @@ -23,10 +24,13 @@ describe("Form Widget Functionality", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ @@ -40,7 +44,7 @@ describe("Form Widget Functionality", function() { it("Form Widget Functionality To Verify The Colour", function() { cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); }); it("Form Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); From bbecba125e6c9933715f3ca378cb3fad4e7b2f55 Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Tue, 18 May 2021 00:42:06 +0530 Subject: [PATCH 06/20] - Wrap the methods passed to editableText component with useCallback - Use destructing for props --- .../editorComponents/EditableText.tsx | 102 +++++++++++------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index b8c2983a02..baa81e42c0 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -92,10 +92,25 @@ const TextContainer = styled.div<{ isValid: boolean; minimal: boolean }>` `; export function EditableText(props: EditableTextProps) { - const [isEditing, setIsEditing] = useState(!!props.isEditingDefault); - const [value, setStateValue] = useState(props.defaultValue); + const { + beforeUnmount, + className, + defaultValue, + editInteractionKind, + forceDefault, + hideEditIcon, + isEditingDefault, + isInvalid, + minimal, + onBlur, + onTextChanged, + placeholder, + updating, + valueTransform, + } = props; + const [isEditing, setIsEditing] = useState(!!isEditingDefault); + const [value, setStateValue] = useState(defaultValue); const inputValRef = useRef(""); - const { beforeUnmount } = props; const setValue = useCallback((value) => { inputValRef.current = value; @@ -103,16 +118,16 @@ export function EditableText(props: EditableTextProps) { }, []); useEffect(() => { - setValue(props.defaultValue); - }, [props.defaultValue]); + setValue(defaultValue); + }, [defaultValue]); useEffect(() => { - setIsEditing(!!props.isEditingDefault); - }, [props.defaultValue, props.isEditingDefault]); + setIsEditing(!!isEditingDefault); + }, [defaultValue, isEditingDefault]); useEffect(() => { - if (props.forceDefault === true) setValue(props.defaultValue); - }, [props.forceDefault, props.defaultValue]); + if (forceDefault === true) setValue(defaultValue); + }, [forceDefault, defaultValue]); // at times onTextChange is not fired // for example when the modal is closed on clicking the overlay @@ -128,58 +143,63 @@ export function EditableText(props: EditableTextProps) { e.preventDefault(); e.stopPropagation(); }; - const onChange = (_value: string) => { - props.onBlur && props.onBlur(); - const isInvalid = props.isInvalid ? props.isInvalid(_value) : false; - if (!isInvalid) { - props.onTextChanged(_value); - setIsEditing(false); - } else { - Toaster.show({ - text: "Invalid name", - variant: Variant.danger, - }); - } - }; + const onChange = useCallback( + (_value: string) => { + onBlur && onBlur(); + const _isInvalid = isInvalid ? isInvalid(_value) : false; + if (!_isInvalid) { + onTextChanged(_value); + setIsEditing(false); + } else { + Toaster.show({ + text: "Invalid name", + variant: Variant.danger, + }); + } + }, + [isInvalid], + ); - const onInputchange = (_value: string) => { - let finalVal: string = _value; - if (props.valueTransform) { - finalVal = props.valueTransform(_value); - } - setValue(finalVal); - }; + const onInputchange = useCallback( + (_value: string) => { + let finalVal: string = _value; + if (valueTransform) { + finalVal = valueTransform(_value); + } + setValue(finalVal); + }, + [valueTransform], + ); - const errorMessage = props.isInvalid && props.isInvalid(value); + const errorMessage = isInvalid && isInvalid(value); const error = errorMessage ? errorMessage : undefined; return ( - + - {!props.minimal && - !props.hideEditIcon && - !props.updating && - !isEditing && } + {!minimal && !hideEditIcon && !updating && !isEditing && ( + + )} From c5133fca87f0367dc216a079a8b3bbfcfcec7dd8 Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Fri, 21 May 2021 00:55:05 +0530 Subject: [PATCH 07/20] - Patch the blueprint core module with the editabletext performance optimization --- app/client/package.json | 5 +- .../patches/@blueprintjs+core+3.36.0.patch | 22 ++++++++ app/client/yarn.lock | 55 ++++++++++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 app/client/patches/@blueprintjs+core+3.36.0.patch diff --git a/app/client/package.json b/app/client/package.json index 02f95af730..3be5115485 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -161,7 +161,8 @@ "test:unit": "$(npm bin)/jest -b --colors --no-cache --coverage --collectCoverage=true --coverageDirectory='../../' --coverageReporters='json-summary'", "test:jest": "$(npm bin)/jest --watch", "storybook": "start-storybook -p 9009 -s public", - "build-storybook": "build-storybook -s public" + "build-storybook": "build-storybook -s public", + "postinstall": "patch-package" }, "resolution": { "jest": "24.8.0" @@ -232,6 +233,8 @@ "mochawesome": "^5.0.0", "mochawesome-report-generator": "^4.1.0", "msw": "^0.28.0", + "patch-package": "^6.4.7", + "postinstall-postinstall": "^2.1.0", "raw-loader": "^4.0.2", "react-docgen-typescript-loader": "^3.6.0", "react-is": "^16.12.0", diff --git a/app/client/patches/@blueprintjs+core+3.36.0.patch b/app/client/patches/@blueprintjs+core+3.36.0.patch new file mode 100644 index 0000000000..3342cbfefc --- /dev/null +++ b/app/client/patches/@blueprintjs+core+3.36.0.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +index 84f03fa..5e5488a 100644 +--- a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js ++++ b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +@@ -188,7 +188,16 @@ var EditableText = /** @class */ (function (_super) { + if (this.state.isEditing && !prevState.isEditing) { + (_b = (_a = this.props).onEdit) === null || _b === void 0 ? void 0 : _b.call(_a, this.state.value); + } +- this.updateInputDimensions(); ++ // updateInputDimensions is an expensive method. Call it only when the props ++ // it depends on change ++ if (this.state.value !== prevState.value || ++ this.props.alwaysRenderInput !== prevProps.alwaysRenderInput || ++ this.props.maxLines !== prevProps.maxLines || ++ this.props.minLines !== prevProps.minLines || ++ this.props.minWidth !== prevProps.minWidth || ++ this.props.multiline !== prevProps.multiline) { ++ this.updateInputDimensions(); ++ } + }; + EditableText.prototype.renderInput = function (value) { + var _a = this.props, disabled = _a.disabled, maxLength = _a.maxLength, multiline = _a.multiline, type = _a.type, placeholder = _a.placeholder; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index a6af49a55e..f980d2d04a 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -4610,6 +4610,11 @@ version "4.2.2" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + abab@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -6674,7 +6679,7 @@ cross-fetch@^3.0.4: dependencies: node-fetch "2.6.1" -cross-spawn@6.0.5, cross-spawn@^6.0.0: +cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" dependencies: @@ -8604,6 +8609,13 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -8746,7 +8758,7 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-extra@^7.0.0: +fs-extra@^7.0.0, fs-extra@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" dependencies: @@ -11064,6 +11076,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" @@ -12536,6 +12555,14 @@ open@^7.0.0, open@^7.0.2, open@^7.1.0: is-docker "^2.0.0" is-wsl "^2.1.1" +open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + opencollective-postinstall@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" @@ -12802,6 +12829,25 @@ pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" +patch-package@^6.4.7: + version "6.4.7" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.4.7.tgz#2282d53c397909a0d9ef92dae3fdeb558382b148" + integrity sha512-S0vh/ZEafZ17hbhgqdnpunKDfzHQibQizx9g8yEf5dcVk3KOflOfdufRXQX8CSEkyOQwuM/bNz1GwKvFj54kaQ== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^2.4.2" + cross-spawn "^6.0.5" + find-yarn-workspace-root "^2.0.0" + fs-extra "^7.0.1" + is-ci "^2.0.0" + klaw-sync "^6.0.0" + minimist "^1.2.0" + open "^7.4.2" + rimraf "^2.6.3" + semver "^5.6.0" + slash "^2.0.0" + tmp "^0.0.33" + path-browserify@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" @@ -13619,6 +13665,11 @@ postcss@^8.1.0: nanoid "^3.1.15" source-map "^0.6.1" +postinstall-postinstall@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postinstall-postinstall/-/postinstall-postinstall-2.1.0.tgz#4f7f77441ef539d1512c40bd04c71b06a4704ca3" + integrity sha512-7hQX6ZlZXIoRiWNrbMQaLzUUfH+sSx39u8EJ9HYuDc1kLo9IXKWjM5RSquZN1ad5GnH8CGFM78fsAAQi3OKEEQ== + preact@8.2.9: version "8.2.9" resolved "https://registry.yarnpkg.com/preact/-/preact-8.2.9.tgz#813ba9dd45e5d97c5ea0d6c86d375b3be711cc40" From 4d9aa2bd00266185baa567166f1d7b984d72d282 Mon Sep 17 00:00:00 2001 From: Paul Li Date: Thu, 20 May 2021 15:28:02 -0400 Subject: [PATCH 08/20] FIX-4362 : Unexpected data types in the Recaptcha field for buttons breaks the widget -- Add a guard for invalid googleRecaptcha key --- .../blueprint/ButtonComponent.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index 44b5338312..daa1459448 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -180,8 +180,25 @@ function RecaptchaComponent( }); props.onClick && props.onClick(event); } + + // Check if a string is a valid JSON string + const checkValidJson = (inputString: string): boolean => { + try { + JSON.parse(inputString); + return true; + } catch (err) { + return false; + } + }; + + let validGoogleRecaptchaKey = props.googleRecaptchaKey; + + if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) { + validGoogleRecaptchaKey = undefined; + } + const status = useScript( - `https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`, + `https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`, ); return (
Date: Mon, 24 May 2021 18:27:50 +0530 Subject: [PATCH 09/20] FIX #4573 : added custom fusion chart config to override default message --- app/client/src/mockResponses/WidgetConfigResponse.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 521ce177cf..39a6ca5a91 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -541,6 +541,12 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, xAxisName: "Last Week", yAxisName: "Total Order Revenue $", + customFusionChartConfig: { + config: { + type: "", + dataSource: {}, + }, + }, }, FORM_BUTTON_WIDGET: { rows: 1 * GRID_DENSITY_MIGRATION_V1, From 312ea0013854c0391efbd2a372b27c58fe877f18 Mon Sep 17 00:00:00 2001 From: "vicky.bansal@primathon.in" Date: Wed, 26 May 2021 10:22:08 +0530 Subject: [PATCH 10/20] Increase width of default table widget so that scrollbars are not required by default --- app/client/src/mockResponses/WidgetConfigResponse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 521ce177cf..488afe4c4e 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -148,7 +148,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, TABLE_WIDGET: { rows: 7 * GRID_DENSITY_MIGRATION_V1, - columns: 8 * GRID_DENSITY_MIGRATION_V1, + columns: 9 * GRID_DENSITY_MIGRATION_V1, label: "Data", widgetName: "Table", searchKey: "", From c014485268e211a82ab4f07fede45fe53e783789 Mon Sep 17 00:00:00 2001 From: bhavin Date: Thu, 27 May 2021 16:27:10 +0530 Subject: [PATCH 11/20] added string check on options prop --- app/client/src/widgets/DropdownWidget.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index e0e4dd8782..e242ed6e6b 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -149,7 +149,11 @@ class DropdownWidget extends BaseWidget { } getPageView() { - const options = this.props.options || []; + const options = this.props.options + ? typeof this.props.options === "string" + ? [] + : this.props.options + : []; const selectedIndex = _.findIndex(this.props.options, { value: this.props.selectedOptionValue, }); From 9de9b1eb3075202978d034e7c95af7f5a72f83f1 Mon Sep 17 00:00:00 2001 From: Yash Date: Fri, 28 May 2021 14:49:36 +0530 Subject: [PATCH 12/20] FIX #4573 : added sample custom fusion chart config and updated data shape to override default message --- .../mockResponses/WidgetConfigResponse.tsx | 46 +++++++++++++++++-- app/client/src/widgets/ChartWidget/index.tsx | 2 +- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 39a6ca5a91..231fd137da 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -542,9 +542,49 @@ const WidgetConfigResponse: WidgetConfigReducerState = { xAxisName: "Last Week", yAxisName: "Total Order Revenue $", customFusionChartConfig: { - config: { - type: "", - dataSource: {}, + type: "column2d", + dataSource: { + chart: { + caption: "Monthly revenue for last year", + subCaption: "Harry's SuperMart", + xAxisName: "Month", + yAxisName: "Revenues (In USD)", + numberPrefix: "$", + theme: "fusion", + }, + data: [ + { + label: "Jan", + value: "420000", + }, + { + label: "Feb", + value: "810000", + }, + { + label: "Mar", + value: "720000", + }, + { + label: "Apr", + value: "550000", + }, + { + label: "May", + value: "910000", + }, + ], + trendlines: [ + { + line: [ + { + startvalue: "700000", + valueOnRight: "1", + displayvalue: "Monthly Target", + }, + ], + }, + ], }, }, }, diff --git a/app/client/src/widgets/ChartWidget/index.tsx b/app/client/src/widgets/ChartWidget/index.tsx index 84432435a9..b06ec6153c 100644 --- a/app/client/src/widgets/ChartWidget/index.tsx +++ b/app/client/src/widgets/ChartWidget/index.tsx @@ -92,7 +92,7 @@ export interface ChartData { export interface ChartWidgetProps extends WidgetProps, WithMeta { chartType: ChartType; chartData: AllChartData; - customFusionChartConfig: { config: CustomFusionChartConfig }; + customFusionChartConfig: CustomFusionChartConfig; xAxisName: string; yAxisName: string; chartName: string; From 2b9cc622cc35c6995204dc59e98d0ae1139c4d38 Mon Sep 17 00:00:00 2001 From: bhavin Date: Tue, 1 Jun 2021 09:36:47 +0530 Subject: [PATCH 13/20] replaced string check with array check --- app/client/src/widgets/DropdownWidget.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/client/src/widgets/DropdownWidget.tsx b/app/client/src/widgets/DropdownWidget.tsx index e242ed6e6b..933314d2a4 100644 --- a/app/client/src/widgets/DropdownWidget.tsx +++ b/app/client/src/widgets/DropdownWidget.tsx @@ -149,11 +149,7 @@ class DropdownWidget extends BaseWidget { } getPageView() { - const options = this.props.options - ? typeof this.props.options === "string" - ? [] - : this.props.options - : []; + const options = _.isArray(this.props.options) ? this.props.options : []; const selectedIndex = _.findIndex(this.props.options, { value: this.props.selectedOptionValue, }); From 53598e80277af046a1ce80bfd865fd9950207758 Mon Sep 17 00:00:00 2001 From: bhavin Date: Tue, 1 Jun 2021 14:50:13 +0530 Subject: [PATCH 14/20] added onclose property and handlers on ModalWidget --- .../blueprint/ModalComponent.tsx | 9 ++++++ .../ActionConstants.tsx | 1 + app/client/src/widgets/ModalWidget.tsx | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx index acf3aa652d..e3dd12ead4 100644 --- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx @@ -56,6 +56,7 @@ const Content = styled.div<{ export type ModalComponentProps = { isOpen: boolean; onClose: (e: any) => void; + onModalClose?: () => void; children: ReactNode; width?: number; className?: string; @@ -76,6 +77,14 @@ export function ModalComponent(props: ModalComponentProps) { const modalContentRef: RefObject = useRef( null, ); + useEffect(() => { + return () => { + // handle modal close events when this component unmounts + // will be called in all cases :- + // escape key press, click out side, close click from other btn widget + if (props.onModalClose) props.onModalClose(); + }; + }, []); useEffect(() => { if (!props.scrollContents) { modalContentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx index 7e63eb2edc..6f8dc9c1dc 100644 --- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx +++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx @@ -56,6 +56,7 @@ export enum EventType { ON_HOVER = "ON_HOVER", ON_TOGGLE = "ON_TOGGLE", ON_LOAD = "ON_LOAD", + ON_MODAL_CLOSE = "ON_MODAL_CLOSE", ON_TEXT_CHANGE = "ON_TEXT_CHANGE", ON_SUBMIT = "ON_SUBMIT", ON_CHECK_CHANGE = "ON_CHECK_CHANGE", diff --git a/app/client/src/widgets/ModalWidget.tsx b/app/client/src/widgets/ModalWidget.tsx index a740c9eaec..6fae51bd08 100644 --- a/app/client/src/widgets/ModalWidget.tsx +++ b/app/client/src/widgets/ModalWidget.tsx @@ -3,6 +3,7 @@ import React, { ReactNode } from "react"; import { connect } from "react-redux"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import WidgetFactory from "utils/WidgetFactory"; import ModalComponent from "components/designSystems/blueprint/ModalComponent"; import { @@ -70,6 +71,20 @@ export class ModalWidget extends BaseWidget { }, ], }, + { + sectionName: "Actions", + children: [ + { + helpText: "Triggers an action when the modal is closed", + propertyName: "onClose", + label: "onClose", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, ]; } static defaultProps = { @@ -99,6 +114,18 @@ export class ModalWidget extends BaseWidget { return WidgetFactory.createWidget(childWidgetData, this.props.renderMode); }; + onModalClose = () => { + if (this.props.onClose) { + super.executeAction({ + triggerPropertyName: "onClose", + dynamicString: this.props.onClose, + event: { + type: EventType.ON_MODAL_CLOSE, + }, + }); + } + }; + closeModal = (e: any) => { this.props.showPropertyPane(undefined); // TODO(abhinav): Create a static property with is a map of widget properties @@ -124,6 +151,7 @@ export class ModalWidget extends BaseWidget { height={MODAL_SIZE[this.props.size].height} isOpen={!!this.props.isVisible} onClose={this.closeModal} + onModalClose={this.onModalClose} scrollContents={!!this.props.shouldScrollContents} width={this.getModalWidth()} > @@ -159,6 +187,7 @@ export interface ModalWidgetProps extends WidgetProps, WithMeta { canEscapeKeyClose?: boolean; shouldScrollContents?: boolean; size: string; + onClose: string; mainContainer: WidgetProps; } From 8008aefcc7f9b7924b956423b88682bc378086cb Mon Sep 17 00:00:00 2001 From: Yash Date: Thu, 3 Jun 2021 20:50:23 +0530 Subject: [PATCH 15/20] FIX #4573 : custom fusion chart sample data make consistent as chartData property --- .../mockResponses/WidgetConfigResponse.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 231fd137da..2ad873bb0c 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -545,42 +545,48 @@ const WidgetConfigResponse: WidgetConfigReducerState = { type: "column2d", dataSource: { chart: { - caption: "Monthly revenue for last year", - subCaption: "Harry's SuperMart", - xAxisName: "Month", - yAxisName: "Revenues (In USD)", - numberPrefix: "$", + caption: "Last week's revenue", + xAxisName: "Last Week", + yAxisName: "Total Order Revenue $", theme: "fusion", }, data: [ { - label: "Jan", - value: "420000", + label: "Mon", + value: 10000, }, { - label: "Feb", - value: "810000", + label: "Tue", + value: 12000, }, { - label: "Mar", - value: "720000", + label: "Wed", + value: 32000, }, { - label: "Apr", - value: "550000", + label: "Thu", + value: 28000, }, { - label: "May", - value: "910000", + label: "Fri", + value: 14000, + }, + { + label: "Sat", + value: 19000, + }, + { + label: "Sun", + value: 36000, }, ], trendlines: [ { line: [ { - startvalue: "700000", + startvalue: "38000", valueOnRight: "1", - displayvalue: "Monthly Target", + displayvalue: "Weekly Target", }, ], }, From d2a74b5ac988e9af7acf8770df4deb7859240488 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Mon, 7 Jun 2021 10:43:16 +0530 Subject: [PATCH 16/20] Feature: firestore plugin support for multiple where conditions (#3740) * allow users to add multiple where conditions to firestore's get documents query. --- .../components/formControls/BaseControl.tsx | 1 + .../formControls/FieldArrayControl.tsx | 106 ++++++++++++++++ .../Editor/QueryEditor/EditorJSONtoForm.tsx | 2 +- app/client/src/sagas/QueryPaneSagas.ts | 29 +++-- app/client/src/utils/FormControlRegistry.tsx | 8 ++ .../com/external/plugins/FirestorePlugin.java | 117 ++++++++---------- .../external/utils/WhereConditionUtils.java | 90 ++++++++++++++ .../src/main/resources/editor.json | 71 ++++------- .../external/plugins/FirestorePluginTest.java | 75 +++++++++-- .../server/migrations/DatabaseChangelog.java | 95 ++++++++++++++ 10 files changed, 467 insertions(+), 127 deletions(-) create mode 100644 app/client/src/components/formControls/FieldArrayControl.tsx create mode 100644 app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java diff --git a/app/client/src/components/formControls/BaseControl.tsx b/app/client/src/components/formControls/BaseControl.tsx index 9087cb0178..4c9f6d26d3 100644 --- a/app/client/src/components/formControls/BaseControl.tsx +++ b/app/client/src/components/formControls/BaseControl.tsx @@ -51,6 +51,7 @@ export interface ControlData { isRequired?: boolean; hidden?: HiddenType; placeholderText?: string; + schema?: any; } export interface ControlFunctions { diff --git a/app/client/src/components/formControls/FieldArrayControl.tsx b/app/client/src/components/formControls/FieldArrayControl.tsx new file mode 100644 index 0000000000..6dba80f6b7 --- /dev/null +++ b/app/client/src/components/formControls/FieldArrayControl.tsx @@ -0,0 +1,106 @@ +import React, { useEffect } from "react"; +import FormControl from "pages/Editor/FormControl"; +import Text, { TextType } from "components/ads/Text"; +import Icon, { IconSize } from "components/ads/Icon"; +import { Classes } from "components/ads/common"; +import styled from "styled-components"; +import { FieldArray } from "redux-form"; +import FormLabel from "components/editorComponents/FormLabel"; +import { ControlProps } from "./BaseControl"; + +const CenteredIcon = styled(Icon)` + margin-top: 25px; + &.hide { + opacity: 0; + pointer-events: none; + } +`; + +const PrimaryBox = styled.div` + display: flex; + flex-direction: column; + border: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg}; + padding: 10px; + border-radius: 5px; +`; + +const SecondaryBox = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 5px; +`; + +const AddMoreAction = styled.div` + width: fit-content; + cursor: pointer; + display: flex; + margin-top: 16px; + .${Classes.TEXT} { + margin-left: 8px; + color: #03b365; + } +`; + +function NestedComponents(props: any) { + useEffect(() => { + if (props.fields.length < 1) { + props.fields.push({}); + } + }, [props.fields.length]); + return ( + + {props.fields && + props.fields.length > 0 && + props.fields.map((field: string, index: number) => { + return ( + + {props.schema.map((sch: any, idx: number) => { + sch = { + ...sch, + configProperty: `${field}.${sch.key}`, + }; + return ( + + ); + })} + { + e.stopPropagation(); + props.fields.remove(index); + }} + size={IconSize.XXL} + /> + + ); + })} + props.fields.push({})}> + {/*Hardcoded label to be removed */} + + Add Condition (And) + + + ); +} + +export default function FieldArrayControl(props: FieldArrayControlProps) { + const { configProperty, formName, label, schema } = props; + return ( + <> + {label} + + + ); +} + +export type FieldArrayControlProps = ControlProps; diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index 0d718d56c3..d6fbe5e8be 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -461,7 +461,7 @@ export function EditorJSONtoForm(props: Props) { const renderEachConfig = (formName: string) => (section: any): any => { return section.children.map((formControlOrSection: ControlProps) => { if (isHidden(props.formData, section.hidden)) return null; - if ("children" in formControlOrSection) { + if (formControlOrSection.hasOwnProperty("children")) { return renderEachConfig(formName)(formControlOrSection); } else { try { diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index 018583d8e1..c54f1954f9 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -37,6 +37,7 @@ import { Toaster } from "components/ads/Toast"; import { Datasource } from "entities/Datasource"; import _ from "lodash"; import { createMessage, ERROR_ACTION_RENAME_FAIL } from "constants/messages"; +import get from "lodash/get"; function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { const { id } = actionPayload.payload; @@ -114,13 +115,27 @@ function* formValueChangeSaga( return; } - yield put( - setActionProperty({ - actionId: values.id, - propertyName: field, - value: actionPayload.payload, - }), - ); + if ( + actionPayload.type === ReduxFormActionTypes.ARRAY_REMOVE || + actionPayload.type === ReduxFormActionTypes.ARRAY_PUSH + ) { + const value = get(values, field); + yield put( + setActionProperty({ + actionId: values.id, + propertyName: field, + value, + }), + ); + } else { + yield put( + setActionProperty({ + actionId: values.id, + propertyName: field, + value: actionPayload.payload, + }), + ); + } } function* handleQueryCreatedSaga(actionPayload: ReduxAction) { diff --git a/app/client/src/utils/FormControlRegistry.tsx b/app/client/src/utils/FormControlRegistry.tsx index d66a647f09..c25ae1440d 100644 --- a/app/client/src/utils/FormControlRegistry.tsx +++ b/app/client/src/utils/FormControlRegistry.tsx @@ -31,6 +31,9 @@ import DynamicInputTextControl, { DynamicInputControlProps, } from "components/formControls/DynamicInputTextControl"; import InputNumberControl from "components/formControls/InputNumberControl"; +import FieldArrayControl, { + FieldArrayControlProps, +} from "components/formControls/FieldArrayControl"; class FormControlRegistry { static registerFormControlBuilders() { @@ -93,6 +96,11 @@ class FormControlRegistry { return ; }, }); + FormControlFactory.registerControlBuilder("ARRAY_FIELD", { + buildPropertyControl(controlProps: FieldArrayControlProps): JSX.Element { + return ; + }, + }); } } diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java index 29301e269c..dd5e34540b 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/plugins/FirestorePlugin.java @@ -57,6 +57,7 @@ import java.util.stream.StreamSupport; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_PATH; import static com.appsmith.external.helpers.PluginUtils.getActionConfigurationPropertyPath; +import static com.external.utils.WhereConditionUtils.applyWhereConditional; /** * Datasource properties: @@ -71,9 +72,7 @@ public class FirestorePlugin extends BasePlugin { private static final int ORDER_PROPERTY_INDEX = 1; private static final int LIMIT_PROPERTY_INDEX = 2; - private static final int QUERY_PROPERTY_INDEX = 3; - private static final int OPERATOR_PROPERTY_INDEX = 4; - private static final int QUERY_VALUE_PROPERTY_INDEX = 5; + private static final int WHERE_CONDITIONAL_PROPERTY_INDEX = 3; private static final int START_AFTER_PROPERTY_INDEX = 6; private static final int END_BEFORE_PROPERTY_INDEX = 7; private static final int FIELDVALUE_TIMESTAMP_PROPERTY_INDEX = 8; @@ -543,14 +542,6 @@ public class FirestorePlugin extends BasePlugin { List requestParams) { final String limitString = getPropertyAt(properties, LIMIT_PROPERTY_INDEX, "10"); final int limit = StringUtils.isEmpty(limitString) ? 10 : Integer.parseInt(limitString); - - final String queryFieldPath = getPropertyAt(properties, QUERY_PROPERTY_INDEX, null); - - final String operatorString = getPropertyAt(properties, OPERATOR_PROPERTY_INDEX, null); - final Op operator = StringUtils.isEmpty(operatorString) ? null : Op.valueOf(operatorString); - - final String queryValue = getPropertyAt(properties, QUERY_VALUE_PROPERTY_INDEX, null); - final String orderByString = getPropertyAt(properties, ORDER_PROPERTY_INDEX, ""); requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(ORDER_PROPERTY_INDEX), orderByString, null, null, null)); @@ -589,12 +580,6 @@ public class FirestorePlugin extends BasePlugin { requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(LIMIT_PROPERTY_INDEX), limitString == null ? "" : limitString, null, null, null)); - requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(QUERY_PROPERTY_INDEX), - queryFieldPath == null ? "" : queryFieldPath, null, null, null)); - requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(OPERATOR_PROPERTY_INDEX), - operatorString == null ? "" : operatorString, null, null, null)); - requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(QUERY_VALUE_PROPERTY_INDEX), - queryValue == null ? "" : queryValue, null, null, null)); final Map startAfter = startAfterTemp; final Map endBefore = endBeforeTemp; @@ -633,53 +618,42 @@ public class FirestorePlugin extends BasePlugin { }) // Apply where condition, if provided. .flatMap(query1 -> { - if (StringUtils.isEmpty(queryFieldPath) || operator == null || queryValue == null) { + if (!isWhereMethodUsed(properties)) { return Mono.just(query1); } - switch (operator) { - case LT: - return Mono.just(query1.whereLessThan(queryFieldPath, queryValue)); - case LTE: - return Mono.just(query1.whereLessThanOrEqualTo(queryFieldPath, queryValue)); - case EQ: - return Mono.just(query1.whereEqualTo(queryFieldPath, queryValue)); - // TODO: NOT_EQ operator support is awaited in the next version of Firestore driver. - // case NOT_EQ: - // return Mono.just(query1.whereNotEqualTo(queryFieldPath, queryValue)); - case GT: - return Mono.just(query1.whereGreaterThan(queryFieldPath, queryValue)); - case GTE: - return Mono.just(query1.whereGreaterThanOrEqualTo(queryFieldPath, queryValue)); - case ARRAY_CONTAINS: - return Mono.just(query1.whereArrayContains(queryFieldPath, queryValue)); - case ARRAY_CONTAINS_ANY: - try { - return Mono.just(query1.whereArrayContainsAny(queryFieldPath, parseList(queryValue))); - } catch (IOException e) { - return Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, - "Unable to parse condition value as a JSON list." - )); - } - case IN: - try { - return Mono.just(query1.whereIn(queryFieldPath, parseList(queryValue))); - } catch (IOException e) { - return Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, - "Unable to parse condition value as a JSON list." - )); - } - // TODO: NOT_IN operator support is awaited in the next version of Firestore driver. - // case NOT_IN: - // return Mono.just(query1.whereNotIn(queryFieldPath, queryValue)); - default: - return Mono.error(new AppsmithPluginException( - AppsmithPluginError.PLUGIN_ERROR, - "Unsupported operator for `where` condition " + operator.toString() + "." - )); + List conditionList = (List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue(); + requestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(WHERE_CONDITIONAL_PROPERTY_INDEX), + conditionList, null, null, null)); + + for(Object condition : conditionList) { + String path = ((Map)condition).get("path"); + String operatorString = ((Map)condition).get("operator"); + String value = ((Map)condition).get("value"); + + /** + * - If all values in all where condition tuples are null, then isWhereMethodUsed(...) + * function will indicate that where conditions are not used effectively and program + * execution would return without reaching here. + */ + if (StringUtils.isEmpty(path) || StringUtils.isEmpty(operatorString) || StringUtils.isEmpty(value)) { + return Mono.error( + new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "One of the where condition fields has been found empty. Please fill " + + "all the where condition fields and try again." + ) + ); + } + + try { + query1 = applyWhereConditional(query1, path, operatorString, value); + } catch (AppsmithPluginException e) { + return Mono.error(e); + } } + + return Mono.just(query1); }) // Apply limit, always provided, since without it we can inadvertently end up processing too much data. .map(query1 -> query1.limit(limit)) @@ -710,6 +684,25 @@ public class FirestorePlugin extends BasePlugin { }); } + private boolean isWhereMethodUsed(List properties) { + // Check if the where property list does not exist or is null + if(properties.size() <= WHERE_CONDITIONAL_PROPERTY_INDEX || properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX) == null + || CollectionUtils.isEmpty((List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue())) { + return false; + } + + // Check if all values in the where property list are null. + boolean allValuesNull = ((List) properties.get(WHERE_CONDITIONAL_PROPERTY_INDEX).getValue()).stream() + .allMatch(valueMap -> valueMap == null || + ((Map) valueMap).entrySet().stream().allMatch(e -> ((Map.Entry) e).getValue() == null)); + + if (allValuesNull) { + return false; + } + + return true; + } + private Mono methodAddToCollection(CollectionReference collection, Map mapBody) { return Mono.justOrEmpty(collection.add(mapBody)) .flatMap(future -> { @@ -878,10 +871,6 @@ public class FirestorePlugin extends BasePlugin { return invalids; } - private List parseList(String arrayJson) throws IOException { - return (List) objectMapper.readValue(arrayJson, ArrayList.class); - } - @Override public Mono testDatasource(DatasourceConfiguration datasourceConfiguration) { return datasourceCreate(datasourceConfiguration) diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java new file mode 100644 index 0000000000..579e4aa430 --- /dev/null +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/java/com/external/utils/WhereConditionUtils.java @@ -0,0 +1,90 @@ +package com.external.utils; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.external.plugins.Op; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.cloud.firestore.FieldPath; +import com.google.cloud.firestore.Query; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class WhereConditionUtils { + + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + public static Query applyWhereConditional(Query query, String path, String operatorString, String value) throws AppsmithPluginException { + + if (query == null) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith server has found null query object when applying where conditional on Firestore " + + "query. Please contact Appsmith's customer support to resolve this." + ); + } + + Op operator; + try { + operator = StringUtils.isEmpty(operatorString) ? null : Op.valueOf(operatorString); + } catch (IllegalArgumentException e) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith server has encountered an invalid operator for Firestore query's where conditional." + + " Please contact Appsmith's customer support to resolve this." + ); + } + + FieldPath fieldPath = FieldPath.of(path.split("\\.")); + switch (operator) { + case LT: + return query.whereLessThan(fieldPath, value); + case LTE: + return query.whereLessThanOrEqualTo(fieldPath, value); + case EQ: + return query.whereEqualTo(fieldPath, value); + // TODO: NOT_EQ operator support is awaited in the next version of Firestore driver. + // case NOT_EQ: + // return Mono.just(query.whereNotEqualTo(path, value)); + case GT: + return query.whereGreaterThan(fieldPath, value); + case GTE: + return query.whereGreaterThanOrEqualTo(fieldPath, value); + case ARRAY_CONTAINS: + return query.whereArrayContains(fieldPath, value); + case ARRAY_CONTAINS_ANY: + try { + return query.whereArrayContainsAny(fieldPath, parseList(value)); + } catch (IOException e) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "Unable to parse condition value as a JSON list." + ); + } + case IN: + try { + return query.whereIn(fieldPath, parseList(value)); + } catch (IOException e) { + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, + "Unable to parse condition value as a JSON list." + ); + } + // TODO: NOT_IN operator support is awaited in the next version of Firestore driver. + // case NOT_IN: + // return Mono.just(query.whereNotIn(fieldPath, value)); + default: + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith server has encountered an invalid operator for Firestore query's where conditional." + + " Please contact Appsmith's customer support to resolve this." + ); + } + } + + private static List parseList(String arrayJson) throws IOException { + return (List) objectMapper.readValue(arrayJson, ArrayList.class); + } +} diff --git a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json index 464ed3b03a..308158cf77 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/firestorePlugin/src/main/resources/editor.json @@ -167,43 +167,32 @@ "initialValue": "10" }, { - "sectionName": "Query", - "id": 2, - "children": [ + "label": "Where Conditions Key", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].key", + "controlType": "INPUT_TEXT", + "hidden": true, + "initialValue": "whereConditionTuples" + }, + { + "label": "Where Conditions", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].value", + "controlType": "ARRAY_FIELD", + "hidden": { + "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", + "comparison": "NOT_EQUALS", + "value": "GET_COLLECTION" + }, + "schema": [ { - "label": "Field Path Key", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].key", - "controlType": "INPUT_TEXT", - "hidden": true, - "initialValue": "fieldPath" - }, - { - "label": "Where Condition: Field Path (leave empty to not apply any conditions)", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].value", + "label": "Path", + "key": "path", "controlType": "QUERY_DYNAMIC_INPUT_TEXT", - "hidden": { - "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "comparison": "NOT_EQUALS", - "value": "GET_COLLECTION" - }, - "initialValue": "" + "placeholderText": "key1/nestedKey2" }, { - "label": "Operator Key", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[4].key", - "controlType": "INPUT_TEXT", - "hidden": true, - "initialValue": "operator" - }, - { - "label": "Where Condition: Operator", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[4].value", + "label": "Operator", + "key": "operator", "controlType": "DROP_DOWN", - "hidden": { - "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "comparison": "NOT_EQUALS", - "value": "GET_COLLECTION" - }, "initialValue": "EQ", "options": [ { @@ -241,22 +230,10 @@ ] }, { - "label": "Value Key", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[5].key", - "controlType": "INPUT_TEXT", - "hidden": true, - "initialValue": "fieldValue" - }, - { - "label": "Where Condition: Value", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[5].value", + "label": "Value", + "key": "value", "controlType": "QUERY_DYNAMIC_INPUT_TEXT", - "hidden": { - "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "comparison": "NOT_EQUALS", - "value": "GET_COLLECTION" - }, - "initialValue": "" + "placeholderText": "value" } ] }, diff --git a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java index d0325aa8eb..2a0eba77fa 100644 --- a/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java +++ b/app/server/appsmith-plugins/firestorePlugin/src/test/java/com/external/plugins/FirestorePluginTest.java @@ -77,7 +77,8 @@ public class FirestorePluginTest { .build() .getService(); - firestoreConnection.document("initial/one").set(Map.of("value", 1, "name", "one", "isPlural", false)).get(); + firestoreConnection.document("initial/one").set(Map.of("value", 1, "name", "one", "isPlural", false, + "category", "test")).get(); final Map twoData = new HashMap<>(Map.of( "value", 2, "name", "two", @@ -85,7 +86,8 @@ public class FirestorePluginTest { "geo", new GeoPoint(-90, 90), "dt", FieldValue.serverTimestamp(), "ref", firestoreConnection.document("initial/one"), - "bytes", Blob.fromBytes("abc def".getBytes(StandardCharsets.UTF_8)) + "bytes", Blob.fromBytes("abc def".getBytes(StandardCharsets.UTF_8)), + "category", "test" )); twoData.put("null-ref", null); firestoreConnection.document("initial/two").set(twoData).get(); @@ -146,6 +148,7 @@ public class FirestorePluginTest { assertFalse((Boolean) first.remove("isPlural")); assertEquals(1L, first.remove("value")); assertEquals(Map.of("id", "one", "path", "initial/one"), first.remove("_ref")); + assertEquals("test", first.remove("category")); assertEquals(Collections.emptyMap(), first); /* @@ -184,6 +187,7 @@ public class FirestorePluginTest { assertEquals("abc def", ((Blob) doc.remove("bytes")).toByteString().toStringUtf8()); assertNull(doc.remove("null-ref")); assertEquals(Map.of("id", "two", "path", "initial/two"), doc.remove("_ref")); + assertEquals("test", doc.remove("category")); assertEquals(Collections.emptyMap(), doc); }) .verifyComplete(); @@ -240,6 +244,7 @@ public class FirestorePluginTest { assertFalse((Boolean) first.remove("isPlural")); assertEquals(1L, first.remove("value")); assertEquals(Map.of("id", "one", "path", "initial/one"), first.remove("_ref")); + assertEquals("test", first.remove("category")); assertEquals(Collections.emptyMap(), first); final Map second = results.stream().filter(d -> "two".equals(d.get("name"))).findFirst().orElse(null); @@ -253,6 +258,7 @@ public class FirestorePluginTest { assertEquals("abc def", ((Blob) second.remove("bytes")).toByteString().toStringUtf8()); assertNull(second.remove("null-ref")); assertEquals(Map.of("id", "two", "path", "initial/two"), second.remove("_ref")); + assertEquals("test", second.remove("category")); assertEquals(Collections.emptyMap(), second); final Map third = results.stream().filter(d -> "third".equals(d.get("name"))).findFirst().orElse(null); @@ -637,12 +643,6 @@ public class FirestorePluginTest { null, null)); // End before expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(2), "15", null, null, null)); // Limit - expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(3), "", null, - null, null)); // Field Path - expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(4), "", null, - null, null)); // Operator - expectedRequestParams.add(new RequestParamDTO(getActionConfigurationPropertyPath(5), "", null, - null, null)); // Value assertEquals(result.getRequest().getRequestParams().toString(), expectedRequestParams.toString()); }) @@ -696,6 +696,65 @@ public class FirestorePluginTest { } @Test + public void testWhereConditional() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setPath("initial"); + List pluginSpecifiedTemplates = new ArrayList<>(); + pluginSpecifiedTemplates.add(new Property("method", "GET_COLLECTION")); + pluginSpecifiedTemplates.add(new Property("order", null)); + pluginSpecifiedTemplates.add(new Property("limit", null)); + Property whereProperty = new Property("where", null); + whereProperty.setValue(new ArrayList<>()); + /* + * - get all documents where category == test. + * - this returns 2 documents. + */ + ((List)whereProperty.getValue()).add(new HashMap() {{ + put("path", "category"); + put("operator", "EQ"); + put("value", "test"); + }}); + + /* + * - get all documents where name == two. + * - Of the two documents returned by above condition, this will narrow it down to one. + */ + ((List)whereProperty.getValue()).add(new HashMap() {{ + put("path", "name"); + put("operator", "EQ"); + put("value", "two"); + }}); + + pluginSpecifiedTemplates.add(whereProperty); + actionConfiguration.setPluginSpecifiedTemplates(pluginSpecifiedTemplates); + + Mono resultMono = pluginExecutor + .executeParameterized(firestoreConnection, null, dsConfig, actionConfiguration); + + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + + List> results = (List) result.getBody(); + assertEquals(1, results.size()); + + final Map second = results.stream().findFirst().orElse(null); + assertNotNull(second); + assertEquals("two", second.remove("name")); + assertTrue((Boolean) second.remove("isPlural")); + assertEquals(2L, second.remove("value")); + assertEquals(Map.of("path", "initial/one", "id", "one"), second.remove("ref")); + assertEquals(new GeoPoint(-90, 90), second.remove("geo")); + assertNotNull(second.remove("dt")); + assertEquals("abc def", ((Blob) second.remove("bytes")).toByteString().toStringUtf8()); + assertNull(second.remove("null-ref")); + assertEquals(Map.of("id", "two", "path", "initial/two"), second.remove("_ref")); + assertEquals("test", second.remove("category")); + assertEquals(Collections.emptyMap(), second); + }) + .verifyComplete(); + } + public void testUpdateDocumentWithFieldValueTimestamp() { List properties = new ArrayList<>(); properties.add(new Property("method", "UPDATE_DOCUMENT")); // index 0 diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java index 1d2053317d..ae884e8c3e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/DatabaseChangelog.java @@ -2291,4 +2291,99 @@ public class DatabaseChangelog { mongockTemplate.save(action); } } + + /** + * - Older firestore action form had support for only on where condition, which mapped path, operator and value to + * three different indexes on the pluginSpecifiedTemplates list. + * - In the newer form, the three properties are treated as a tuple, and a list of tuples is mapped to only one + * index in pluginSpecifiedTemplates list. + * - [... path, operator, value, ...] --> [... [ {"path":path, "operator":operator, "value":value} ] ...] + */ + @ChangeSet(order = "070", id = "update-firestore-where-conditions-data", author = "") + public void updateFirestoreWhereConditionsData(MongoTemplate mongoTemplate) { + Plugin firestorePlugin = mongoTemplate + .findOne(query(where("packageName").is("firestore-plugin")), Plugin.class); + + Query query = query(new Criteria().andOperator( + where("pluginId").is(firestorePlugin.getId()), + new Criteria().orOperator( + where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.3").exists(true), + where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.4").exists(true), + where("unpublishedAction.actionConfiguration.pluginSpecifiedTemplates.5").exists(true), + where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.3").exists(true), + where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.4").exists(true), + where("publishedAction.actionConfiguration.pluginSpecifiedTemplates.5").exists(true) + ))); + + List firestoreActionQueries = mongoTemplate.find(query, NewAction.class); + + firestoreActionQueries.stream() + .forEach(action -> { + // For unpublished action + if (action.getUnpublishedAction() != null + && action.getUnpublishedAction().getActionConfiguration() != null + && action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates() != null + && action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates().size() > 3) { + + String path = null; + String op = null; + String value = null; + List properties = action.getUnpublishedAction().getActionConfiguration().getPluginSpecifiedTemplates(); + if (properties.size() > 3 && properties.get(3) != null) { + path = (String) properties.get(3).getValue(); + } + if (properties.size() > 4 && properties.get(4) != null) { + op = (String) properties.get(4).getValue(); + properties.set(4, null); // Index 4 does not map to any value in the new query format + } + if (properties.size() > 5 && properties.get(5) != null) { + value = (String) properties.get(5).getValue(); + properties.set(5, null); // Index 5 does not map to any value in the new query format + } + + Map newFormat = new HashMap(); + newFormat.put("path", path); + newFormat.put("operator", op); + newFormat.put("value", value); + properties.set(3, new Property("whereConditionTuples", List.of(newFormat))); + } + + // For published action + if (action.getPublishedAction() != null + && action.getPublishedAction().getActionConfiguration() != null + && action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates() != null + && action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates().size() > 3) { + + String path = null; + String op = null; + String value = null; + List properties = action.getPublishedAction().getActionConfiguration().getPluginSpecifiedTemplates(); + if (properties.size() > 3 && properties.get(3) != null) { + path = (String) properties.get(3).getValue(); + } + if (properties.size() > 4 && properties.get(4) != null) { + op = (String) properties.get(4).getValue(); + properties.set(4, null); // Index 4 does not map to any value in the new query format + } + if (properties.size() > 5 && properties.get(5) != null) { + value = (String) properties.get(5).getValue(); + properties.set(5, null); // Index 5 does not map to any value in the new query format + } + + HashMap newFormat = new HashMap(); + newFormat.put("path", path); + newFormat.put("operator", op); + newFormat.put("value", value); + properties.set(3, new Property("whereConditionTuples", List.of(newFormat))); + } + }); + + /** + * - Save changes only after all the processing is done so that in case any data manipulation fails, no data + * write occurs. + * - Write data back to db only if all data manipulations done above have succeeded. + */ + firestoreActionQueries.stream() + .forEach(action -> mongoTemplate.save(action)); + } } From 55e6fcfc2f6194423b32542a50dabcf72f7de21d Mon Sep 17 00:00:00 2001 From: Pranav Kanade Date: Mon, 7 Jun 2021 11:30:10 +0530 Subject: [PATCH 17/20] App viewer will not be able to export app (#4938) --- app/client/src/pages/Applications/ApplicationCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/pages/Applications/ApplicationCard.tsx b/app/client/src/pages/Applications/ApplicationCard.tsx index e65dc5f501..e7ed55a90e 100644 --- a/app/client/src/pages/Applications/ApplicationCard.tsx +++ b/app/client/src/pages/Applications/ApplicationCard.tsx @@ -300,7 +300,7 @@ export function ApplicationCard(props: ApplicationCardProps) { cypressSelector: "t--fork-app", }); } - if (!!props.enableImportExport) { + if (!!props.enableImportExport && hasEditPermission) { moreActionItems.push({ onSelect: exportApplicationAsJSONFile, text: "Export", From 2e6b262f29d2f391f66a1d0fad19480856397936 Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Mon, 7 Jun 2021 16:23:18 +0530 Subject: [PATCH 18/20] Revert specific handling at setup for macs with apple chips (#4591) --- app/client/package.json | 1 - app/client/start-https.sh | 8 -------- contributions/ClientSetup.md | 1 - 3 files changed, 10 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 430c1e6f98..099390880b 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -152,7 +152,6 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start", - "start-m1": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=0.0.0.0 craco start", "build": "./build.sh", "build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js", "build-staging": "REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js", diff --git a/app/client/start-https.sh b/app/client/start-https.sh index fdf492df30..259c14286b 100755 --- a/app/client/start-https.sh +++ b/app/client/start-https.sh @@ -87,14 +87,6 @@ case "${uname_out}" in " ;; Darwin*) machine=Mac - # workaround for apple silicon until host.docker.interal works as expected - if [[ "$(uname -m)" = "arm64" ]]; then - # if no server was passed - if [[ -z $1 ]]; then - server_proxy_pass="http://"$(ipconfig getifaddr en0)":8080" - fi - client_proxy_pass="http://"$(ipconfig getifaddr en0)":3000" - fi echo " Starting nginx for MacOS... " diff --git a/contributions/ClientSetup.md b/contributions/ClientSetup.md index 13da4c58cf..cfcefa1104 100644 --- a/contributions/ClientSetup.md +++ b/contributions/ClientSetup.md @@ -71,7 +71,6 @@ On your development machine, please ensure that: - By default your client app points to the local api server - `http://host.docker.internal:8080` for MacOS or `http://localhost:8080` for Linux. Your page will load with errors if you don't have the api server running on your local system. To setup the api server on your local system please follow the instructions [here](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md) - In case you are unable to setup the api server on your local system, you can also [use Appsmith's staging API server](#if-you-would-like-to-hit-a-different-appsmith-server). -- In case you are using a M1 chip Macbook please run the client with `yarn start-m1`. #### If yarn start throws mismatch node version error From 8e7b95fd628408879039e8a3cf9c497b58dc0306 Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:44:35 +0530 Subject: [PATCH 19/20] Omnibar UI/UX improvements (#4677) - Open omnibar on cmd + p as well - Add analytics event for opening discord link - Update api icon to keep same width - Click anywhere on search result row will now open the link instead of just clicking on the icon - Change text from "Recents" to "Recent Entities" - Show a footer with a few instructions. --- .../editorComponents/GlobalSearch/Footer.tsx | 49 +++++++++++++++++++ .../GlobalSearch/ResultsNotFound.tsx | 2 + .../GlobalSearch/SearchResults.tsx | 16 +++--- .../editorComponents/GlobalSearch/index.tsx | 4 +- app/client/src/pages/Editor/GlobalHotKeys.tsx | 20 +++++--- app/client/src/utils/AnalyticsUtil.tsx | 3 +- 6 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 app/client/src/components/editorComponents/GlobalSearch/Footer.tsx diff --git a/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx new file mode 100644 index 0000000000..ad5dfb33b4 --- /dev/null +++ b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import styled from "styled-components"; + +const Wrapper = styled.div` + span { + color: white; + font-variant: all-small-caps; + font-size: ${(props) => props.theme.fontSizes[2]}px; + margin-right: ${(props) => props.theme.spaces[1]}px; + } + div { + margin-right: ${(props) => props.theme.spaces[7]}px; + font-size: ${(props) => props.theme.fontSizes[2]}px; + text-align: center; + } + color: ${(props) => props.theme.colors.globalSearch.searchItemText}; + padding: ${(props) => props.theme.spaces[1]}px + ${(props) => props.theme.spaces[6]}px; + display: flex; + flex-direction: row; +`; + +const FOOTER_INFO = [ + { + action: "\u2191\u2193", + description: "Select", + }, + { + action: "ENTER", + description: "Open", + }, +]; + +function Footer() { + return ( + + {FOOTER_INFO.map((info) => { + return ( +
+ {info.action} + {info.description} +
+ ); + })} +
+ ); +} + +export default Footer; diff --git a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx index f1af2aa4fc..84766c4720 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx @@ -4,6 +4,7 @@ import NoSearchDataImage from "assets/images/no_search_data.png"; import { NO_SEARCH_DATA_TEXT } from "constants/messages"; import { getTypographyByKey } from "constants/DefaultTheme"; import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg"; +import AnalyticsUtil from "utils/AnalyticsUtil"; const Container = styled.div` display: flex; @@ -49,6 +50,7 @@ function ResultsNotFound() { className="discord-link" onClick={() => { window.open("https://discord.gg/rBTTVJp", "_blank"); + AnalyticsUtil.logEvent("DISCORD_LINK_CLICK"); }} > diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx index 5da83a7c9e..42a7cb03e3 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx @@ -25,6 +25,7 @@ import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers"; import { AppState } from "reducers"; import { keyBy, noop } from "lodash"; import { getPageList } from "selectors/editorSelectors"; +import { PluginType } from "entities/Action"; const DocumentIcon = HelpIcons.DOCUMENT; @@ -158,10 +159,13 @@ function ActionItem(props: { return state.entities.plugins.list; }); const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]); - const icon = getActionConfig(pluginType)?.getIcon( - item.config, - pluginGroups[item.config.datasource.pluginId], - ); + const icon = + pluginType === PluginType.API + ? getActionConfig(pluginType)?.icon + : getActionConfig(pluginType)?.getIcon( + item.config, + pluginGroups[item.config.datasource.pluginId], + ); let title = getItemTitle(item); const pageName = usePageName(config.pageId); @@ -293,9 +297,7 @@ function SearchItemComponent(props: ItemProps) { itemType !== SEARCH_ITEM_TYPES.placeholder ) { setActiveItemIndex(index); - if (itemType !== SEARCH_ITEM_TYPES.document) { - searchContext?.handleItemLinkClick(item, "SEARCH_ITEM"); - } + searchContext?.handleItemLinkClick(item, "SEARCH_ITEM"); } }} ref={itemRef} diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx index 113936eb1b..07577ddb08 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx @@ -46,6 +46,7 @@ import { keyBy, noop } from "lodash"; import EntitiesIcon from "assets/icons/ads/entities.svg"; import DocsIcon from "assets/icons/ads/docs.svg"; import RecentIcon from "assets/icons/ads/recent.svg"; +import Footer from "./Footer"; const StyledContainer = styled.div` width: 750px; @@ -204,7 +205,7 @@ function GlobalSearch() { ); }, [pages, query]); - const recentsSectionTitle = getSectionTitle("Recents", RecentIcon); + const recentsSectionTitle = getSectionTitle("Recent Entities", RecentIcon); const docsSectionTitle = getSectionTitle("Documentation Links", DocsIcon); const entitiesSectionTitle = getSectionTitle("Entities", EntitiesIcon); @@ -397,6 +398,7 @@ function GlobalSearch() { )} + {!query &&