From b447aa83453bec71883b4cf06be222e5da70fa7f Mon Sep 17 00:00:00 2001 From: Aswath K Date: Tue, 30 Mar 2021 22:10:23 +0530 Subject: [PATCH 01/52] Allow removing filter on Dropdown Widget --- .../designSystems/blueprint/DropdownComponent.tsx | 3 ++- app/client/src/widgets/DropdownWidget.tsx | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx index 5c1b1de9b09..cd4613ab79e 100644 --- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx @@ -259,7 +259,7 @@ class DropDownComponent extends React.Component { { isBindProperty: true, isTriggerProperty: false, }, + { + propertyName: "isFilterable", + label: "Filterable", + helpText: "Makes the dropdown list filterable", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, { propertyName: "isRequired", label: "Required", @@ -111,6 +120,7 @@ class DropdownWidget extends BaseWidget { options: VALIDATION_TYPES.OPTIONS_DATA, selectionType: VALIDATION_TYPES.TEXT, isRequired: VALIDATION_TYPES.BOOLEAN, + isFilterable: VALIDATION_TYPES.BOOLEAN, // onOptionChange: VALIDATION_TYPES.ACTION_SELECTOR, selectedOptionValues: VALIDATION_TYPES.ARRAY, selectedOptionLabels: VALIDATION_TYPES.ARRAY, @@ -180,6 +190,7 @@ class DropdownWidget extends BaseWidget { selectedIndexArr={computedSelectedIndexArr} label={`${this.props.label}`} isLoading={this.props.isLoading} + isFilterable={this.props.isFilterable} disabled={this.props.isDisabled} /> ); @@ -277,6 +288,7 @@ export interface DropdownWidgetProps extends WidgetProps, WithMeta { onOptionChange?: string; defaultOptionValue?: string | string[]; isRequired: boolean; + isFilterable: boolean; selectedOptionValue: string; selectedOptionValueArr: string[]; selectedOptionLabels: string[]; From a6afa8ce4dc728dbf7cd01270b42834a109954a4 Mon Sep 17 00:00:00 2001 From: Aswath K Date: Wed, 31 Mar 2021 08:42:27 +0530 Subject: [PATCH 02/52] Makes isFilterable true by default for DropdownWidget --- app/client/src/mockResponses/WidgetConfigResponse.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index c2fb30230a3..78fa5c42abf 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -173,6 +173,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { ], widgetName: "Dropdown", defaultOptionValue: "VEG", + isFilterable: true, version: 1, }, CHECKBOX_WIDGET: { From e0130336eb156eaab513d3a7bf0bffbc067ec074 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Mon, 26 Apr 2021 12:40:24 +0530 Subject: [PATCH 03/52] added lib for excel export --- app/client/package.json | 3 +- app/client/yarn.lock | 82 +++-------------------------------------- 2 files changed, 8 insertions(+), 77 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 79bd92c5e48..02fb149675d 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -56,6 +56,7 @@ "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "eslint": "^7.11.0", + "export-from-json": "^1.3.5", "fast-deep-equal": "^3.1.1", "fast-xml-parser": "^3.17.5", "flow-bin": "^0.91.0", @@ -73,6 +74,7 @@ "lint-staged": "^9.2.5", "localforage": "^1.7.3", "lodash": "^4.17.19", + "lodash-es": "4.17.14", "lodash-move": "^1.1.1", "loglevel": "^1.6.7", "lottie-web": "^5.7.4", @@ -118,7 +120,6 @@ "redux-form": "^8.2.6", "redux-saga": "^1.1.3", "reselect": "^4.0.0", - "lodash-es": "4.17.14", "scroll-into-view-if-needed": "^2.2.26", "shallowequal": "^1.1.0", "showdown": "^1.9.1", diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 4a3012d16bb..0eb016e21dd 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -1627,7 +1627,6 @@ "@babel/runtime@^7.12.5": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d" - integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw== dependencies: regenerator-runtime "^0.13.4" @@ -2308,7 +2307,6 @@ "@jest/types@^26.6.2": version "26.6.2" resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -2370,7 +2368,6 @@ "@mswjs/cookies@^0.1.4": version "0.1.4" resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.1.4.tgz#85ef872997eea2acd888f21af0b2067224dac244" - integrity sha512-gdtmSv21D4wHTnqF4rrZVX6ye7mQ4nRCTIHYnHBr4SkgoXaiqe3sMvUzXm43+H4PnL0EAKvUTxRVSSXz2xebeg== dependencies: "@types/set-cookie-parser" "^2.4.0" set-cookie-parser "^2.4.6" @@ -2378,7 +2375,6 @@ "@mswjs/interceptors@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.8.1.tgz#8ef43a8b7b25c7b9a2bac67b3702167e25e5fc07" - integrity sha512-OI9FYmtURESZG3QDNz4Yt3osy3HY4T3FjlRw+AG4QS1UDdTSZ0tuPFAkp23nGR9ojmbSSj4gSMjf5+R8Oi/qtQ== dependencies: "@open-draft/until" "^1.0.3" debug "^4.3.0" @@ -2416,7 +2412,6 @@ "@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" - integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== "@optimizely/js-sdk-datafile-manager@^0.8.0": version "0.8.0" @@ -2558,7 +2553,6 @@ "@sentry/browser@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.2.4.tgz#b74757b1f76e7a525e6eaca39668db36db82cb21" - integrity sha512-OV1CQUxNawncpSEcrA+YccOu72rLC0tyYq/Pc4D/ihpfJmvR0o0L8vZYESay55V5lcqnJPFp8IyCJ2bF8IZTsA== dependencies: "@sentry/core" "6.2.4" "@sentry/types" "6.2.4" @@ -2578,7 +2572,6 @@ "@sentry/core@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.2.4.tgz#613102074208958c580df4e7e06e9aa6b4d5f40c" - integrity sha512-8Z98OTM4wFS2n3T+V8a6cYWHDAk1byWuMb8JquZLdYgR5O1jkSpSFrhksQ+B/wDbVw05VOolSNFJsDTC2D5qXg== dependencies: "@sentry/hub" "6.2.4" "@sentry/minimal" "6.2.4" @@ -2589,7 +2582,6 @@ "@sentry/hub@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.2.4.tgz#0e25b851dc04b806713c8d878b1e11696ccf47ea" - integrity sha512-dY8Vj3c4oIirNNNzWkJvoRMzjlU8Nw3PJ/IwhdWjiQhj5/oqOzJwJQSMeOKdOGIhArAifr0hSXdy1+tHGEOOdQ== dependencies: "@sentry/types" "6.2.4" "@sentry/utils" "6.2.4" @@ -2598,7 +2590,6 @@ "@sentry/minimal@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.2.4.tgz#7d8d490b0942e14cde544c0a77550693e4702d97" - integrity sha512-KN+Abbz5CCAceSMvwymSG8GIVPaz4Y/xuY7R7dA8IlzncHaWRQ/Ss0PXjYUWL4YoTlTK6id1AW0i3JMICHMVgw== dependencies: "@sentry/hub" "6.2.4" "@sentry/types" "6.2.4" @@ -2607,7 +2598,6 @@ "@sentry/react@^6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.2.4.tgz#7d5a67a6e5f01238bf88e91433841da5180916f0" - integrity sha512-0TqM51HwnAUoDSYyK38Bq/m6xLqWHsOL98Uu4HoMMmx6VXW1xf1UDxhjmIQFfjWfYT5tlld0CoDRfTJJlc82Ow== dependencies: "@sentry/browser" "6.2.4" "@sentry/minimal" "6.2.4" @@ -2619,7 +2609,6 @@ "@sentry/tracing@^6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.2.4.tgz#4af650c180a41b72e130c7b92838fa9d1040792e" - integrity sha512-FNPTd22Q487SVyGM4BXlVeeRwPr9CG0OV8bz+GRHQtpVDhL+zdkGlIJYbxZnrOcdyYNVgLCJUPDHqyv55nhU4A== dependencies: "@sentry/hub" "6.2.4" "@sentry/minimal" "6.2.4" @@ -2630,12 +2619,10 @@ "@sentry/types@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.2.4.tgz#5974e64f000e6084d92d752e6ca199dc2ef4438f" - integrity sha512-c+vEExoj8H67NPaskTvxJBSAtDWzfFXOmlkicEZPUWbkL+Yxxlbzp1lI8K6GOks56UYMUBUU/fwQvv/34cO96g== "@sentry/utils@6.2.4": version "6.2.4" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.2.4.tgz#ab6a0bdfa2a32428f6b1ee87082d9bd40a226b11" - integrity sha512-lavbb3yQMUleVffmDkPH7X3dlgbXlyiFNmfER+swJ6WRxa4Yq6I8yea2s6maoqnZMhZe+yztn455DPwXIItfCA== dependencies: "@sentry/types" "6.2.4" tslib "^1.9.3" @@ -3400,7 +3387,6 @@ "@testing-library/dom@^7.28.1": version "7.30.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.30.1.tgz#07b6f3ccd7f1f1e34ab0406932073e2971817f3d" - integrity sha512-RQUvqqq2lxTCOffhSNxpX/9fCoR+nwuQPmG5uhuuEH5KBAzNf2bK3OzBoWjm5zKM78SLjnGRAKt8hRjQA4E46A== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" @@ -3427,7 +3413,6 @@ "@testing-library/react@^11.2.5": version "11.2.5" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9" - integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" @@ -3435,7 +3420,6 @@ "@testing-library/user-event@^13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.1.1.tgz#1e011de944cf4d2a917cef6c3046c26389943e24" - integrity sha512-B4roX+0mpXKGj8ndd38YoIo3IV9pmTTWxr/2cOke5apTtrNabEUE0KMBccpcAcYlfPcr7uMu+dxeeC3HdXd9qQ== dependencies: "@babel/runtime" "^7.12.5" @@ -3504,15 +3488,9 @@ dependencies: "@types/tern" "*" -"@types/component-emitter@^1.2.10": - version "1.2.10" - resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" - integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== - "@types/cookie@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" - integrity sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg== "@types/deep-diff@^1.0.0": version "1.0.0" @@ -3582,7 +3560,6 @@ "@types/inquirer@^7.3.1": version "7.3.1" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" - integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== dependencies: "@types/through" "*" rxjs "^6.4.0" @@ -3634,7 +3611,6 @@ "@types/js-levenshtein@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.0.tgz#9541eec4ad6e3ec5633270a3a2b55d981edc44a9" - integrity sha512-14t0v1ICYRtRVcHASzes0v/O+TIeASb8aD55cWF1PidtInhFWSXcmhzhHqGjUWf9SUq1w70cvd1cWKUULubAfQ== "@types/json-schema@*", "@types/json-schema@^7.0.3", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.6": version "7.0.6" @@ -3651,7 +3627,6 @@ "@types/marked@^1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-1.2.2.tgz#1f858a0e690247ecf3b2eef576f98f86e8d960d4" - integrity sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw== "@types/mdast@^3.0.0": version "3.0.3" @@ -3832,7 +3807,6 @@ "@types/react-window@^1.8.2": version "1.8.2" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe" - integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ== dependencies: "@types/react" "*" @@ -3871,7 +3845,6 @@ "@types/set-cookie-parser@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.0.tgz#10cc0446bad372827671a5195fbd14ebce4a9baf" - integrity sha512-w7BFUq81sy7H/0jN0K5cax8MwRN6NOSURpY4YuO4+mOgoicxCZ33BUYz+gyF/sUf7uDl2We2yGJfppxzEXoAXQ== dependencies: "@types/node" "*" @@ -3939,7 +3912,6 @@ "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" - integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== dependencies: "@types/node" "*" @@ -5414,7 +5386,6 @@ bluebird@^3.3.5, bluebird@^3.5.5, bluebird@^3.7.2: bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== bn.js@^5.1.1: version "5.1.3" @@ -5947,7 +5918,6 @@ chokidar@^3.4.1: chokidar@^3.4.2: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" - integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== dependencies: anymatch "~3.1.1" braces "~3.0.2" @@ -6100,7 +6070,6 @@ cliui@^6.0.0: cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" @@ -6151,7 +6120,6 @@ code-point-at@^1.0.0: codemirror@^5.59.2: version "5.59.2" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.2.tgz#ee674d3a4a8d241af38d52afc482625ba7393922" - integrity sha512-/D5PcsKyzthtSy2NNKCyJi3b+htRkoKv3idswR/tR6UAvMNKA7SrmyZy6fOONJxSRs1JlUWEDAbxqfdArbK8iA== collapse-white-space@^1.0.2: version "1.0.6" @@ -6275,7 +6243,6 @@ compression@^1.7.4: compute-scroll-into-view@^1.0.16: version "1.0.16" resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.16.tgz#5b7bf4f7127ea2c19b750353d7ce6776a90ee088" - integrity sha512-a85LHKY81oQnikatZYA90pufpZ6sQx++BoCxOEMsjpZx+ZnaKGQnCyCehTRr/1p9GBIAHTjcU9k71kSYWloLiQ== concat-map@0.0.1: version "0.0.1" @@ -6358,7 +6325,6 @@ cookie@0.4.0: cookie@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== copy-concurrently@^1.0.0: version "1.0.5" @@ -6407,7 +6373,6 @@ core-js@^3.0.1, core-js@^3.0.4, core-js@^3.6.5: core-js@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae" - integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -6696,7 +6661,6 @@ cssesc@^3.0.0: cssfontparser@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/cssfontparser/-/cssfontparser-1.2.1.tgz#f4022fc8f9700c68029d542084afbaf425a3f3e3" - integrity sha1-9AIvyPlwDGgCnVQghK+69CWj8+M= cssnano-preset-default@^4.0.7: version "4.0.7" @@ -6920,10 +6884,9 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@^4.3.0, debug@~4.3.1: +debug@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" @@ -7186,7 +7149,6 @@ doctypes@^1.1.0: dom-accessibility-api@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" - integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== dom-converter@^0.2: version "0.2.0" @@ -7356,7 +7318,6 @@ element-resize-detector@^1.2.1: elliptic@^6.5.3: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== dependencies: bn.js "^4.11.9" brorand "^1.1.0" @@ -7880,7 +7841,6 @@ events@^3.0.0: events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== eventsource@^1.0.7: version "1.0.7" @@ -7994,6 +7954,10 @@ expect@^26.6.0, expect@^26.6.1: jest-message-util "^26.6.1" jest-regex-util "^26.0.0" +export-from-json@^1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/export-from-json/-/export-from-json-1.3.5.tgz#52e8dab6aacf2827c84fcc304940a1ea5347124b" + express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -8536,7 +8500,6 @@ fsevents@^2.1.2, fsevents@^2.1.3, fsevents@~2.1.1, fsevents@~2.1.2: fsevents@~2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== fstream@^1.0.0, fstream@^1.0.12: version "1.0.12" @@ -8846,7 +8809,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6 graphql@^15.4.0: version "15.5.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" - integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== growl@1.10.5: version "1.10.5" @@ -9032,7 +8994,6 @@ he@1.2.0, he@^1.2.0: headers-utils@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/headers-utils/-/headers-utils-3.0.2.tgz#dfc65feae4b0e34357308aefbcafa99c895e59ef" - integrity sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ== hex-color-regex@^1.1.0: version "1.1.0" @@ -10042,7 +10003,6 @@ jake@^10.6.1: jest-canvas-mock@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/jest-canvas-mock/-/jest-canvas-mock-2.3.1.tgz#9535d14bc18ccf1493be36ac37dd349928387826" - integrity sha512-5FnSZPrX3Q2ZfsbYNE3wqKR3+XorN8qFzDzB5o0golWgt6EOX1+emBnpOc9IAQ+NXFj8Nzm3h7ZdE/9H0ylBcg== dependencies: cssfontparser "^1.2.1" moo-color "^1.0.2" @@ -10471,7 +10431,6 @@ jest-util@^24.9.0: jest-util@^26.1.0: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: "@jest/types" "^26.6.2" "@types/node" "*" @@ -10563,7 +10522,6 @@ js-base64@^2.1.8: js-levenshtein@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" - integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g== js-sha256@^0.9.0: version "0.9.0" @@ -11024,12 +10982,10 @@ locate-path@^5.0.0: lodash-es@4.17.14: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.14.tgz#12a95a963cc5955683cee3b74e85458954f37ecc" - integrity sha512-7zchRrGa8UZXjD/4ivUWP1867jDkhzTG2c/uj739utSd7O/pFFdxspCemIFKEEjErbcqRzn8nKnGsi7mvTgRPA== lodash-move@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/lodash-move/-/lodash-move-1.1.1.tgz#59f76e0f1ac57e6d8683f531bec07c5b6ea4e348" - integrity sha1-WfduDxrFfm2Gg/UxvsB8W26k40g= dependencies: lodash "^4.6.1" @@ -11106,20 +11062,14 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@4.x: +lodash@4.x, lodash@^4.6.1: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== "lodash@>=3.5 <5", lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.16.2, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5, lodash@~4.17.10: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" -lodash@^4.6.1: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - log-symbols@3.0.0, log-symbols@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" @@ -11285,7 +11235,6 @@ markdown-to-jsx@^6.10.3, markdown-to-jsx@^6.11.4: marked@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.0.tgz#9662bbcb77ebbded0662a7be66ff929a8611cee5" - integrity sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q== marker-clusterer-plus@^2.1.4: version "2.1.4" @@ -11738,7 +11687,6 @@ moment-timezone@*, moment-timezone@^0.5.27, moment-timezone@^0.5.31: moo-color@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/moo-color/-/moo-color-1.0.2.tgz#837c40758d2d58763825d1359a84e330531eca64" - integrity sha512-5iXz5n9LWQzx/C2WesGFfpE6RLamzdHwsn3KpfzShwbfIqs7stnoEpaNErf/7+3mbxwZ4s8Foq7I0tPxw7BWHg== dependencies: color-name "^1.1.4" @@ -11768,7 +11716,6 @@ ms@2.1.2, ms@^2.1.1: msw@^0.28.0: version "0.28.0" resolved "https://registry.yarnpkg.com/msw/-/msw-0.28.0.tgz#abed17416f59241a2100fe6c8740cc1c9a32339b" - integrity sha512-Hh+dPp613tethIFwNg90lvAzrW9T0U39D6AYzV8qIOAWskP49CErrqVWZnmPDQC87o69GzZ9Hl3RGz/65mms3A== dependencies: "@mswjs/cookies" "^0.1.4" "@mswjs/interceptors" "^0.8.0" @@ -11965,7 +11912,6 @@ node-libs-browser@^2.2.1: node-match-path@^0.6.1: version "0.6.2" resolved "https://registry.yarnpkg.com/node-match-path/-/node-match-path-0.6.2.tgz#29a05ed7eda4d325f29d7abb088c12bbf1578e87" - integrity sha512-2VYsUKiovaCZDq1t/3kEqh09743H91WE6B3RzSdjsKh+S/a5z+LQoujMI1JI/RYXqNKFvoqMfye1H0g3Dg9u+g== node-modules-regexp@^1.0.0: version "1.0.0" @@ -12567,7 +12513,6 @@ path-to-regexp@^1.7.0: path-to-regexp@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38" - integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg== path-type@^1.0.0: version "1.1.0" @@ -13405,7 +13350,6 @@ pretty-format@^26.6.0, pretty-format@^26.6.1: pretty-format@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== dependencies: "@jest/types" "^26.6.2" ansi-regex "^5.0.0" @@ -13419,7 +13363,6 @@ pretty-hrtime@^1.0.3: prismjs@^1.23.0, prismjs@^1.8.4: version "1.23.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" - integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== optionalDependencies: clipboard "^2.0.0" @@ -14390,7 +14333,6 @@ react-window@^1.8.2: react-window@^1.8.6: version "1.8.6" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" - integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== dependencies: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" @@ -15179,7 +15121,6 @@ scriptjs@^2.5.8: scroll-into-view-if-needed@^2.2.26: version "2.2.26" resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.26.tgz#e4917da0c820135ff65ad6f7e4b7d7af568c4f13" - integrity sha512-SQ6AOKfABaSchokAmmaxVnL9IArxEnLEX9j4wAZw+x4iUTb40q7irtHG3z4GtAWz5veVZcCnubXDBRyLVQaohw== dependencies: compute-scroll-into-view "^1.0.16" @@ -15223,7 +15164,6 @@ semver@7.3.2: semver@7.x: version "7.3.5" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== dependencies: lru-cache "^6.0.0" @@ -15309,7 +15249,6 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: set-cookie-parser@^2.4.6: version "2.4.8" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" - integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" @@ -15720,7 +15659,6 @@ static-extend@^0.1.1: statuses@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== stdout-stream@^1.4.0: version "1.4.1" @@ -15773,12 +15711,10 @@ stream-shift@^1.0.0: strict-event-emitter@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.1.0.tgz#fd742c1fb7e3852f0b964ecdae2d7666a6fb7ef8" - integrity sha512-8hSYfU+WKLdNcHVXJ0VxRXiPESalzRe7w1l8dg9+/22Ry+iZQUoQuoJ27R30GMD1TiyYINWsIEGY05WrskhSKw== strict-event-emitter@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.0.tgz#78e2f75dc6ea502e5d8a877661065a1e2deedecd" - integrity sha512-zv7K2egoKwkQkZGEaH8m+i2D0XiKzx5jNsiSul6ja2IYFvil10A59Z9Y7PPAAe5OW53dQUf9CfsHKzjZzKkm1w== dependencies: events "^3.3.0" @@ -16456,7 +16392,6 @@ ts-dedent@^1.1.0: ts-jest@^26.5.4: version "26.5.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.4.tgz#207f4c114812a9c6d5746dd4d1cdf899eafc9686" - integrity sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -17476,7 +17411,6 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" string-width "^4.1.0" @@ -17546,12 +17480,10 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: y18n@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" - integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== y18n@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" - integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== yallist@^2.1.2: version "2.1.2" @@ -17579,7 +17511,6 @@ yargs-parser@13.1.2, yargs-parser@^13.1.2: yargs-parser@20.x, yargs-parser@^20.2.2: version "20.2.7" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== yargs-parser@^15.0.1: version "15.0.1" @@ -17653,7 +17584,6 @@ yargs@^15.4.1: yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: cliui "^7.0.2" escalade "^3.1.1" From 398fe551cc6fd03fde341bb5bd84d5c88cd8650b Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Wed, 28 Apr 2021 18:37:35 +0530 Subject: [PATCH 04/52] Handle table data download as excel --- app/client/package.json | 6 +- .../TableComponent/TableDataDownload.tsx | 159 ++++++++++++++++-- app/client/yarn.lock | 94 ++++++++++- 3 files changed, 241 insertions(+), 18 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 02fb149675d..2e3b0c51c69 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -22,6 +22,7 @@ "@sentry/tracing": "^6.2.4", "@sentry/webpack-plugin": "^1.12.1", "@types/chance": "^1.0.7", + "@types/file-saver": "^2.0.2", "@types/lodash": "^4.14.120", "@types/moment-timezone": "^0.5.10", "@types/nanoid": "^2.0.0", @@ -56,9 +57,9 @@ "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "eslint": "^7.11.0", - "export-from-json": "^1.3.5", "fast-deep-equal": "^3.1.1", "fast-xml-parser": "^3.17.5", + "file-saver": "^2.0.5", "flow-bin": "^0.91.0", "fuse.js": "^3.4.5", "fusioncharts": "^3.16.0", @@ -133,7 +134,8 @@ "typescript": "^3.9.2", "unescape-js": "^1.1.4", "url-search-params-polyfill": "^8.0.0", - "worker-loader": "^3.0.2" + "worker-loader": "^3.0.2", + "xlsx": "^0.16.9" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index 57ac1fdcadc..93871361883 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -1,4 +1,10 @@ import React from "react"; +import { + Popover, + Classes, + PopoverInteractionKind, + Position, +} from "@blueprintjs/core"; import { IconWrapper } from "constants/IconConstants"; import { Colors } from "constants/Colors"; import { ReactComponent as DownloadIcon } from "assets/icons/control/download-table.svg"; @@ -6,6 +12,43 @@ import { ReactTableColumnProps } from "components/designSystems/appsmith/TableCo import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers"; import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon"; import { isString } from "lodash"; +import styled from "styled-components"; + +const DropDownWrapper = styled.div` + display: flex; + flex-direction: column; + background: white; + z-index: 1; + border-radius: 4px; + border: 1px solid ${Colors.ATHENS_GRAY}; + padding: 8px; +`; + +const OptionWrapper = styled.div` + display: flex; + width: calc(100% - 20px); + justify-content: space-between; + align-items: center; + height: 32px; + box-sizing: border-box; + padding: 8px; + color: ${Colors.OXFORD_BLUE}; + opacity: 0.7; + min-width: 200px; + cursor: pointer; + margin-bottom: 4px; + background: ${Colors.WHITE}; + border-left: none; + border-radius: 4px; + .option-title { + font-weight: 500; + font-size: 14px; + line-height: 24px; + } + &:hover { + background: ${Colors.POLAR}; + } +`; interface TableDataDownloadProps { data: Array>; @@ -13,10 +56,76 @@ interface TableDataDownloadProps { widgetName: string; } +type FileDownloadType = "CSV" | "EXCEL"; + +interface DownloadOptionProps { + label: string; + value: FileDownloadType; +} + +const dowloadOptions: DownloadOptionProps[] = [ + { + label: "CSV", + value: "CSV", + }, + { + label: "Excel", + value: "EXCEL", + }, +]; + const TableDataDownload = (props: TableDataDownloadProps) => { - const [selected, toggleButtonClick] = React.useState(false); - const downloadTableData = () => { - toggleButtonClick(true); + const [selected, selectMenu] = React.useState(false); + const downloadFile = (type: string) => { + if (type === "CSV") { + downloadTableDataAsCsv(); + } else if (type === "EXCEL") { + downloadTableDataAsExcel(); + } + }; + const downloadTableDataAsExcel = () => { + const tableData: Array<{ [key: string]: any }> = []; + const tableHeaders = props.columns + .map((column: ReactTableColumnProps) => { + if (column.metaProperties && !column.metaProperties.isHidden) { + return column.Header; + } + return null; + }) + .filter((i) => !!i); + tableData.push(tableHeaders); + for (let row = 0; row < props.data.length; row++) { + const data: { [key: string]: any } = props.data[row]; + const tableRow = []; + for (let colIndex = 0; colIndex < props.columns.length; colIndex++) { + const column = props.columns[colIndex]; + if (column.metaProperties && !column.metaProperties.isHidden) { + tableRow.push(data[column.accessor]); + } + } + tableData.push(tableRow); + } + import("xlsx").then((XLSX) => { + const workSheet = XLSX.utils.aoa_to_sheet(tableData); + const workBook = { + Sheets: { data: workSheet, cols: [] }, + SheetNames: ["data"], + }; + const excelBuffer = XLSX.write(workBook, { + bookType: "xlsx", + type: "array", + }); + const fileData = new Blob([excelBuffer], { + type: + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8", + }); + import("file-saver").then((FileSaver) => { + FileSaver.saveAs(fileData, `${props.widgetName}.xlsx`); + }); + }); + }; + const downloadTableDataAsCsv = () => { + selectMenu(true); const csvData = []; csvData.push( props.columns @@ -70,7 +179,7 @@ const TableDataDownload = (props: TableDataDownloadProps) => { anchor.click(); document.body.removeChild(anchor); } - toggleButtonClick(false); + selectMenu(false); }; if (props.columns.length === 0) { @@ -83,16 +192,42 @@ const TableDataDownload = (props: TableDataDownloadProps) => { ); } return ( - { - downloadTableData(); + { + selectMenu(false); }} - className="t--table-download-btn" + isOpen={selected} > - - + { + selectMenu(selected); + }} + className="t--table-download-btn" + > + + + + {dowloadOptions.map((item: DownloadOptionProps, index: number) => { + return ( + { + downloadFile(item.value); + }} + className={`${Classes.POPOVER_DISMISS} t--table-download-data-option`} + > + {item.label} + + ); + })} + + ); }; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 0eb016e21dd..ad399001f26 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -3519,6 +3519,10 @@ version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" +"@types/file-saver@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.2.tgz#bd593ccfaee42ff94a5c1c83bf69ae9be83493b9" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -4476,6 +4480,13 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" +adler-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + agent-base@6: version "6.0.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -5791,6 +5802,14 @@ ccount@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" +cfb@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.0.tgz#6a4d0872b525ed60349e1ef51fb4b0bf73eca9a8" + dependencies: + adler-32 "~1.2.0" + crc-32 "~1.2.0" + printj "~1.1.2" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -6121,6 +6140,13 @@ codemirror@^5.59.2: version "5.59.2" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.2.tgz#ee674d3a4a8d241af38d52afc482625ba7393922" +codepage@~1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" + dependencies: + commander "~2.14.1" + exit-on-epipe "~1.0.1" + collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -6204,6 +6230,14 @@ commander@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" +commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + +commander@~2.17.1: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -6414,6 +6448,13 @@ craco-babel-loader@^0.1.4: dependencies: "@craco/craco" "^5.0.0" +crc-32@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" + dependencies: + exit-on-epipe "~1.0.1" + printj "~1.1.0" + create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -7927,6 +7968,10 @@ exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" +exit-on-epipe@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -7954,10 +7999,6 @@ expect@^26.6.0, expect@^26.6.1: jest-message-util "^26.6.1" jest-regex-util "^26.0.0" -export-from-json@^1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/export-from-json/-/export-from-json-1.3.5.tgz#52e8dab6aacf2827c84fcc304940a1ea5347124b" - express@^4.17.0, express@^4.17.1: version "4.17.1" resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" @@ -8158,6 +8199,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" +fflate@^0.3.8: + version "0.3.11" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d" + figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -8201,6 +8246,10 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.5.0" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + file-system-cache@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f" @@ -8415,6 +8464,10 @@ forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -13360,6 +13413,10 @@ pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" +printj@~1.1.0, printj@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + prismjs@^1.23.0, prismjs@^1.8.4: version "1.23.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" @@ -15590,6 +15647,12 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" +ssf@~0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" + dependencies: + frac "~1.1.2" + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -17218,10 +17281,18 @@ with@^7.0.0: assert-never "^1.2.1" babel-walk "3.0.0-canary-5" +wmf@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" + word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" +word@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" + workbox-background-sync@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12" @@ -17461,6 +17532,21 @@ ws@^7.2.3: version "7.3.1" resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" +xlsx@^0.16.9: + version "0.16.9" + resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.16.9.tgz#dacd5bb46bda6dd3743940c9c3dc1e2171826256" + dependencies: + adler-32 "~1.2.0" + cfb "^1.1.4" + codepage "~1.14.0" + commander "~2.17.1" + crc-32 "~1.2.0" + exit-on-epipe "~1.0.1" + fflate "^0.3.8" + ssf "~0.11.2" + wmf "~1.0.1" + word "~0.3.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" From 92764c8f5c15f139d388ba233887f77b9ca4b718 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Thu, 29 Apr 2021 14:50:43 +0530 Subject: [PATCH 05/52] Used zipcelx instaed of xlsx lib to download table data as excel --- app/client/package.json | 5 +- .../TableComponent/TableDataDownload.tsx | 66 +++++---- app/client/yarn.lock | 140 +++++------------- 3 files changed, 75 insertions(+), 136 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 00fa7086657..8ed6285f099 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -22,7 +22,6 @@ "@sentry/tracing": "^6.2.4", "@sentry/webpack-plugin": "^1.12.1", "@types/chance": "^1.0.7", - "@types/file-saver": "^2.0.2", "@types/lodash": "^4.14.120", "@types/moment-timezone": "^0.5.10", "@types/nanoid": "^2.0.0", @@ -37,6 +36,7 @@ "@types/react-table": "^7.0.13", "@types/styled-components": "^5.1.3", "@types/tinycolor2": "^1.4.2", + "@types/zipcelx": "^1.5.0", "@uppy/core": "^1.16.0", "@uppy/dashboard": "^1.16.0", "@uppy/file-input": "^1.4.22", @@ -59,7 +59,6 @@ "eslint": "^7.11.0", "fast-deep-equal": "^3.1.1", "fast-xml-parser": "^3.17.5", - "file-saver": "^2.0.5", "flow-bin": "^0.91.0", "fuse.js": "^3.4.5", "fusioncharts": "^3.16.0", @@ -137,7 +136,7 @@ "unescape-js": "^1.1.4", "url-search-params-polyfill": "^8.0.0", "worker-loader": "^3.0.2", - "xlsx": "^0.16.9" + "zipcelx": "^1.6.2" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index 3b3be89aa10..fd914ce941e 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -13,6 +13,7 @@ import { TableIconWrapper } from "components/designSystems/appsmith/TableCompone import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon"; import styled from "styled-components"; import { transformTableDataIntoCsv } from "./CommonUtilities"; +import zipcelx from "zipcelx"; const DropDownWrapper = styled.div` display: flex; @@ -57,6 +58,11 @@ interface TableDataDownloadProps { type FileDownloadType = "CSV" | "EXCEL"; +type DataCellProps = { + value: string | number; + type: "string" | "number"; +}; + interface DownloadOptionProps { label: string; value: FileDownloadType; @@ -64,11 +70,11 @@ interface DownloadOptionProps { const dowloadOptions: DownloadOptionProps[] = [ { - label: "CSV", + label: "Download as CSV", value: "CSV", }, { - label: "Excel", + label: "Download as Excel", value: "EXCEL", }, ]; @@ -104,27 +110,6 @@ const downloadDataAsCSV = (props: { } }; -const downloadDataAsExcel = (tableData: string[][], fileName: string) => { - import("xlsx").then((XLSX) => { - const workSheet = XLSX.utils.aoa_to_sheet(tableData); - const workBook = { - Sheets: { data: workSheet, cols: [] }, - SheetNames: ["data"], - }; - const excelBuffer = XLSX.write(workBook, { - bookType: "xlsx", - type: "array", - }); - const fileData = new Blob([excelBuffer], { - type: - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8", - }); - import("file-saver").then((FileSaver) => { - FileSaver.saveAs(fileData, `${fileName}.xlsx`); - }); - }); -}; - function TableDataDownload(props: TableDataDownloadProps) { const [selected, selectMenu] = React.useState(false); const downloadFile = (type: string) => { @@ -135,28 +120,49 @@ function TableDataDownload(props: TableDataDownloadProps) { } }; const downloadTableDataAsExcel = () => { - const tableData: string[][] = []; - const tableHeaders: string[] = props.columns + const tableData: Array> = []; + const tableHeaders: Array<{ + value: string | number; + type: string; + }> = props.columns .map((column: ReactTableColumnProps) => { if (column.metaProperties && !column.metaProperties.isHidden) { - return column.Header; + return { + value: column.Header, + type: + column.columnProperties?.columnType === "number" + ? "number" + : "string", + }; } - return ""; + return null; }) .filter((i) => !!i); tableData.push(tableHeaders); for (let row = 0; row < props.data.length; row++) { const data: { [key: string]: any } = props.data[row]; - const tableRow: string[] = []; + const tableRow: Array = []; for (let colIndex = 0; colIndex < props.columns.length; colIndex++) { const column = props.columns[colIndex]; + const type = + column.columnProperties?.columnType === "number" + ? "number" + : "string"; if (column.metaProperties && !column.metaProperties.isHidden) { - tableRow.push(data[column.accessor]); + tableRow.push({ + value: data[column.accessor], + type: type, + }); } } tableData.push(tableRow); } - downloadDataAsExcel(tableData, props.widgetName); + zipcelx({ + filename: props.widgetName, + sheet: { + data: tableData, + }, + }); }; const downloadTableDataAsCsv = () => { selectMenu(true); diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 30d01ec58c0..e7f1d6339f5 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -3413,7 +3413,6 @@ "@testing-library/react@^11.2.6": version "11.2.6" resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" - integrity sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^7.28.1" @@ -3520,10 +3519,6 @@ version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" -"@types/file-saver@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.2.tgz#bd593ccfaee42ff94a5c1c83bf69ae9be83493b9" - "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -3800,7 +3795,6 @@ "@types/react-test-renderer@^17.0.1": version "17.0.1" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" - integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== dependencies: "@types/react" "*" @@ -3984,10 +3978,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/zipcelx@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@types/zipcelx/-/zipcelx-1.5.0.tgz#e06e9ed51fadbc7fbcf15fbeb2bcbf446750c72e" + "@typescript-eslint/eslint-plugin@^4.15.0": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz#835f64aa0a403e5e9e64c10ceaf8d05c3f015180" - integrity sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw== dependencies: "@typescript-eslint/experimental-utils" "4.15.1" "@typescript-eslint/scope-manager" "4.15.1" @@ -4013,7 +4010,6 @@ "@typescript-eslint/experimental-utils@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz#d744d1ac40570a84b447f7aa1b526368afd17eec" - integrity sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ== dependencies: "@types/json-schema" "^7.0.3" "@typescript-eslint/scope-manager" "4.15.1" @@ -4046,7 +4042,6 @@ "@typescript-eslint/parser@^4.15.0": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.15.1.tgz#4c91a0602733db63507e1dbf13187d6c71a153c4" - integrity sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA== dependencies: "@typescript-eslint/scope-manager" "4.15.1" "@typescript-eslint/types" "4.15.1" @@ -4065,7 +4060,6 @@ "@typescript-eslint/scope-manager@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz#f6511eb38def2a8a6be600c530c243bbb56ac135" - integrity sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA== dependencies: "@typescript-eslint/types" "4.15.1" "@typescript-eslint/visitor-keys" "4.15.1" @@ -4084,7 +4078,6 @@ "@typescript-eslint/types@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.15.1.tgz#da702f544ef1afae4bc98da699eaecd49cf31c8c" - integrity sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw== "@typescript-eslint/types@4.6.0": version "4.6.0" @@ -4106,7 +4099,6 @@ "@typescript-eslint/typescript-estree@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz#fa9a9ff88b4a04d901ddbe5b248bc0a00cd610be" - integrity sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw== dependencies: "@typescript-eslint/types" "4.15.1" "@typescript-eslint/visitor-keys" "4.15.1" @@ -4138,7 +4130,6 @@ "@typescript-eslint/visitor-keys@4.15.1": version "4.15.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz#c76abbf2a3be8a70ed760f0e5756bf62de5865dd" - integrity sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww== dependencies: "@typescript-eslint/types" "4.15.1" eslint-visitor-keys "^2.0.0" @@ -4558,13 +4549,6 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -adler-32@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" - agent-base@6: version "6.0.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -5880,14 +5864,6 @@ ccount@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.5.tgz#ac82a944905a65ce204eb03023157edf29425c17" -cfb@^1.1.4: - version "1.2.0" - resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.0.tgz#6a4d0872b525ed60349e1ef51fb4b0bf73eca9a8" - dependencies: - adler-32 "~1.2.0" - crc-32 "~1.2.0" - printj "~1.1.2" - chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -6218,13 +6194,6 @@ codemirror@^5.59.2: version "5.59.2" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.59.2.tgz#ee674d3a4a8d241af38d52afc482625ba7393922" -codepage@~1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" - dependencies: - commander "~2.14.1" - exit-on-epipe "~1.0.1" - collapse-white-space@^1.0.2: version "1.0.6" resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" @@ -6308,14 +6277,6 @@ commander@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" -commander@~2.14.1: - version "2.14.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" - -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" - common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -6526,13 +6487,6 @@ craco-babel-loader@^0.1.4: dependencies: "@craco/craco" "^5.0.0" -crc-32@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.1.0" - create-ecdh@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" @@ -8046,10 +8000,6 @@ exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -8176,7 +8126,6 @@ extsprintf@^1.2.0: factory.ts@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/factory.ts/-/factory.ts-0.5.1.tgz#4bab72d8457078906aa6ab396c0d341e8a3ab382" - integrity sha512-jwAq8w7MmxUojIFzKezMwTzDc5QoxcqzAA8+n9A0EAWBje2CRHUeBrW9x/ioV2DRjHgkHX7i0G0ipfDhlatIQw== dependencies: clone-deep "^4.0.1" source-map-support "^0.5.9" @@ -8285,14 +8234,9 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fflate@^0.3.8: - version "0.3.11" - resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d" - figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" - integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== figures@^1.7.0: version "1.7.0" @@ -8333,7 +8277,7 @@ file-loader@^4.2.0: loader-utils "^1.2.3" schema-utils "^2.5.0" -file-saver@^2.0.5: +file-saver@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" @@ -8551,10 +8495,6 @@ forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" -frac@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" - fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -10854,6 +10794,15 @@ jstransformer@1.0.0: array-includes "^3.1.1" object.assign "^4.1.1" +jszip@^3.1.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.6.0.tgz#839b72812e3f97819cc13ac4134ffced95dd6af9" + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -10963,6 +10912,12 @@ lie@3.1.1: dependencies: immediate "~3.0.5" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + dependencies: + immediate "~3.0.5" + line-column@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" @@ -11145,6 +11100,10 @@ lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" +lodash.escape@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98" + lodash.flow@^3.3.0: version "3.5.0" resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a" @@ -12496,7 +12455,7 @@ paging-algorithm@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/paging-algorithm/-/paging-algorithm-1.0.1.tgz#18abe482a6a202bfaab4b023a407c8cc2072cb8a" -pako@~1.0.5: +pako@~1.0.2, pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -13500,10 +13459,6 @@ pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" -printj@~1.1.0, printj@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" - prismjs@^1.23.0, prismjs@^1.8.4: version "1.23.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" @@ -13865,7 +13820,6 @@ raw-loader@^4.0.2: rc-pagination@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.3.tgz#afd779839fefab2cb14248d5e7b74027960bb48b" - integrity sha512-Z7CdC4xGkedfAwcUHPtfqNhYwVyDgkmhkvfsmoByCOwAd89p42t5O5T3ORar1wRmVWf3jxk/Bf4k0atenNvlFA== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.1" @@ -15402,6 +15356,10 @@ set-cookie-parser@^2.4.6: version "2.4.8" resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -15742,12 +15700,6 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" -ssf@~0.11.2: - version "0.11.2" - resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c" - dependencies: - frac "~1.1.2" - sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -15765,7 +15717,6 @@ sshpk@^1.7.0: ssri@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" - integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== dependencies: figgy-pudding "^3.5.1" @@ -16601,7 +16552,6 @@ tslib@^2.0.0, tslib@^2.0.1: tslib@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" - integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== tslib@~1.13.0: version "1.13.0" @@ -16707,7 +16657,6 @@ typescript-tuple@^2.2.1: typescript@^4.1.3: version "4.1.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" - integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== ua-parser-js@^0.7.18: version "0.7.22" @@ -17383,18 +17332,10 @@ with@^7.0.0: assert-never "^1.2.1" babel-walk "3.0.0-canary-5" -wmf@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da" - word-wrap@^1.2.3, word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" -word@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961" - workbox-background-sync@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-5.1.4.tgz#5ae0bbd455f4e9c319e8d827c055bb86c894fd12" @@ -17634,21 +17575,6 @@ ws@^7.2.3: version "7.3.1" resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" -xlsx@^0.16.9: - version "0.16.9" - resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.16.9.tgz#dacd5bb46bda6dd3743940c9c3dc1e2171826256" - dependencies: - adler-32 "~1.2.0" - cfb "^1.1.4" - codepage "~1.14.0" - commander "~2.17.1" - crc-32 "~1.2.0" - exit-on-epipe "~1.0.1" - fflate "^0.3.8" - ssf "~0.11.2" - wmf "~1.0.1" - word "~0.3.0" - xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -17788,6 +17714,14 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" +zipcelx@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/zipcelx/-/zipcelx-1.6.2.tgz#ae99aa8c04f440d17c52fcdcbc6abc79d6993b3b" + dependencies: + file-saver "^2.0.0" + jszip "^3.1.3" + lodash.escape "^4.0.1" + zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920" From 9469571a0cc95b6b2160d982ca6136b67afad14d Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Thu, 29 Apr 2021 15:33:40 +0530 Subject: [PATCH 06/52] Fix type issue --- .../TableComponent/TableDataDownload.tsx | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index fd914ce941e..29b63999f27 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -121,23 +121,19 @@ function TableDataDownload(props: TableDataDownloadProps) { }; const downloadTableDataAsExcel = () => { const tableData: Array> = []; - const tableHeaders: Array<{ - value: string | number; - type: string; - }> = props.columns - .map((column: ReactTableColumnProps) => { - if (column.metaProperties && !column.metaProperties.isHidden) { - return { - value: column.Header, - type: - column.columnProperties?.columnType === "number" - ? "number" - : "string", - }; - } - return null; + const tableHeaders: Array = props.columns + .filter((column: ReactTableColumnProps) => { + return column.metaProperties && !column.metaProperties.isHidden; }) - .filter((i) => !!i); + .map((column: ReactTableColumnProps) => { + return { + value: column.Header, + type: + column.columnProperties?.columnType === "number" + ? "number" + : "string", + }; + }); tableData.push(tableHeaders); for (let row = 0; row < props.data.length; row++) { const data: { [key: string]: any } = props.data[row]; From 914eb6f0c2a374c250622466fc17ba16e5f14c1c Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Thu, 29 Apr 2021 16:11:40 +0530 Subject: [PATCH 07/52] Initial version that creates and gets notifications --- .../com/appsmith/server/constants/Url.java | 1 + .../controllers/NotificationController.java | 21 +++++++ .../com/appsmith/server/domains/Comment.java | 6 ++ .../server/domains/CommentNotification.java | 12 ++++ .../appsmith/server/domains/Notification.java | 24 ++++++++ .../appsmith/server/helpers/PolicyUtils.java | 16 +++++ .../CustomNotificationRepository.java | 6 ++ .../CustomNotificationRepositoryImpl.java | 13 ++++ .../repositories/NotificationRepository.java | 12 ++++ .../server/services/CommentServiceImpl.java | 26 +++++++- .../server/services/NotificationService.java | 7 +++ .../services/NotificationServiceImpl.java | 61 +++++++++++++++++++ 12 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java index 7d4d23540da..87d84464847 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/Url.java @@ -29,4 +29,5 @@ public interface Url { String MARKETPLACE_ITEM_URL = BASE_URL + VERSION + "/items"; String ASSET_URL = BASE_URL + VERSION + "/assets"; String COMMENT_URL = BASE_URL + VERSION + "/comments"; + String NOTIFICATION_URL = BASE_URL + VERSION + "/notifications"; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java new file mode 100644 index 00000000000..b7a00dec57e --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/NotificationController.java @@ -0,0 +1,21 @@ +package com.appsmith.server.controllers; + +import com.appsmith.server.constants.Url; +import com.appsmith.server.domains.Notification; +import com.appsmith.server.services.NotificationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping(Url.NOTIFICATION_URL) +public class NotificationController extends BaseController { + + @Autowired + public NotificationController(NotificationService service) { + super(service); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java index dc67d106f4a..541b0264240 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Comment.java @@ -32,6 +32,12 @@ public class Comment extends BaseDomain { Body body; + /** + * Indicates whether this comment is the leading comment in it's thread. Such a comment cannot be deleted. + */ + @JsonIgnore + Boolean leading; + @Data public static class Body { List blocks; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java new file mode 100644 index 00000000000..0f68abbadce --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java @@ -0,0 +1,12 @@ +package com.appsmith.server.domains; + +import lombok.Data; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document +@Data +public class CommentNotification extends Notification { + + Comment comment; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java new file mode 100644 index 00000000000..b972eda2b65 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java @@ -0,0 +1,24 @@ +package com.appsmith.server.domains; + +import com.appsmith.external.models.BaseDomain; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@EqualsAndHashCode(callSuper = true) +@Document +public class Notification extends BaseDomain { + + // TODO: This class extends BaseDomain, so it has policies. Should we use information from policies instead of this field? + String forUsername; + + /** + * Read status for this notification. If it is `true`, then this notification is read. If `false` or `null`, it's unread. + */ + Boolean isRead; + + public void cleanForClient() { + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java index b67a6d967a5..7c88e954328 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/PolicyUtils.java @@ -13,11 +13,13 @@ import com.appsmith.server.repositories.DatasourceRepository; import com.appsmith.server.repositories.NewActionRepository; import com.appsmith.server.repositories.NewPageRepository; +import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -282,4 +284,18 @@ public Boolean isPermissionPresentForUser(Set policies, String permissio return false; } + + public Set findUsernamesWithPermission(Set policies, AclPermission permission) { + if (CollectionUtils.isNotEmpty(policies) && permission != null) { + final String permissionString = permission.getValue(); + for (Policy policy : policies) { + if (permissionString.equals(policy.getPermission())) { + return policy.getUsers(); + } + } + } + + return Collections.emptySet(); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java new file mode 100644 index 00000000000..9f420f5cf7a --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepository.java @@ -0,0 +1,6 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; + +public interface CustomNotificationRepository extends AppsmithRepository { +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java new file mode 100644 index 00000000000..6c4b50c95c4 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/CustomNotificationRepositoryImpl.java @@ -0,0 +1,13 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MongoConverter; + +public class CustomNotificationRepositoryImpl extends BaseAppsmithRepositoryImpl implements CustomNotificationRepository { + + public CustomNotificationRepositoryImpl(ReactiveMongoOperations mongoOperations, MongoConverter mongoConverter) { + super(mongoOperations, mongoConverter); + } + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java new file mode 100644 index 00000000000..eb81b59bb51 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/repositories/NotificationRepository.java @@ -0,0 +1,12 @@ +package com.appsmith.server.repositories; + +import com.appsmith.server.domains.Notification; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; + +@Repository +public interface NotificationRepository extends BaseRepository, CustomNotificationRepository { + + Flux findByForUsername(String userId); + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java index 17f372b4e48..a1dd4fe781a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java @@ -5,10 +5,13 @@ import com.appsmith.server.constants.FieldName; import com.appsmith.server.domains.Application; import com.appsmith.server.domains.Comment; +import com.appsmith.server.domains.CommentNotification; import com.appsmith.server.domains.CommentThread; +import com.appsmith.server.domains.Notification; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.helpers.PolicyUtils; import com.appsmith.server.repositories.CommentRepository; import com.appsmith.server.repositories.CommentThreadRepository; import lombok.extern.slf4j.Slf4j; @@ -41,8 +44,10 @@ public class CommentServiceImpl extends BaseService create(String threadId, Comment comment) { String authorName = user.getName() != null ? user.getName(): user.getUsername(); comment1.setAuthorName(authorName); return repository.save(comment1); + }) + .flatMap(savedComment -> { + final Set usernames = policyUtils.findUsernamesWithPermission( + savedComment.getPolicies(), AclPermission.READ_COMMENT); + + List> monos = new ArrayList<>(); + for (String username : usernames) { + final CommentNotification notification = new CommentNotification(); + notification.setComment(savedComment); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } + + return Flux.concat(monos).then(Mono.just(savedComment)); }); } @@ -137,6 +160,7 @@ public Mono createThread(CommentThread commentThread) { List> commentSaverMonos = new ArrayList<>(); if (!CollectionUtils.isEmpty(thread.getComments())) { + thread.getComments().get(0).setLeading(true); for (final Comment comment : thread.getComments()) { comment.setId(null); commentSaverMonos.add(create(thread.getId(), comment)); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java new file mode 100644 index 00000000000..cce58ac3805 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationService.java @@ -0,0 +1,7 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Notification; + +public interface NotificationService extends CrudService { + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java new file mode 100644 index 00000000000..614e68fddcf --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/NotificationServiceImpl.java @@ -0,0 +1,61 @@ +package com.appsmith.server.services; + +import com.appsmith.server.domains.Notification; +import com.appsmith.server.repositories.NotificationRepository; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +import javax.validation.Validator; + +@Slf4j +@Service +public class NotificationServiceImpl + extends BaseService + implements NotificationService { + + private final SessionUserService sessionUserService; + + public NotificationServiceImpl( + Scheduler scheduler, + Validator validator, + MongoConverter mongoConverter, + ReactiveMongoTemplate reactiveMongoTemplate, + NotificationRepository repository, + AnalyticsService analyticsService, + SessionUserService sessionUserService + ) { + super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService); + this.sessionUserService = sessionUserService; + } + + @Override + public Mono create(Notification notification) { + Mono notificationWithUsernameMono; + if (StringUtils.isEmpty(notification.getForUsername())) { + notificationWithUsernameMono = sessionUserService.getCurrentUser() + .map(user -> { + notification.setForUsername(user.getUsername()); + return notification; + }); + } else { + notificationWithUsernameMono = Mono.just(notification); + } + + return notificationWithUsernameMono + .flatMap(super::create); + } + + @Override + public Flux get(MultiValueMap params) { + return sessionUserService.getCurrentUser() + .flatMapMany(user -> repository.findByForUsername(user.getUsername())); + } + +} From a29b20eca50835b27395d23cdb196ccd9208adc6 Mon Sep 17 00:00:00 2001 From: bhavin Date: Wed, 5 May 2021 11:53:59 +0530 Subject: [PATCH 08/52] fix: set date picker calendar popover position to bottom --- .../components/designSystems/blueprint/DatePickerComponent2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx index 13f2aaf2e5f..3fd16fe5e78 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx @@ -150,6 +150,7 @@ class DatePickerComponent extends React.Component< onChange={this.onDateSelected} parseDate={this.parseDate} placeholder={"Select Date"} + popoverProps={{ position: "bottom" }} showActionsBar timePrecision={TimePrecision.MINUTE} value={value} From e96fa63f6d97910f0b1c349d5049b3ece6c4f3f9 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Wed, 5 May 2021 12:39:15 +0530 Subject: [PATCH 09/52] Disable client side search when server side search is enabled in table widget --- app/client/src/widgets/TableWidget/derived.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/client/src/widgets/TableWidget/derived.js b/app/client/src/widgets/TableWidget/derived.js index 1d5a39e4fe6..77fb93e56d9 100644 --- a/app/client/src/widgets/TableWidget/derived.js +++ b/app/client/src/widgets/TableWidget/derived.js @@ -360,7 +360,10 @@ export default { }, }; - const searchKey = props.searchText ? props.searchText.toLowerCase() : ""; + const searchKey = + props.searchText && !props.onSearchTextChanged + ? props.searchText.toLowerCase() + : ""; const finalTableData = sortedTableData.filter((item) => { const searchFound = searchKey From a5dbb5261b0d98f8dfe4c55c96c659ac7681c0e9 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Thu, 6 May 2021 13:54:18 +0530 Subject: [PATCH 10/52] Added test case to validate onSearchTextChange function if configured in table widget --- app/client/cypress/fixtures/tableWidgetDsl.json | 6 +++++- .../ClientSideTests/Binding/Bind_tableApi_spec.js | 13 +++++++++++++ app/client/cypress/locators/Widgets.json | 2 ++ .../designSystems/appsmith/SearchComponent.tsx | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/client/cypress/fixtures/tableWidgetDsl.json b/app/client/cypress/fixtures/tableWidgetDsl.json index 349f6049037..9929225f5e1 100644 --- a/app/client/cypress/fixtures/tableWidgetDsl.json +++ b/app/client/cypress/fixtures/tableWidgetDsl.json @@ -67,7 +67,11 @@ "bottomRow": 10, "parentId": "tyiwk4xuq0", "widgetId": "5up3r2iuvs", - "dynamicBindingPathList": [] + "dynamicTriggerPathList": [{ + "key": "onSearchTextChanged" + }], + "onSearchTextChanged": "{{Api1.run()}}", + "selectedRow": "{{Table1.tableData[Table1.selectedRowIndex]}}" } ], "widgetId": "tyiwk4xuq0", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js index d65d22e80d1..fb8eeb6b6ef 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_tableApi_spec.js @@ -3,6 +3,7 @@ const dsl = require("../../../../fixtures/tableWidgetDsl.json"); const pages = require("../../../../locators/Pages.json"); const apiPage = require("../../../../locators/ApiEditor.json"); const publishPage = require("../../../../locators/publishWidgetspage.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); describe("Test Create Api and Bind to Table widget", function() { let apiData; @@ -43,6 +44,18 @@ describe("Test Create Api and Bind to Table widget", function() { cy.readTabledataPublish("0", "1").then((tabData) => { expect(apiData).to.eq(`\"${tabData}\"`); }); + cy.get(commonlocators.backToEditor).click(); + }); + + it("Validate onSearchTextChanged function is called when configured for search text", function() { + cy.SearchEntityandOpen("Table1"); + cy.get(".t--widget-tablewidget .t--search-input") + .first() + .type("Currey"); + cy.wait(5000); + cy.readTabledataPublish("0", "1").then((tabData) => { + expect(apiData).to.eq(`\"${tabData}\"`); + }); }); afterEach(() => { diff --git a/app/client/cypress/locators/Widgets.json b/app/client/cypress/locators/Widgets.json index 3e67eecebbf..514ff56cc25 100644 --- a/app/client/cypress/locators/Widgets.json +++ b/app/client/cypress/locators/Widgets.json @@ -38,6 +38,8 @@ "textInputval": ".t--draggable-textwidget span.t--widget-name", "textCenterAlign": ".t--property-control-textalign .t--icon-tab-CENTER", "ColumnAction": ".t--property-control-rowbutton button", + "SearchTextChangeAction": ".t--property-control-onsearchtextchanged button", + "tableSearchTextChangeSelected": ".t--property-control-onsearchtextchanged", "videoWidget": ".t--draggable-videowidget", "autoPlay": ".t--property-control-autoplay > .bp3-control > .bp3-control-indicator", "defaultOption": ".t--property-control-defaultoption .CodeMirror-code", diff --git a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx index 46bfac9a870..df90ccb1a4c 100644 --- a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx @@ -52,6 +52,7 @@ class SearchComponent extends React.Component< render() { return ( Date: Fri, 7 May 2021 15:22:53 +0530 Subject: [PATCH 11/52] - Collapse all but the first two records in react-json-view --- .../editorComponents/CodeEditor/EvaluatedValuePopup.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx index c921c70fb87..39ca18f94fb 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx @@ -216,6 +216,10 @@ export const CurrentValueViewer = memo( }, collapsed: 2, collapseStringsAfterLength: 20, + shouldCollapse: (field: any) => { + const index = field.name * 1; + return index >= 2 ? true : false; + }, }; content = ( From 4c824a1214275a6298da0105d8a710e62131538c Mon Sep 17 00:00:00 2001 From: bhavin Date: Mon, 10 May 2021 13:14:29 +0530 Subject: [PATCH 12/52] fix: datepicker popover adjust height and width. --- .../designSystems/blueprint/DatePickerComponent2.tsx | 1 - app/client/src/globalStyles/popover.ts | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx index 3fd16fe5e78..13f2aaf2e5f 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx @@ -150,7 +150,6 @@ class DatePickerComponent extends React.Component< onChange={this.onDateSelected} parseDate={this.parseDate} placeholder={"Select Date"} - popoverProps={{ position: "bottom" }} showActionsBar timePrecision={TimePrecision.MINUTE} value={value} diff --git a/app/client/src/globalStyles/popover.ts b/app/client/src/globalStyles/popover.ts index 82cb292a1d6..1da1bde98fd 100644 --- a/app/client/src/globalStyles/popover.ts +++ b/app/client/src/globalStyles/popover.ts @@ -5,4 +5,11 @@ export const PopoverStyles = createGlobalStyle` .${Classes.POPOVER} { box-shadow: 0px 0px 2px rgb(0 0 0 / 20%), 0px 2px 10px rgb(0 0 0 / 10%); } + + .bp3-datepicker { + .DayPicker { + min-height: 251px !important ; + min-width: 233px !important ; + } + } `; From 30510c56b468bf74d1775b2e2ba71c57291eee2f Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Mon, 10 May 2021 15:08:23 +0530 Subject: [PATCH 13/52] Fix download dropdown UI --- .../icons/control/download-data-icon.svg | 3 +++ .../TableComponent/TableDataDownload.tsx | 23 +++++++++---------- app/client/src/constants/Colors.tsx | 1 + 3 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 app/client/src/assets/icons/control/download-data-icon.svg diff --git a/app/client/src/assets/icons/control/download-data-icon.svg b/app/client/src/assets/icons/control/download-data-icon.svg new file mode 100644 index 00000000000..2045fa1fb3b --- /dev/null +++ b/app/client/src/assets/icons/control/download-data-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index 29b63999f27..5c5c80f7dc3 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -7,7 +7,7 @@ import { } from "@blueprintjs/core"; import { IconWrapper } from "constants/IconConstants"; import { Colors } from "constants/Colors"; -import { ReactComponent as DownloadIcon } from "assets/icons/control/download-table.svg"; +import { ReactComponent as DownloadIcon } from "assets/icons/control/download-data-icon.svg"; import { ReactTableColumnProps } from "components/designSystems/appsmith/TableComponent/Constants"; import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers"; import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon"; @@ -21,33 +21,32 @@ const DropDownWrapper = styled.div` background: white; z-index: 1; border-radius: 4px; - border: 1px solid ${Colors.ATHENS_GRAY}; - padding: 8px; + box-shadow: 0px 12px 28px -8px rgba(0, 0, 0, 0.1); + padding: 0; `; const OptionWrapper = styled.div` display: flex; - width: calc(100% - 20px); + width: 100%; justify-content: space-between; align-items: center; height: 32px; box-sizing: border-box; - padding: 8px; - color: ${Colors.OXFORD_BLUE}; - opacity: 0.7; + padding: 6px 12px; + color: ${Colors.CHARCOAL}; min-width: 200px; cursor: pointer; - margin-bottom: 4px; background: ${Colors.WHITE}; border-left: none; - border-radius: 4px; + border-radius: none; .option-title { font-weight: 500; - font-size: 14px; - line-height: 24px; + font-size: 13px; + line-height: 20px; } &:hover { - background: ${Colors.POLAR}; + background: ${Colors.SEA_SHELL}; + color: ${Colors.CODE_GRAY}; } `; interface TableDataDownloadProps { diff --git a/app/client/src/constants/Colors.tsx b/app/client/src/constants/Colors.tsx index b3e9bbe418a..55b7efec2c4 100644 --- a/app/client/src/constants/Colors.tsx +++ b/app/client/src/constants/Colors.tsx @@ -75,6 +75,7 @@ export const Colors: Record = { Galliano: "#E0B30E", ROYAL_BLUE: "#457AE6", ALTO2: "#E0DEDE", + SEA_SHELL: "#F1F1F1", }; export type Color = typeof Colors[keyof typeof Colors]; From 9ab8da4671e2e689dcfb94e91d84653156af11cf Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Mon, 10 May 2021 16:54:50 +0530 Subject: [PATCH 14/52] Table header style changes --- .../assets/icons/control/download-table.svg | 1 - .../src/assets/icons/control/filter-icon.svg | 4 +- .../appsmith/SearchComponent.tsx | 6 +- .../appsmith/TableComponent/TableAction.tsx | 61 +++++++++++++++++++ .../TableComponent/TableActionIcon.tsx | 2 +- .../TableComponent/TableDataDownload.tsx | 12 ++-- .../appsmith/TableComponent/TableFilters.tsx | 8 +-- .../appsmith/TableComponent/TableHeader.tsx | 11 ++-- .../TableComponent/TableStyledWrappers.tsx | 10 ++- app/client/src/constants/Colors.tsx | 5 +- 10 files changed, 95 insertions(+), 25 deletions(-) delete mode 100755 app/client/src/assets/icons/control/download-table.svg mode change 100755 => 100644 app/client/src/assets/icons/control/filter-icon.svg create mode 100644 app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx diff --git a/app/client/src/assets/icons/control/download-table.svg b/app/client/src/assets/icons/control/download-table.svg deleted file mode 100755 index 129ee6b5adf..00000000000 --- a/app/client/src/assets/icons/control/download-table.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/client/src/assets/icons/control/filter-icon.svg b/app/client/src/assets/icons/control/filter-icon.svg old mode 100755 new mode 100644 index 1723d3efe87..e5a827016ee --- a/app/client/src/assets/icons/control/filter-icon.svg +++ b/app/client/src/assets/icons/control/filter-icon.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx index 46bfac9a870..61fc672d05f 100644 --- a/app/client/src/components/designSystems/appsmith/SearchComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/SearchComponent.tsx @@ -2,6 +2,7 @@ import React from "react"; import styled from "styled-components"; import { InputGroup } from "@blueprintjs/core"; import { debounce } from "lodash"; +import { Colors } from "constants/Colors"; interface SearchProps { onSearch: (value: any) => void; @@ -13,9 +14,12 @@ const SearchInputWrapper = styled(InputGroup)` &&& input { box-shadow: none; font-size: 12px; + color: ${Colors.SILVER_CHALICE}; } &&& svg { - opacity: 0.6; + path { + fill: ${Colors.SILVER_CHALICE}; + } } margin: 5px 16px; width: 250px; diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx new file mode 100644 index 00000000000..b4733bf2e5c --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { IconWrapper } from "constants/IconConstants"; +import { Colors } from "constants/Colors"; +import styled from "styled-components"; + +interface TableActionProps { + selected: boolean; + selectMenu: (selected: boolean) => void; + className: string; + title: string; + children: React.ReactNode; + icon?: React.ReactNode; +} + +export const TableIconWrapper = styled.div<{ + selected?: boolean; + disabled?: boolean; +}>` + background: ${(props) => (props.selected ? Colors.Gallery : "transparent")}; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + opacity: ${(props) => (props.disabled ? 0.6 : 1)}; + cursor: ${(props) => !props.disabled && "pointer"}; + color: ${(props) => (props.selected ? Colors.CODE_GRAY : Colors.GRAY)}; + .action-title { + margin-left: 4px; + } + position: relative; + margin-left: 5px; + padding: 0 5px; + &:hover { + background: ${Colors.ATHENS_GRAY}; + } +`; + +function TableAction(props: TableActionProps) { + return ( + { + props.selectMenu(!props.selected); + e.stopPropagation(); + }} + selected={props.selected} + > + + {props.children} + + {props.title} + {props.icon ? props.icon : null} + + ); +} + +export default TableAction; diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx index d59cdacf23c..63899aaceca 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx @@ -34,7 +34,7 @@ function TableActionIcon(props: TableActionIconProps) { selected={props.selected} > diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index 5c5c80f7dc3..71bbd73b339 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -9,8 +9,9 @@ import { IconWrapper } from "constants/IconConstants"; import { Colors } from "constants/Colors"; import { ReactComponent as DownloadIcon } from "assets/icons/control/download-data-icon.svg"; import { ReactTableColumnProps } from "components/designSystems/appsmith/TableComponent/Constants"; -import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers"; -import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon"; +import TableAction, { + TableIconWrapper, +} from "components/designSystems/appsmith/TableComponent/TableAction"; import styled from "styled-components"; import { transformTableDataIntoCsv } from "./CommonUtilities"; import zipcelx from "zipcelx"; @@ -178,6 +179,7 @@ function TableDataDownload(props: TableDataDownloadProps) { + Download ); } @@ -192,16 +194,16 @@ function TableDataDownload(props: TableDataDownloadProps) { }} position={Position.BOTTOM} > - { selectMenu(selected); }} selected={selected} - tooltip="Download" + title="Download" > - + {dowloadOptions.map((item: DownloadOptionProps, index: number) => { return ( diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx index 145ace9fe55..5347f5235fb 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableFilters.tsx @@ -12,7 +12,7 @@ import { ReactComponent as FilterIcon } from "assets/icons/control/filter-icon.s import { TableIconWrapper } from "components/designSystems/appsmith/TableComponent/TableStyledWrappers"; import Button from "components/editorComponents/Button"; import CascadeFields from "components/designSystems/appsmith/TableComponent/CascadeFields"; -import TableActionIcon from "components/designSystems/appsmith/TableComponent/TableActionIcon"; +import TableAction from "components/designSystems/appsmith/TableComponent/TableAction"; import { ReactTableColumnProps, Condition, @@ -173,7 +173,7 @@ function TableFilters(props: TableFilterProps) { position={Position.BOTTOM} usePortal > - - + e.stopPropagation()}> {filters.map((filter: ReactTableFilter, index: number) => { diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx index 8c5ae271c12..0c7e5ffc34b 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableHeader.tsx @@ -25,11 +25,11 @@ import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; const PageNumberInputWrapper = styled(NumericInput)` &&& input { box-shadow: none; + border: 1px solid ${Colors.DANUBE}; background: linear-gradient(0deg, ${Colors.WHITE}, ${Colors.WHITE}), ${Colors.POLAR}; - border: 1px solid ${Colors.GREEN}; + border-radius: none; box-sizing: border-box; - border-radius: 4px; width: 24px; height: 24px; line-height: 24px; @@ -37,6 +37,9 @@ const PageNumberInputWrapper = styled(NumericInput)` text-align: center; font-size: 12px; } + &&&.bp3-control-group > :only-child { + border-radius: 0; + } margin: 0 8px; `; @@ -173,7 +176,7 @@ function TableHeader(props: TableHeaderProps) { props.updatePageNo(pageNo + 1, EventType.ON_PREV_PAGE); }} > - + Page{" "} @@ -196,7 +199,7 @@ function TableHeader(props: TableHeaderProps) { props.updatePageNo(pageNo + 1, EventType.ON_NEXT_PAGE); }} > - + )} diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx index e516d2d432a..25cc63fcd12 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableStyledWrappers.tsx @@ -232,18 +232,16 @@ export const PaginationWrapper = styled.div` justify-content: flex-end; align-items: center; padding: 8px 20px; + color: ${Colors.GRAY}; `; export const PaginationItemWrapper = styled.div<{ disabled?: boolean; selected?: boolean; }>` - background: ${(props) => - props.disabled ? Colors.ATHENS_GRAY : Colors.WHITE}; - border: 1px solid - ${(props) => (props.selected ? Colors.GREEN : Colors.GEYSER_LIGHT)}; + background: ${(props) => (props.disabled ? Colors.MERCURY : Colors.WHITE)}; + border: 1px solid ${Colors.ALTO2}; box-sizing: border-box; - border-radius: 4px; width: 24px; height: 24px; display: flex; @@ -477,7 +475,7 @@ export const RowWrapper = styled.div` justify-content: center; font-size: 12px; line-height: 20px; - color: ${Colors.THUNDER}; + color: ${Colors.GRAY}; margin: 0 4px; white-space: nowrap; `; diff --git a/app/client/src/constants/Colors.tsx b/app/client/src/constants/Colors.tsx index 55b7efec2c4..0099b1ee0f5 100644 --- a/app/client/src/constants/Colors.tsx +++ b/app/client/src/constants/Colors.tsx @@ -29,7 +29,7 @@ export const Colors: Record = { TUNDORA: "#404040", DOVE_GRAY: "#6D6D6D", SLATE_GRAY: "#768896", - SILVER_CHALICE: "#9F9F9F", + SILVER_CHALICE: "#A9A7A7", PORCELAIN: "#EBEEF0", HIT_GRAY: "#A1ACB3", JUNGLE_MIST: "#BCCCD9", @@ -61,7 +61,7 @@ export const Colors: Record = { TROUT_DARK: "#535B62", ALABASTER: "#F9F8F8", WATUSI: "#FFE0D2", - GRAY: "#828282", + GRAY: "#858282", ATHENS_GRAY_DARKER: "#F8F9FA", POMEGRANATE: "#F44336", RIVER_BED: "#4A545B", @@ -76,6 +76,7 @@ export const Colors: Record = { ROYAL_BLUE: "#457AE6", ALTO2: "#E0DEDE", SEA_SHELL: "#F1F1F1", + DANUBE: "#6A86CE", }; export type Color = typeof Colors[keyof typeof Colors]; From f14ca6b6c3c5f35bea82dbc5537eccc206321725 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Mon, 10 May 2021 17:04:17 +0530 Subject: [PATCH 15/52] Hide sub icon --- .../designSystems/appsmith/TableComponent/TableActionIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx index 63899aaceca..46622d072ea 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx @@ -40,7 +40,7 @@ function TableActionIcon(props: TableActionIconProps) { > {props.children} - {props.icon ? props.icon : null} + {/* {props.icon ? props.icon : null} */} ); From 8763f9518ba17aef421e3dcfb880b69904811141 Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Tue, 11 May 2021 11:55:15 +0530 Subject: [PATCH 16/52] - Memoize EditorContext value - Change mapDispatchToProps to object form --- .../EditorContextProvider.tsx | 102 ++++++++---------- 1 file changed, 47 insertions(+), 55 deletions(-) diff --git a/app/client/src/components/editorComponents/EditorContextProvider.tsx b/app/client/src/components/editorComponents/EditorContextProvider.tsx index b2dd17cc846..fb0a46f584e 100644 --- a/app/client/src/components/editorComponents/EditorContextProvider.tsx +++ b/app/client/src/components/editorComponents/EditorContextProvider.tsx @@ -1,4 +1,4 @@ -import React, { Context, createContext, ReactNode } from "react"; +import React, { Context, createContext, ReactNode, useMemo } from "react"; import { connect } from "react-redux"; import { WidgetOperation } from "widgets/BaseWidget"; @@ -65,66 +65,58 @@ function EditorContextProvider(props: EditorContextProviderProps) { deleteWidgetProperty, batchUpdateWidgetProperty, } = props; + + // Memoize the context provider to prevent + // unnecessary renders + const contextValue = useMemo( + () => ({ + executeAction, + updateWidget, + updateWidgetProperty, + updateWidgetMetaProperty, + disableDrag, + resetChildrenMetaProperty, + deleteWidgetProperty, + batchUpdateWidgetProperty, + }), + [ + executeAction, + updateWidget, + updateWidgetProperty, + updateWidgetMetaProperty, + disableDrag, + resetChildrenMetaProperty, + deleteWidgetProperty, + batchUpdateWidgetProperty, + ], + ); return ( - + {children} ); } -const mapDispatchToProps = (dispatch: any) => { - return { - updateWidgetProperty: ( - widgetId: string, - propertyName: string, - propertyValue: any, - ) => - dispatch( - updateWidgetPropertyRequest( - widgetId, - propertyName, - propertyValue, - RenderModes.CANVAS, - ), - ), - executeAction: (actionPayload: ExecuteActionPayload) => - dispatch(executeAction(actionPayload)), - updateWidget: ( - operation: WidgetOperation, - widgetId: string, - payload: any, - ) => dispatch(updateWidget(operation, widgetId, payload)), - updateWidgetMetaProperty: ( - widgetId: string, - propertyName: string, - propertyValue: any, - ) => - dispatch(updateWidgetMetaProperty(widgetId, propertyName, propertyValue)), - resetChildrenMetaProperty: (widgetId: string) => - dispatch(resetChildrenMetaProperty(widgetId)), - disableDrag: (disable: boolean) => { - dispatch(disableDragAction(disable)); - }, - deleteWidgetProperty: (widgetId: string, propertyPaths: string[]) => - dispatch(deletePropertyAction(widgetId, propertyPaths)), - batchUpdateWidgetProperty: ( - widgetId: string, - updates: BatchPropertyUpdatePayload, - ) => { - dispatch(batchUpdatePropertyAction(widgetId, updates)); - }, - }; +const mapDispatchToProps = { + updateWidgetProperty: ( + widgetId: string, + propertyName: string, + propertyValue: any, + ) => + updateWidgetPropertyRequest( + widgetId, + propertyName, + propertyValue, + RenderModes.CANVAS, + ), + + executeAction, + updateWidget, + updateWidgetMetaProperty, + resetChildrenMetaProperty, + disableDrag: disableDragAction, + deleteWidgetProperty: deletePropertyAction, + batchUpdateWidgetProperty: batchUpdatePropertyAction, }; export default connect(null, mapDispatchToProps)(EditorContextProvider); From 812e893eeeb5f8ff260528d984e6664a9454bc69 Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Tue, 11 May 2021 12:03:07 +0530 Subject: [PATCH 17/52] - Add name to memoized resizableComponent --- .../src/components/editorComponents/ResizableComponent.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index 9c07e1fe6e1..d2eb4dfcd92 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -43,8 +43,9 @@ export type ResizableComponentProps = WidgetProps & { paddingOffset: number; }; -/* eslint-disable react/display-name */ -export const ResizableComponent = memo((props: ResizableComponentProps) => { +export const ResizableComponent = memo(function ResizableComponent( + props: ResizableComponentProps, +) { const resizableRef = useRef(null); // Fetch information from the context const { updateWidget } = useContext(EditorContext); From 78bad85e244fd0ae92b92ae5e929c0715e34d7b0 Mon Sep 17 00:00:00 2001 From: "vicky-primathon.in" Date: Tue, 11 May 2021 12:10:13 +0530 Subject: [PATCH 18/52] Used useCallBack hook and remove inline arrow function --- .../appsmith/TableComponent/TableAction.tsx | 15 +++++++++------ .../appsmith/TableComponent/TableActionIcon.tsx | 1 - .../appsmith/TableComponent/TableDataDownload.tsx | 12 ++++++------ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx index b4733bf2e5c..e3c1723f6dc 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableAction.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useCallback } from "react"; import { IconWrapper } from "constants/IconConstants"; import { Colors } from "constants/Colors"; import styled from "styled-components"; @@ -36,13 +36,17 @@ export const TableIconWrapper = styled.div<{ `; function TableAction(props: TableActionProps) { + const handleIconClick = useCallback( + (e: React.MouseEvent) => { + props.selectMenu(!props.selected); + e.stopPropagation(); + }, + [props.selected], + ); return ( { - props.selectMenu(!props.selected); - e.stopPropagation(); - }} + onClick={handleIconClick} selected={props.selected} > {props.title} - {props.icon ? props.icon : null} ); } diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx index 46622d072ea..37b1dac6012 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableActionIcon.tsx @@ -40,7 +40,6 @@ function TableActionIcon(props: TableActionIconProps) { > {props.children} - {/* {props.icon ? props.icon : null} */} ); diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx index 71bbd73b339..bd195b1c2fe 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/TableDataDownload.tsx @@ -173,6 +173,10 @@ function TableDataDownload(props: TableDataDownloadProps) { selectMenu(false); }; + const handleCloseMenu = () => { + selectMenu(false); + }; + if (props.columns.length === 0) { return ( @@ -189,16 +193,12 @@ function TableDataDownload(props: TableDataDownloadProps) { interactionKind={PopoverInteractionKind.CLICK} isOpen={selected} minimal - onClose={() => { - selectMenu(false); - }} + onClose={handleCloseMenu} position={Position.BOTTOM} > { - selectMenu(selected); - }} + selectMenu={selectMenu} selected={selected} title="Download" > From 8bd947671dd77e166016e55bc6153e73fa73463b Mon Sep 17 00:00:00 2001 From: Satish Gandham Date: Tue, 11 May 2021 20:09:09 +0530 Subject: [PATCH 19/52] Move the expensive getBoundingReactangle call to isColliding function to avoid unnecessary calls. --- .../components/editorComponents/ResizableComponent.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index d2eb4dfcd92..e49b884098b 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -104,15 +104,18 @@ export const ResizableComponent = memo(function ResizableComponent( possibleBoundingElements.length > 0 ? possibleBoundingElements[0] : undefined; - const boundingElementClientRect = boundingElement - ? boundingElement.getBoundingClientRect() - : undefined; // onResize handler // Checks if the current resize position has any collisions // If yes, set isColliding flag to true. // If no, set isColliding flag to false. const isColliding = (newDimensions: UIElementSize, position: XYCoord) => { + // Moving the bounding element calculations inside + // to make this expensive operation only whne + const boundingElementClientRect = boundingElement + ? boundingElement.getBoundingClientRect() + : undefined; + const bottom = props.topRow + position.y / props.parentRowSpace + From 2c2da7327d6713226e9a60138512c8e05c1a9eae Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Fri, 14 May 2021 12:39:58 +0530 Subject: [PATCH 20/52] Add a isFromSignup to /applications --- .../AuthenticationSuccessHandler.java | 24 +++++++++++++++---- .../appsmith/server/solutions/UserSignup.java | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationSuccessHandler.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationSuccessHandler.java index 6d1226b32f5..ae488137637 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationSuccessHandler.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/authentication/handlers/AuthenticationSuccessHandler.java @@ -45,13 +45,23 @@ public class AuthenticationSuccessHandler implements ServerAuthenticationSuccess * @return Publishes empty, that completes after handler tasks are finished. */ @Override - public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, - Authentication authentication) { + public Mono onAuthenticationSuccess( + WebFilterExchange webFilterExchange, + Authentication authentication + ) { + return onAuthenticationSuccess(webFilterExchange, authentication, false); + } + + public Mono onAuthenticationSuccess( + WebFilterExchange webFilterExchange, + Authentication authentication, + boolean isFromSignup + ) { log.debug("Login succeeded for user: {}", authentication.getPrincipal()); Mono redirectionMono = authentication instanceof OAuth2AuthenticationToken ? handleOAuth2Redirect(webFilterExchange) - : handleRedirect(webFilterExchange); + : handleRedirect(webFilterExchange, isFromSignup); return sessionUserService.getCurrentUser() .flatMap(user -> userDataService.ensureViewedCurrentVersionReleaseNotes(user).thenReturn(user)) @@ -101,13 +111,19 @@ private Mono handleOAuth2Redirect(WebFilterExchange webFilterExchange) { return this.redirectStrategy.sendRedirect(exchange, defaultRedirectLocation); } - private Mono handleRedirect(WebFilterExchange webFilterExchange) { + private Mono handleRedirect(WebFilterExchange webFilterExchange, boolean isFromSignup) { ServerWebExchange exchange = webFilterExchange.getExchange(); // On authentication success, we send a redirect to the client's home page. This ensures that the session // is set in the cookie on the browser. return Mono.just(exchange.getRequest()) .flatMap(redirectHelper::getRedirectUrl) + .map(url -> { + if (isFromSignup) { + url += (url.contains("?") ? "&" : "?") + "isFromSignup=true"; + } + return url; + }) .map(URI::create) .flatMap(redirectUri -> redirectStrategy.sendRedirect(exchange, redirectUri)); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java index 2ba4981499c..ab352a4f939 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/UserSignup.java @@ -77,7 +77,7 @@ public Mono signupAndLogin(User user, ServerWebExchange exchange) { final WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, EMPTY_WEB_FILTER_CHAIN); return authenticationSuccessHandler - .onAuthenticationSuccess(webFilterExchange, authentication) + .onAuthenticationSuccess(webFilterExchange, authentication, true) .thenReturn(savedUser); }); } From cab7f4a099be7e104404ce56cd1b9a7a6dba828b Mon Sep 17 00:00:00 2001 From: bhavin Date: Fri, 14 May 2021 14:53:22 +0530 Subject: [PATCH 21/52] fix: select disabled option popover overflow --- .../designSystems/blueprint/DropdownComponent.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx index 3487aab4b82..bbea0c34db9 100644 --- a/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DropdownComponent.tsx @@ -267,6 +267,11 @@ class DropDownComponent extends React.Component { popoverProps={{ minimal: true, usePortal: true, + modifiers: { + preventOverflow: { + enabled: false, + }, + }, popoverClassName: "select-popover-wrapper", }} > @@ -294,6 +299,11 @@ class DropDownComponent extends React.Component { popoverProps={{ minimal: true, usePortal: true, + modifiers: { + preventOverflow: { + enabled: false, + }, + }, popoverClassName: "select-popover-wrapper", }} resetOnSelect From 5abd651e25d0661d24cd5799bc323d23892c8c7a Mon Sep 17 00:00:00 2001 From: Apple Date: Mon, 17 May 2021 21:36:46 +0530 Subject: [PATCH 22/52] updated test --- .../ClientSideTests/FormWidgets/DatePicker_2_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1f8676f85c7..9938981e83c 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 @@ -74,9 +74,9 @@ describe("DatePicker Widget Property pane tests with js bindings", function() { cy.get(".t--property-control-defaultdate .bp3-input").type("2020-02-01"); cy.closePropertyPane(); cy.openPropertyPane("datepickerwidget2"); - cy.get(formWidgetsPage.toggleJsMinDate).click(); + cy.get(formWidgetsPage.toggleJsMinDate).click({ force: true }); cy.get(".t--property-control-mindate .bp3-input").type("2020-01-01"); - cy.get(formWidgetsPage.toggleJsMaxDate).click(); + cy.get(formWidgetsPage.toggleJsMaxDate).click({ force: true }); cy.get(".t--property-control-maxdate .bp3-input").type("2020-02-10"); cy.closePropertyPane(); }); From 75479448ccf95e0cfe6fb8bb35f5b673c1bfc482 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 18 May 2021 14:51:31 +0530 Subject: [PATCH 23/52] Watch notification collection in RTS --- app/rts/src/server.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/app/rts/src/server.ts b/app/rts/src/server.ts index 5e627d7298f..27c772250cc 100644 --- a/app/rts/src/server.ts +++ b/app/rts/src/server.ts @@ -233,6 +233,34 @@ async function watchMongoDB(io) { } }) + const notificationsStream = db.collection("notification").watch( + [ + // Prevent server-internal fields from being sent to the client. + { + $unset: [ + "deletedAt", + "deleted", + "_class", + ].map(f => "fullDocument." + f) + }, + ], + { fullDocument: "updateLookup" } + ); + + notificationsStream.on("change", async (event: mongodb.ChangeEventCR) => { + console.log("notification event", event) + const notification = event.fullDocument + + if (notification == null) { + // This happens when `event.operationType === "drop"`, when a notification is deleted. + console.error("Null document recieved for notification change event", event) + return + } + + const eventName = event.operationType + ":" + event.ns.coll + io.to("email:" + notification.forUsername).emit(eventName, { notification }) + }) + process.on("exit", () => { (commentChangeStream != null ? commentChangeStream.close() : Promise.bind(client).resolve()) .then(client.close.bind(client)) From 28cab61e5b5683e2514f7f868105944159ceda37 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 18 May 2021 17:16:09 +0530 Subject: [PATCH 24/52] Separate notifications for comment threads --- .../server/domains/CommentNotification.java | 4 +- .../domains/CommentThreadNotification.java | 14 +++++ .../appsmith/server/domains/Notification.java | 3 +- .../server/services/CommentServiceImpl.java | 51 +++++++++++++++---- 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java index 0f68abbadce..b2277fd173e 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentNotification.java @@ -1,10 +1,12 @@ package com.appsmith.server.domains; import lombok.Data; +import lombok.EqualsAndHashCode; import org.springframework.data.mongodb.core.mapping.Document; -@Document @Data +@EqualsAndHashCode(callSuper = true) +@Document public class CommentNotification extends Notification { Comment comment; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java new file mode 100644 index 00000000000..ab3834805c1 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/CommentThreadNotification.java @@ -0,0 +1,14 @@ +package com.appsmith.server.domains; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.data.mongodb.core.mapping.Document; + +@Data +@EqualsAndHashCode(callSuper = true) +@Document +public class CommentThreadNotification extends Notification { + + CommentThread commentThread; + +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java index b972eda2b65..79ffee5af1d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Notification.java @@ -18,7 +18,8 @@ public class Notification extends BaseDomain { */ Boolean isRead; - public void cleanForClient() { + public String getType() { + return getClass().getSimpleName(); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java index b1b1b4b50da..1935b125b0c 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CommentServiceImpl.java @@ -8,6 +8,7 @@ import com.appsmith.server.domains.Comment; import com.appsmith.server.domains.CommentNotification; import com.appsmith.server.domains.CommentThread; +import com.appsmith.server.domains.CommentThreadNotification; import com.appsmith.server.domains.Notification; import com.appsmith.server.domains.User; import com.appsmith.server.exceptions.AppsmithError; @@ -79,6 +80,10 @@ public CommentServiceImpl( @Override public Mono create(String threadId, Comment comment) { + return create(threadId, comment, true); + } + + public Mono create(String threadId, Comment comment, boolean shouldCreateNotification) { if (StringUtils.isWhitespace(comment.getAuthorName())) { // Error: User can't explicitly set the author name. It will be the currently logged in user. return Mono.empty(); @@ -117,18 +122,26 @@ public Mono create(String threadId, Comment comment) { String authorName = user.getName() != null ? user.getName(): user.getUsername(); comment.setAuthorName(authorName); - return repository.save(comment); + return Mono.zip( + Mono.just(user), + repository.save(comment) + ); }) - .flatMap(savedComment -> { + .flatMap(tuple -> { + final User user = tuple.getT1(); + final Comment savedComment = tuple.getT2(); + final Set usernames = policyUtils.findUsernamesWithPermission( savedComment.getPolicies(), AclPermission.READ_COMMENT); List> monos = new ArrayList<>(); for (String username : usernames) { - final CommentNotification notification = new CommentNotification(); - notification.setComment(savedComment); - notification.setForUsername(username); - monos.add(notificationService.create(notification)); + if (!username.equals(user.getUsername())) { + final CommentNotification notification = new CommentNotification(); + notification.setComment(savedComment); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } } return Flux.concat(monos).then(Mono.just(savedComment)); @@ -190,9 +203,11 @@ public Mono createThread(CommentThread commentThread) { if (!CollectionUtils.isEmpty(thread.getComments())) { thread.getComments().get(0).setLeading(true); + boolean isFirst = true; for (final Comment comment : thread.getComments()) { comment.setId(null); - commentSaverMonos.add(create(thread.getId(), comment)); + commentSaverMonos.add(create(thread.getId(), comment, !isFirst)); + isFirst = false; } } @@ -201,10 +216,28 @@ public Mono createThread(CommentThread commentThread) { return Flux.concat(commentSaverMonos); }) .collectList() - .map(comments -> { + .zipWith(sessionUserService.getCurrentUser()) + .flatMap(tuple -> { + final List comments = tuple.getT1(); + final User user = tuple.getT2(); + commentThread.setComments(comments); commentThread.setIsViewed(true); - return commentThread; + + final Set usernames = policyUtils.findUsernamesWithPermission( + commentThread.getPolicies(), AclPermission.READ_THREAD); + + List> monos = new ArrayList<>(); + for (String username : usernames) { + if (!username.equals(user.getUsername())) { + final CommentThreadNotification notification = new CommentThreadNotification(); + notification.setCommentThread(commentThread); + notification.setForUsername(username); + monos.add(notificationService.create(notification)); + } + } + + return Flux.concat(monos).then(Mono.just(commentThread)); }); } From 08966499bed5f476bcf3c26974565ae4a4539d9d Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Tue, 18 May 2021 17:17:22 +0530 Subject: [PATCH 25/52] Remove request/response data from copies of an executed API (#4521) --- app/client/src/reducers/entityReducers/actionsReducer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/client/src/reducers/entityReducers/actionsReducer.tsx b/app/client/src/reducers/entityReducers/actionsReducer.tsx index 187f55d40d1..01b84929b1f 100644 --- a/app/client/src/reducers/entityReducers/actionsReducer.tsx +++ b/app/client/src/reducers/entityReducers/actionsReducer.tsx @@ -303,6 +303,7 @@ const actionsReducer = createReducer(initialState, { .filter((a) => a.config.id === action.payload.id) .map((a) => ({ ...a, + data: undefined, config: { ...a.config, id: "TEMP_COPY_ID", From 4733edfccd03f7cbf8c8aa4d486103803c84959b Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Tue, 18 May 2021 17:17:43 +0530 Subject: [PATCH 26/52] Fixed query params parsing in API pane (#4482) * Fixed query params parsing when there are mulitple "="s. * Added cypress tests to validate query params parsing --- app/client/cypress/fixtures/testdata.json | 3 ++- .../ClientSideTests/ApiPaneTests/API_Edit_spec.js | 12 ++++++++++++ app/client/cypress/locators/apiWidgetslocator.json | 5 ++++- app/client/cypress/support/commands.js | 8 ++++++++ app/client/src/sagas/ApiPaneSagas.ts | 6 +++++- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index 61d619d2924..cc934021caa 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -122,5 +122,6 @@ "accessTokenUrl": "https://oauth.mocklab.io/oauth/token", "oauthResponse": "169444434892406", "authorizationURL": "https://oauth.mocklab.io/oauth/authorize", - "basicURl": "https://envyenksqii9nf3.m.pipedream.net" + "basicURl": "https://envyenksqii9nf3.m.pipedream.net", + "methodWithQueryParam": "users?q=mimeType='application/vnd.google-apps.spreadsheet'" } diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js index d46a3b1ec09..918c4592da5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js @@ -39,4 +39,16 @@ describe("API Panel Test Functionality", function() { cy.xpath(apiwidget.headerKey).should("be.visible"); cy.xpath(apiwidget.headerKey).should("have.value", ""); }); + + it("Should correctly parse query params", function() { + cy.NavigateToAPI_Panel(); + cy.CreateAPI("APIWithQueryParams"); + cy.get("textarea").should( + "have.attr", + "placeholder", + "https://mock-api.appsmith.com/users", + ); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methodWithQueryParam); + cy.ValidateQueryParams({ key: "q", value:"mimeType='application/vnd.google-apps.spreadsheet'" }); + }); }); diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 2727f72efe7..9bdb4100dd2 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -52,5 +52,8 @@ "actionlist": ".action div div", "settings": "li:contains('Settings')", "onPageLoad": "[data-cy=executeOnLoad]", - "renameEntity": ".single-select >div:contains('Edit Name')" + "renameEntity": ".single-select >div:contains('Edit Name')", + "paramsTab": "//li//span[text()='Params']", + "paramKey": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].key.0')]//textarea)[1]", + "paramValue": "(//div[contains(@class,'t--actionConfiguration.queryParameters[0].value.0')]//textarea)[1]" } diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 1054ea37671..3bd45b491d8 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2342,3 +2342,11 @@ Cypress.Commands.add("callApi", (apiname) => { Cypress.Commands.add("assertPageSave", () => { cy.get(commonlocators.saveStatusSuccess); }); + +Cypress.Commands.add("ValidateQueryParams", (param) => { + cy.xpath(apiwidget.paramsTab) + .should("be.visible") + .click({ force: true }); + cy.xpath(apiwidget.paramKey).first().contains(param.key); + cy.xpath(apiwidget.paramValue).first().contains(param.value); +}); diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 8b2f7bd0db2..8ea71706e05 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -90,7 +90,11 @@ function* syncApiParamsSaga( if (value.indexOf("?") > -1) { const paramsString = value.substr(value.indexOf("?") + 1); const params = paramsString.split("&").map((p) => { - const keyValue = p.split("="); + const firstEqualPos = p.indexOf("="); + const keyValue = + firstEqualPos > -1 + ? [p.substring(0, firstEqualPos), p.substring(firstEqualPos + 1)] + : []; return { key: keyValue[0], value: keyValue[1] || "" }; }); if (params.length < 2) { From 51b98eed2f59521fa85eccca4e162a0ae710ce4e Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Tue, 18 May 2021 17:21:32 +0530 Subject: [PATCH 27/52] Fix: Debugger shows errors for evaluations done on page load (#4552) --- .../cypress/fixtures/debuggerTableDsl.json | 42 +++++++++++++++++ .../Debugger/PageOnLoad_spec.js | 23 ++++++++++ app/client/src/actions/actionActions.ts | 6 +++ .../src/constants/ReduxActionConstants.tsx | 1 + app/client/src/sagas/ActionExecutionSagas.ts | 3 ++ app/client/src/sagas/DebuggerSagas.ts | 45 +++++++++++++++++-- 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 app/client/cypress/fixtures/debuggerTableDsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js diff --git a/app/client/cypress/fixtures/debuggerTableDsl.json b/app/client/cypress/fixtures/debuggerTableDsl.json new file mode 100644 index 00000000000..afaed3b2cf9 --- /dev/null +++ b/app/client/cypress/fixtures/debuggerTableDsl.json @@ -0,0 +1,42 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1280, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 9, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "label": "Data", + "widgetName": "Table1", + "searchKey": "", + "tableData": "{{TestApi.data.users}}", + "type": "TABLE_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 1, + "rightColumn": 9, + "topRow": 7, + "bottomRow": 14, + "parentId": "0", + "widgetId": "7miqot30xy", + "dynamicBindingPathList": [] + } + ] + } + } \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js new file mode 100644 index 00000000000..2fd3a73d96a --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/PageOnLoad_spec.js @@ -0,0 +1,23 @@ +const dsl = require("../../../../fixtures/debuggerTableDsl.json"); +const explorer = require("../../../../locators/explorerlocators.json"); +const debuggerLocators = require("../../../../locators/Debugger.json"); +const testdata = require("../../../../fixtures/testdata.json"); + +describe("Check debugger logs state when there are onPageLoad actions", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Check debugger logs state when there are onPageLoad actions", function() { + cy.openPropertyPane("tablewidget"); + cy.testJsontext("tabledata", "{{TestApi.data.users}}"); + cy.NavigateToAPI_Panel(); + cy.CreateAPI("TestApi"); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); + cy.SaveAndRunAPI(); + + cy.get(explorer.addWidget).click(); + + cy.reload(); + cy.contains(debuggerLocators.debuggerIcon, 0); + }); +}); diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 673f0b12823..3e79882a473 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -233,6 +233,12 @@ export const updateActionProperty = ( }); }; +export const executePageLoadActionsComplete = () => { + return { + type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE, + }; +}; + export const setActionsToExecuteOnPageLoad = ( actions: Array<{ executeOnLoad: boolean; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 0d02d16487d..6a38c03ab52 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -226,6 +226,7 @@ export const ReduxActionTypes: { [key: string]: string } = { RESET_PASSWORD_VERIFY_TOKEN_SUCCESS: "RESET_PASSWORD_VERIFY_TOKEN_SUCCESS", RESET_PASSWORD_VERIFY_TOKEN_INIT: "RESET_PASSWORD_VERIFY_TOKEN_INIT", EXECUTE_PAGE_LOAD_ACTIONS: "EXECUTE_PAGE_LOAD_ACTIONS", + EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE: "EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE", SWITCH_ORGANIZATION_INIT: "SWITCH_ORGANIZATION_INIT", SWITCH_ORGANIZATION_SUCCESS: "SWITCH_ORGANIZATION_SUCCESS", FETCH_ORG_ROLES_INIT: "FETCH_ORG_ROLES_INIT", diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 78ae82de5d8..7414a7edc3f 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -44,6 +44,7 @@ import { import { executeApiActionRequest, executeApiActionSuccess, + executePageLoadActionsComplete, showRunActionConfirmModal, updateAction, } from "actions/actionActions"; @@ -999,6 +1000,8 @@ function* executePageLoadActionsSaga() { PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.EXECUTE_PAGE_LOAD_ACTIONS, ); + + yield put(executePageLoadActionsComplete()); } catch (e) { log.error(e); diff --git a/app/client/src/sagas/DebuggerSagas.ts b/app/client/src/sagas/DebuggerSagas.ts index d072aad5397..e2c6c223e86 100644 --- a/app/client/src/sagas/DebuggerSagas.ts +++ b/app/client/src/sagas/DebuggerSagas.ts @@ -12,13 +12,15 @@ import { call, } from "redux-saga/effects"; import { getDataTree } from "selectors/dataTreeSelectors"; -import { isEmpty, set } from "lodash"; +import { isEmpty, set, get } from "lodash"; import { getDebuggerErrors } from "selectors/debuggerSelectors"; import { getAction } from "selectors/entitiesSelector"; import { Action, PluginType } from "entities/Action"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; -import { DataTree } from "entities/DataTree/dataTreeFactory"; +import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { isWidget } from "workers/evaluationUtils"; +import { getWidget } from "./selectors"; +import { WidgetProps } from "widgets/BaseWidget"; function* onWidgetUpdateSaga(payload: LogActionPayload) { if (!payload.source) return; @@ -125,6 +127,7 @@ function* debuggerLogSaga(action: ReduxAction) { if (payload.source && payload.source.propertyPath) { if (payload.text) { yield put(errorLog(payload)); + yield put(debuggerLog(payload)); } } @@ -166,6 +169,42 @@ function* debuggerLogSaga(action: ReduxAction) { } } +// Pass through error list once after on page load actions executions are complete +function* onExecutePageActionsCompleteSaga() { + yield take(ReduxActionTypes.SET_EVALUATED_TREE); + + const dataTree: DataTree = yield select(getDataTree); + const errors = yield select(getDebuggerErrors); + const updatedErrors = { ...errors }; + const errorIds = Object.keys(errors); + + for (const id of errorIds) { + const splits = id.split("-"); + const entityId = splits[0]; + const propertyName = splits[1]; + const widget: WidgetProps | null = yield select(getWidget, entityId); + + if (widget) { + const dataTreeWidget = dataTree[widget.widgetName] as DataTreeWidget; + + if (!get(dataTreeWidget.invalidProps, propertyName, null)) { + delete updatedErrors[id]; + } + } + } + + yield put({ + type: ReduxActionTypes.DEBUGGER_UPDATE_ERROR_LOGS, + payload: updatedErrors, + }); +} + export default function* debuggerSagasListeners() { - yield all([takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga)]); + yield all([ + takeEvery(ReduxActionTypes.DEBUGGER_LOG_INIT, debuggerLogSaga), + takeEvery( + ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS_COMPLETE, + onExecutePageActionsCompleteSaga, + ), + ]); } From b1c523eef499419da77f119e1b810fe4019ba7dd Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 18 May 2021 17:48:40 +0530 Subject: [PATCH 28/52] Fix flaky UserData test --- .../server/services/UserDataServiceTest.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java index c6ce42a8999..784e9a648a5 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/UserDataServiceTest.java @@ -1,12 +1,10 @@ package com.appsmith.server.services; import com.appsmith.server.domains.Asset; -import com.appsmith.server.domains.Collection; import com.appsmith.server.domains.User; import com.appsmith.server.domains.UserData; import com.appsmith.server.exceptions.AppsmithException; import com.appsmith.server.repositories.AssetRepository; -import org.apache.commons.compress.utils.Lists; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -185,17 +183,20 @@ public void testUpdateLastUsedOrgList_WhenListIsEmpty_orgIdPrepended() { @Test @WithUserDetails(value = "api_user") public void testUpdateLastUsedOrgList_WhenListIsNotEmpty_orgIdPrepended() { - // set some org id to current user's list - UserData existingUserData = new UserData(); - existingUserData.setRecentlyUsedOrgIds(Arrays.asList("123", "456")); - userDataService.updateForCurrentUser(existingUserData).subscribe(); - - // now check whether new org id is put at first - String sampleOrgId = "abcd"; - final Mono saveMono = userDataService.updateLastUsedOrgList(sampleOrgId); - StepVerifier.create(saveMono).assertNext(userData -> { + // Set an initial list of org ids to the current user. + UserData changes = new UserData(); + changes.setRecentlyUsedOrgIds(Arrays.asList("123", "456")); + + final Mono resultMono = userDataService.updateForCurrentUser(changes) + .flatMap(userData -> { + // Now check whether a new org id is put at first. + String sampleOrgId = "sample-org-id"; + return userDataService.updateLastUsedOrgList(sampleOrgId); + }); + + StepVerifier.create(resultMono).assertNext(userData -> { Assert.assertEquals(3, userData.getRecentlyUsedOrgIds().size()); - Assert.assertEquals(sampleOrgId, userData.getRecentlyUsedOrgIds().get(0)); + Assert.assertEquals("sample-org-id", userData.getRecentlyUsedOrgIds().get(0)); }).verifyComplete(); } } From 9e082a6156384f28ad3a5b5656178117961a5077 Mon Sep 17 00:00:00 2001 From: akash-codemonk <67054171+akash-codemonk@users.noreply.github.com> Date: Tue, 18 May 2021 19:24:40 +0530 Subject: [PATCH 29/52] Feature: Show widget error state (#4558) --- .../Debugger/Widget_Error_spec.js | 17 + app/client/cypress/support/commands.js | 2 +- .../CodeEditor/styledComponents.ts | 10 +- .../editorComponents/Debugger/index.tsx | 10 +- .../WidgetNameComponent/SettingsControl.tsx | 47 +- .../WidgetNameComponent/index.tsx | 5 +- .../mockResponses/WidgetConfigResponse.tsx | 3 + app/client/src/sagas/EvaluationsSaga.ts | 3 + .../src/utils/WidgetPropsUtils.test.tsx | 542 +++++++++++++++++- app/client/src/utils/WidgetPropsUtils.tsx | 72 +++ app/client/src/utils/helpers.test.ts | 86 +++ app/client/src/utils/helpers.tsx | 25 + app/client/src/widgets/BaseWidget.tsx | 7 + app/client/src/workers/DataTreeEvaluator.ts | 4 +- app/client/src/workers/evaluationUtils.ts | 22 +- 15 files changed, 831 insertions(+), 24 deletions(-) create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js create mode 100644 app/client/src/utils/helpers.test.ts diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js new file mode 100644 index 00000000000..3daf51823b8 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Widget_Error_spec.js @@ -0,0 +1,17 @@ +const dsl = require("../../../../fixtures/buttondsl.json"); + +describe("Widget error state", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Check widget error state", function() { + cy.openPropertyPane("buttonwidget"); + + cy.get(".t--property-control-visible") + .find(".t--js-toggle") + .click(); + cy.testJsontext("visible", "Test"); + + cy.contains(".t--widget-error-count", 1); + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 3bd45b491d8..9e4d949e445 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1143,7 +1143,7 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(inputcss) .first() .trigger("mouseover", { force: true }); - cy.get(innercss).should("have.text", text); + cy.contains(innercss, text); }); Cypress.Commands.add("editColName", (text) => { diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index 3272c2f646c..1054a84b21d 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -422,20 +422,13 @@ export const DynamicAutocompleteInputWrapper = styled.div<{ height: 100%; flex: 1; position: relative; - border-color: ${(props) => - !props.isError && props.isActive && props.skin === Skin.DARK - ? Colors.ALABASTER - : "transparent"}; + border: 1px solid ${(props) => (!props.isError ? "transparent" : "red")}; > span:first-of-type { width: 30px; position: absolute; right: 0px; } &:hover { - border-color: ${(props) => - !props.isError && props.skin === Skin.DARK - ? Colors.ALABASTER - : "transparent"}; .lightning-menu { background: ${(props) => (!props.isNotHover ? "#090707" : "")}; svg { @@ -451,7 +444,6 @@ export const DynamicAutocompleteInputWrapper = styled.div<{ } } } - border: 0px; border-radius: 0px; .lightning-menu { z-index: 1 !important; diff --git a/app/client/src/components/editorComponents/Debugger/index.tsx b/app/client/src/components/editorComponents/Debugger/index.tsx index 2e7ebb0fb78..368d73be84a 100644 --- a/app/client/src/components/editorComponents/Debugger/index.tsx +++ b/app/client/src/components/editorComponents/Debugger/index.tsx @@ -18,7 +18,7 @@ const Container = styled.div<{ errorCount: number }>` right: 20px; bottom: 20px; cursor: pointer; - padding: 19px; + padding: ${(props) => props.theme.spaces[6]}px; color: ${(props) => props.theme.colors.debugger.floatingButton.color}; border-radius: 50px; box-shadow: ${(props) => props.theme.colors.debugger.floatingButton.shadow}; @@ -33,11 +33,9 @@ const Container = styled.div<{ errorCount: number }>` .debugger-count { color: ${Colors.WHITE}; - font-size: 14px; - font-weight: 500; ${(props) => getTypographyByKey(props, "h6")} - height: 20px; - padding: 6px; + height: 16px; + padding: ${(props) => props.theme.spaces[1]}px; background-color: ${(props) => !!props.errorCount ? props.theme.colors.debugger.floatingButton.errorCount @@ -75,7 +73,7 @@ function Debugger() { errorCount={errorCount} onClick={onClick} > - +
{errorCount}
); diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx index b33a28bd0f0..6e3dfc22555 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx @@ -1,5 +1,6 @@ import React, { CSSProperties } from "react"; import { ControlIcons } from "icons/ControlIcons"; +import Icon, { IconSize } from "components/ads/Icon"; import { Colors } from "constants/Colors"; import styled from "styled-components"; import { Tooltip, Classes } from "@blueprintjs/core"; @@ -34,18 +35,41 @@ const SettingsWrapper = styled.div` `; const WidgetName = styled.span` - margin-right: 5px; + margin-right: ${(props) => props.theme.spaces[1] + 1}px; + margin-left: ${(props) => props.theme.spaces[3]}px; +`; + +const StyledErrorIcon = styled(Icon)` + &:hover { + svg { + path { + fill: ${Colors.WHITE}; + } + } + } + margin-right: ${(props) => props.theme.spaces[1]}px; `; type SettingsControlProps = { toggleSettings: (e: any) => void; activity: Activities; name: string; + errorCount: number; }; const SettingsIcon = ControlIcons.SETTINGS_CONTROL; -const getStyles = (activity: Activities): CSSProperties | undefined => { +const getStyles = ( + activity: Activities, + errorCount: number, +): CSSProperties | undefined => { + if (errorCount > 0) { + return { + background: "red", + color: Colors.WHITE, + }; + } + switch (activity) { case Activities.ACTIVE: return { @@ -69,7 +93,9 @@ export function SettingsControl(props: SettingsControlProps) { const settingsIcon = ( ); + const errorIcon = ( + + ); return ( + {!!props.errorCount && ( + <> + {errorIcon} + {props.errorCount} + + )} {props.name} {settingsIcon} diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx index 0e5a653bde2..98d46c85d12 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx @@ -41,6 +41,7 @@ type WidgetNameComponentProps = { parentId?: string; type: WidgetType; showControls?: boolean; + errorCount: number; }; export function WidgetNameComponent(props: WidgetNameComponentProps) { @@ -95,7 +96,8 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { props.showControls || ((focusedWidget === props.widgetId || selectedWidget === props.widgetId) && !isDragging && - !isResizing); + !isResizing) || + !!props.errorCount; let currentActivity = Activities.NONE; if (focusedWidget === props.widgetId) currentActivity = Activities.HOVERING; @@ -111,6 +113,7 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 12822190424..7899c9e3781 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -129,6 +129,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { columns: 5, widgetName: "DatePicker", defaultDate: moment().toISOString(), + minDate: "2001-01-01 00:00", + maxDate: "2041-12-31 23:59", version: 2, isRequired: false, }, @@ -303,6 +305,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { FILE_PICKER_WIDGET: { rows: 1, files: [], + allowedFileTypes: [], label: "Select Files", columns: 4, maxNumFiles: 1, diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 79b6db90b7a..83edd6db0cc 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -58,6 +58,9 @@ const evalErrorHandler = (errors: EvalError[]) => { text: `${error.message} Node was: ${node}`, variant: Variant.danger, }); + AppsmithConsole.error({ + text: `${error.message} Node was: ${node}`, + }); // Send the generic error message to sentry for better grouping Sentry.captureException(new Error(error.message), { tags: { diff --git a/app/client/src/utils/WidgetPropsUtils.test.tsx b/app/client/src/utils/WidgetPropsUtils.test.tsx index 592130a3b0a..07e2edcf18b 100644 --- a/app/client/src/utils/WidgetPropsUtils.test.tsx +++ b/app/client/src/utils/WidgetPropsUtils.test.tsx @@ -1,6 +1,9 @@ import * as generators from "../utils/generators"; import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; -import { migrateChartDataFromArrayToObject } from "./WidgetPropsUtils"; +import { + migrateChartDataFromArrayToObject, + migrateInitialValues, +} from "./WidgetPropsUtils"; describe("WidgetProps tests", () => { it("it checks if array to object migration functions for chart widget ", () => { @@ -88,3 +91,540 @@ describe("WidgetProps tests", () => { expect(result).toStrictEqual(output); }); }); + +describe("Initial value migration test", () => { + const containerWidget = { + widgetName: "MainContainer", + backgroundColor: "none", + rightColumn: 1118, + snapColumns: 16, + detachFromLayout: true, + widgetId: "0", + topRow: 0, + bottomRow: 560, + snapRows: 33, + isLoading: false, + parentRowSpace: 1, + type: WidgetTypes.CANVAS_WIDGET, + renderMode: RenderModes.CANVAS, + canExtend: true, + version: 18, + minHeight: 600, + parentColumnSpace: 1, + dynamicTriggerPathList: [], + dynamicBindingPathList: [], + leftColumn: 0, + }; + + it("Input widget", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "Input1", + rightColumn: 8, + widgetId: "ra3vyy3nt2", + topRow: 1, + bottomRow: 2, + parentRowSpace: 40, + isVisible: true, + label: "", + type: WidgetTypes.INPUT_WIDGET, + version: 1, + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 3, + inputType: "TEXT", + renderMode: RenderModes.CANVAS, + resetOnSubmit: false, + }, + ], + }; + const output = { + ...containerWidget, + children: [ + { + widgetName: "Input1", + rightColumn: 8, + widgetId: "ra3vyy3nt2", + topRow: 1, + bottomRow: 2, + parentRowSpace: 40, + isVisible: true, + label: "", + type: "INPUT_WIDGET", + version: 1, + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + renderMode: "CANVAS", + leftColumn: 3, + inputType: "TEXT", + // will not override existing property + resetOnSubmit: false, + // following properties get added + isRequired: false, + isDisabled: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("DROP_DOWN_WIDGET", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "Select1", + rightColumn: 6, + widgetId: "1e3ytl2pl9", + topRow: 3, + bottomRow: 4, + parentRowSpace: 40, + isVisible: true, + label: "", + type: WidgetTypes.DROP_DOWN_WIDGET, + version: 1, + parentId: "0", + isLoading: false, + defaultOptionValue: "GREEN", + selectionType: "SINGLE_SELECT", + parentColumnSpace: 67.375, + renderMode: RenderModes.CANVAS, + leftColumn: 1, + options: [ + { + label: "Blue", + value: "BLUE", + }, + { + label: "Green", + value: "GREEN", + }, + { + label: "Red", + value: "RED", + }, + ], + }, + ], + }; + + const output = { + ...containerWidget, + children: [ + { + widgetName: "Select1", + rightColumn: 6, + widgetId: "1e3ytl2pl9", + topRow: 3, + bottomRow: 4, + parentRowSpace: 40, + isVisible: true, + label: "", + type: WidgetTypes.DROP_DOWN_WIDGET, + version: 1, + parentId: "0", + isLoading: false, + defaultOptionValue: "GREEN", + selectionType: "SINGLE_SELECT", + parentColumnSpace: 67.375, + renderMode: "CANVAS", + leftColumn: 1, + options: [ + { + label: "Blue", + value: "BLUE", + }, + { + label: "Green", + value: "GREEN", + }, + { + label: "Red", + value: "RED", + }, + ], + // following properties get added + isRequired: false, + isDisabled: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("DATE_PICKER_WIDGET2", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "DatePicker1", + defaultDate: "2021-05-12T06:50:51.743Z", + rightColumn: 7, + dateFormat: "YYYY-MM-DD HH:mm", + widgetId: "5jbfazqnca", + topRow: 2, + bottomRow: 3, + parentRowSpace: 40, + isVisible: true, + datePickerType: "DATE_PICKER", + label: "", + type: WidgetTypes.DATE_PICKER_WIDGET2, + renderMode: RenderModes.CANVAS, + version: 2, + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 2, + isDisabled: false, + }, + ], + }; + + const output = { + ...containerWidget, + children: [ + { + widgetName: "DatePicker1", + defaultDate: "2021-05-12T06:50:51.743Z", + rightColumn: 7, + dateFormat: "YYYY-MM-DD HH:mm", + widgetId: "5jbfazqnca", + topRow: 2, + bottomRow: 3, + parentRowSpace: 40, + isVisible: true, + datePickerType: "DATE_PICKER", + label: "", + type: WidgetTypes.DATE_PICKER_WIDGET2, + renderMode: RenderModes.CANVAS, + version: 2, + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 2, + isDisabled: false, + // following properties get added + isRequired: false, + minDate: "2001-01-01 00:00", + maxDate: "2041-12-31 23:59", + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("SWITCH_WIDGET", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "Switch1", + rightColumn: 5, + widgetId: "4ksqurxmwn", + topRow: 2, + bottomRow: 3, + parentRowSpace: 40, + isVisible: true, + label: "Label", + type: WidgetTypes.SWITCH_WIDGET, + renderMode: RenderModes.CANVAS, + defaultSwitchState: true, + version: 1, + alignWidget: "LEFT", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 3, + }, + ], + }; + + const output = { + ...containerWidget, + children: [ + { + widgetName: "Switch1", + rightColumn: 5, + widgetId: "4ksqurxmwn", + topRow: 2, + bottomRow: 3, + parentRowSpace: 40, + isVisible: true, + label: "Label", + type: WidgetTypes.SWITCH_WIDGET, + renderMode: RenderModes.CANVAS, + defaultSwitchState: true, + version: 1, + alignWidget: "LEFT", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 3, + // following properties get added + isDisabled: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("Video widget", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "Video1", + rightColumn: 9, + dynamicPropertyPathList: [], + widgetId: "ti5b5f5hvq", + topRow: 3, + bottomRow: 10, + parentRowSpace: 40, + isVisible: true, + type: WidgetTypes.VIDEO_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + onPlay: "", + url: "https://www.youtube.com/watch?v=mzqK0QIZRLs", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 2, + autoPlay: false, + }, + ], + }; + const output = { + ...containerWidget, + children: [ + { + widgetName: "Video1", + rightColumn: 9, + dynamicPropertyPathList: [], + widgetId: "ti5b5f5hvq", + topRow: 3, + bottomRow: 10, + parentRowSpace: 40, + isVisible: true, + type: WidgetTypes.VIDEO_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + onPlay: "", + url: "https://www.youtube.com/watch?v=mzqK0QIZRLs", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 2, + autoPlay: false, + // following properties get added + isRequired: false, + isDisabled: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("CHECKBOX_WIDGET", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "Checkbox1", + rightColumn: 8, + widgetId: "djxhhl1p7t", + topRow: 4, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "Label", + type: WidgetTypes.CHECKBOX_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + alignWidget: "LEFT", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 5, + defaultCheckedState: true, + }, + ], + }; + const output = { + ...containerWidget, + children: [ + { + widgetName: "Checkbox1", + rightColumn: 8, + widgetId: "djxhhl1p7t", + topRow: 4, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "Label", + type: WidgetTypes.CHECKBOX_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + alignWidget: "LEFT", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 5, + defaultCheckedState: true, + // following properties get added + isDisabled: false, + isRequired: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("RADIO_GROUP_WIDGET", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "RadioGroup1", + rightColumn: 5, + widgetId: "4ixyqnw2no", + topRow: 3, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "", + type: WidgetTypes.RADIO_GROUP_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + parentId: "0", + isLoading: false, + defaultOptionValue: "Y", + parentColumnSpace: 67.375, + leftColumn: 2, + options: [ + { + label: "Yes", + value: "Y", + }, + { + label: "No", + value: "N", + }, + ], + }, + ], + }; + const output = { + ...containerWidget, + children: [ + { + widgetName: "RadioGroup1", + rightColumn: 5, + widgetId: "4ixyqnw2no", + topRow: 3, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "", + type: WidgetTypes.RADIO_GROUP_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + parentId: "0", + isLoading: false, + defaultOptionValue: "Y", + parentColumnSpace: 67.375, + leftColumn: 2, + options: [ + { + label: "Yes", + value: "Y", + }, + { + label: "No", + value: "N", + }, + ], + // following properties get added + isDisabled: false, + isRequired: false, + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); + + it("FILE_PICKER_WIDGET", () => { + const input = { + ...containerWidget, + children: [ + { + widgetName: "FilePicker1", + rightColumn: 5, + isDefaultClickDisabled: true, + widgetId: "fzajyy8qft", + topRow: 4, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "Select Files", + maxFileSize: 5, + type: WidgetTypes.FILE_PICKER_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + fileDataType: "Base64", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 1, + files: [], + maxNumFiles: 1, + }, + ], + }; + + const output = { + ...containerWidget, + children: [ + { + widgetName: "FilePicker1", + rightColumn: 5, + isDefaultClickDisabled: true, + widgetId: "fzajyy8qft", + topRow: 4, + bottomRow: 5, + parentRowSpace: 40, + isVisible: true, + label: "Select Files", + maxFileSize: 5, + type: WidgetTypes.FILE_PICKER_WIDGET, + renderMode: RenderModes.CANVAS, + version: 1, + fileDataType: "Base64", + parentId: "0", + isLoading: false, + parentColumnSpace: 67.375, + leftColumn: 1, + files: [], + maxNumFiles: 1, + // following properties get added + isDisabled: false, + isRequired: false, + allowedFileTypes: [], + }, + ], + }; + + expect(migrateInitialValues(input)).toEqual(output); + }); +}); diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index ef6498b3934..62656657c42 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -546,6 +546,73 @@ export const calculateDynamicHeight = ( return minmumHeight; }; +export const migrateInitialValues = ( + currentDSL: ContainerWidgetProps, +) => { + currentDSL.children = currentDSL.children?.map((child: WidgetProps) => { + if (child.type === WidgetTypes.INPUT_WIDGET) { + child = { + isRequired: false, + isDisabled: false, + resetOnSubmit: false, + ...child, + }; + } else if (child.type === WidgetTypes.DROP_DOWN_WIDGET) { + child = { + isRequired: false, + isDisabled: false, + ...child, + }; + } else if (child.type === WidgetTypes.DATE_PICKER_WIDGET2) { + child = { + minDate: "2001-01-01 00:00", + maxDate: "2041-12-31 23:59", + isRequired: false, + ...child, + }; + } else if (child.type === WidgetTypes.SWITCH_WIDGET) { + child = { + isDisabled: false, + ...child, + }; + } else if (child.type === WidgetTypes.ICON_WIDGET) { + child = { + isRequired: false, + ...child, + }; + } else if (child.type === WidgetTypes.VIDEO_WIDGET) { + child = { + isRequired: false, + isDisabled: false, + ...child, + }; + } else if (child.type === WidgetTypes.CHECKBOX_WIDGET) { + child = { + isDisabled: false, + isRequired: false, + ...child, + }; + } else if (child.type === WidgetTypes.RADIO_GROUP_WIDGET) { + child = { + isDisabled: false, + isRequired: false, + ...child, + }; + } else if (child.type === WidgetTypes.FILE_PICKER_WIDGET) { + child = { + isDisabled: false, + isRequired: false, + allowedFileTypes: [], + ...child, + }; + } else if (child.children && child.children.length > 0) { + child = migrateInitialValues(child); + } + return child; + }); + return currentDSL; +}; + // A rudimentary transform function which updates the DSL based on its version. // A more modular approach needs to be designed. const transformDSL = (currentDSL: ContainerWidgetProps) => { @@ -654,6 +721,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = 18; } + if (currentDSL.version === 18) { + currentDSL = migrateInitialValues(currentDSL); + currentDSL.version = 19; + } + return currentDSL; }; diff --git a/app/client/src/utils/helpers.test.ts b/app/client/src/utils/helpers.test.ts new file mode 100644 index 00000000000..b9a613979cc --- /dev/null +++ b/app/client/src/utils/helpers.test.ts @@ -0,0 +1,86 @@ +import { flattenObject } from "./helpers"; + +describe("flattenObject test", () => { + it("Check if non nested object is returned correctly", () => { + const testObject = { + isVisible: true, + isDisabled: false, + tableData: false, + }; + + expect(flattenObject(testObject)).toStrictEqual(testObject); + }); + + it("Check if nested objects are returned correctly", () => { + const tests = [ + { + input: { + isVisible: true, + isDisabled: false, + tableData: false, + settings: { + color: [ + { + headers: { + left: true, + }, + }, + ], + }, + }, + output: { + isVisible: true, + isDisabled: false, + tableData: false, + "settings.color[0].headers.left": true, + }, + }, + { + input: { + isVisible: true, + isDisabled: false, + tableData: false, + settings: { + color: true, + }, + }, + output: { + isVisible: true, + isDisabled: false, + tableData: false, + "settings.color": true, + }, + }, + { + input: { + numbers: [1, 2, 3], + color: { header: "red" }, + }, + output: { + "numbers[0]": 1, + "numbers[1]": 2, + "numbers[2]": 3, + "color.header": "red", + }, + }, + { + input: { + name: null, + color: { header: {} }, + users: { + id: undefined, + }, + }, + output: { + "color.header": {}, + name: null, + "users.id": undefined, + }, + }, + ]; + + tests.map((test) => + expect(flattenObject(test.input)).toStrictEqual(test.output), + ); + }); +}); diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 6a9f52bf055..5534d658df3 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -287,3 +287,28 @@ export const scrollbarWidth = () => { document.body.removeChild(scrollDiv); return scrollbarWidth; }; + +// Flatten object +// From { isValid: false, settings: { color: false}} +// To { isValid: false, settings.color: false} +export const flattenObject = (data: Record) => { + const result: Record = {}; + function recurse(cur: any, prop: any) { + if (Object(cur) !== cur) { + result[prop] = cur; + } else if (Array.isArray(cur)) { + for (let i = 0, l = cur.length; i < l; i++) + recurse(cur[i], prop + "[" + i + "]"); + if (cur.length == 0) result[prop] = []; + } else { + let isEmpty = true; + for (const p in cur) { + isEmpty = false; + recurse(cur[p], prop ? prop + "." + p : p); + } + if (isEmpty && prop) result[prop] = {}; + } + } + recurse(data, ""); + return result; +}; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index baa8de6de35..0e47cd75799 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -15,6 +15,7 @@ import { CSSUnit, CONTAINER_GRID_PADDING, } from "constants/WidgetConstants"; +import { memoize } from "lodash"; import DraggableComponent from "components/editorComponents/DraggableComponent"; import ResizableComponent from "components/editorComponents/ResizableComponent"; import { WidgetExecuteActionPayload } from "constants/AppsmithActionConstants/ActionConstants"; @@ -35,6 +36,7 @@ import OverlayCommentsWrapper from "comments/inlineComments/OverlayCommentsWrapp import PreventInteractionsOverlay from "components/editorComponents/PreventInteractionsOverlay"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; +import { flattenObject } from "utils/helpers"; /*** * BaseWidget @@ -176,6 +178,10 @@ abstract class BaseWidget< }; } + getErrorCount = memoize((invalidProps) => { + return Object.values(flattenObject(invalidProps)).filter((e) => !!e).length; + }, JSON.stringify); + render() { return this.getWidgetView(); } @@ -209,6 +215,7 @@ abstract class BaseWidget< <> {!this.props.disablePropertyPane && ( { + const errors: EvalError[] = []; + const validatedTree = Object.keys(tree).reduce((tree, entityKey: string) => { const entity = tree[entityKey] as DataTreeWidget; if (!isWidget(entity)) { return tree; @@ -295,6 +298,18 @@ export function getValidatedTree(tree: DataTree) { const safeEvaluatedValue = removeFunctions(evaluatedValue); _.set(parsedEntity, `evaluatedValues.${property}`, safeEvaluatedValue); if (!isValid) { + errors.push({ + type: EvalErrorTypes.WIDGET_PROPERTY_VALIDATION_ERROR, + message: message || "", + context: { + source: { + id: parsedEntity.widgetId, + name: parsedEntity.widgetName, + type: ENTITY_TYPE.WIDGET, + propertyPath: property, + }, + }, + }); _.set(parsedEntity, `invalidProps.${property}`, true); _.set(parsedEntity, `validationMessages.${property}`, message); } else { @@ -304,6 +319,11 @@ export function getValidatedTree(tree: DataTree) { }); return { ...tree, [entityKey]: parsedEntity }; }, tree); + + return { + validatedTree, + errors, + }; } export const getAllPaths = ( From b14f04317280319990d9ea7a57766a92d27aa5dd Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Tue, 18 May 2021 21:40:53 +0530 Subject: [PATCH 30/52] Fix/datasource request headers in api (#4481) * Show datasource headers when used in an API --- .../fields/EmbeddedDatasourcePathField.tsx | 27 +-- .../form/fields/KeyValueFieldArray.tsx | 37 ++-- .../src/pages/Editor/APIEditor/Form.tsx | 163 +++++++++++++++++- 3 files changed, 196 insertions(+), 31 deletions(-) diff --git a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx index 0955c7e8910..49f5d63d399 100644 --- a/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx +++ b/app/client/src/components/editorComponents/form/fields/EmbeddedDatasourcePathField.tsx @@ -12,7 +12,8 @@ import CodeEditor, { import { API_EDITOR_FORM_NAME } from "constants/forms"; import { AppState } from "reducers"; import { connect } from "react-redux"; -import _ from "lodash"; +import get from "lodash/get"; +import merge from "lodash/merge"; import { DEFAULT_DATASOURCE, EmbeddedRestDatasource, @@ -61,19 +62,27 @@ const DatasourceContainer = styled.div` position: relative; width: calc(100% - 155px); `; + +const apiFormValueSelector = formValueSelector(API_EDITOR_FORM_NAME); class EmbeddedDatasourcePathComponent extends React.Component { handleDatasourceUrlUpdate = (datasourceUrl: string) => { const { datasource, orgId, pluginId } = this.props; const urlHasUpdated = datasourceUrl !== datasource.datasourceConfiguration?.url; if (urlHasUpdated) { - this.props.updateDatasource({ - ...DEFAULT_DATASOURCE(pluginId, orgId), + const isDatasourceRemoved = + datasourceUrl.indexOf(datasource.datasourceConfiguration?.url) === -1; + let newDatasource = isDatasourceRemoved + ? { ...DEFAULT_DATASOURCE(pluginId, orgId) } + : { ...datasource }; + newDatasource = { + ...newDatasource, datasourceConfiguration: { - ...datasource.datasourceConfiguration, + ...newDatasource.datasourceConfiguration, url: datasourceUrl, }, - }); + }; + this.props.updateDatasource(newDatasource); } }; @@ -95,7 +104,7 @@ class EmbeddedDatasourcePathComponent extends React.Component { path: "", }; } - if ("id" in datasource && datasource.id) { + if (datasource && datasource.hasOwnProperty("id")) { const datasourceUrl = datasource.datasourceConfiguration.url; if (value.includes(datasourceUrl)) { return { @@ -213,7 +222,7 @@ class EmbeddedDatasourcePathComponent extends React.Component { datasource, input: { value }, } = this.props; - const datasourceUrl = _.get(datasource, "datasourceConfiguration.url", ""); + const datasourceUrl = get(datasource, "datasourceConfiguration.url", ""); const displayValue = `${datasourceUrl}${value}`; const input = { ...this.props.input, @@ -261,8 +270,6 @@ class EmbeddedDatasourcePathComponent extends React.Component { } } -const apiFormValueSelector = formValueSelector(API_EDITOR_FORM_NAME); - const mapStateToProps = ( state: AppState, ownProps: { pluginId: string }, @@ -275,7 +282,7 @@ const mapStateToProps = ( (d) => d.id === datasourceFromAction.id, ); if (datasourceFromDataSourceList) { - datasourceMerged = _.merge( + datasourceMerged = merge( {}, datasourceFromAction, datasourceFromDataSourceList, diff --git a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx index 514a02a0169..83a488a1c25 100644 --- a/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx +++ b/app/client/src/components/editorComponents/form/fields/KeyValueFieldArray.tsx @@ -14,8 +14,12 @@ import { import Text, { Case, TextType } from "components/ads/Text"; import { Classes } from "components/ads/common"; -const KeyValueStackContainer = styled.div` - padding: ${(props) => props.theme.spaces[4]}px +type CustomStack = { + removeTopPadding?: boolean; +}; + +const KeyValueStackContainer = styled.div` + padding: ${(props) => (props.removeTopPadding ? 0 : props.theme.spaces[4])}px ${(props) => props.theme.spaces[14]}px ${(props) => props.theme.spaces[11] + 1}px ${(props) => props.theme.spaces[11] + 2}px; @@ -89,19 +93,21 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) { }, [props.fields, props.pushFields]); return ( - - - - - Key - - - - - Value - - - + + {!props.hideHeader && ( + + + + Key + + + + + Value + + + + )} {props.fields.length > 0 && ( <> {props.fields.map((field: any, index: number) => { @@ -224,6 +230,7 @@ type Props = { placeholder?: string; pushFields?: boolean; dataTreePath?: string; + hideHeader?: boolean; theme?: EditorTheme; }; diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index 67dd533dcef..bf56507d335 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -37,6 +37,8 @@ import { createMessage, WIDGET_BIND_HELP } from "constants/messages"; import AnalyticsUtil from "utils/AnalyticsUtil"; import CloseEditor from "components/editorComponents/CloseEditor"; import { useParams } from "react-router"; +import get from "lodash/get"; +import { Icon as ButtonIcon } from "@blueprintjs/core"; const Form = styled.form` display: flex; @@ -163,7 +165,6 @@ const Link = styled.a` margin-left: ${(props) => props.theme.spaces[1] + 1}px; } `; - interface APIFormProps { pluginId: string; onRunClick: (paginationField?: PaginationField) => void; @@ -174,6 +175,7 @@ interface APIFormProps { appName: string; httpMethodFromForm: string; actionConfigurationHeaders?: any; + datasourceHeaders?: any; actionName: string; apiId: string; apiName: string; @@ -195,8 +197,98 @@ export const NameWrapper = styled.div` } `; +export const ShowHideImportedHeaders = styled.button` + background: #ebebeb; + color: #4b4848; + padding: 3px 5px; + border: none; + display: flex; + align-items: center; + cursor: pointer; + font-size: 12px; + height: 20px; +`; + +const Flex = styled.div<{ size: number }>` + flex: ${(props) => props.size}; + ${(props) => + props.size === 3 + ? ` + margin-left: ${props.theme.spaces[4]}px; + ` + : null}; + width: 100%; + position: relative; + min-height: 32px; + height: auto; + background-color: #fafafa; + border-color: #d3dee3; + border-bottom: 1px solid #e8e8e8; + color: #4b4848; + display: flex; + align-items: center; +`; + +const FlexContainer = styled.div` + display: flex; + align-items: center; + width: calc(100% - 30px); + + .key-value { + padding: ${(props) => props.theme.spaces[2]}px 0px + ${(props) => props.theme.spaces[2]}px + ${(props) => props.theme.spaces[1]}px; + .${Classes.TEXT} { + color: ${(props) => props.theme.colors.apiPane.text}; + } + } + .key-value:nth-child(2) { + margin-left: ${(props) => props.theme.spaces[4]}px; + } + .disabled { + background: #e8e8e8; + } +`; + +const KeyValueStackContainer = styled.div` + padding: ${(props) => props.theme.spaces[4]}px + ${(props) => props.theme.spaces[14]}px 0 + ${(props) => props.theme.spaces[11] + 2}px; +`; +const FormRowWithLabel = styled(FormRow)` + flex-wrap: wrap; + ${FormLabel} { + width: 100%; + } + & svg { + cursor: pointer; + } +`; + +function ImportedHeaders(props: { headers: any }) { + return ( + <> + {props.headers.map((header: any, index: number) => { + return ( + + + + {header.key} + + + {header.value} + + + + ); + })} + + ); +} + function ApiEditorForm(props: Props) { const [selectedIndex, setSelectedIndex] = useState(0); + const [showInheritedAttributes, toggleInheritedAttributes] = useState(false); const [ apiBindHelpSectionVisible, setApiBindHelpSectionVisible, @@ -327,9 +419,54 @@ function ApiEditorForm(props: Props) { /> )} + {props.datasourceHeaders.length > 0 && ( + + { + e.preventDefault(); + toggleInheritedAttributes(!showInheritedAttributes); + }} + > + +    + + {showInheritedAttributes + ? "Showing inherited headers" + : `${props.datasourceHeaders.length} headers`} + + + + )} + {props.datasourceHeaders.length > 0 && ( + + + + + + Key + + + + + Value + + + + + {showInheritedAttributes && ( + + )} + + )} { const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod"); - const actionConfigurationHeaders = selector( - state, - "actionConfiguration.headers", - ); + const actionConfigurationHeaders = + selector(state, "actionConfiguration.headers") || []; + let datasourceFromAction = selector(state, "datasource"); + if (datasourceFromAction && datasourceFromAction.hasOwnProperty("id")) { + datasourceFromAction = state.entities.datasources.list.find( + (d) => d.id === datasourceFromAction.id, + ); + } + const datasourceHeaders = + get(datasourceFromAction, "datasourceConfiguration.headers") || []; const apiId = selector(state, "id"); const actionName = getApiName(state, apiId) || ""; const headers = selector(state, "actionConfiguration.headers"); @@ -420,7 +563,14 @@ export default connect((state: AppState) => { const validHeaders = headers.filter( (value) => value.key && value.key !== "", ); - headersCount = validHeaders.length; + headersCount += validHeaders.length; + } + + if (Array.isArray(datasourceHeaders)) { + const validHeaders = datasourceHeaders.filter( + (value: any) => value.key && value.key !== "", + ); + headersCount += validHeaders.length; } const params = selector(state, "actionConfiguration.queryParameters"); @@ -437,6 +587,7 @@ export default connect((state: AppState) => { apiId, httpMethodFromForm, actionConfigurationHeaders, + datasourceHeaders, headersCount, paramsCount, hintMessages, From 8c8141650a78f484346486dcc9ec96b53e7a7127 Mon Sep 17 00:00:00 2001 From: Ashok Kumar M <35134347+marks0351@users.noreply.github.com> Date: Tue, 18 May 2021 23:59:39 +0530 Subject: [PATCH 31/52] Feature: Widget grouping Phase I (Multi select and Bulk Delete) + Canvas Enhancements. (#4219) * Feature: Canvas layer enhancements(DIP) * feedback fixes * fixing build * dip * dip * dip * fixing build * dip * dev fixes * dip * Fixing top bottom resize handles * dip * reposition widget name on top edges. * dip * dip * dip * dip * renaming selectedWidget to lastSelectedWidget * code clean up * Fixing list widget as per grid scale. * Fixing existing specs. * Adding migration test cases. * dip * FIxing proppane in modal. * fixing modal z-indedx. * fix for modal name. * dip * dip * dip * adding test cases for hotkeys. * dip * dip * fixing build * Trying some performance improvements for jests. * 17 mins with runinband lets try without it. * minor bug fixes. * code clean up * save migrated app on fetch. * fixing few cypress tests * fixing cypress tests * fixing cypress tests. * fixing cypress * updated DSL * Addressing code review comments. * test fails * dip * eslint fixes. * fixing debugger cypress tests. * updating latest page version. * updating migration changes to cypress dsl's. * updating chart data fixes for cypress tests. Co-authored-by: Apple --- .../cypress/fixtures/CanvasResizeDsl.json | 2 +- app/client/cypress/fixtures/Mapdsl.json | 2 +- .../cypress/fixtures/chartUpdatedDsl.json | 184 +++++++++++ .../cypress/fixtures/displayWidgetDsl.json | 1 - app/client/cypress/fixtures/formdsl.json | 2 +- .../Applications/DuplicateApplication_spec.js | 1 + .../Applications/ForkApplication_spec.js | 1 + .../ClientSideTests/Debugger/Logs_spec.js | 1 + .../DisplayWidgets/Chart_spec.js | 8 +- .../DisplayWidgets/Modal_spec.js | 2 +- .../Entity_Explorer_DragAndDropWidget_spec.js | 3 +- ...y_Paste_Delete_Undo_Keyboard_Event_spec.js | 3 +- .../GlobalSearch/GlobalSearch_spec.js | 4 +- .../Onboarding/Onboarding_spec.js | 2 +- app/client/cypress/support/commands.js | 8 +- app/client/jest.config.js | 3 + app/client/src/actions/pageActions.tsx | 6 + app/client/src/actions/widgetActions.tsx | 20 +- .../appsmith/PositionedContainer.tsx | 16 +- .../designSystems/appsmith/TabsComponent.tsx | 2 +- .../blueprint/ModalComponent.tsx | 3 +- .../editorComponents/Debugger/index.tsx | 2 + .../editorComponents/DragLayerComponent.tsx | 5 +- .../editorComponents/DraggableComponent.tsx | 24 +- .../editorComponents/DropTargetComponent.tsx | 9 +- .../components/editorComponents/Dropzone.tsx | 3 + .../editorComponents/ResizableComponent.tsx | 17 +- .../components/editorComponents/Sidebar.tsx | 3 +- .../WidgetNameComponent/SettingsControl.tsx | 1 + .../WidgetNameComponent/index.tsx | 35 +- app/client/src/constants/Colors.tsx | 1 + app/client/src/constants/DefaultTheme.tsx | 4 +- app/client/src/constants/Layers.tsx | 17 +- .../src/constants/ReduxActionConstants.tsx | 4 + app/client/src/constants/WidgetConstants.tsx | 12 +- app/client/src/constants/messages.ts | 4 + .../mockResponses/WidgetConfigResponse.tsx | 191 +++++++---- app/client/src/pages/Editor/Canvas.tsx | 6 +- .../Editor/Explorer/Widgets/WidgetEntity.tsx | 2 +- .../Editor/Explorer/Widgets/WidgetGroup.tsx | 2 +- .../src/pages/Editor/GlobalHotKeys.test.tsx | 105 ++++++ app/client/src/pages/Editor/GlobalHotKeys.tsx | 57 +++- .../src/pages/Editor/PropertyPane/index.tsx | 4 +- app/client/src/pages/Editor/WidgetCard.tsx | 16 +- app/client/src/pages/Editor/WidgetsEditor.tsx | 8 +- .../reducers/uiReducers/dragResizeReducer.ts | 49 ++- app/client/src/sagas/OnboardingSagas.ts | 19 +- app/client/src/sagas/PageSagas.tsx | 15 +- app/client/src/sagas/WidgetOperationSagas.tsx | 307 +++++++++++++----- app/client/src/sagas/index.tsx | 73 +++-- app/client/src/sagas/selectors.tsx | 22 +- .../src/selectors/propertyPaneSelectors.tsx | 23 +- app/client/src/selectors/ui.ts | 7 + app/client/src/selectors/ui.tsx | 4 - app/client/src/templates/default.ts | 4 +- app/client/src/utils/AppsmithUtils.tsx | 4 + .../src/utils/WidgetPropsUtils.test.tsx | 67 ++++ app/client/src/utils/WidgetPropsUtils.tsx | 35 ++ .../src/utils/hooks/dragResizeHooks.tsx | 12 +- .../src/utils/hooks/useClickOpenPropPane.tsx | 23 +- app/client/src/widgets/BaseWidget.tsx | 1 + app/client/src/widgets/InputWidget.tsx | 7 +- .../widgets/ListWidget/ListWidget.test.tsx | 3 +- .../src/widgets/ListWidget/ListWidget.tsx | 6 +- app/client/src/widgets/ModalWidget.tsx | 6 +- .../src/widgets/Tabs/TabsWidget.test.tsx | 39 +-- app/client/src/widgets/Tabs/TabsWidget.tsx | 6 +- .../test/factories/WidgetFactoryUtils.ts | 7 + app/client/test/sagas.ts | 59 ++++ app/client/test/setup.ts | 26 +- app/client/test/testCommon.tsx | 81 +++++ app/client/test/testUtils.tsx | 44 ++- 72 files changed, 1396 insertions(+), 359 deletions(-) create mode 100644 app/client/cypress/fixtures/chartUpdatedDsl.json create mode 100644 app/client/src/pages/Editor/GlobalHotKeys.test.tsx create mode 100644 app/client/src/selectors/ui.ts delete mode 100644 app/client/src/selectors/ui.tsx create mode 100644 app/client/test/sagas.ts create mode 100644 app/client/test/testCommon.tsx diff --git a/app/client/cypress/fixtures/CanvasResizeDsl.json b/app/client/cypress/fixtures/CanvasResizeDsl.json index 0ade4a1ac28..f26b52f98f3 100644 --- a/app/client/cypress/fixtures/CanvasResizeDsl.json +++ b/app/client/cypress/fixtures/CanvasResizeDsl.json @@ -7,7 +7,7 @@ "detachFromLayout": true, "widgetId": "0", "topRow": 0, - "bottomRow": 2960, + "bottomRow": 740, "containerStyle": "none", "snapRows": 33, "parentRowSpace": 1, diff --git a/app/client/cypress/fixtures/Mapdsl.json b/app/client/cypress/fixtures/Mapdsl.json index 490ab5f87bf..2f75e8c8607 100644 --- a/app/client/cypress/fixtures/Mapdsl.json +++ b/app/client/cypress/fixtures/Mapdsl.json @@ -144,7 +144,7 @@ "parentRowSpace": 38, "leftColumn": 3, "rightColumn": 11, - "topRow": 0, + "topRow": 1, "bottomRow": 12, "parentId": "yt4ouwn0sk", "widgetId": "673etzjyv9", diff --git a/app/client/cypress/fixtures/chartUpdatedDsl.json b/app/client/cypress/fixtures/chartUpdatedDsl.json new file mode 100644 index 00000000000..20a57d6a5ca --- /dev/null +++ b/app/client/cypress/fixtures/chartUpdatedDsl.json @@ -0,0 +1,184 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 966, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 5168, + "containerStyle": "none", + "snapRows": 129, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 20, + "minHeight": 450, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container1", + "rightColumn": 32, + "orientation": "VERTICAL", + "snapColumns": 16, + "widgetId": "mxbaasg65u", + "containerStyle": "card", + "topRow": 0, + "bottomRow": 36, + "parentRowSpace": 38, + "isVisible": true, + "type": "CONTAINER_WIDGET", + "version": 1, + "isLoading": false, + "parentColumnSpace": 75.25, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "59gdivzv7s", + "rightColumn": 2408, + "orientation": "VERTICAL", + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "bxekwxgc1i", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 20, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "dynamicBindingPathList": [], + "children": [] + } + ] + }, + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container3", + "rightColumn": 64, + "orientation": "VERTICAL", + "snapColumns": 16, + "widgetId": "i331vll2mg", + "containerStyle": "card", + "topRow": 36, + "bottomRow": 92, + "parentRowSpace": 38, + "isVisible": true, + "type": "CONTAINER_WIDGET", + "version": 1, + "isLoading": false, + "parentColumnSpace": 75.25, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "rhfg2vf1n5", + "rightColumn": 4816, + "orientation": "VERTICAL", + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "rglduihhzk", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 20, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "dynamicBindingPathList": [], + "children": [ + { + "isVisible": true, + "widgetName": "Chart1", + "chartType": "LINE_CHART", + "chartName": "Last week's revenue", + "allowHorizontalScroll": false, + "version": 1, + "chartData": { + "q4pm3w97mo": { + "seriesName": "Sales", + "data": "" + } + }, + "xAxisName": "Last Week", + "yAxisName": "Total Order Revenue $", + "type": "CHART_WIDGET", + "isLoading": false, + "parentColumnSpace": 14.59375, + "parentRowSpace": 10, + "leftColumn": 20, + "rightColumn": 44, + "topRow": 16, + "bottomRow": 48, + "parentId": "rglduihhzk", + "widgetId": "snzfh3qjo8", + "dynamicBindingPathList": [], + "dynamicTriggerPathList": [] + } + ] + } + ] + }, + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container4", + "rightColumn": 64, + "orientation": "VERTICAL", + "snapColumns": 16, + "widgetId": "qznzsquf70", + "containerStyle": "card", + "topRow": 0, + "bottomRow": 36, + "parentRowSpace": 38, + "isVisible": true, + "type": "CONTAINER_WIDGET", + "version": 1, + "isLoading": false, + "parentColumnSpace": 75.25, + "dynamicBindingPathList": [], + "leftColumn": 32, + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "3bn6uv0vy4", + "rightColumn": 2408, + "orientation": "VERTICAL", + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "7vm5mdu8ey", + "containerStyle": "none", + "topRow": 0, + "bottomRow": 1368, + "parentRowSpace": 1, + "isVisible": true, + "type": "CANVAS_WIDGET", + "canExtend": false, + "version": 1, + "isLoading": false, + "parentColumnSpace": 1, + "leftColumn": 0, + "dynamicBindingPathList": [], + "children": [ + null + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/fixtures/displayWidgetDsl.json b/app/client/cypress/fixtures/displayWidgetDsl.json index 01da343433c..e2fbfa257b7 100644 --- a/app/client/cypress/fixtures/displayWidgetDsl.json +++ b/app/client/cypress/fixtures/displayWidgetDsl.json @@ -14,7 +14,6 @@ "type": "CANVAS_WIDGET", "canExtend": true, "dynamicBindingPathList": [], - "version": 4, "minHeight": 1292, "parentColumnSpace": 1, "leftColumn": 0, diff --git a/app/client/cypress/fixtures/formdsl.json b/app/client/cypress/fixtures/formdsl.json index 866f2a02550..f8859c48a55 100644 --- a/app/client/cypress/fixtures/formdsl.json +++ b/app/client/cypress/fixtures/formdsl.json @@ -199,7 +199,7 @@ "backgroundColor": "Gray", "rightColumn": 11, "widgetId": "z62mnh15y5", - "topRow": 0, + "topRow": 1, "bottomRow": 13, "parentRowSpace": 38, "isVisible": true, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js index e824916c332..31427f6462f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/DuplicateApplication_spec.js @@ -6,6 +6,7 @@ let duplicateApplicationDsl; describe("Duplicate application", function() { before(() => { + dsl.dsl.version = 20; // latest migrated version cy.addDsl(dsl); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js index f0d29008c1b..4defd83f1be 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/ForkApplication_spec.js @@ -5,6 +5,7 @@ let forkedApplicationDsl; describe("Fork application across orgs", function() { before(() => { + dsl.dsl.version = 20; // latest migrated version cy.addDsl(dsl); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js index 992fe23a683..50309718f13 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Debugger/Logs_spec.js @@ -15,6 +15,7 @@ describe("Debugger logs", function() { }); it("Reset debugger state", function() { + cy.openPropertyPane("buttonwidget"); cy.get(".t--property-control-visible") .find(".t--js-toggle") .click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js index 261a1741fe2..91abacd7e75 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Chart_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../../locators/ViewWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); -const dsl = require("../../../../fixtures/displayWidgetDsl.json"); +const dsl = require("../../../../fixtures/chartUpdatedDsl.json"); const pages = require("../../../../locators/Pages.json"); describe("Chart Widget Functionality", function() { @@ -43,7 +43,11 @@ describe("Chart Widget Functionality", function() { cy.get(viewWidgetsPage.chartType) .last() .should("have.text", "Column Chart"); - cy.testJsontext("chartseries", JSON.stringify(this.data.chartInput)); + cy.testJsontext( + "chart-series-data-control", + JSON.stringify(this.data.chartInput), + ); + cy.get(".t--propertypane").click("right"); cy.get(viewWidgetsPage.chartWidget) .should("be.visible") .and((chart) => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js index ba257c5ae2e..e36c3f6648d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Modal_spec.js @@ -8,7 +8,7 @@ describe("Modal Widget Functionality", function() { it("Add new Modal", () => { cy.get(explorer.addWidget).click(); - cy.dragAndDropToCanvas("modalwidget", { x: 300, y: -300 }); + cy.dragAndDropToCanvas("modalwidget", { x: 300, y: 300 }); cy.get(".t--modal-widget").should("exist"); }); 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 a2d387bd313..06b63c3340c 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 @@ -15,7 +15,8 @@ describe("Entity explorer Drag and Drop widgets testcases", function() { cy.get(commonlocators.entityExplorersearch) .clear() .type("form"); - cy.dragAndDropToCanvas("formwidget", { x: 300, y: -300 }); + cy.dragAndDropToCanvas("formwidget", { x: 300, y: 80 }); + cy.get(formWidgetsPage.formD).click(); /** * @param{Text} Random Text * @param{FormWidget}Mouseover 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 79701336e50..2a4774093a3 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 @@ -28,7 +28,8 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { cy.wait(500); cy.get(commonlocators.toastBody) .first() - .contains("Copied"); + .contains("Copied") + .click(); cy.get("body").type(`{${modifierKey}}v`, { force: true }); cy.wait("@updateLayout").should( "have.nested.property", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js index 3f60f9e8194..b6fabfd216a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GlobalSearch/GlobalSearch_spec.js @@ -42,8 +42,8 @@ describe("GlobalSearch", function() { .its("store") .invoke("getState") .then((state) => { - const { selectedWidget } = state.ui.widgetDragResize; - expect(selectedWidget).to.be.equal(table.widgetId); + const { lastSelectedWidget } = state.ui.widgetDragResize; + expect(lastSelectedWidget).to.be.equal(table.widgetId); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js index d8712160d26..98de0012e71 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Onboarding/Onboarding_spec.js @@ -50,7 +50,7 @@ describe("Onboarding", function() { // Add widget cy.get(".t--add-widget").click(); - cy.dragAndDropToCanvas("tablewidget", { x: 30, y: -30 }); + cy.dragAndDropToCanvas("tablewidget", { x: 360, y: 20 }); // wait for animation duration // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 9e4d949e445..5b8e20e9333 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2011,8 +2011,8 @@ Cypress.Commands.add("dragAndDropToCanvas", (widgetType, { x, y }) => { .trigger("mousedown", { button: 0 }, { force: true }) .trigger("mousemove", x, y, { force: true }); cy.get(explorer.dropHere) - .click() - .trigger("mouseup", { force: true }); + .click(x, y + 20) + .trigger("mouseup", x, y + 20, { force: true }); }); Cypress.Commands.add("executeDbQuery", (queryName) => { @@ -2035,7 +2035,9 @@ Cypress.Commands.add("openPropertyPane", (widgetType) => { .first() .trigger("mouseover", { force: true }) .wait(500); - cy.get(`${selector}:first-of-type .t--widget-propertypane-toggle`) + cy.get( + `${selector}:first-of-type .t--widget-propertypane-toggle > .t--widget-name`, + ) .first() .click({ force: true }); // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/app/client/jest.config.js b/app/client/jest.config.js index c88bf5f8ec1..f390ccddc20 100644 --- a/app/client/jest.config.js +++ b/app/client/jest.config.js @@ -27,6 +27,9 @@ module.exports = { "test/(.*)": "/test/$1", }, globals: { + "ts-jest": { + isolatedModules: true, + }, APPSMITH_FEATURE_CONFIGS: { sentry: { dsn: parseConfig("__APPSMITH_SENTRY_DSN__"), diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index f454c0349df..3bf4ab40ab0 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -203,6 +203,12 @@ export type WidgetDelete = { isShortcut?: boolean; }; +export type MultipleWidgetDeletePayload = { + widgetIds: string[]; + disallowUndo?: boolean; + isShortcut?: boolean; +}; + export type WidgetResize = { widgetId: string; leftColumn: number; diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index 03e1f59548b..be7a5674d48 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -65,11 +65,27 @@ export const focusWidget = ( export const selectWidget = ( widgetId?: string, -): ReduxAction<{ widgetId?: string }> => ({ + isMultiSelect?: boolean, +): ReduxAction<{ widgetId?: string; isMultiSelect?: boolean }> => ({ type: ReduxActionTypes.SELECT_WIDGET, - payload: { widgetId }, + payload: { widgetId, isMultiSelect }, }); +export const selectAllWidgets = ( + widgetIds?: string[], +): ReduxAction<{ widgetIds?: string[] }> => { + return { + type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS, + payload: { widgetIds }, + }; +}; + +export const selectAllWidgetsInit = () => { + return { + type: ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT, + }; +}; + export const showModal = (id: string) => { return { type: ReduxActionTypes.SHOW_MODAL, diff --git a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx index db47d63a731..141b14ce284 100644 --- a/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx +++ b/app/client/src/components/designSystems/appsmith/PositionedContainer.tsx @@ -1,9 +1,11 @@ -import React, { CSSProperties, ReactNode, useMemo } from "react"; +import React, { CSSProperties, ReactNode, useCallback, useMemo } from "react"; import { BaseStyle } from "widgets/BaseWidget"; import { WIDGET_PADDING } from "constants/WidgetConstants"; import { generateClassName } from "utils/generators"; import styled from "styled-components"; import { useClickOpenPropPane } from "utils/hooks/useClickOpenPropPane"; +import { stopEventPropagation } from "utils/AppsmithUtils"; +import { Layers } from "constants/Layers"; const PositionedWidget = styled.div` &:hover { @@ -42,14 +44,24 @@ export function PositionedContainer(props: PositionedContainerProps) { height: props.style.componentHeight + (props.style.heightUnit || "px"), width: props.style.componentWidth + (props.style.widthUnit || "px"), padding: padding + "px", + zIndex: Layers.positionedWidget, + backgroundColor: "inherit", }; }, [props.style]); + const openPropPane = useCallback((e) => openPropertyPane(e, props.widgetId), [ + props.widgetId, + openPropertyPane, + ]); + return ( diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx index 75a541f9057..db529b820ca 100644 --- a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx @@ -44,7 +44,7 @@ const TabsContainerWrapper = styled.div<{ `; const ChildrenWrapper = styled.div` - height: 100%; + height: calc(100% - 40px); width: 100%; position: relative; background: ${(props) => props.theme.colors.builderBodyBG}; diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx index e287d0662ef..7057751358a 100644 --- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx @@ -2,6 +2,7 @@ import React, { ReactNode, RefObject, useRef, useEffect } from "react"; import { Overlay, Classes } from "@blueprintjs/core"; import styled from "styled-components"; import { getCanvasClassName } from "utils/generators"; +import { Layers } from "constants/Layers"; const Container = styled.div<{ width: number; @@ -80,7 +81,7 @@ export function ModalComponent(props: ModalComponentProps) { left={props.left} top={props.top} width={props.width} - zIndex={props.zIndex !== undefined ? props.zIndex : 2} + zIndex={props.zIndex !== undefined ? props.zIndex : Layers.modalWidget} > ` + z-index: ${Layers.debugger}; background-color: ${(props) => props.theme.colors.debugger.floatingButton.background}; position: fixed; diff --git a/app/client/src/components/editorComponents/DragLayerComponent.tsx b/app/client/src/components/editorComponents/DragLayerComponent.tsx index cba0ce3b780..a4c82fa59e4 100644 --- a/app/client/src/components/editorComponents/DragLayerComponent.tsx +++ b/app/client/src/components/editorComponents/DragLayerComponent.tsx @@ -31,8 +31,9 @@ const WrappedDragLayer = styled.div<{ ); background-size: ${(props) => props.columnWidth}px ${(props) => props.rowHeight}px; - background-position: -${(props) => props.columnWidth / 2}px -${(props) => - props.rowHeight / 2}px; + background-position: -${(props) => props.columnWidth / 2 - 3.5}px -${( + props, + ) => props.rowHeight / 2 - 1.5}px; `; type DragLayerProps = { diff --git a/app/client/src/components/editorComponents/DraggableComponent.tsx b/app/client/src/components/editorComponents/DraggableComponent.tsx index 6f20eedd6b2..60f334bf90d 100644 --- a/app/client/src/components/editorComponents/DraggableComponent.tsx +++ b/app/client/src/components/editorComponents/DraggableComponent.tsx @@ -68,9 +68,12 @@ function DraggableComponent(props: DraggableComponentProps) { // This state tells us which widget is selected // The value is the widgetId of the selected widget const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, ); + const selectedWidgets = useSelector( + (state: AppState) => state.ui.widgetDragResize.selectedWidgets, + ); // This state tels us which widget is focused // The value is the widgetId of the focused widget. const focusedWidget = useSelector( @@ -142,16 +145,6 @@ function DraggableComponent(props: DraggableComponentProps) { // True when any widget is dragging or resizing, including this one const isResizingOrDragging = !!isResizing || !!isDragging; - // When the draggable is clicked - const handleClick = (e: any) => { - if (!isResizingOrDragging) { - selectWidget && - selectedWidget !== props.widgetId && - selectWidget(props.widgetId); - } - e.stopPropagation(); - }; - // When mouse is over this draggable const handleMouseOver = (e: any) => { focusWidget && @@ -160,7 +153,9 @@ function DraggableComponent(props: DraggableComponentProps) { focusWidget(props.widgetId); e.stopPropagation(); }; - + const shouldRenderComponent = !( + selectedWidgets.includes(props.widgetId) && isDragging + ); // Display this draggable based on the current drag state const style: CSSProperties = { display: isCurrentWidgetDragging ? "none" : "block", @@ -187,14 +182,9 @@ function DraggableComponent(props: DraggableComponentProps) { const className = `${classNameForTesting}`; - const shouldRenderComponent = !( - selectedWidget === props.widgetId && isDragging - ); - return ( state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, ); const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, @@ -99,7 +99,7 @@ export function DropTargetComponent(props: DropTargetComponentProps) { const [rows, setRows] = useState(snapRows); const showPropertyPane = useShowPropertyPane(); - const { focusWidget, selectWidget } = useWidgetSelection(); + const { deselectAll, focusWidget, selectWidget } = useWidgetSelection(); const updateCanvasSnapRows = useCanvasSnapRowsUpdateHook(); useEffect(() => { @@ -237,12 +237,9 @@ export function DropTargetComponent(props: DropTargetComponentProps) { const handleFocus = (e: any) => { if (!isResizing && !isDragging) { if (!props.parentId) { - selectWidget && selectWidget(props.widgetId); + deselectAll(); focusWidget && focusWidget(props.widgetId); showPropertyPane && showPropertyPane(); - } else { - selectWidget && selectWidget(props.parentId); - focusWidget && focusWidget(props.parentId); } } // commenting this out to allow propagation of click events diff --git a/app/client/src/components/editorComponents/Dropzone.tsx b/app/client/src/components/editorComponents/Dropzone.tsx index 29a83003d35..78cd278b8cb 100644 --- a/app/client/src/components/editorComponents/Dropzone.tsx +++ b/app/client/src/components/editorComponents/Dropzone.tsx @@ -4,6 +4,7 @@ import styled from "styled-components"; import { snapToGrid } from "utils/helpers"; import { IntentColors } from "constants/DefaultTheme"; import { useSpring, animated, interpolate, config } from "react-spring"; +import { Layers } from "constants/Layers"; const SPRING_CONFIG = { ...config.gentle, @@ -124,6 +125,7 @@ export const DropZone = forwardRef( height={props.height * props.parentRowHeight} ref={ref} style={{ + zIndex: Layers.animatedDropZone, transform: interpolate( [X, Y], (x: number, y: number) => `translate3d(${x}px,${y}px,0)`, @@ -135,6 +137,7 @@ export const DropZone = forwardRef( candrop={props.canDrop} height={props.height * props.parentRowHeight} style={{ + zIndex: Layers.animatedSnappingDropZone, transform: interpolate( [snappedX, snappedY], (x: number, y: number) => `translate3d(${x}px,${y}px,0)`, diff --git a/app/client/src/components/editorComponents/ResizableComponent.tsx b/app/client/src/components/editorComponents/ResizableComponent.tsx index 5fc2e4edc60..493e1d15e79 100644 --- a/app/client/src/components/editorComponents/ResizableComponent.tsx +++ b/app/client/src/components/editorComponents/ResizableComponent.tsx @@ -22,7 +22,7 @@ import { import { useSelector } from "react-redux"; import { AppState } from "reducers"; import Resizable from "resizable"; -import { isDropZoneOccupied, getSnapColumns } from "utils/WidgetPropsUtils"; +import { getSnapColumns, isDropZoneOccupied } from "utils/WidgetPropsUtils"; import { VisibilityContainer, LeftHandleStyles, @@ -58,7 +58,10 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => { const { selectWidget } = useWidgetSelection(); const { setIsResizing } = useWidgetDragResize(); const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, + ); + const selectedWidgets = useSelector( + (state: AppState) => state.ui.widgetDragResize.selectedWidgets, ); const focusedWidget = useSelector( (state: AppState) => state.ui.widgetDragResize.focusedWidget, @@ -70,6 +73,7 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => { const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, ); + const occupiedSpacesBySiblingWidgets = occupiedSpaces && props.parentId && occupiedSpaces[props.parentId] ? occupiedSpaces[props.parentId] @@ -77,7 +81,9 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => { // isFocused (string | boolean) -> isWidgetFocused (boolean) const isWidgetFocused = - focusedWidget === props.widgetId || selectedWidget === props.widgetId; + focusedWidget === props.widgetId || + selectedWidget === props.widgetId || + selectedWidgets.includes(props.widgetId); // Calculate the dimensions of the widget, // The ResizableContainer's size prop is controlled @@ -139,9 +145,10 @@ export const ResizableComponent = memo((props: ResizableComponentProps) => { return true; } + // Minimum row and columns to be set to a widget. if ( - newRowCols.rightColumn - newRowCols.leftColumn < 1 || - newRowCols.bottomRow - newRowCols.topRow < 1 + newRowCols.rightColumn - newRowCols.leftColumn < 2 || + newRowCols.bottomRow - newRowCols.topRow < 4 ) { return true; } diff --git a/app/client/src/components/editorComponents/Sidebar.tsx b/app/client/src/components/editorComponents/Sidebar.tsx index 8282baf2ff3..73093e7658e 100644 --- a/app/client/src/components/editorComponents/Sidebar.tsx +++ b/app/client/src/components/editorComponents/Sidebar.tsx @@ -7,12 +7,13 @@ import * as Sentry from "@sentry/react"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { Layers } from "constants/Layers"; const SidebarWrapper = styled.div` background-color: ${Colors.MINE_SHAFT}; padding: 0; width: ${(props) => props.theme.sidebarWidth}; - z-index: 3; + z-index: ${Layers.sideBar}; color: ${(props) => props.theme.colors.textOnDarkBG}; overflow-y: auto; diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx index 6e3dfc22555..e0409bc3157 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/SettingsControl.tsx @@ -119,6 +119,7 @@ export function SettingsControl(props: SettingsControlProps) { > diff --git a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx index 98d46c85d12..c4e1d5473c3 100644 --- a/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx +++ b/app/client/src/components/editorComponents/WidgetNameComponent/index.tsx @@ -9,19 +9,20 @@ import { useWidgetSelection, } from "utils/hooks/dragResizeHooks"; import AnalyticsUtil from "utils/AnalyticsUtil"; -import { WidgetType } from "constants/WidgetConstants"; +import { WidgetType, WidgetTypes } from "constants/WidgetConstants"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; -const PositionStyle = styled.div` +const PositionStyle = styled.div<{ topRow: number }>` position: absolute; - top: -${(props) => props.theme.spaces[10]}px; + top: ${(props) => + props.topRow > 2 ? `${-1 * props.theme.spaces[10]}px` : "calc(100%)"}; height: ${(props) => props.theme.spaces[10]}px; - width: 100%; - left: 0; + right: 0; display: flex; padding: 0 4px; + cursor: pointer; `; const ControlGroup = styled.div` @@ -41,6 +42,7 @@ type WidgetNameComponentProps = { parentId?: string; type: WidgetType; showControls?: boolean; + topRow: number; errorCount: number; }; @@ -52,7 +54,10 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { (state: AppState) => state.ui.propertyPane, ); const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, + ); + const selectedWidgets = useSelector( + (state: AppState) => state.ui.widgetDragResize.selectedWidgets, ); const focusedWidget = useSelector( (state: AppState) => state.ui.widgetDragResize.focusedWidget, @@ -91,25 +96,35 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) { e.preventDefault(); e.stopPropagation(); }; + const showAsSelected = + selectedWidget === props.widgetId || + selectedWidgets.includes(props.widgetId); const showWidgetName = props.showControls || - ((focusedWidget === props.widgetId || selectedWidget === props.widgetId) && + ((focusedWidget === props.widgetId || showAsSelected) && !isDragging && !isResizing) || !!props.errorCount; - let currentActivity = Activities.NONE; + let currentActivity = + props.type === WidgetTypes.MODAL_WIDGET + ? Activities.HOVERING + : Activities.NONE; if (focusedWidget === props.widgetId) currentActivity = Activities.HOVERING; - if (selectedWidget === props.widgetId) currentActivity = Activities.SELECTED; + if (showAsSelected) currentActivity = Activities.SELECTED; if ( + showAsSelected && propertyPaneState.isVisible && propertyPaneState.widgetId === props.widgetId ) currentActivity = Activities.ACTIVE; return showWidgetName ? ( - + = { MYSTIC: "#E1E8ED", AQUA_HAZE: "#EEF2F5", GRAY_CHATEAU: "#A2A6A8", + DARK_GRAY: "#A9A7A7", LIGHT_GREYISH_BLUE: "#B0BFCB", SUNGLOW: "#FFCB33", SOFT_ORANGE: "#f7c75b", diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 44040d2d356..b2aa0c35bc5 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -2151,7 +2151,7 @@ export const theme: Theme = { paneTextUnderline: Colors.LIGHT_GREYISH_BLUE, paneSectionLabel: Colors.CADET_BLUE, navBG: Colors.SHARK, - grid: Colors.TROUT, + grid: Colors.ALTO2, containerBorder: Colors.FRENCH_PASS, menuButtonBGInactive: Colors.JUNGLE_MIST, menuIconColorInactive: Colors.OXFORD_BLUE, @@ -2229,7 +2229,7 @@ export const theme: Theme = { }, headerHeight: "48px", smallHeaderHeight: "35px", - canvasPadding: "20px 0 200px 0", + canvasPadding: "0 0 200px 0", sideNav: { maxWidth: 220, minWidth: 50, diff --git a/app/client/src/constants/Layers.tsx b/app/client/src/constants/Layers.tsx index 79426855142..d920d0a20ee 100644 --- a/app/client/src/constants/Layers.tsx +++ b/app/client/src/constants/Layers.tsx @@ -14,9 +14,24 @@ enum Indices { export const Layers = { dropZone: Indices.Layer0, + dragPreview: Indices.Layer1, - widgetName: Indices.Layer2, + // All Widgets Parent layer + positionedWidget: Indices.Layer1, + // Modal needs to higher than other widgets. + modalWidget: Indices.Layer2, + // Layers when dragging + animatedSnappingDropZone: Indices.Layer2, + + animatedDropZone: Indices.Layer3, + // Must be higher than any widget + widgetName: Indices.Layer3, apiPane: Indices.Layer3, + // Propane needs to match sidebar to show propane on top side bar. + // Sidebar needs to be more than modal so that u can use side bar whilst u have the modal showing up on the canvas. + sideBar: Indices.Layer3, + propertyPane: Indices.Layer3, + help: Indices.Layer4, dynamicAutoComplete: Indices.Layer5, debugger: Indices.Layer6, diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 6a38c03ab52..3bc34f20714 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -102,6 +102,8 @@ export const ReduxActionTypes: { [key: string]: string } = { WIDGET_MOVE: "WIDGET_MOVE", WIDGET_RESIZE: "WIDGET_RESIZE", WIDGET_DELETE: "WIDGET_DELETE", + WIDGET_BULK_DELETE: "WIDGET_BULK_DELETE", + WIDGET_SINGLE_DELETE: "WIDGET_SINGLE_DELETE", SHOW_PROPERTY_PANE: "SHOW_PROPERTY_PANE", UPDATE_CANVAS_LAYOUT: "UPDATE_CANVAS_LAYOUT", UPDATE_WIDGET_PROPERTY_REQUEST: "UPDATE_WIDGET_PROPERTY_REQUEST", @@ -258,6 +260,8 @@ export const ReduxActionTypes: { [key: string]: string } = { INVITED_USER_SIGNUP_INIT: "INVITED_USER_SIGNUP_INIT", DISABLE_WIDGET_DRAG: "DISABLE_WIDGET_DRAG", SELECT_WIDGET: "SELECT_WIDGET", + SELECT_MULTIPLE_WIDGETS: "SELECT_MULTIPLE_WIDGETS", + SELECT_MULTIPLE_WIDGETS_INIT: "SELECT_MULTIPLE_WIDGETS_INIT", FOCUS_WIDGET: "FOCUS_WIDGET", SET_WIDGET_DRAGGING: "SET_WIDGET_DRAGGING", SET_WIDGET_RESIZING: "SET_WIDGET_RESIZING", diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 8d477e78d71..12c34a9ff14 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -96,19 +96,23 @@ export const layoutConfigurations: LayoutConfigurations = { FLUID: { minWidth: -1, maxWidth: -1 }, }; +export const LATEST_PAGE_VERSION = 20; + export const GridDefaults = { DEFAULT_CELL_SIZE: 1, DEFAULT_WIDGET_WIDTH: 200, DEFAULT_WIDGET_HEIGHT: 100, - DEFAULT_GRID_COLUMNS: 16, - DEFAULT_GRID_ROW_HEIGHT: 40, + DEFAULT_GRID_COLUMNS: 64, + DEFAULT_GRID_ROW_HEIGHT: 10, CANVAS_EXTENSION_OFFSET: 2, }; +// calculated as (GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 2) * 0.8; export const CONTAINER_GRID_PADDING = - (GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 2) * 0.8; + GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 0.4; -export const WIDGET_PADDING = (GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 2) * 0.2; +// calculated as (GridDefaults.DEFAULT_GRID_ROW_HEIGHT / 0.5) * 0.2; +export const WIDGET_PADDING = GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 0.4; export const WIDGET_CLASSNAME_PREFIX = "WIDGET_"; export const MAIN_CONTAINER_WIDGET_ID = "0"; diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index 363db122e2e..38c0ec9c7c5 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -247,6 +247,8 @@ export const ERROR_EVAL_TRIGGER = (message: string) => export const WIDGET_DELETE = (widgetName: string) => `${widgetName} widget deleted`; +export const WIDGET_BULK_DELETE = (widgetName: string) => + `${widgetName} widgets deleted`; export const WIDGET_COPY = (widgetName: string) => `Copied ${widgetName}`; export const ERROR_WIDGET_COPY_NO_WIDGET_SELECTED = () => `Please select a widget to copy`; @@ -255,6 +257,8 @@ export const ERROR_WIDGET_COPY_NOT_ALLOWED = () => export const WIDGET_CUT = (widgetName: string) => `Cut ${widgetName}`; export const ERROR_WIDGET_CUT_NO_WIDGET_SELECTED = () => `Please select a widget to cut`; +export const SELECT_ALL_WIDGETS_MSG = () => + `All widgets in this page including modals have been selected`; export const ERROR_ADD_WIDGET_FROM_QUERY = () => `Failed to add widget`; export const REST_API_AUTHORIZATION_SUCCESSFUL = () => diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 7899c9e3781..521ce177cf3 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -9,6 +9,10 @@ import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReduc import { getDynamicBindings } from "utils/DynamicBindingUtils"; import { Colors } from "constants/Colors"; import FileDataTypes from "widgets/FileDataTypes"; +/* + ********************************{Grid Density Migration}********************************* + */ +export const GRID_DENSITY_MIGRATION_V1 = 4; /** * this config sets the default values of properties being used in the widget @@ -18,8 +22,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { BUTTON_WIDGET: { text: "Submit", buttonStyle: "PRIMARY_BUTTON", - rows: 1, - columns: 2, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 2 * GRID_DENSITY_MIGRATION_V1, widgetName: "Button", isDisabled: false, isVisible: true, @@ -32,15 +36,15 @@ const WidgetConfigResponse: WidgetConfigReducerState = { fontStyle: "BOLD", textAlign: "LEFT", textColor: Colors.THUNDER, - rows: 1, - columns: 4, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 4 * GRID_DENSITY_MIGRATION_V1, widgetName: "Text", version: 1, }, RICH_TEXT_EDITOR_WIDGET: { defaultText: "This is the initial content of the editor", - rows: 5, - columns: 8, + rows: 5 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, isDisabled: false, isVisible: true, widgetName: "RichTextEditor", @@ -54,16 +58,16 @@ const WidgetConfigResponse: WidgetConfigReducerState = { imageShape: "RECTANGLE", maxZoomLevel: 1, image: "", - rows: 3, - columns: 4, + rows: 3 * GRID_DENSITY_MIGRATION_V1, + columns: 4 * GRID_DENSITY_MIGRATION_V1, widgetName: "Image", version: 1, }, INPUT_WIDGET: { inputType: "TEXT", - rows: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, label: "", - columns: 5, + columns: 5 * GRID_DENSITY_MIGRATION_V1, widgetName: "Input", version: 1, resetOnSubmit: true, @@ -72,8 +76,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, SWITCH_WIDGET: { label: "Label", - rows: 1, - columns: 2, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 2 * GRID_DENSITY_MIGRATION_V1, defaultSwitchState: true, widgetName: "Switch", alignWidget: "LEFT", @@ -82,14 +86,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, ICON_WIDGET: { widgetName: "Icon", - rows: 1, - columns: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 1 * GRID_DENSITY_MIGRATION_V1, version: 1, }, CONTAINER_WIDGET: { backgroundColor: "#FFFFFF", - rows: 10, - columns: 8, + rows: 10 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, widgetName: "Container", containerStyle: "card", children: [], @@ -112,10 +116,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = { DATE_PICKER_WIDGET: { isDisabled: false, datePickerType: "DATE_PICKER", - rows: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, label: "", dateFormat: "YYYY-MM-DD HH:mm", - columns: 5, + columns: 5 * GRID_DENSITY_MIGRATION_V1, widgetName: "DatePicker", defaultDate: moment().format("YYYY-MM-DD HH:mm"), version: 1, @@ -123,10 +127,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = { DATE_PICKER_WIDGET2: { isDisabled: false, datePickerType: "DATE_PICKER", - rows: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, label: "", dateFormat: "YYYY-MM-DD HH:mm", - columns: 5, + columns: 5 * GRID_DENSITY_MIGRATION_V1, widgetName: "DatePicker", defaultDate: moment().toISOString(), minDate: "2001-01-01 00:00", @@ -135,16 +139,16 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isRequired: false, }, VIDEO_WIDGET: { - rows: 7, - columns: 7, + rows: 7 * GRID_DENSITY_MIGRATION_V1, + columns: 7 * GRID_DENSITY_MIGRATION_V1, widgetName: "Video", url: "https://www.youtube.com/watch?v=mzqK0QIZRLs", autoPlay: false, version: 1, }, TABLE_WIDGET: { - rows: 7, - columns: 8, + rows: 7 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, label: "Data", widgetName: "Table", searchKey: "", @@ -262,8 +266,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { version: 1, }, DROP_DOWN_WIDGET: { - rows: 1, - columns: 5, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 5 * GRID_DENSITY_MIGRATION_V1, label: "", selectionType: "SINGLE_SELECT", options: [ @@ -278,8 +282,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isDisabled: false, }, CHECKBOX_WIDGET: { - rows: 1, - columns: 3, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 3 * GRID_DENSITY_MIGRATION_V1, label: "Label", defaultCheckedState: true, widgetName: "Checkbox", @@ -289,8 +293,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isRequired: false, }, RADIO_GROUP_WIDGET: { - rows: 2, - columns: 3, + rows: 2 * GRID_DENSITY_MIGRATION_V1, + columns: 3 * GRID_DENSITY_MIGRATION_V1, label: "", options: [ { label: "Yes", value: "Y" }, @@ -303,11 +307,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isDisabled: false, }, FILE_PICKER_WIDGET: { - rows: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, files: [], allowedFileTypes: [], label: "Select Files", - columns: 4, + columns: 4 * GRID_DENSITY_MIGRATION_V1, maxNumFiles: 1, maxFileSize: 5, fileDataType: FileDataTypes.Base64, @@ -318,8 +322,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isDisabled: false, }, TABS_WIDGET: { - rows: 7, - columns: 8, + rows: 7 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, shouldScrollContents: false, widgetName: "Tabs", tabsObj: { @@ -367,8 +371,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { version: 2, }, MODAL_WIDGET: { - rows: 6, - columns: 6, + rows: 6 * GRID_DENSITY_MIGRATION_V1, + columns: 6 * GRID_DENSITY_MIGRATION_V1, size: "MODAL_SMALL", canEscapeKeyClose: true, // detachFromLayout is set true for widgets that are not bound to the widgets within the layout. @@ -396,8 +400,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = { view: [ { type: "ICON_WIDGET", - position: { left: 14, top: 0 }, - size: { rows: 1, cols: 2 }, + position: { left: 14 * GRID_DENSITY_MIGRATION_V1, top: 1 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 2 * GRID_DENSITY_MIGRATION_V1, + }, props: { iconName: "cross", iconSize: 24, @@ -407,8 +414,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "TEXT_WIDGET", - position: { left: 0, top: 0 }, - size: { rows: 1, cols: 10 }, + position: { left: 1, top: 1 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 10 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "Modal Title", fontSize: "HEADING1", @@ -417,8 +427,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "BUTTON_WIDGET", - position: { left: 9, top: 4 }, - size: { rows: 1, cols: 3 }, + position: { + left: 9 * GRID_DENSITY_MIGRATION_V1, + top: 4 * GRID_DENSITY_MIGRATION_V1, + }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 3 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "Cancel", buttonStyle: "SECONDARY_BUTTON", @@ -427,8 +443,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "BUTTON_WIDGET", - position: { left: 12, top: 4 }, - size: { rows: 1, cols: 4 }, + position: { + left: 12 * GRID_DENSITY_MIGRATION_V1, + top: 4 * GRID_DENSITY_MIGRATION_V1, + }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 4 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "Confirm", buttonStyle: "PRIMARY_BUTTON", @@ -475,8 +497,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { version: 1, }, CHART_WIDGET: { - rows: 8, - columns: 6, + rows: 8 * GRID_DENSITY_MIGRATION_V1, + columns: 6 * GRID_DENSITY_MIGRATION_V1, widgetName: "Chart", chartType: "LINE_CHART", chartName: "Last week's revenue", @@ -521,16 +543,16 @@ const WidgetConfigResponse: WidgetConfigReducerState = { yAxisName: "Total Order Revenue $", }, FORM_BUTTON_WIDGET: { - rows: 1, - columns: 3, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 3 * GRID_DENSITY_MIGRATION_V1, widgetName: "FormButton", text: "Submit", isDefaultClickDisabled: true, version: 1, }, FORM_WIDGET: { - rows: 13, - columns: 7, + rows: 13 * GRID_DENSITY_MIGRATION_V1, + columns: 7 * GRID_DENSITY_MIGRATION_V1, widgetName: "Form", backgroundColor: "white", children: [], @@ -549,8 +571,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = { view: [ { type: "TEXT_WIDGET", - size: { rows: 1, cols: 6 }, - position: { top: 0, left: 0 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 6 * GRID_DENSITY_MIGRATION_V1, + }, + position: { top: 1, left: 1.5 }, props: { text: "Form", fontSize: "HEADING1", @@ -559,8 +584,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "FORM_BUTTON_WIDGET", - size: { rows: 1, cols: 4 }, - position: { top: 11, left: 12 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 4 * GRID_DENSITY_MIGRATION_V1, + }, + position: { + top: 11.25 * GRID_DENSITY_MIGRATION_V1, + left: 11.6 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "Submit", buttonStyle: "PRIMARY_BUTTON", @@ -571,8 +602,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "FORM_BUTTON_WIDGET", - size: { rows: 1, cols: 4 }, - position: { top: 11, left: 8 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 4 * GRID_DENSITY_MIGRATION_V1, + }, + position: { + top: 11.25 * GRID_DENSITY_MIGRATION_V1, + left: 7.5 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "Reset", buttonStyle: "SECONDARY_BUTTON", @@ -589,8 +626,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, }, MAP_WIDGET: { - rows: 12, - columns: 8, + rows: 12 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, isDisabled: false, isVisible: true, widgetName: "Map", @@ -604,8 +641,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, SKELETON_WIDGET: { isLoading: true, - rows: 1, - columns: 1, + rows: 1 * GRID_DENSITY_MIGRATION_V1, + columns: 1 * GRID_DENSITY_MIGRATION_V1, widgetName: "Skeleton", version: 1, }, @@ -619,8 +656,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { [WidgetTypes.LIST_WIDGET]: { backgroundColor: "", itemBackgroundColor: "white", - rows: 10, - columns: 8, + rows: 10 * GRID_DENSITY_MIGRATION_V1, + columns: 8 * GRID_DENSITY_MIGRATION_V1, gridType: "vertical", enhancements: { child: { @@ -719,7 +756,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = { view: [ { type: "CONTAINER_WIDGET", - size: { rows: 4, cols: 16 }, + size: { + rows: 4 * GRID_DENSITY_MIGRATION_V1, + cols: 16 * GRID_DENSITY_MIGRATION_V1, + }, position: { top: 0, left: 0 }, props: { backgroundColor: "white", @@ -744,7 +784,10 @@ const WidgetConfigResponse: WidgetConfigReducerState = { view: [ { type: "IMAGE_WIDGET", - size: { rows: 3, cols: 4 }, + size: { + rows: 3 * GRID_DENSITY_MIGRATION_V1, + cols: 4 * GRID_DENSITY_MIGRATION_V1, + }, position: { top: 0, left: 0 }, props: { defaultImage: @@ -762,8 +805,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "TEXT_WIDGET", - size: { rows: 1, cols: 6 }, - position: { top: 0, left: 4 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 6 * GRID_DENSITY_MIGRATION_V1, + }, + position: { + top: 0, + left: 4 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "{{currentItem.name}}", textStyle: "HEADING", @@ -778,8 +827,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, { type: "TEXT_WIDGET", - size: { rows: 1, cols: 6 }, - position: { top: 1, left: 4 }, + size: { + rows: 1 * GRID_DENSITY_MIGRATION_V1, + cols: 6 * GRID_DENSITY_MIGRATION_V1, + }, + position: { + top: 1 * GRID_DENSITY_MIGRATION_V1, + left: 4 * GRID_DENSITY_MIGRATION_V1, + }, props: { text: "{{currentItem.num}}", textStyle: "BODY", diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 36d71047ed9..f1bb5c20da5 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -16,7 +16,11 @@ const Canvas = memo((props: CanvasProps) => { return ( <> - + {props.dsl.widgetId && WidgetFactory.createWidget(props.dsl, RenderModes.CANVAS)} diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx index adc67bc287d..4c959e173cd 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx @@ -80,7 +80,7 @@ const useWidget = ( parentModalId?: string, ) => { const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, ); const isWidgetSelected = useMemo(() => selectedWidget === widgetId, [ selectedWidget, diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx index 4b42a13cedb..4cd0ed234fb 100644 --- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx +++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetGroup.tsx @@ -32,7 +32,7 @@ const StyledLink = styled(Link)` export const ExplorerWidgetGroup = memo((props: ExplorerWidgetGroupProps) => { const params = useParams(); const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, + (state: AppState) => state.ui.widgetDragResize.lastSelectedWidget, ); const childNode = ( diff --git a/app/client/src/pages/Editor/GlobalHotKeys.test.tsx b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx new file mode 100644 index 00000000000..f3c7c759978 --- /dev/null +++ b/app/client/src/pages/Editor/GlobalHotKeys.test.tsx @@ -0,0 +1,105 @@ +// These need to be at the top to avoid imports not being mocked. ideally should be in setup.ts but will override for all other tests +const mockGenerator = function*() { + yield all([]); +}; + +// top avoid the first middleware run which wud initiate all sagas. +jest.mock("sagas", () => ({ + rootSaga: mockGenerator, +})); + +// only the deafault exports are mocked to avoid overriding utilities exported out of them. defaults are marked to avoid worker initiation and page api calls in tests. +jest.mock("sagas/EvaluationsSaga", () => ({ + ...jest.requireActual("sagas/EvaluationsSaga"), + default: mockGenerator, +})); +jest.mock("sagas/PageSagas", () => ({ + ...jest.requireActual("sagas/PageSagas"), + default: mockGenerator, +})); + +import React from "react"; +import { + buildChildren, + widgetCanvasFactory, +} from "test/factories/WidgetFactoryUtils"; +import { render, fireEvent } from "test/testUtils"; +import GlobalHotKeys from "./GlobalHotKeys"; +import MainContainer from "./MainContainer"; +import { MemoryRouter } from "react-router-dom"; +import * as utilities from "selectors/editorSelectors"; +import store from "store"; +import { sagasToRunForTests } from "test/sagas"; +import { all } from "@redux-saga/core/effects"; +import { + dispatchTestKeyboardEventWithCode, + MockApplication, + useMockDsl, +} from "test/testCommon"; +const mockGetCanvasWidgetDsl = jest.spyOn(utilities, "getCanvasWidgetDsl"); +const mockGetIsFetchingPage = jest.spyOn(utilities, "getIsFetchingPage"); +function UpdatedMainContaner({ dsl }: any) { + useMockDsl(dsl); + return ; +} + +it("Cmd + A - select all widgets on canvas", () => { + const children: any = buildChildren([ + { type: "TABS_WIDGET" }, + { type: "SWITCH_WIDGET" }, + ]); + const dsl: any = widgetCanvasFactory.build({ + children, + }); + mockGetCanvasWidgetDsl.mockImplementation(() => dsl); + mockGetIsFetchingPage.mockImplementation(() => false); + + const component = render( + + + + + + + , + { initialState: store.getState(), sagasToRun: sagasToRunForTests }, + ); + let propPane = component.queryByTestId("t--propertypane"); + expect(propPane).toBeNull(); + const canvasWidgets = component.queryAllByTestId("test-widget"); + expect(canvasWidgets.length).toBe(2); + if (canvasWidgets[1].firstChild) { + fireEvent.mouseOver(canvasWidgets[1].firstChild); + fireEvent.click(canvasWidgets[1].firstChild); + } + propPane = component.queryByTestId("t--propertypane"); + expect(propPane).not.toBeNull(); + + const artBoard: any = component.queryByTestId("t--canvas-artboard"); + // deselect all other widgets + fireEvent.click(artBoard); + + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "A", + 65, + false, + true, + ); + let selectedWidgets = component.queryAllByTestId( + "t--widget-propertypane-toggle", + ); + expect(selectedWidgets.length).toBe(2); + dispatchTestKeyboardEventWithCode( + component.container, + "keydown", + "escape", + 27, + false, + false, + ); + selectedWidgets = component.queryAllByTestId("t--widget-propertypane-toggle"); + expect(selectedWidgets.length).toBe(0); +}); +afterAll(() => jest.resetModules()); diff --git a/app/client/src/pages/Editor/GlobalHotKeys.tsx b/app/client/src/pages/Editor/GlobalHotKeys.tsx index 616fc39d594..fd619c508e6 100644 --- a/app/client/src/pages/Editor/GlobalHotKeys.tsx +++ b/app/client/src/pages/Editor/GlobalHotKeys.tsx @@ -4,14 +4,17 @@ import { AppState } from "reducers"; import { Hotkey, Hotkeys } from "@blueprintjs/core"; import { HotkeysTarget } from "@blueprintjs/core/lib/esnext/components/hotkeys/hotkeysTarget.js"; import { + closePropertyPane, copyWidget, cutWidget, deleteSelectedWidget, pasteWidget, + selectAllWidgetsInit, + selectAllWidgets, } from "actions/widgetActions"; import { toggleShowGlobalSearchModal } from "actions/globalSearchActions"; import { isMac } from "utils/helpers"; -import { getSelectedWidget } from "selectors/ui"; +import { getSelectedWidget, getSelectedWidgets } from "selectors/ui"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import { getSelectedText } from "utils/helpers"; import AnalyticsUtil from "utils/AnalyticsUtil"; @@ -30,7 +33,11 @@ type Props = { toggleShowGlobalSearchModal: () => void; resetCommentMode: () => void; openDebugger: () => void; + closeProppane: () => void; + selectAllWidgetsInit: () => void; + deselectAllWidgets: () => void; selectedWidget?: string; + selectedWidgets: string[]; isDebuggerOpen: boolean; children: React.ReactNode; }; @@ -38,9 +45,13 @@ type Props = { @HotkeysTarget class GlobalHotKeys extends React.Component { public stopPropagationIfWidgetSelected(e: KeyboardEvent): boolean { - if ( + const multipleWidgetsSelected = + this.props.selectedWidgets && this.props.selectedWidgets.length; + const singleWidgetSelected = this.props.selectedWidget && - this.props.selectedWidget != MAIN_CONTAINER_WIDGET_ID && + this.props.selectedWidget != MAIN_CONTAINER_WIDGET_ID; + if ( + (singleWidgetSelected || multipleWidgetsSelected) && !getSelectedText() ) { e.preventDefault(); @@ -50,6 +61,12 @@ class GlobalHotKeys extends React.Component { return false; } + public areMultipleWidgetsSelected() { + const multipleWidgetsSelected = + this.props.selectedWidgets && this.props.selectedWidgets.length >= 2; + return !!multipleWidgetsSelected; + } + public renderHotkeys() { return ( @@ -103,7 +120,10 @@ class GlobalHotKeys extends React.Component { group="Canvas" label="Copy Widget" onKeyDown={(e: any) => { - if (this.stopPropagationIfWidgetSelected(e)) { + if ( + this.stopPropagationIfWidgetSelected(e) && + !this.areMultipleWidgetsSelected() + ) { this.props.copySelectedWidget(); } }} @@ -145,16 +165,35 @@ class GlobalHotKeys extends React.Component { group="Canvas" label="Cut Widget" onKeyDown={(e: any) => { - if (this.stopPropagationIfWidgetSelected(e)) { + if ( + this.stopPropagationIfWidgetSelected(e) && + !this.areMultipleWidgetsSelected() + ) { this.props.cutSelectedWidget(); } }} /> + { + this.props.selectAllWidgetsInit(); + e.preventDefault(); + }} + /> { + this.props.resetCommentMode(); + this.props.deselectAllWidgets(); + this.props.closeProppane(); + e.preventDefault(); + }} /> ); @@ -167,6 +206,7 @@ class GlobalHotKeys extends React.Component { const mapStateToProps = (state: AppState) => ({ selectedWidget: getSelectedWidget(state), + selectedWidgets: getSelectedWidgets(state), isDebuggerOpen: state.ui.debugger.isOpen, }); @@ -179,6 +219,9 @@ const mapDispatchToProps = (dispatch: any) => { toggleShowGlobalSearchModal: () => dispatch(toggleShowGlobalSearchModal()), resetCommentMode: () => dispatch(setCommentModeAction(false)), openDebugger: () => dispatch(showDebugger()), + closeProppane: () => dispatch(closePropertyPane()), + selectAllWidgetsInit: () => dispatch(selectAllWidgetsInit()), + deselectAllWidgets: () => dispatch(selectAllWidgets([])), }; }; diff --git a/app/client/src/pages/Editor/PropertyPane/index.tsx b/app/client/src/pages/Editor/PropertyPane/index.tsx index 33820668289..b7b3c705f4d 100644 --- a/app/client/src/pages/Editor/PropertyPane/index.tsx +++ b/app/client/src/pages/Editor/PropertyPane/index.tsx @@ -39,6 +39,7 @@ import PropertyPaneHelpButton from "pages/Editor/PropertyPaneHelpButton"; import { getProppanePreference } from "selectors/usersSelectors"; import { PropertyPanePositionConfig } from "reducers/uiReducers/usersReducer"; import { get } from "lodash"; +import { Layers } from "constants/Layers"; const PropertyPaneWrapper = styled(PaneWrapper)<{ themeMode?: EditorTheme; @@ -204,7 +205,7 @@ class PropertyPane extends Component { position={this.props?.propPanePreference?.position} targetNode={el} themeMode={this.getPopperTheme()} - zIndex={3} + zIndex={Layers.propertyPane} > {content} @@ -228,6 +229,7 @@ class PropertyPane extends Component { return ( { e.stopPropagation(); }} diff --git a/app/client/src/pages/Editor/WidgetCard.tsx b/app/client/src/pages/Editor/WidgetCard.tsx index 545596a2b90..12fdcbbf761 100644 --- a/app/client/src/pages/Editor/WidgetCard.tsx +++ b/app/client/src/pages/Editor/WidgetCard.tsx @@ -6,14 +6,11 @@ import styled from "styled-components"; import { WidgetIcons } from "icons/WidgetIcons"; import { useWidgetDragResize, - useShowPropertyPane, useWidgetSelection, } from "utils/hooks/dragResizeHooks"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { generateReactKey } from "utils/generators"; import { Colors } from "constants/Colors"; -import { AppState } from "reducers"; -import { useSelector } from "react-redux"; type CardProps = { details: WidgetCardProps; @@ -81,15 +78,9 @@ export const IconLabel = styled.h5` function WidgetCard(props: CardProps) { const { setIsDragging } = useWidgetDragResize(); - const { selectWidget } = useWidgetSelection(); - - const selectedWidget = useSelector( - (state: AppState) => state.ui.widgetDragResize.selectedWidget, - ); - + const { deselectAll } = useWidgetSelection(); // Generate a new widgetId which can be used in the future for this widget. const [widgetId, setWidgetId] = useState(generateReactKey()); - const showPropertyPane = useShowPropertyPane(); const [, drag, preview] = useDrag({ item: { ...props.details, widgetId }, begin: () => { @@ -97,11 +88,8 @@ function WidgetCard(props: CardProps) { widgetType: props.details.type, widgetName: props.details.widgetCardName, }); - showPropertyPane && showPropertyPane(undefined); setIsDragging && setIsDragging(true); - - // Make sure that this widget is selected - selectWidget && selectedWidget !== widgetId && selectWidget(widgetId); + deselectAll(); }, end: (widget, monitor) => { AnalyticsUtil.logEvent("WIDGET_CARD_DROP", { diff --git a/app/client/src/pages/Editor/WidgetsEditor.tsx b/app/client/src/pages/Editor/WidgetsEditor.tsx index 365361f36da..9e50109c1ee 100644 --- a/app/client/src/pages/Editor/WidgetsEditor.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor.tsx @@ -25,6 +25,7 @@ import { getCurrentApplication } from "selectors/applicationSelectors"; import { MainContainerLayoutControl } from "./MainContainerLayoutControl"; import { useDynamicAppLayout } from "utils/hooks/useDynamicAppLayout"; import Debugger from "components/editorComponents/Debugger"; +import { closePropertyPane } from "actions/widgetActions"; const EditorWrapper = styled.div` display: flex; @@ -53,7 +54,7 @@ const CanvasContainer = styled.section` /* eslint-disable react/display-name */ function WidgetsEditor() { - const { focusWidget, selectWidget } = useWidgetSelection(); + const { deselectAll, focusWidget, selectWidget } = useWidgetSelection(); const params = useParams<{ applicationId: string; pageId: string }>(); const dispatch = useDispatch(); @@ -98,8 +99,9 @@ function WidgetsEditor() { const handleWrapperClick = useCallback(() => { focusWidget && focusWidget(); - selectWidget && selectWidget(); - }, [focusWidget, selectWidget]); + deselectAll && deselectAll(); + dispatch(closePropertyPane()); + }, [focusWidget, deselectAll]); const pageLoading = ( diff --git a/app/client/src/reducers/uiReducers/dragResizeReducer.ts b/app/client/src/reducers/uiReducers/dragResizeReducer.ts index 5c825b68777..c7cd1a6dc3d 100644 --- a/app/client/src/reducers/uiReducers/dragResizeReducer.ts +++ b/app/client/src/reducers/uiReducers/dragResizeReducer.ts @@ -1,11 +1,13 @@ import { createImmerReducer } from "utils/AppsmithUtils"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; const initialState: WidgetDragResizeState = { isDraggingDisabled: false, isDragging: false, isResizing: false, - selectedWidget: undefined, + lastSelectedWidget: undefined, + selectedWidgets: [], focusedWidget: undefined, selectedWidgetAncestory: [], }; @@ -31,9 +33,47 @@ export const widgetDraggingReducer = createImmerReducer(initialState, { }, [ReduxActionTypes.SELECT_WIDGET]: ( state: WidgetDragResizeState, - action: ReduxAction<{ widgetId?: string }>, + action: ReduxAction<{ widgetId?: string; isMultiSelect?: boolean }>, + ) => { + if (action.payload.widgetId === MAIN_CONTAINER_WIDGET_ID) return; + if (action.payload.isMultiSelect) { + const widgetId = action.payload.widgetId || ""; + const removeSelection = state.selectedWidgets.includes(widgetId); + if (removeSelection) { + state.selectedWidgets = state.selectedWidgets.filter( + (each) => each !== widgetId, + ); + } else if (!!widgetId) { + state.selectedWidgets = [...state.selectedWidgets, widgetId]; + } + if (state.selectedWidgets.length === 1) { + state.lastSelectedWidget = state.selectedWidgets[0]; + } else { + state.lastSelectedWidget = ""; + } + } else { + state.lastSelectedWidget = action.payload.widgetId; + if (action.payload.widgetId) { + state.selectedWidgets = [action.payload.widgetId]; + } else { + state.selectedWidgets = []; + } + } + }, + [ReduxActionTypes.SELECT_MULTIPLE_WIDGETS]: ( + state: WidgetDragResizeState, + action: ReduxAction<{ widgetIds?: string[] }>, ) => { - state.selectedWidget = action.payload.widgetId; + const { widgetIds } = action.payload; + if (widgetIds) { + if (widgetIds.length > 1) { + state.selectedWidgets = widgetIds || []; + state.lastSelectedWidget = ""; + } else { + state.selectedWidgets = []; + state.lastSelectedWidget = widgetIds[0]; + } + } }, [ReduxActionTypes.FOCUS_WIDGET]: ( state: WidgetDragResizeState, @@ -53,9 +93,10 @@ export type WidgetDragResizeState = { isDraggingDisabled: boolean; isDragging: boolean; isResizing: boolean; - selectedWidget?: string; + lastSelectedWidget?: string; focusedWidget?: string; selectedWidgetAncestory: string[]; + selectedWidgets: string[]; }; export default widgetDraggingReducer; diff --git a/app/client/src/sagas/OnboardingSagas.ts b/app/client/src/sagas/OnboardingSagas.ts index 256836c9a39..0a3372fc4ce 100644 --- a/app/client/src/sagas/OnboardingSagas.ts +++ b/app/client/src/sagas/OnboardingSagas.ts @@ -90,6 +90,7 @@ import { } from "../actions/controlActions"; import OnSubmitGif from "assets/gifs/onsubmit.gif"; import { checkAndGetPluginFormConfigsSaga } from "sagas/PluginSagas"; +import { GRID_DENSITY_MIGRATION_V1 } from "mockResponses/WidgetConfigResponse"; export const getCurrentStep = (state: AppState) => state.ui.onBoarding.currentStep; @@ -674,14 +675,14 @@ function* addWidget(widgetConfig: any) { } const getStandupTableDimensions = () => { - const columns = 16; - const rows = 15; - const topRow = 2; + const columns = 16 * GRID_DENSITY_MIGRATION_V1; + const rows = 15 * GRID_DENSITY_MIGRATION_V1; + const topRow = 2 * GRID_DENSITY_MIGRATION_V1; const bottomRow = rows + topRow; return { parentRowSpace: 40, parentColumnSpace: 1, - topRow: 2, + topRow, bottomRow, leftColumn: 0, rightColumn: columns, @@ -691,13 +692,13 @@ const getStandupTableDimensions = () => { }; const getStandupInputDimensions = () => { - const columns = 6; - const rows = 1; - const leftColumn = 5; + const columns = 6 * GRID_DENSITY_MIGRATION_V1; + const rows = 1 * GRID_DENSITY_MIGRATION_V1; + const leftColumn = 5 * GRID_DENSITY_MIGRATION_V1; const rightColumn = leftColumn + columns; return { - topRow: 1, - bottomRow: 2, + topRow: 1 * GRID_DENSITY_MIGRATION_V1, + bottomRow: 2 * GRID_DENSITY_MIGRATION_V1, leftColumn, rightColumn, rows, diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 807b63af6c4..4c7d33a3058 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -20,6 +20,7 @@ import { updateCurrentPage, updateWidgetNameSuccess, updateAndSaveLayout, + saveLayout, } from "actions/pageActions"; import PageApi, { ClonePageRequest, @@ -49,7 +50,10 @@ import { import history from "utils/history"; import { BUILDER_PAGE_URL } from "constants/routes"; import { isNameValid } from "utils/helpers"; -import { extractCurrentDSL } from "utils/WidgetPropsUtils"; +import { + checkIfMigrationIsNeeded, + extractCurrentDSL, +} from "utils/WidgetPropsUtils"; import { getAllPageIds, getEditorConfigs, @@ -182,6 +186,7 @@ export function* fetchPageSaga( id, }); const isValidResponse = yield validateResponse(fetchPageResponse); + const willPageBeMigrated = checkIfMigrationIsNeeded(fetchPageResponse); if (isValidResponse) { // Clear any existing caches @@ -196,12 +201,16 @@ export function* fetchPageSaga( yield put(updateCurrentPage(id)); // dispatch fetch page success yield put(fetchPageSuccess()); - + const extractedDSL = extractCurrentDSL(fetchPageResponse); yield put({ type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE, - payload: extractCurrentDSL(fetchPageResponse), + payload: extractedDSL, }); + if (willPageBeMigrated) { + yield put(saveLayout()); + } + PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.FETCH_PAGE_API, ); diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 4430950c51b..a7e16db98be 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -4,6 +4,7 @@ import { ReduxActionTypes, } from "constants/ReduxActionConstants"; import { + MultipleWidgetDeletePayload, updateAndSaveLayout, WidgetAddChild, WidgetAddChildren, @@ -18,6 +19,7 @@ import { import { getSelectedWidget, getWidget, + getWidgetImmediateChildren, getWidgetMetaProps, getWidgets, } from "./selectors"; @@ -52,7 +54,7 @@ import { isPathADynamicTrigger, } from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; -import _, { cloneDeep, isString, set, remove } from "lodash"; +import _, { cloneDeep, flattenDeep, isString, set, remove } from "lodash"; import WidgetFactory from "utils/WidgetFactory"; import { buildWidgetBlueprint, @@ -68,7 +70,9 @@ import { WidgetType, WidgetTypes, } from "constants/WidgetConstants"; -import WidgetConfigResponse from "mockResponses/WidgetConfigResponse"; +import WidgetConfigResponse, { + GRID_DENSITY_MIGRATION_V1, +} from "mockResponses/WidgetConfigResponse"; import { flushDeletedWidgets, getCopiedWidgets, @@ -88,6 +92,8 @@ import { import { closePropertyPane, forceOpenPropertyPane, + selectAllWidgets, + selectWidget, } from "actions/widgetActions"; import { getDataTree } from "selectors/dataTreeSelectors"; import { @@ -111,7 +117,9 @@ import { WIDGET_COPY, WIDGET_CUT, WIDGET_DELETE, + WIDGET_BULK_DELETE, ERROR_WIDGET_COPY_NOT_ALLOWED, + SELECT_ALL_WIDGETS_MSG, } from "constants/messages"; import AppsmithConsole from "utils/AppsmithConsole"; import { ENTITY_TYPE } from "entities/AppsmithConsole"; @@ -120,6 +128,7 @@ import { doesTriggerPathsContainPropertyPath, handleSpecificCasesWhilePasting, } from "./WidgetOperationUtils"; +import { getSelectedWidgets } from "selectors/ui"; import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers"; function* getChildWidgetProps( @@ -448,6 +457,107 @@ const resizeCanvasToLowestWidget = ( GridDefaults.DEFAULT_GRID_ROW_HEIGHT; }; +export function* deleteAllSelectedWidgetsSaga( + deleteAction: ReduxAction, +) { + try { + const { disallowUndo = false, isShortcut } = deleteAction.payload; + const stateWidgets = yield select(getWidgets); + const widgets = { ...stateWidgets }; + const selectedWidgets: string[] = yield select(getSelectedWidgets); + if (!(selectedWidgets && selectedWidgets.length !== 1)) return; + const widgetsToBeDeleted = yield all( + selectedWidgets.map((eachId) => { + return call(getAllWidgetsInTree, eachId, widgets); + }), + ); + const falttendedWidgets: any = flattenDeep(widgetsToBeDeleted); + const parentUpdatedWidgets = falttendedWidgets.reduce( + (allWidgets: any, eachWidget: any) => { + const { parentId, widgetId } = eachWidget; + const stateParent: FlattenedWidgetProps = allWidgets[parentId]; + let parent = { ...stateParent }; + if (parent.children) { + parent = { + ...parent, + children: parent.children.filter((c) => c !== widgetId), + }; + allWidgets[parentId] = parent; + } + return allWidgets; + }, + widgets, + ); + const finalWidgets: CanvasWidgetsReduxState = _.omit( + parentUpdatedWidgets, + falttendedWidgets.map((widgets: any) => widgets.widgetId), + ); + + yield put(updateAndSaveLayout(finalWidgets)); + yield put(selectWidget("")); + const bulkDeleteKey = selectedWidgets.join(","); + const saveStatus: boolean = yield saveDeletedWidgets( + falttendedWidgets, + bulkDeleteKey, + ); + if (saveStatus && !disallowUndo) { + // close property pane after delete + yield put(closePropertyPane()); + Toaster.show({ + text: createMessage(WIDGET_BULK_DELETE, `${selectedWidgets.length}`), + hideProgressBar: false, + variant: Variant.success, + dispatchableAction: { + type: ReduxActionTypes.UNDO_DELETE_WIDGET, + payload: { + widgetId: bulkDeleteKey, + }, + }, + }); + setTimeout(() => { + if (bulkDeleteKey) { + flushDeletedWidgets(bulkDeleteKey); + AppsmithConsole.info({ + logType: LOG_TYPE.ENTITY_DELETED, + text: `${selectedWidgets.length} were deleted`, + source: { + name: "Group Delete", + type: ENTITY_TYPE.WIDGET, + id: bulkDeleteKey, + }, + }); + } + }, WIDGET_DELETE_UNDO_TIMEOUT); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR, + payload: { + action: ReduxActionTypes.WIDGET_DELETE, + error, + }, + }); + } +} + +export function* deleteSagaInit(deleteAction: ReduxAction) { + const { widgetId } = deleteAction.payload; + const selectedWidget = yield select(getSelectedWidget); + const selectedWidgets: string[] = yield select(getSelectedWidgets); + if (selectedWidgets.length > 1) { + yield put({ + type: ReduxActionTypes.WIDGET_BULK_DELETE, + payload: deleteAction.payload, + }); + } + if (!!widgetId || !!selectedWidget) { + yield put({ + type: ReduxActionTypes.WIDGET_SINGLE_DELETE, + payload: deleteAction.payload, + }); + } +} + export function* deleteSaga(deleteAction: ReduxAction) { try { let { parentId, widgetId } = deleteAction.payload; @@ -497,7 +607,7 @@ export function* deleteSaga(deleteAction: ReduxAction) { widgets[parentId] = parent; const otherWidgetsToDelete = getAllWidgetsInTree(widgetId, widgets); - const saveStatus = yield saveDeletedWidgets( + const saveStatus: boolean = yield saveDeletedWidgets( otherWidgetsToDelete, widgetId, ); @@ -608,89 +718,108 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) { const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets( action.payload.widgetId, ); + const deletedWidgetIds = action.payload.widgetId.split(","); if (deletedWidgets && Array.isArray(deletedWidgets)) { - // Find the parent in the list of deleted widgets - const deletedWidget = deletedWidgets.find( - (widget) => widget.widgetId === action.payload.widgetId, - ); - - // If the deleted widget is in fact available. - if (deletedWidget) { - // Log an undo event - AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", { - widgetName: deletedWidget.widgetName, - widgetType: deletedWidget.type, - }); - } - // Get the current list of widgets from reducer + const formTree = deletedWidgets.reduce((widgetTree, each) => { + widgetTree[each.widgetId] = each; + return widgetTree; + }, {} as CanvasWidgetsReduxState); const stateWidgets = yield select(getWidgets); - let widgets = { ...stateWidgets }; - // For each deleted widget - deletedWidgets.forEach((widget) => { - // Add it to the widgets list we fetched from reducer - widgets[widget.widgetId] = widget; - // If the widget in question is the deleted widget - if (widget.widgetId === action.payload.widgetId) { - //SPECIAL HANDLING FOR TAB IN A TABS WIDGET - if ( - widget.tabId && - widget.type === WidgetTypes.CANVAS_WIDGET && - widget.parentId - ) { - const parent = cloneDeep(widgets[widget.parentId]); - if (parent.tabsObj) { - try { - const tabs = Object.values(parent.tabsObj); - parent.tabsObj[widget.tabId] = { - id: widget.tabId, - widgetId: widget.widgetId, - label: widget.tabName || widget.widgetName, - isVisible: true, - }; + const deletedWidgetGroups = deletedWidgetIds.map((each) => ({ + widget: formTree[each], + widgetsToRestore: getAllWidgetsInTree(each, formTree), + })); + const finalWidgets = deletedWidgetGroups.reduce( + (reducedWidgets, deletedWidgetGroup) => { + const { + widget: deletedWidget, + widgetsToRestore: deletedWidgets, + } = deletedWidgetGroup; + let widgets = cloneDeep(reducedWidgets); + + // If the deleted widget is in fact available. + if (deletedWidget) { + // Log an undo event + AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", { + widgetName: deletedWidget.widgetName, + widgetType: deletedWidget.type, + }); + } + + // For each deleted widget + deletedWidgets.forEach((widget: FlattenedWidgetProps) => { + // Add it to the widgets list we fetched from reducer + widgets[widget.widgetId] = widget; + // If the widget in question is the deleted widget + if (deletedWidgetIds.includes(widget.widgetId)) { + //SPECIAL HANDLING FOR TAB IN A TABS WIDGET + if ( + widget.tabId && + widget.type === WidgetTypes.CANVAS_WIDGET && + widget.parentId + ) { + const parent = cloneDeep(widgets[widget.parentId]); + if (parent.tabsObj) { + try { + const tabs = Object.values(parent.tabsObj); + parent.tabsObj[widget.tabId] = { + id: widget.tabId, + widgetId: widget.widgetId, + label: widget.tabName || widget.widgetName, + isVisible: true, + }; + widgets = { + ...widgets, + [widget.parentId]: { + ...widgets[widget.parentId], + tabsObj: parent.tabsObj, + }, + }; + } catch (error) { + log.debug("Error deleting tabs widget: ", { error }); + } + } else { + parent.tabs = JSON.stringify([ + { + id: widget.tabId, + widgetId: widget.widgetId, + label: widget.tabName || widget.widgetName, + }, + ]); + widgets = { + ...widgets, + [widget.parentId]: parent, + }; + } + } + let newChildren = [widget.widgetId]; + if (widget.parentId && widgets[widget.parentId].children) { + // Concatenate the list of parents children with the current widgetId + newChildren = newChildren.concat( + widgets[widget.parentId].children, + ); + } + if (widget.parentId) { widgets = { ...widgets, [widget.parentId]: { ...widgets[widget.parentId], - tabsObj: parent.tabsObj, + children: newChildren, }, }; - } catch (error) { - log.debug("Error deleting tabs widget: ", { error }); } - } else { - parent.tabs = JSON.stringify([ - { - id: widget.tabId, - widgetId: widget.widgetId, - label: widget.tabName || widget.widgetName, - }, - ]); - widgets = { - ...widgets, - [widget.parentId]: parent, - }; } - } - let newChildren = [widget.widgetId]; - if (widget.parentId && widgets[widget.parentId].children) { - // Concatenate the list of parents children with the current widgetId - newChildren = newChildren.concat(widgets[widget.parentId].children); - } - if (widget.parentId) { - widgets = { - ...widgets, - [widget.parentId]: { - ...widgets[widget.parentId], - children: newChildren, - }, - }; - } - } - }); + }); + return widgets; + }, + stateWidgets, + ); - yield put(updateAndSaveLayout(widgets)); - yield put(forceOpenPropertyPane(action.payload.widgetId)); + yield put(updateAndSaveLayout(finalWidgets)); + if (deletedWidgetIds.length === 1) { + yield put(forceOpenPropertyPane(action.payload.widgetId)); + } yield flushDeletedWidgets(action.payload.widgetId); } } @@ -1631,8 +1760,8 @@ function* cutWidgetSaga() { function* addTableWidgetFromQuerySaga(action: ReduxAction) { try { - const columns = 8; - const rows = 7; + const columns = 8 * GRID_DENSITY_MIGRATION_V1; + const rows = 7 * GRID_DENSITY_MIGRATION_V1; const queryName = action.payload; const widgets = yield select(getWidgets); const evalTree = yield select(getDataTree); @@ -1743,6 +1872,21 @@ function* selectedWidgetAncestrySaga( } } +function* selectAllWidgetsSaga() { + const allWidgetsOnMainContainer: string[] = yield select( + getWidgetImmediateChildren, + MAIN_CONTAINER_WIDGET_ID, + ); + if (allWidgetsOnMainContainer && allWidgetsOnMainContainer.length) { + yield put(selectAllWidgets(allWidgetsOnMainContainer)); + Toaster.show({ + text: createMessage(SELECT_ALL_WIDGETS_MSG), + variant: Variant.info, + duration: 3000, + }); + } +} + export default function* widgetOperationSagas() { yield all([ takeEvery( @@ -1750,7 +1894,12 @@ export default function* widgetOperationSagas() { addTableWidgetFromQuerySaga, ), takeEvery(ReduxActionTypes.WIDGET_ADD_CHILD, addChildSaga), - takeEvery(ReduxActionTypes.WIDGET_DELETE, deleteSaga), + takeEvery(ReduxActionTypes.WIDGET_DELETE, deleteSagaInit), + takeEvery(ReduxActionTypes.WIDGET_SINGLE_DELETE, deleteSaga), + takeEvery( + ReduxActionTypes.WIDGET_BULK_DELETE, + deleteAllSelectedWidgetsSaga, + ), takeLatest(ReduxActionTypes.WIDGET_MOVE, moveSaga), takeLatest(ReduxActionTypes.WIDGET_RESIZE, resizeSaga), takeEvery( @@ -1784,5 +1933,9 @@ export default function* widgetOperationSagas() { takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga), takeEvery(ReduxActionTypes.WIDGET_ADD_CHILDREN, addChildrenSaga), takeLatest(ReduxActionTypes.SELECT_WIDGET, selectedWidgetAncestrySaga), + takeLatest( + ReduxActionTypes.SELECT_MULTIPLE_WIDGETS_INIT, + selectAllWidgetsSaga, + ), ]); } diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index 6caee17a09c..eda82abbdc2 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -32,43 +32,44 @@ import debuggerSagas from "./DebuggerSagas"; import log from "loglevel"; import * as sentry from "@sentry/react"; -export function* rootSaga() { - const sagas = [ - initSagas, - pageSagas, - fetchWidgetCardsSaga, - watchActionSagas, - watchActionExecutionSagas, - widgetOperationSagas, - errorSagas, - watchDatasourcesSagas, - applicationSagas, - apiPaneSagas, - userSagas, - pluginSagas, - orgSagas, - importedCollectionsSagas, - providersSagas, - curlImportSagas, - queryPaneSagas, - modalSagas, - batchSagas, - themeSagas, - evaluationsSaga, - onboardingSaga, - actionExecutionChangeListeners, - utilSagas, - saaSPaneSagas, - globalSearchSagas, - recentEntitiesSagas, - commentSagas, - websocketSagas, - debuggerSagas, - utilSagas, - saaSPaneSagas, - ]; +const sagas = [ + initSagas, + pageSagas, + fetchWidgetCardsSaga, + watchActionSagas, + watchActionExecutionSagas, + widgetOperationSagas, + errorSagas, + watchDatasourcesSagas, + applicationSagas, + apiPaneSagas, + userSagas, + pluginSagas, + orgSagas, + importedCollectionsSagas, + providersSagas, + curlImportSagas, + queryPaneSagas, + modalSagas, + batchSagas, + themeSagas, + evaluationsSaga, + onboardingSaga, + actionExecutionChangeListeners, + utilSagas, + saaSPaneSagas, + globalSearchSagas, + recentEntitiesSagas, + commentSagas, + websocketSagas, + debuggerSagas, + utilSagas, + saaSPaneSagas, +]; + +export function* rootSaga(sagasToRun = sagas) { yield all( - sagas.map((saga) => + sagasToRun.map((saga) => spawn(function*() { while (true) { try { diff --git a/app/client/src/sagas/selectors.tsx b/app/client/src/sagas/selectors.tsx index 05acd6e33d8..68aceef6282 100644 --- a/app/client/src/sagas/selectors.tsx +++ b/app/client/src/sagas/selectors.tsx @@ -123,7 +123,27 @@ export const getPluginIdOfPackageName = ( }; export const getSelectedWidget = (state: AppState) => { - const selectedWidgetId = state.ui.widgetDragResize.selectedWidget; + const selectedWidgetId = state.ui.widgetDragResize.lastSelectedWidget; if (!selectedWidgetId) return; return state.entities.canvasWidgets[selectedWidgetId]; }; + +export const getWidgetImmediateChildren = createSelector( + getWidget, + (widget: WidgetProps) => { + const childrenIds: string[] = []; + if (widget === undefined) { + return []; + } + const { children = [] } = widget; + if (children && children.length) { + for (const childIndex in children) { + if (children.hasOwnProperty(childIndex)) { + const child = children[childIndex]; + childrenIds.push(child); + } + } + } + return childrenIds; + }, +); diff --git a/app/client/src/selectors/propertyPaneSelectors.tsx b/app/client/src/selectors/propertyPaneSelectors.tsx index 0c8d3b453ec..6522862a3ba 100644 --- a/app/client/src/selectors/propertyPaneSelectors.tsx +++ b/app/client/src/selectors/propertyPaneSelectors.tsx @@ -8,6 +8,7 @@ import { getDataTree } from "selectors/dataTreeSelectors"; import { DataTree, DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer"; import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer"; +import { getSelectedWidget, getSelectedWidgets } from "./ui"; const getPropertyPaneState = (state: AppState): PropertyPaneReduxState => state.ui.propertyPane; @@ -64,6 +65,24 @@ const isResizingorDragging = (state: AppState) => export const getIsPropertyPaneVisible = createSelector( getPropertyPaneState, isResizingorDragging, - (pane: PropertyPaneReduxState, isResizingorDragging: boolean) => - !!(!isResizingorDragging && pane.isVisible && pane.widgetId), + getSelectedWidget, + getSelectedWidgets, + ( + pane: PropertyPaneReduxState, + isResizingorDragging: boolean, + lastSelectedWidget, + widgets, + ) => { + const isWidgetSelected = pane.widgetId + ? lastSelectedWidget === pane.widgetId || widgets.includes(pane.widgetId) + : false; + const multipleWidgetsSelected = !!(widgets && widgets.length >= 2); + return !!( + isWidgetSelected && + !multipleWidgetsSelected && + !isResizingorDragging && + pane.isVisible && + pane.widgetId + ); + }, ); diff --git a/app/client/src/selectors/ui.ts b/app/client/src/selectors/ui.ts new file mode 100644 index 00000000000..a0de172b7a3 --- /dev/null +++ b/app/client/src/selectors/ui.ts @@ -0,0 +1,7 @@ +import { AppState } from "reducers"; + +export const getSelectedWidget = (state: AppState) => + state.ui.widgetDragResize.lastSelectedWidget; + +export const getSelectedWidgets = (state: AppState) => + state.ui.widgetDragResize.selectedWidgets; diff --git a/app/client/src/selectors/ui.tsx b/app/client/src/selectors/ui.tsx deleted file mode 100644 index b506d7d8031..00000000000 --- a/app/client/src/selectors/ui.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { AppState } from "reducers"; - -export const getSelectedWidget = (state: AppState) => - state.ui.widgetDragResize.selectedWidget; diff --git a/app/client/src/templates/default.ts b/app/client/src/templates/default.ts index 7755fa680b7..249f498bbd5 100644 --- a/app/client/src/templates/default.ts +++ b/app/client/src/templates/default.ts @@ -1,8 +1,10 @@ +import { GridDefaults } from "constants/WidgetConstants"; + export default { widgetName: "MainContainer", backgroundColor: "none", rightColumn: 1242, - snapColumns: 16, + snapColumns: GridDefaults.DEFAULT_GRID_COLUMNS, widgetId: "0", topRow: 0, bottomRow: 1292, diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx index ae8d6dc29b8..20976e2c598 100644 --- a/app/client/src/utils/AppsmithUtils.tsx +++ b/app/client/src/utils/AppsmithUtils.tsx @@ -153,6 +153,10 @@ export const noop = () => { console.log("noop"); }; +export const stopEventPropagation = (e: any) => { + e.stopPropagation(); +}; + export const createNewQueryName = ( queries: ActionDataState, pageId: string, diff --git a/app/client/src/utils/WidgetPropsUtils.test.tsx b/app/client/src/utils/WidgetPropsUtils.test.tsx index 07e2edcf18b..2b774e45fb5 100644 --- a/app/client/src/utils/WidgetPropsUtils.test.tsx +++ b/app/client/src/utils/WidgetPropsUtils.test.tsx @@ -2,8 +2,15 @@ import * as generators from "../utils/generators"; import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; import { migrateChartDataFromArrayToObject, + migrateToNewLayout, migrateInitialValues, } from "./WidgetPropsUtils"; +import { + buildChildren, + buildDslWithChildren, +} from "test/factories/WidgetFactoryUtils"; +import { cloneDeep } from "lodash"; +import { GRID_DENSITY_MIGRATION_V1 } from "mockResponses/WidgetConfigResponse"; describe("WidgetProps tests", () => { it("it checks if array to object migration functions for chart widget ", () => { @@ -90,6 +97,66 @@ describe("WidgetProps tests", () => { expect(result).toStrictEqual(output); }); + it("Grid density migration - Main container widgets", () => { + const dsl: any = buildDslWithChildren([{ type: "TABS_WIDGET" }]); + const newMigratedDsl: any = migrateToNewLayout(cloneDeep(dsl)); + expect(dsl.children[0].topRow * GRID_DENSITY_MIGRATION_V1).toBe( + newMigratedDsl.children[0].topRow, + ); + expect(dsl.children[0].bottomRow * GRID_DENSITY_MIGRATION_V1).toBe( + newMigratedDsl.children[0].bottomRow, + ); + expect(dsl.children[0].rightColumn * GRID_DENSITY_MIGRATION_V1).toBe( + newMigratedDsl.children[0].rightColumn, + ); + expect(dsl.children[0].leftColumn * GRID_DENSITY_MIGRATION_V1).toBe( + newMigratedDsl.children[0].leftColumn, + ); + }); + + it("Grid density migration - widgets inside a container", () => { + const childrenInsideContainer = buildChildren([ + { type: "SWITCH_WIDGET" }, + { type: "FORM_WIDGET" }, + { type: "CONTAINER_WIDGET" }, + ]); + const dslWithContainer: any = buildDslWithChildren([ + { type: "CONTAINER_WIDGET", children: childrenInsideContainer }, + ]); + const newMigratedDsl: any = migrateToNewLayout(cloneDeep(dslWithContainer)); + // Container migrated checks + const containerWidget = dslWithContainer.children[0]; + const migratedContainer = newMigratedDsl.children[0]; + expect(containerWidget.topRow * GRID_DENSITY_MIGRATION_V1).toBe( + migratedContainer.topRow, + ); + expect(containerWidget.bottomRow * GRID_DENSITY_MIGRATION_V1).toBe( + migratedContainer.bottomRow, + ); + expect(containerWidget.rightColumn * GRID_DENSITY_MIGRATION_V1).toBe( + migratedContainer.rightColumn, + ); + expect(containerWidget.leftColumn * GRID_DENSITY_MIGRATION_V1).toBe( + migratedContainer.leftColumn, + ); + // Children inside container miragted + + containerWidget.children.forEach((eachChild: any, index: any) => { + const migratedChild = migratedContainer.children[index]; + expect(eachChild.topRow * GRID_DENSITY_MIGRATION_V1).toBe( + migratedChild.topRow, + ); + expect(eachChild.bottomRow * GRID_DENSITY_MIGRATION_V1).toBe( + migratedChild.bottomRow, + ); + expect(eachChild.rightColumn * GRID_DENSITY_MIGRATION_V1).toBe( + migratedChild.rightColumn, + ); + expect(eachChild.leftColumn * GRID_DENSITY_MIGRATION_V1).toBe( + migratedChild.leftColumn, + ); + }); + }); }); describe("Initial value migration test", () => { diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 62656657c42..54979e9b1ba 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -10,6 +10,7 @@ import { } from "widgets/BaseWidget"; import { GridDefaults, + LATEST_PAGE_VERSION, MAIN_CONTAINER_WIDGET_ID, RenderMode, WidgetType, @@ -32,6 +33,7 @@ import * as Sentry from "@sentry/react"; import { migrateTextStyleFromTextWidget } from "./migrations/TextWidgetReplaceTextStyle"; import { nextAvailableRowInContainer } from "entities/Widget/utils"; import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; +import { GRID_DENSITY_MIGRATION_V1 } from "mockResponses/WidgetConfigResponse"; export type WidgetOperationParams = { operation: WidgetOperation; @@ -726,9 +728,42 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = 19; } + if (currentDSL.version === 19) { + currentDSL.snapColumns = GridDefaults.DEFAULT_GRID_COLUMNS; + currentDSL.snapRows = getCanvasSnapRows( + currentDSL.bottomRow, + currentDSL.detachFromLayout || false, + ); + currentDSL = migrateToNewLayout(currentDSL); + currentDSL.version = LATEST_PAGE_VERSION; + } + return currentDSL; }; +export const migrateToNewLayout = (dsl: ContainerWidgetProps) => { + const scaleWidget = (widgetProps: WidgetProps) => { + widgetProps.bottomRow *= GRID_DENSITY_MIGRATION_V1; + widgetProps.topRow *= GRID_DENSITY_MIGRATION_V1; + widgetProps.leftColumn *= GRID_DENSITY_MIGRATION_V1; + widgetProps.rightColumn *= GRID_DENSITY_MIGRATION_V1; + if (widgetProps.children && widgetProps.children.length) { + widgetProps.children.forEach((eachWidgetProp: WidgetProps) => { + scaleWidget(eachWidgetProp); + }); + } + }; + scaleWidget(dsl); + return dsl; +}; + +export const checkIfMigrationIsNeeded = ( + fetchPageResponse?: FetchPageResponse, +) => { + const currentDSL = fetchPageResponse?.data.layouts[0].dsl || defaultDSL; + return currentDSL.version !== LATEST_PAGE_VERSION; +}; + export const extractCurrentDSL = ( fetchPageResponse?: FetchPageResponse, ): ContainerWidgetProps => { diff --git a/app/client/src/utils/hooks/dragResizeHooks.tsx b/app/client/src/utils/hooks/dragResizeHooks.tsx index f31ff283531..496fda89c8a 100644 --- a/app/client/src/utils/hooks/dragResizeHooks.tsx +++ b/app/client/src/utils/hooks/dragResizeHooks.tsx @@ -1,6 +1,10 @@ import { useDispatch } from "react-redux"; import { ReduxActionTypes } from "constants/ReduxActionConstants"; -import { focusWidget, selectWidget } from "actions/widgetActions"; +import { + focusWidget, + selectAllWidgets, + selectWidget, +} from "actions/widgetActions"; import { useCallback, useEffect, useState } from "react"; export const useShowPropertyPane = () => { @@ -63,8 +67,8 @@ export const useWidgetSelection = () => { const dispatch = useDispatch(); return { selectWidget: useCallback( - (widgetId?: string) => { - dispatch(selectWidget(widgetId)); + (widgetId?: string, isMultiSelect?: boolean) => { + dispatch(selectWidget(widgetId, isMultiSelect)); }, [dispatch], ), @@ -72,9 +76,9 @@ export const useWidgetSelection = () => { (widgetId?: string) => dispatch(focusWidget(widgetId)), [dispatch], ), + deselectAll: useCallback(() => dispatch(selectAllWidgets([])), [dispatch]), }; }; - export const useWidgetDragResize = () => { const dispatch = useDispatch(); return { diff --git a/app/client/src/utils/hooks/useClickOpenPropPane.tsx b/app/client/src/utils/hooks/useClickOpenPropPane.tsx index 2697d0c1457..91b7f3c1c88 100644 --- a/app/client/src/utils/hooks/useClickOpenPropPane.tsx +++ b/app/client/src/utils/hooks/useClickOpenPropPane.tsx @@ -1,5 +1,7 @@ -import { useShowPropertyPane } from "utils/hooks/dragResizeHooks"; -import { selectWidget } from "actions/widgetActions"; +import { + useShowPropertyPane, + useWidgetSelection, +} from "utils/hooks/dragResizeHooks"; import { getCurrentWidgetId, getIsPropertyPaneVisible, @@ -11,6 +13,7 @@ import { getAppMode } from "selectors/applicationSelectors"; export const useClickOpenPropPane = () => { const showPropertyPane = useShowPropertyPane(); + const { selectWidget } = useWidgetSelection(); const isPropPaneVisible = useSelector(getIsPropertyPaneVisible); const selectedWidgetId = useSelector(getCurrentWidgetId); const focusedWidget = useSelector( @@ -26,15 +29,25 @@ export const useClickOpenPropPane = () => { const isDragging = useSelector( (state: AppState) => state.ui.widgetDragResize.isDragging, ); - const openPropertyPane = () => { + const openPropertyPane = (e: any, targetWidgetId: string) => { // ignore click captures if the component was resizing or dragging coz it is handled internally in draggable component - if (isResizing || isDragging || appMode !== APP_MODE.EDIT) return; + if ( + isResizing || + isDragging || + appMode !== APP_MODE.EDIT || + targetWidgetId !== focusedWidget + ) + return; if ( (!isPropPaneVisible && selectedWidgetId === focusedWidget) || selectedWidgetId !== focusedWidget ) { - selectWidget(focusedWidget); + const isMultiSelect = e.metaKey || e.ctrlKey; + selectWidget(focusedWidget, isMultiSelect); showPropertyPane(focusedWidget, undefined, true); + if (isMultiSelect) { + e.stopPropagation(); + } } }; return openPropertyPane; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index 0e47cd75799..a8dc16f6093 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -218,6 +218,7 @@ abstract class BaseWidget< errorCount={this.getErrorCount(this.props.invalidProps)} parentId={this.props.parentId} showControls={showControls} + topRow={this.props.detachFromLayout ? 4 : this.props.topRow} type={this.props.type} widgetId={this.props.widgetId} widgetName={this.props.widgetName} diff --git a/app/client/src/widgets/InputWidget.tsx b/app/client/src/widgets/InputWidget.tsx index 29ad9fa8421..7517e7b5a6a 100644 --- a/app/client/src/widgets/InputWidget.tsx +++ b/app/client/src/widgets/InputWidget.tsx @@ -13,6 +13,7 @@ import { createMessage, FIELD_REQUIRED_ERROR } from "constants/messages"; import { DerivedPropertiesMap } from "utils/WidgetFactory"; import * as Sentry from "@sentry/react"; import withMeta, { WithMeta } from "./MetaHOC"; +import { GRID_DENSITY_MIGRATION_V1 } from "mockResponses/WidgetConfigResponse"; class InputWidget extends BaseWidget { constructor(props: InputWidgetProps) { @@ -314,8 +315,10 @@ class InputWidget extends BaseWidget { isLoading={this.props.isLoading} label={this.props.label} multiline={ - this.props.bottomRow - this.props.topRow > 1 && - this.props.inputType === "TEXT" + // GRID_DENSITY_MIGRATION_V1 used to adjust code as per new scaled canvas. + (this.props.bottomRow - this.props.topRow) / + GRID_DENSITY_MIGRATION_V1 > + 1 && this.props.inputType === "TEXT" } onFocusChange={this.handleFocusChange} onKeyDown={this.handleKeyDown} diff --git a/app/client/src/widgets/ListWidget/ListWidget.test.tsx b/app/client/src/widgets/ListWidget/ListWidget.test.tsx index b6e65184c57..aa6cdb8fedb 100644 --- a/app/client/src/widgets/ListWidget/ListWidget.test.tsx +++ b/app/client/src/widgets/ListWidget/ListWidget.test.tsx @@ -14,7 +14,8 @@ describe("", () => { const initialState = { ui: { widgetDragResize: { - selectedWidget: "Widget1", + lastSelectedWidget: "Widget1", + selectedWidgets: ["Widget1"], }, propertyPane: { isVisible: true, diff --git a/app/client/src/widgets/ListWidget/ListWidget.tsx b/app/client/src/widgets/ListWidget/ListWidget.tsx index 8cd8a453bd8..26dbf5c5086 100644 --- a/app/client/src/widgets/ListWidget/ListWidget.tsx +++ b/app/client/src/widgets/ListWidget/ListWidget.tsx @@ -398,7 +398,8 @@ class ListWidget extends BaseWidget, WidgetState> { return children.map((child: ContainerWidgetProps, index) => { return { ...child, - onClick: () => this.onItemClick(index, this.props.onListItemClick), + onClickCapture: () => + this.onItemClick(index, this.props.onListItemClick), }; }); }; @@ -498,7 +499,8 @@ class ListWidget extends BaseWidget, WidgetState> { const { children, items } = this.props; const { componentHeight } = this.getComponentDimensions(); const templateBottomRow = get(children, "0.children.0.bottomRow"); - const templateHeight = templateBottomRow * 40; + const templateHeight = + templateBottomRow * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; try { gridGap = parseInt(gridGap); diff --git a/app/client/src/widgets/ModalWidget.tsx b/app/client/src/widgets/ModalWidget.tsx index 07cada407ec..a740c9eaec1 100644 --- a/app/client/src/widgets/ModalWidget.tsx +++ b/app/client/src/widgets/ModalWidget.tsx @@ -20,11 +20,13 @@ import { getWidget } from "sagas/selectors"; const MODAL_SIZE: { [id: string]: { width: number; height: number } } = { MODAL_SMALL: { width: 456, - height: GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 6, + // adjust if DEFAULT_GRID_ROW_HEIGHT changes + height: GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 24, }, MODAL_LARGE: { width: 532, - height: GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 15, + // adjust if DEFAULT_GRID_ROW_HEIGHT changes + height: GridDefaults.DEFAULT_GRID_ROW_HEIGHT * 60, }, }; diff --git a/app/client/src/widgets/Tabs/TabsWidget.test.tsx b/app/client/src/widgets/Tabs/TabsWidget.test.tsx index ca749b4ef0a..fff9f140c45 100644 --- a/app/client/src/widgets/Tabs/TabsWidget.test.tsx +++ b/app/client/src/widgets/Tabs/TabsWidget.test.tsx @@ -3,48 +3,20 @@ import { widgetCanvasFactory, } from "test/factories/WidgetFactoryUtils"; import { render, fireEvent } from "test/testUtils"; - import Canvas from "pages/Editor/Canvas"; import React from "react"; -import { useDispatch } from "react-redux"; -import { editorInitializer } from "utils/EditorUtils"; -import { initCanvasLayout } from "actions/pageActions"; -import { getCanvasWidgetsPayload } from "sagas/PageSagas"; -import { noop } from "utils/AppsmithUtils"; +import { MockPageDSL } from "test/testCommon"; -Element.prototype.scrollTo = noop; -function SetCanvas({ children, dsl }: any) { - const dispatch = useDispatch(); - const mockResp: any = { - data: { - id: "asa", - name: "App", - applicationId: "asa", - layouts: [ - { - id: "w323", - dsl, - layoutOnLoadActions: [], - layoutActions: [], - }, - ], - }, - }; - const canvasWidgetsPayload = getCanvasWidgetsPayload(mockResp); - dispatch(initCanvasLayout(canvasWidgetsPayload)); - return children; -} describe("Tabs widget functional cases", () => { it("Should render 2 tabs by default", () => { - editorInitializer(); const children: any = buildChildren([{ type: "TABS_WIDGET" }]); const dsl: any = widgetCanvasFactory.build({ children, }); const component = render( - + - , + , ); const tab1 = component.queryByText("Tab 1"); const tab2 = component.queryByText("Tab 2"); @@ -53,7 +25,6 @@ describe("Tabs widget functional cases", () => { }); it("Should render components inside tabs by default", () => { - editorInitializer(); const tab1Children = buildChildren([ { type: "SWITCH_WIDGET", label: "Tab1 Switch" }, { type: "CHECKBOX_WIDGET", label: "Tab1 Checkbox" }, @@ -69,9 +40,9 @@ describe("Tabs widget functional cases", () => { children, }); const component = render( - + - , + , ); const tab1 = component.queryByText("Tab 1"); const tab2: any = component.queryByText("Tab 2"); diff --git a/app/client/src/widgets/Tabs/TabsWidget.tsx b/app/client/src/widgets/Tabs/TabsWidget.tsx index 35e9d7430bb..f8cd1ec930a 100644 --- a/app/client/src/widgets/Tabs/TabsWidget.tsx +++ b/app/client/src/widgets/Tabs/TabsWidget.tsx @@ -10,6 +10,7 @@ import { WidgetOperations } from "widgets/BaseWidget"; import * as Sentry from "@sentry/react"; import { generateReactKey } from "utils/generators"; import withMeta, { WithMeta } from "../MetaHOC"; +import { GRID_DENSITY_MIGRATION_V1 } from "mockResponses/WidgetConfigResponse"; class TabsWidget extends BaseWidget< TabsWidgetProps, @@ -196,8 +197,11 @@ class TabsWidget extends BaseWidget< const columns = (this.props.rightColumn - this.props.leftColumn) * this.props.parentColumnSpace; + // GRID_DENSITY_MIGRATION_V1 used to adjust code as per new scaled canvas. const rows = - (this.props.bottomRow - this.props.topRow - 1) * + (this.props.bottomRow - + this.props.topRow - + GRID_DENSITY_MIGRATION_V1) * this.props.parentRowSpace; const config = { type: WidgetTypes.CANVAS_WIDGET, diff --git a/app/client/test/factories/WidgetFactoryUtils.ts b/app/client/test/factories/WidgetFactoryUtils.ts index 55f06e9d597..7200938d476 100644 --- a/app/client/test/factories/WidgetFactoryUtils.ts +++ b/app/client/test/factories/WidgetFactoryUtils.ts @@ -26,3 +26,10 @@ export const buildChildren = (children: Partial[]) => { console.error("Check if child widget data provided"); } }; + +export const buildDslWithChildren = (childData: Partial[]) => { + const children: any = buildChildren(childData); + return widgetCanvasFactory.build({ + children, + }); +}; diff --git a/app/client/test/sagas.ts b/app/client/test/sagas.ts new file mode 100644 index 00000000000..3a15551947f --- /dev/null +++ b/app/client/test/sagas.ts @@ -0,0 +1,59 @@ +import initSagas from "../src/sagas/InitSagas"; +import apiPaneSagas from "../src/sagas/ApiPaneSagas"; +import userSagas from "../src/sagas/userSagas"; +import pluginSagas from "../src/sagas/PluginSagas"; +import orgSagas from "../src/sagas/OrgSagas"; +import importedCollectionsSagas from "../src/sagas/CollectionSagas"; +import providersSagas from "../src/sagas/ProvidersSaga"; +import curlImportSagas from "../src/sagas/CurlImportSagas"; +import queryPaneSagas from "../src/sagas/QueryPaneSagas"; +import modalSagas from "../src/sagas/ModalSagas"; +import batchSagas from "../src/sagas/BatchSagas"; +import themeSagas from "../src/sagas/ThemeSaga"; +import onboardingSaga from "../src/sagas/OnboardingSagas"; +import utilSagas from "../src/sagas/UtilSagas"; +import saaSPaneSagas from "../src/sagas/SaaSPaneSagas"; +import actionExecutionChangeListeners from "../src/sagas/WidgetLoadingSaga"; +import globalSearchSagas from "../src/sagas/GlobalSearchSagas"; +import recentEntitiesSagas from "../src/sagas/RecentEntitiesSagas"; +import commentSagas from "../src/sagas/CommentSagas"; +import websocketSagas from "../src/sagas/WebsocketSagas"; +import debuggerSagas from "../src/sagas/DebuggerSagas"; +import { fetchWidgetCardsSaga } from "../src/sagas/WidgetSidebarSagas"; +import { watchActionSagas } from "../src/sagas/ActionSagas"; +import { watchActionExecutionSagas } from "../src/sagas/ActionExecutionSagas"; +import widgetOperationSagas from "../src/sagas/WidgetOperationSagas"; +import applicationSagas from "../src/sagas/ApplicationSagas"; +import { watchDatasourcesSagas } from "../src/sagas/DatasourcesSagas"; + +export const sagasToRunForTests = [ + initSagas, + fetchWidgetCardsSaga, + watchActionSagas, + watchActionExecutionSagas, + widgetOperationSagas, + watchDatasourcesSagas, + applicationSagas, + apiPaneSagas, + userSagas, + pluginSagas, + orgSagas, + importedCollectionsSagas, + providersSagas, + curlImportSagas, + queryPaneSagas, + modalSagas, + batchSagas, + themeSagas, + onboardingSaga, + actionExecutionChangeListeners, + utilSagas, + saaSPaneSagas, + globalSearchSagas, + recentEntitiesSagas, + commentSagas, + websocketSagas, + debuggerSagas, + utilSagas, + saaSPaneSagas, +]; diff --git a/app/client/test/setup.ts b/app/client/test/setup.ts index 9a04bae5424..207fdeaa708 100644 --- a/app/client/test/setup.ts +++ b/app/client/test/setup.ts @@ -3,9 +3,29 @@ import { handlers } from "./__mocks__/apiHandlers"; export const server = setupServer(...handlers); // establish API mocking before all tests -beforeAll(() => server.listen()) +beforeAll(() => server.listen()); // reset any request handlers that are declared as a part of our tests // (i.e. for testing one-time error scenarios) -afterEach(() => server.resetHandlers()) +afterEach(() => server.resetHandlers()); // clean up once the tests are done -afterAll(() => server.close()) +afterAll(() => server.close()); + +// popper.js fix for jest tests +document.createRange = () => { + const range = new Range(); + + range.getBoundingClientRect = jest.fn(); + + range.getClientRects = () => { + return { + item: () => null, + length: 0, + [Symbol.iterator]: jest.fn(), + }; + }; + + return range; +}; + +// jest events doesnt seem to be handling scrollTo +Element.prototype.scrollTo = () => {}; diff --git a/app/client/test/testCommon.tsx b/app/client/test/testCommon.tsx new file mode 100644 index 00000000000..ee3740ff6a6 --- /dev/null +++ b/app/client/test/testCommon.tsx @@ -0,0 +1,81 @@ +import { getCanvasWidgetsPayload } from "sagas/PageSagas"; +import { updateCurrentPage } from "actions/pageActions"; +import { editorInitializer } from "utils/EditorUtils"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { initEditor } from "actions/initActions"; +import { useDispatch } from "react-redux"; + +export const useMockDsl = (dsl: any) => { + const dispatch = useDispatch(); + const mockResp: any = { + data: { + id: "page_id", + name: "Page1", + applicationId: "app_id", + layouts: [ + { + id: "layout_id", + dsl, + layoutOnLoadActions: [], + layoutActions: [], + }, + ], + }, + }; + const canvasWidgetsPayload = getCanvasWidgetsPayload(mockResp); + dispatch({ + type: "UPDATE_LAYOUT", + payload: { widgets: canvasWidgetsPayload.widgets }, + }); + + dispatch(updateCurrentPage(mockResp.data.id)); +}; +export function MockPageDSL({ dsl, children }: any) { + editorInitializer(); + useMockDsl(dsl); + return children; +} +export function MockApplication({ children }: any) { + editorInitializer(); + const dispatch = useDispatch(); + dispatch(initEditor("app_id", "page_id")); + const mockResp: any = { + organizationId: "org_id", + pages: [{ id: "page_id", name: "Page1", isDefault: true }], + id: "app_id", + isDefault: true, + name: "Page1", + }; + dispatch({ + type: ReduxActionTypes.FETCH_APPLICATION_SUCCESS, + payload: mockResp, + }); + return children; +} + +//got it from @blueprintjs/test-commons to dispatch hotkeys events +export function dispatchTestKeyboardEventWithCode( + target: EventTarget, + eventType: string, + key: string, + keyCode: number, + shift = false, + meta = false, +) { + const event = document.createEvent("KeyboardEvent"); + (event as any).initKeyboardEvent( + eventType, + true, + true, + window, + key, + 0, + meta, + false, + shift, + ); + Object.defineProperty(event, "key", { get: () => key }); + Object.defineProperty(event, "which", { get: () => keyCode }); + + target.dispatchEvent(event); +} diff --git a/app/client/test/testUtils.tsx b/app/client/test/testUtils.tsx index 11f9f623f70..22134739c5b 100644 --- a/app/client/test/testUtils.tsx +++ b/app/client/test/testUtils.tsx @@ -1,20 +1,53 @@ import React, { ReactElement } from "react"; import { render, RenderOptions, queries } from "@testing-library/react"; -import { Provider } from "react-redux"; +import { Provider, useDispatch } from "react-redux"; import { ThemeProvider } from "../src/constants/DefaultTheme"; -import store, { testStore } from "../src/store"; import { getCurrentThemeDetails } from "../src/selectors/themeSelectors"; import * as customQueries from "./customQueries"; import { BrowserRouter } from "react-router-dom"; -import { AppState } from "reducers"; +import appReducer, { AppState } from "reducers"; import { DndProvider } from "react-dnd"; import TouchBackend from "react-dnd-touch-backend"; +import { noop } from "utils/AppsmithUtils"; +import { getCanvasWidgetsPayload } from "sagas/PageSagas"; +import { updateCurrentPage } from "actions/pageActions"; +import { editorInitializer } from "utils/EditorUtils"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { initEditor } from "actions/initActions"; +import { applyMiddleware, compose, createStore } from "redux"; +import { reduxBatch } from "@manaflair/redux-batch"; +import createSagaMiddleware from "redux-saga"; +import store, { testStore } from "store"; +import { sagasToRunForTests } from "./sagas"; +import { all, call, spawn } from "redux-saga/effects"; +const testSagaMiddleware = createSagaMiddleware(); + +const testStoreWithTestMiddleWare = (initialState: Partial) => + createStore( + appReducer, + initialState, + compose(reduxBatch, applyMiddleware(testSagaMiddleware), reduxBatch), + ); + +const rootSaga = function*(sagasToRun = sagasToRunForTests) { + yield all( + sagasToRun.map((saga) => + spawn(function*() { + while (true) { + yield call(saga); + break; + } + }), + ), + ); +}; const customRender = ( ui: ReactElement, state?: { url?: string; initialState?: Partial; + sagasToRun?: typeof sagasToRunForTests; }, options?: Omit, ) => { @@ -23,6 +56,11 @@ const customRender = ( if (state && state.initialState) { reduxStore = testStore(state.initialState || {}); } + if (state && state.sagasToRun) { + reduxStore = testStoreWithTestMiddleWare(reduxStore.getState()); + testSagaMiddleware.run(() => rootSaga(state.sagasToRun)); + } + const defaultTheme = getCurrentThemeDetails(reduxStore.getState()); return render( From b1d7258dcbbea823d4855027bd6ff7c5d1922d16 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Wed, 19 May 2021 10:48:51 +0530 Subject: [PATCH 32/52] Adding support for multipart form data (#4547) * Handling multipart form data in the CURL import flow * Adding Cypress test for curl import with multipart/form-data * Also fixing minor bug where the form values need not always be double-quoted --- .../ApiPaneTests/API_CurlPOSTImport_spec.js | 31 +++++++- .../src/constants/ApiEditorConstants.ts | 8 +- .../pages/Editor/APIEditor/PostBodyData.tsx | 9 ++- .../com/external/plugins/RestApiPlugin.java | 74 ++++++++++++------- .../external/plugins/RestApiPluginTest.java | 29 +++++++- .../server/services/CurlImporterService.java | 44 ++++++++--- .../services/CurlImporterServiceTest.java | 20 ++++- 7 files changed, 165 insertions(+), 50 deletions(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js index b23b1b8fdbd..2d8be44b26a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_CurlPOSTImport_spec.js @@ -2,7 +2,7 @@ const ApiEditor = require("../../../../locators/ApiEditor.json"); const apiwidget = require("../../../../locators/apiWidgetslocator.json"); describe("Test curl import flow", function() { - it("Test curl import flow for POST action", function() { + it("Test curl import flow for POST action with JSON body", function() { localStorage.setItem("ApiPaneV2", "ApiPaneV2"); cy.NavigateToApiEditor(); cy.get(ApiEditor.curlImage).click({ force: true }); @@ -26,4 +26,33 @@ describe("Test curl import flow", function() { }); }); }); + + it("Test curl import flow for POST action with multipart form data", function() { + localStorage.setItem("ApiPaneV2", "ApiPaneV2"); + cy.NavigateToApiEditor(); + cy.get(ApiEditor.curlImage).click({ force: true }); + cy.get("textarea").type( + "curl --request POST http://httpbin.org/post -F 'randomKey=randomValue' --form 'randomKey2=\"randomValue2\"'", + { + force: true, + parseSpecialCharSequences: false, + }, + ); + cy.importCurl(); + cy.RunAPI(); + cy.ResponseStatusCheck("200 OK"); + cy.log("Ran the API successfully"); + cy.get("@postExecute").then((response) => { + cy.log(response.response.body); + cy.expect(response.response.body.responseMeta.success).to.eq(true); + // Asserting if the form key value are returned in the response + cy.expect(response.response.body.data.body.form.randomKey).to.eq( + "randomValue", + ); + // Asserting the content type header set in curl import is multipart/form-data + cy.expect( + response.response.body.data.body.headers["Content-Type"], + ).contains("multipart/form-data;boundary"); + }); + }); }); diff --git a/app/client/src/constants/ApiEditorConstants.ts b/app/client/src/constants/ApiEditorConstants.ts index c142a5ca639..c0b31a12d1e 100644 --- a/app/client/src/constants/ApiEditorConstants.ts +++ b/app/client/src/constants/ApiEditorConstants.ts @@ -65,17 +65,11 @@ export const POST_BODY_FORMAT_OPTIONS: Array<{ { label: ApiContentTypes.RAW, value: "raw" }, ]; -export const POST_BODY_FORMAT_OPTIONS_NO_MULTI_PART = POST_BODY_FORMAT_OPTIONS.filter( - (option) => { - return option.value !== "multipart/form-data"; - }, -); - export const POST_BODY_FORMATS = POST_BODY_FORMAT_OPTIONS.map((option) => { return option.value; }); -export const POST_BODY_FORMAT_TITLES_NO_MULTI_PART = POST_BODY_FORMAT_OPTIONS_NO_MULTI_PART.map( +export const POST_BODY_FORMAT_TITLES = POST_BODY_FORMAT_OPTIONS.map( (option) => { return { title: option.label, key: option.value }; }, diff --git a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx index 86f3bbf08c1..e1d3b549c63 100644 --- a/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx +++ b/app/client/src/pages/Editor/APIEditor/PostBodyData.tsx @@ -5,7 +5,7 @@ import { formValueSelector } from "redux-form"; import { ApiContentTypes, POST_BODY_FORMAT_OPTIONS, - POST_BODY_FORMAT_TITLES_NO_MULTI_PART, + POST_BODY_FORMAT_TITLES, } from "constants/ApiEditorConstants"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; @@ -62,7 +62,7 @@ function PostBodyData(props: Props) { updateBodyContentType(title, apiId) } selected={displayFormat} - tabs={POST_BODY_FORMAT_TITLES_NO_MULTI_PART.map((el) => { + tabs={POST_BODY_FORMAT_TITLES.map((el) => { let component = ( ); - if (el.title === ApiContentTypes.FORM_URLENCODED) { + if ( + el.title === ApiContentTypes.FORM_URLENCODED || + el.title === ApiContentTypes.MULTIPART_FORM_DATA + ) { component = ( executeParameterized(APIConnection connection smartJsonSubstitution = false; // Since properties is not empty, we are guaranteed to find the first property. - } else if (properties.get(SMART_JSON_SUBSTITUTION_INDEX) != null){ + } else if (properties.get(SMART_JSON_SUBSTITUTION_INDEX) != null) { Object ssubValue = properties.get(SMART_JSON_SUBSTITUTION_INDEX).getValue(); - if (ssubValue instanceof Boolean) { + if (ssubValue instanceof Boolean) { smartJsonSubstitution = (Boolean) ssubValue; } else if (ssubValue instanceof String) { smartJsonSubstitution = Boolean.parseBoolean((String) ssubValue); @@ -265,16 +267,18 @@ public Mono executeCommon(APIConnection apiConnection, return Mono.just(errorResult); } - String requestBodyAsString = ""; + // We initialize this object to an empty string because body can never be empty + // Based on the content-type, this Object may be of type MultiValueMap or String + Object requestBodyObj = ""; // Add request body only for non GET calls. if (!HttpMethod.GET.equals(httpMethod)) { // Adding request body - requestBodyAsString = (actionConfiguration.getBody() == null) ? "" : actionConfiguration.getBody(); + requestBodyObj = (actionConfiguration.getBody() == null) ? "" : actionConfiguration.getBody(); if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(reqContentType) || MediaType.MULTIPART_FORM_DATA_VALUE.equals(reqContentType)) { - requestBodyAsString = convertPropertyListToReqBody(actionConfiguration.getBodyFormData(), + requestBodyObj = convertPropertyListToReqBody(actionConfiguration.getBodyFormData(), reqContentType, encodeParamsToggle); } @@ -309,7 +313,7 @@ public Mono executeCommon(APIConnection apiConnection, WebClient client = webClientBuilder.exchangeStrategies(EXCHANGE_STRATEGIES).build(); // Triggering the actual REST API call - return httpCall(client, httpMethod, uri, requestBodyAsString, 0, reqContentType) + return httpCall(client, httpMethod, uri, requestBodyObj, 0, reqContentType) .flatMap(clientResponse -> clientResponse.toEntity(byte[].class)) .map(stringResponseEntity -> { HttpHeaders headers = stringResponseEntity.getHeaders(); @@ -380,7 +384,7 @@ public Mono executeCommon(APIConnection apiConnection, return result; }) - .onErrorResume(error -> { + .onErrorResume(error -> { errorResult.setIsExecutionSuccess(false); errorResult.setErrorInfo(error); return Mono.just(errorResult); @@ -415,32 +419,52 @@ private String getSignatureKey(DatasourceConfiguration datasourceConfiguration) return null; } - public String convertPropertyListToReqBody(List bodyFormData, + /** + * This function converts the list of properties in bodyFormData to an appropriate data structure for WebClient + * to consume. Based on the data type, WebClient creates appropriate logic for making the HTTP request. + * This is especially required for data type multipart/form-data + * @param bodyFormData + * @param reqContentType + * @param encodeParamsToggle + * @return Object + */ + public Object convertPropertyListToReqBody(List bodyFormData, String reqContentType, Boolean encodeParamsToggle) { if (bodyFormData == null || bodyFormData.isEmpty()) { return ""; } - String reqBody = bodyFormData.stream() - .map(property -> { - String key = property.getKey(); - String value = (String) property.getValue(); + Object requestBody = null; - if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(reqContentType) - && encodeParamsToggle == true) { - try { - value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - throw new UnsupportedOperationException(e); - } - } + switch (reqContentType) { + case MediaType.APPLICATION_FORM_URLENCODED_VALUE: + // The request body should be a urlEncoded string of key-value pairs + requestBody = bodyFormData.stream() + .map(property -> { + String key = property.getKey(); + String value = (String) property.getValue(); - return key + "=" + value; - }) - .collect(Collectors.joining("&")); + if (encodeParamsToggle == true) { + try { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + throw new UnsupportedOperationException(e); + } + } - return reqBody; + return key + "=" + value; + }) + .collect(Collectors.joining("&")); + break; + + case MediaType.MULTIPART_FORM_DATA_VALUE: + // The request body should be of type MultiValueMap for WebClient to function properly + requestBody = bodyFormData.stream() + .collect(StreamUtils.toMultiMap(Property::getKey, Property::getValue)); + break; + } + return requestBody; } /** @@ -510,7 +534,7 @@ private Mono httpCall(WebClient webClient, HttpMethod httpMethod return webClient .method(httpMethod) .uri(uri) - .body(BodyInserters.fromObject(requestBody)) + .body(BodyInserters.fromValue(requestBody)) .exchange() .doOnError(e -> Mono.error(new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, e))) .flatMap(response -> { diff --git a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java index 1ea8592bb19..b6c6398837e 100644 --- a/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java +++ b/app/server/appsmith-plugins/restApiPlugin/src/test/java/com/external/plugins/RestApiPluginTest.java @@ -71,7 +71,7 @@ public void testValidJsonApiExecution() { @Test public void testEncodingFunctionWithEncodeParamsToggleTrue() throws UnsupportedEncodingException { - String encoded_value = pluginExecutor.convertPropertyListToReqBody(List.of(new Property("key", "valüe")), + Object encoded_value = pluginExecutor.convertPropertyListToReqBody(List.of(new Property("key", "valüe")), "application/x-www-form-urlencoded", true); String expected_value = null; @@ -85,7 +85,7 @@ public void testEncodingFunctionWithEncodeParamsToggleTrue() throws UnsupportedE @Test public void testEncodingFunctionWithEncodeParamsToggleFalse() throws UnsupportedEncodingException { - String encoded_value = pluginExecutor.convertPropertyListToReqBody(List.of(new Property("key", "valüe")), + Object encoded_value = pluginExecutor.convertPropertyListToReqBody(List.of(new Property("key", "valüe")), "application/x-www-form-urlencoded", false); String expected_value = null; @@ -404,4 +404,29 @@ public void testSmartSubstitutionJSONBody() { }) .verifyComplete(); } + + @Test + public void testMultipartFormData() { + DatasourceConfiguration dsConfig = new DatasourceConfiguration(); + dsConfig.setUrl("http://httpbin.org/post"); + + ActionConfiguration actionConfig = new ActionConfiguration(); + actionConfig.setHeaders(List.of(new Property("content-type", "multipart/form-data"))); + + actionConfig.setHttpMethod(HttpMethod.POST); + String requestBody = "{\"key\":\"skdjfh&kjsd\"}"; + List formData = List.of(new Property("key", "skdjfh&kjsd")); + actionConfig.setBodyFormData(formData); + + Mono resultMono = pluginExecutor.executeParameterized(null, new ExecuteActionDTO(), dsConfig, actionConfig); + StepVerifier.create(resultMono) + .assertNext(result -> { + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode data = ((ObjectNode) result.getBody()).get("form"); + assertEquals(requestBody, data.toString()); + }) + .verifyComplete(); + } + } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java index c83f61c9b1d..56b0b1604f2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/CurlImporterService.java @@ -13,7 +13,9 @@ import org.apache.commons.lang.StringUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -33,22 +35,18 @@ public class CurlImporterService extends BaseApiImporter { private static final String RESTAPI_PLUGIN = "restapi-plugin"; private static final String ARG_DATA = "--data"; + private static final String ARG_FORM = "--form"; private static final String ARG_HEADER = "--header"; private static final String ARG_REQUEST = "--request"; private static final String ARG_COOKIE = "--cookie"; private static final String ARG_USER = "--user"; private static final String ARG_USER_AGENT = "--user-agent"; - private static final String CONTENT_TYPE_URLENCODED = "application/x-www-form-urlencoded"; - - private final NewActionService newActionService; private final PluginService pluginService; private final LayoutActionService layoutActionService; - public CurlImporterService(NewActionService newActionService, - PluginService pluginService, + public CurlImporterService(PluginService pluginService, LayoutActionService layoutActionService) { - this.newActionService = newActionService; this.pluginService = pluginService; this.layoutActionService = layoutActionService; } @@ -215,6 +213,9 @@ public List normalize(List tokens) { normalizedTokens.add(token.substring(2)); } + } else if ("-F".equals(token)) { + normalizedTokens.add(ARG_FORM); + } else if ("-H".equals(token)) { normalizedTokens.add(ARG_HEADER); @@ -276,6 +277,7 @@ public ActionDTO parse(List tokens) throws AppsmithException { final List headers = new ArrayList<>(); String contentType = null; final List dataParts = new ArrayList<>(); + final List formParts = new ArrayList<>(); String state = null; @@ -310,6 +312,10 @@ public ActionDTO parse(List tokens) throws AppsmithException { // The `token` is next to `--data-urlencode`. dataParts.add(token); + } else if (ARG_FORM.equals(state)) { + // The token is next to --form + formParts.add(token); + } else if (ARG_COOKIE.equals(state)) { // The `token` is next to `--data-cookie`. headers.add(new Property("Set-Cookie", token)); @@ -347,8 +353,12 @@ public ActionDTO parse(List tokens) throws AppsmithException { } if (contentType == null && !dataParts.isEmpty()) { - contentType = CONTENT_TYPE_URLENCODED; - headers.add(new Property("Content-Type", contentType)); + contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE; + headers.add(new Property(HttpHeaders.CONTENT_TYPE, contentType)); + + } else if (contentType == null && !formParts.isEmpty()) { + contentType = MediaType.MULTIPART_FORM_DATA_VALUE; + headers.add(new Property(HttpHeaders.CONTENT_TYPE, contentType)); } if (!headers.isEmpty()) { @@ -356,7 +366,7 @@ public ActionDTO parse(List tokens) throws AppsmithException { } if (!dataParts.isEmpty()) { - if (CONTENT_TYPE_URLENCODED.equals(contentType)) { + if (MediaType.APPLICATION_FORM_URLENCODED_VALUE.equals(contentType)) { final ArrayList formPairs = new ArrayList<>(); actionConfiguration.setBodyFormData(formPairs); for (String part : dataParts) { @@ -369,6 +379,22 @@ public ActionDTO parse(List tokens) throws AppsmithException { } } + if (!formParts.isEmpty()) { + if (MediaType.MULTIPART_FORM_DATA_VALUE.equals(contentType)) { + final ArrayList formPairs = new ArrayList<>(); + actionConfiguration.setBodyFormData(formPairs); + for (String part : formParts) { + final String[] parts = part.split("=", 2); + // Multipart form values are double quoted. Eg: "value" + // We trim the quotes from the beginning & end of the string + String formValue = (parts.length > 1) ? parts[1].replaceAll("^\"|\"$", "") : ""; + formPairs.add(new Property(parts[0], formValue)); + } + + } else { + actionConfiguration.setBody(StringUtils.join(formParts, '&')); + } + } if (actionConfiguration.getHttpMethod() == null) { // Default HTTP method is POST if there is body data to send, else GET. diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java index 89d59d55116..b31d9d14ebf 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/CurlImporterServiceTest.java @@ -55,9 +55,6 @@ public class CurlImporterServiceTest { @Autowired UserService userService; - @Autowired - OrganizationService organizationService; - String orgId; @Before @@ -664,6 +661,23 @@ public void parseMultipleData() throws AppsmithException { ); } + @Test + public void parseMultiFormData() throws AppsmithException { + // In the curl command, we test for a combination of --form and -F + // Also some values are double-quoted while some aren't. This tests a permutation of all such fields + ActionDTO action = curlImporterService.curlToAction("curl --request POST 'http://httpbin.org/post' -F 'somekey=value' --form 'anotherKey=\"anotherValue\"'"); + assertMethod(action, HttpMethod.POST); + assertUrl(action, "http://httpbin.org"); + assertPath(action, "/post"); + assertHeaders(action, new Property("Content-Type", "multipart/form-data")); + assertEmptyBody(action); + assertBodyFormData( + action, + new Property("somekey", "value"), + new Property("anotherKey", "anotherValue") + ); + } + @Test public void dontEatBackslashesInSingleQuotes() throws AppsmithException { ActionDTO action = curlImporterService.curlToAction("curl http://httpbin.org/post -d 'a\\n'"); From 04659f45b5a268849daddbf71b53473aa21640f3 Mon Sep 17 00:00:00 2001 From: Arpit Mohan Date: Wed, 19 May 2021 10:50:50 +0530 Subject: [PATCH 33/52] Updating release-drafter to add skip-changelog PR (#4484) --- .github/release-drafter-template.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/release-drafter-template.yml b/.github/release-drafter-template.yml index ff28150475f..e087624d0aa 100644 --- a/.github/release-drafter-template.yml +++ b/.github/release-drafter-template.yml @@ -9,6 +9,9 @@ categories: - 'Bug' - title: '📙 Documentation' label: 'Documentation' +# Any PR with the label 'skip-changelog' will not be inserted into the release notes +exclude-labels: + - 'skip-changelog' change-template: '- $TITLE (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. version-resolver: @@ -25,4 +28,4 @@ version-resolver: template: | ## What's new? - $CHANGES \ No newline at end of file + $CHANGES From d5e5be5d6aa1a16a5bdaa215cf7a8636c6b155ad Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Wed, 19 May 2021 11:05:24 +0530 Subject: [PATCH 34/52] [Feature] Mongo Form (#4378) --- app/client/src/entities/Datasource/index.ts | 1 + .../Explorer/Datasources/QueryTemplates.tsx | 1 + .../Editor/QueryEditor/EditorJSONtoForm.tsx | 46 +- .../src/pages/Editor/QueryEditor/Form.tsx | 4 + .../external/models/DatasourceStructure.java | 1 + .../com/external/plugins/MongoPlugin.java | 200 +++--- .../external/plugins/MongoPluginUtils.java | 50 ++ .../external/plugins/commands/Aggregate.java | 82 +++ .../com/external/plugins/commands/Count.java | 53 ++ .../com/external/plugins/commands/Delete.java | 118 ++++ .../external/plugins/commands/Distinct.java | 67 ++ .../com/external/plugins/commands/Find.java | 186 ++++++ .../com/external/plugins/commands/Insert.java | 126 ++++ .../plugins/commands/MongoCommand.java | 61 ++ .../external/plugins/commands/UpdateMany.java | 125 ++++ .../external/plugins/commands/UpdateOne.java | 79 +++ .../plugins/constants/ConfigurationIndex.java | 31 + .../src/main/resources/editor.json | 408 ++++++++++- .../com/external/plugins/MongoPluginTest.java | 631 +++++++++++++++--- .../com/external/plugins/MySqlPlugin.java | 8 +- .../com/external/plugins/MySqlPluginTest.java | 16 +- .../com/external/plugins/PostgresPlugin.java | 8 +- .../external/plugins/PostgresPluginTest.java | 16 +- .../com/external/plugins/RedshiftPlugin.java | 8 +- .../external/plugins/RedshiftPluginTest.java | 16 +- .../server/migrations/DatabaseChangelog.java | 19 + 26 files changed, 2112 insertions(+), 249 deletions(-) create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPluginUtils.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Aggregate.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Count.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Distinct.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/MongoCommand.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateOne.java create mode 100644 app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java diff --git a/app/client/src/entities/Datasource/index.ts b/app/client/src/entities/Datasource/index.ts index 68f96c4c22f..f9c4a3c798d 100644 --- a/app/client/src/entities/Datasource/index.ts +++ b/app/client/src/entities/Datasource/index.ts @@ -25,6 +25,7 @@ export interface DatasourceStructure { export interface QueryTemplate { title: string; body: string; + pluginSpecifiedTemplates?: Array<{ key?: string; value?: unknown }>; } export interface DatasourceTable { type: string; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx index 838cf7408cb..42641e3d328 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx @@ -50,6 +50,7 @@ export function QueryTemplates(props: QueryTemplatesProps) { const queryactionConfiguration: Partial = { actionConfiguration: { body: template.body, + pluginSpecifiedTemplates: template.pluginSpecifiedTemplates, }, }; diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index c08fd6c1530..0d718d56c3f 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -16,7 +16,7 @@ import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import JSONViewer from "./JSONViewer"; import FormControl from "../FormControl"; import Table from "./Table"; -import { Action } from "entities/Action"; +import { Action, QueryAction, SaaSAction } from "entities/Action"; import { useDispatch } from "react-redux"; import ActionNameEditor from "components/editorComponents/ActionNameEditor"; import DropdownField from "components/editorComponents/form/fields/DropdownField"; @@ -47,6 +47,7 @@ import CloseEditor from "components/editorComponents/CloseEditor"; import { setGlobalSearchQuery } from "actions/globalSearchActions"; import { toggleShowGlobalSearchModal } from "actions/globalSearchActions"; import { omnibarDocumentationHelper } from "constants/OmnibarDocumentationConstants"; +import { isHidden } from "components/formControls/utils"; const QueryFormContainer = styled.form` display: flex; @@ -339,6 +340,7 @@ type QueryFormProps = { editorConfig?: any; formName: string; settingConfig: any; + formData: SaaSAction | QueryAction; }; type ReduxProps = { @@ -372,7 +374,6 @@ export function EditorJSONtoForm(props: Props) { runErrorMessage, settingConfig, } = props; - let error = runErrorMessage; let output: Record[] | null = null; let hintMessages: Array = []; @@ -457,6 +458,27 @@ 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) { + return renderEachConfig(formName)(formControlOrSection); + } else { + try { + const { configProperty } = formControlOrSection; + return ( + + + + ); + } catch (e) { + log.error(e); + } + } + return null; + }); + }; + const responseTabs = [ { key: "Response", @@ -685,23 +707,3 @@ export function EditorJSONtoForm(props: Props) { ); } - -const renderEachConfig = (formName: string) => (section: any): any => { - return section.children.map((formControlOrSection: ControlProps) => { - if ("children" in formControlOrSection) { - return renderEachConfig(formName)(formControlOrSection); - } else { - try { - const { configProperty } = formControlOrSection; - return ( - - - - ); - } catch (e) { - log.error(e); - } - } - return null; - }); -}; diff --git a/app/client/src/pages/Editor/QueryEditor/Form.tsx b/app/client/src/pages/Editor/QueryEditor/Form.tsx index edce2bd59ce..b3ae639728a 100644 --- a/app/client/src/pages/Editor/QueryEditor/Form.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Form.tsx @@ -8,6 +8,8 @@ import { getPluginDocumentationLinks, } from "selectors/entitiesSelector"; import { EditorJSONtoForm, EditorJSONtoFormProps } from "./EditorJSONtoForm"; +import { getFormValues } from "redux-form"; +import { QueryAction } from "entities/Action"; const valueSelector = formValueSelector(QUERY_EDITOR_FORM_NAME); const mapStateToProps = (state: AppState) => { @@ -17,6 +19,7 @@ const mapStateToProps = (state: AppState) => { const responseTypes = getPluginResponseTypes(state); const documentationLinks = getPluginDocumentationLinks(state); + const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as QueryAction; return { actionName, @@ -25,6 +28,7 @@ const mapStateToProps = (state: AppState) => { responseType: responseTypes[pluginId], documentationLink: documentationLinks[pluginId], formName: QUERY_EDITOR_FORM_NAME, + formData: formData, }; }; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java index cc88bd52a7c..4dd08211f92 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/models/DatasourceStructure.java @@ -104,6 +104,7 @@ public String getType() { public static class Template { String title; String body; + List pluginSpecifiedTemplates; } ErrorDTO error; diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java index 8fcf54bc3de..1bbeff0b17a 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPlugin.java @@ -1,5 +1,6 @@ package com.external.plugins; +import com.appsmith.external.constants.DisplayDataType; import com.appsmith.external.dtos.ExecuteActionDTO; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; @@ -9,7 +10,6 @@ import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; -import com.appsmith.external.constants.DisplayDataType; import com.appsmith.external.models.Connection; import com.appsmith.external.models.DBAuth; import com.appsmith.external.models.DatasourceConfiguration; @@ -23,6 +23,15 @@ import com.appsmith.external.plugins.BasePlugin; import com.appsmith.external.plugins.PluginExecutor; import com.appsmith.external.plugins.SmartSubstitutionInterface; +import com.external.plugins.commands.Aggregate; +import com.external.plugins.commands.Count; +import com.external.plugins.commands.Delete; +import com.external.plugins.commands.Distinct; +import com.external.plugins.commands.Find; +import com.external.plugins.commands.Insert; +import com.external.plugins.commands.MongoCommand; +import com.external.plugins.commands.UpdateMany; +import com.external.plugins.commands.UpdateOne; import com.mongodb.MongoCommandException; import com.mongodb.MongoTimeoutException; import com.mongodb.reactivestreams.client.MongoClient; @@ -67,6 +76,8 @@ import java.util.stream.Collectors; import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; import static java.lang.Boolean.TRUE; public class MongoPlugin extends BasePlugin { @@ -85,7 +96,9 @@ public class MongoPlugin extends BasePlugin { public static final String N_MODIFIED = "nModified"; - private static final String VALUE_STR = "value"; + private static final String VALUE = "value"; + + private static final String VALUES = "values"; private static final int TEST_DATASOURCE_TIMEOUT_SECONDS = 15; @@ -168,9 +181,9 @@ public Mono executeParameterized(MongoClient mongoClient, smartBsonSubstitution = false; // Since properties is not empty, we are guaranteed to find the first property. - } else if (properties.get(SMART_BSON_SUBSTITUTION_INDEX) != null){ + } else if (properties.get(SMART_BSON_SUBSTITUTION_INDEX) != null) { Object ssubValue = properties.get(SMART_BSON_SUBSTITUTION_INDEX).getValue(); - if (ssubValue instanceof Boolean) { + if (ssubValue instanceof Boolean) { smartBsonSubstitution = (Boolean) ssubValue; } else if (ssubValue instanceof String) { smartBsonSubstitution = Boolean.parseBoolean((String) ssubValue); @@ -209,6 +222,11 @@ public Mono executeParameterized(MongoClient mongoClient, } prepareConfigurationsForExecution(executeActionDTO, actionConfiguration, datasourceConfiguration); + // In case the input type is form instead of raw, parse the same into BSON command + String parsedRawCommand = convertMongoFormInputToRawCommand(actionConfiguration); + if (parsedRawCommand != null) { + actionConfiguration.setBody(parsedRawCommand); + } return this.executeCommon(mongoClient, datasourceConfiguration, actionConfiguration, parameters); } @@ -278,9 +296,9 @@ public Mono executeCommon(MongoClient mongoClient, * we either get the modified new value or the pre-modified old value (depending on the * `new` field in the command. Let's return that value to the user. */ - if (outputJson.has(VALUE_STR)) { + if (outputJson.has(VALUE)) { result.setBody(objectMapper.readTree( - cleanUp(new JSONObject().put(VALUE_STR, outputJson.get(VALUE_STR))).toString() + cleanUp(new JSONObject().put(VALUE, outputJson.get(VALUE))).toString() )); } @@ -301,7 +319,7 @@ public Mono executeCommon(MongoClient mongoClient, */ if (outputJson.has("n")) { JSONObject body = new JSONObject().put("n", outputJson.getBigInteger("n")); - result.setBody(body); + result.setBody(objectMapper.readTree(body.toString())); headerArray.put(body); } @@ -311,10 +329,19 @@ public Mono executeCommon(MongoClient mongoClient, */ if (outputJson.has(N_MODIFIED)) { JSONObject body = new JSONObject().put(N_MODIFIED, outputJson.getBigInteger(N_MODIFIED)); - result.setBody(body); + result.setBody(objectMapper.readTree(body.toString())); headerArray.put(body); } + /** + * The json contains key "values" when distinct command is used. + */ + if (outputJson.has(VALUES)) { + JSONArray outputResult = (JSONArray) cleanUp( + outputJson.getJSONArray("values")); + result.setBody(objectMapper.readTree(outputResult.toString())); + } + /** TODO * Go through all the possible fields that are returned in the output JSON and add all the fields * that are important to the headerArray. @@ -355,6 +382,59 @@ public Mono executeCommon(MongoClient mongoClient, .subscribeOn(scheduler); } + private String convertMongoFormInputToRawCommand(ActionConfiguration actionConfiguration) { + List templates = actionConfiguration.getPluginSpecifiedTemplates(); + if (templates != null) { + if ((templates.size() >= (1 + INPUT_TYPE)) && + (templates.get(INPUT_TYPE) != null) && + ("FORM".equals(templates.get(INPUT_TYPE).getValue())) && + (templates.size() >= (1 + COMMAND)) && + (templates.get(COMMAND) != null) && + (templates.get(COMMAND).getValue() != null)) { + // The user has configured FORM for command input. Parse the commands appropriately + + MongoCommand command = null; + switch ((String) templates.get(COMMAND).getValue()) { + case "INSERT": + command = new Insert(actionConfiguration); + break; + case "FIND": + command = new Find(actionConfiguration); + break; + case "UPDATE_ONE": + command = new UpdateOne(actionConfiguration); + break; + case "UPDATE_MANY": + command = new UpdateMany(actionConfiguration); + break; + case "DELETE": + command = new Delete(actionConfiguration); + break; + case "COUNT": + command = new Count(actionConfiguration); + break; + case "DISTINCT": + command = new Distinct(actionConfiguration); + break; + case "AGGREGATE": + command = new Aggregate(actionConfiguration); + break; + default: + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "No valid mongo command found. Please select a command from the \"Command\" dropdown and try again"); + } + if (!command.isValid()) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Try again after configuring the fields : " + command.getFieldNamesWithNoConfiguration()); + } + + return command.parseCommand().toJson(); + } + } + + // We reached here. This means either this is a RAW command input or some configuration error has happened + // in which case, we default to RAW + return actionConfiguration.getBody(); + } + private String getDatabaseName(DatasourceConfiguration datasourceConfiguration) { // Explicitly set default database. String databaseName = datasourceConfiguration.getConnection().getDefaultDatabaseName(); @@ -456,21 +536,19 @@ public String buildClientURI(DatasourceConfiguration datasourceConfiguration) th if (isUsingURI(datasourceConfiguration)) { if (hasNonEmptyURI(datasourceConfiguration)) { String uriWithHiddenPassword = - (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); + (String) properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); Map extractedInfo = extractInfoFromConnectionStringURI(uriWithHiddenPassword, MONGO_URI_REGEX); if (extractedInfo != null) { - String password = ((DBAuth)datasourceConfiguration.getAuthentication()).getPassword(); - return buildURIfromExtractedInfo(extractedInfo, password); - } - else { + String password = ((DBAuth) datasourceConfiguration.getAuthentication()).getPassword(); + return buildURIfromExtractedInfo(extractedInfo, password); + } else { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Appsmith server has failed to parse the Mongo connection string URI. Please check " + "if the URI has the correct format." ); } - } - else { + } else { throw new AppsmithPluginException( AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, "Could not find any Mongo connection string URI. Please edit the 'Mongo Connection String" + @@ -615,7 +693,7 @@ public Set validateDatasource(DatasourceConfiguration datasourceConfigur invalids.add("'Mongo Connection String URI' field is empty. Please edit the 'Mongo Connection " + "URI' field to provide a connection uri to connect with."); } else { - String mongoUri = (String)properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); + String mongoUri = (String) properties.get(DATASOURCE_CONFIG_MONGO_URI_PROPERTY_INDEX).getValue(); if (!mongoUri.matches(MONGO_URI_REGEX)) { invalids.add("Mongo Connection String URI does not seem to be in the correct format. Please " + "check the URI once."); @@ -848,89 +926,27 @@ private static void generateTemplatesAndStructureForACollection(String collectio columns.sort(Comparator.naturalOrder()); - templates.add( - new DatasourceStructure.Template( - "Find", - "{\n" + - " \"find\": \"" + collectionName + "\",\n" + - ( - filterFieldName == null ? "" : - " \"filter\": {\n" + - " \"" + filterFieldName + "\": \"" + filterFieldValue + "\"\n" + - " },\n" - ) + - " \"sort\": {\n" + - " \"_id\": 1\n" + - " },\n" + - " \"limit\": 10\n" + - "}\n" - ) - ); + Map templateConfiguration = new HashMap<>(); + templateConfiguration.put("collectionName", collectionName); + templateConfiguration.put("filterFieldName", filterFieldName); + templateConfiguration.put("filterFieldValue", filterFieldValue); + templateConfiguration.put("sampleInsertValues", sampleInsertValues); - templates.add( - new DatasourceStructure.Template( - "Find by ID", - "{\n" + - " \"find\": \"" + collectionName + "\",\n" + - " \"filter\": {\n" + - " \"_id\": ObjectId(\"id_to_query_with\")\n" + - " }\n" + - "}\n" - ) + templates.addAll( + new Find().generateTemplate(templateConfiguration) ); - sampleInsertValues.entrySet().stream() - .map(entry -> " \"" + entry.getKey() + "\": " + entry.getValue() + ",\n") - .collect(Collectors.joining("")); - templates.add( - new DatasourceStructure.Template( - "Insert", - "{\n" + - " \"insert\": \"" + collectionName + "\",\n" + - " \"documents\": [\n" + - " {\n" + - sampleInsertValues.entrySet().stream() - .map(entry -> " \"" + entry.getKey() + "\": " + entry.getValue() + ",\n") - .sorted() - .collect(Collectors.joining("")) + - " }\n" + - " ]\n" + - "}\n" - ) + + templates.addAll( + new Insert().generateTemplate(templateConfiguration) ); - templates.add( - new DatasourceStructure.Template( - "Update", - "{\n" + - " \"update\": \"" + collectionName + "\",\n" + - " \"updates\": [\n" + - " {\n" + - " \"q\": {\n" + - " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + - " },\n" + - " \"u\": { \"$set\": { \"" + filterFieldName + "\": \"new value\" } }\n" + - " }\n" + - " ]\n" + - "}\n" - ) + templates.addAll( + new UpdateMany().generateTemplate(templateConfiguration) ); - templates.add( - new DatasourceStructure.Template( - "Delete", - "{\n" + - " \"delete\": \"" + collectionName + "\",\n" + - " \"deletes\": [\n" + - " {\n" + - " \"q\": {\n" + - " \"_id\": \"id_of_document_to_delete\"\n" + - " },\n" + - " \"limit\": 1\n" + - " }\n" + - " ]\n" + - "}\n" - ) + templates.addAll( + new Delete().generateTemplate(templateConfiguration) ); } diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPluginUtils.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPluginUtils.java new file mode 100644 index 00000000000..f2252eaa366 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/MongoPluginUtils.java @@ -0,0 +1,50 @@ +package com.external.plugins; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.Property; +import org.bson.Document; +import org.bson.json.JsonParseException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.constants.ConfigurationIndex.MAX_SIZE; + +public class MongoPluginUtils { + + public static Boolean validConfigurationPresent(List pluginSpecifiedTemplates, int index) { + if (pluginSpecifiedTemplates != null) { + if (pluginSpecifiedTemplates.size() > index) { + if (pluginSpecifiedTemplates.get(index) != null) { + if (pluginSpecifiedTemplates.get(index).getValue() != null) { + return Boolean.TRUE; + } + } + } + } + + return Boolean.FALSE; + } + + public static Document parseSafely(String fieldName, String input) { + try { + return Document.parse(input); + } catch (JsonParseException e) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, fieldName + " could not be parsed into expected JSON format."); + } + } + + public static List generateMongoFormConfigTemplates(Map configuration) { + List templates = new ArrayList<>(); + for (int i = 0; i < MAX_SIZE; i++) { + Property template = new Property(); + if (configuration.containsKey(i)) { + template.setValue(configuration.get(i)); + } + templates.add(template); + } + return templates; + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Aggregate.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Aggregate.java new file mode 100644 index 00000000000..690e8b87780 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Aggregate.java @@ -0,0 +1,82 @@ +package com.external.plugins.commands; + +import com.appsmith.external.constants.DataType; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.DataTypeStringUtils; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.AGGREGATE_PIPELINE; + +@Getter +@Setter +public class Aggregate extends MongoCommand { + String pipeline; + + public Aggregate(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, AGGREGATE_PIPELINE)) { + this.pipeline = (String) pluginSpecifiedTemplates.get(AGGREGATE_PIPELINE).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(pipeline)) { + return Boolean.TRUE; + } else { + fieldNamesWithNoConfiguration.add("Array of Pipelines"); + } + } + + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document commandDocument = new Document(); + + commandDocument.put("aggregate", this.collection); + + DataType dataType = DataTypeStringUtils.stringToKnownDataTypeConverter(this.pipeline); + if (dataType.equals(DataType.ARRAY)) { + try { + List arrayListFromInput = objectMapper.readValue(this.pipeline, List.class); + if (arrayListFromInput.isEmpty()) { + commandDocument.put("pipeline", "[]"); + } else { + commandDocument.put("pipeline", arrayListFromInput); + } + } catch (IOException e) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Array of Pipelines could not be parsed into expected JSON Array format."); + } + } else { + // The command expects the pipelines to be sent in an array. Parse and create a single element array + Document document = parseSafely("Array of Pipelines", this.pipeline); + ArrayList documentArrayList = new ArrayList<>(); + documentArrayList.add(document); + + commandDocument.put("pipeline", documentArrayList); + } + + // Add default cursor + commandDocument.put("cursor", parseSafely("cursor", "{}")); + + return commandDocument; + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Count.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Count.java new file mode 100644 index 00000000000..6bb6db0b98d --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Count.java @@ -0,0 +1,53 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.List; + +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.COUNT_QUERY; + +@Getter +@Setter +public class Count extends MongoCommand { + String query; + + public Count(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, COUNT_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(COUNT_QUERY).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query)) { + return Boolean.TRUE; + } else { + fieldNamesWithNoConfiguration.add("Query"); + } + } + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("count", this.collection); + + document.put("query", parseSafely("Query", this.query)); + + return document; + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java new file mode 100644 index 00000000000..d29c362afc4 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Delete.java @@ -0,0 +1,118 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates; +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.BSON; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.DELETE_LIMIT; +import static com.external.plugins.constants.ConfigurationIndex.DELETE_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; + +@Getter +@Setter +@NoArgsConstructor +public class Delete extends MongoCommand { + String query; + Integer limit = 1; // Can be only 0 or 1. 0 indicates all matching documents, 1 indicates single matching document + + public Delete(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, DELETE_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(DELETE_QUERY).getValue(); + } + + // Default for this is 1 to indicate deleting only one document at a time. + if (validConfigurationPresent(pluginSpecifiedTemplates, DELETE_LIMIT)) { + String limitOption = (String) pluginSpecifiedTemplates.get(DELETE_LIMIT).getValue(); + if ("ALL".equals(limitOption)) { + this.limit = 0; + } + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query)) { + return Boolean.TRUE; + } else { + fieldNamesWithNoConfiguration.add("Query"); + } + } + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("delete", this.collection); + + Document queryDocument = parseSafely("Query", this.query); + + Document delete = new Document(); + delete.put("q", queryDocument); + delete.put("limit", this.limit); + + List deletes = new ArrayList<>(); + deletes.add(delete); + + document.put("deletes", deletes); + + return document; + } + + @Override + public List generateTemplate(Map templateConfiguration) { + String collectionName = (String) templateConfiguration.get("collectionName"); + + Map configMap = new HashMap<>(); + + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, collectionName); + configMap.put(DELETE_QUERY, "{ \"_id\": ObjectId(\"id_of_document_to_delete\") }"); + configMap.put(DELETE_LIMIT, "SINGLE"); + + List pluginSpecifiedTemplates = generateMongoFormConfigTemplates(configMap); + + String rawQuery = "{\n" + + " \"delete\": \"" + collectionName + "\",\n" + + " \"deletes\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": \"id_of_document_to_delete\"\n" + + " },\n" + + " \"limit\": 1\n" + + " }\n" + + " ]\n" + + "}\n"; + + return Collections.singletonList(new DatasourceStructure.Template( + "Delete", + rawQuery, + pluginSpecifiedTemplates + )); + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Distinct.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Distinct.java new file mode 100644 index 00000000000..3d9d0edde66 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Distinct.java @@ -0,0 +1,67 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.List; + +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_KEY; +import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_QUERY; + +@Getter +@Setter +public class Distinct extends MongoCommand { + String query; + String key; + + public Distinct(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, DISTINCT_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(DISTINCT_QUERY).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, DISTINCT_KEY)) { + this.key = (String) pluginSpecifiedTemplates.get(DISTINCT_KEY).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query) && !StringUtils.isNullOrEmpty(key)) { + return Boolean.TRUE; + } else { + if (StringUtils.isNullOrEmpty(query)) { + fieldNamesWithNoConfiguration.add("Query"); + } + if (StringUtils.isNullOrEmpty(key)) { + fieldNamesWithNoConfiguration.add("Key/Field"); + } + } + } + + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("distinct", this.collection); + + document.put("query", parseSafely("Query", this.query)); + + document.put("key", this.key); + + return document; + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java new file mode 100644 index 00000000000..5c82aec0ad2 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Find.java @@ -0,0 +1,186 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates; +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.BSON; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.FIND_LIMIT; +import static com.external.plugins.constants.ConfigurationIndex.FIND_PROJECTION; +import static com.external.plugins.constants.ConfigurationIndex.FIND_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.FIND_SKIP; +import static com.external.plugins.constants.ConfigurationIndex.FIND_SORT; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; + +@Getter +@Setter +@NoArgsConstructor +public class Find extends MongoCommand { + String query; + String sort; + String projection; + String limit; + String skip; + + public Find(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, FIND_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(FIND_QUERY).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, FIND_SORT)) { + this.sort = (String) pluginSpecifiedTemplates.get(FIND_SORT).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, FIND_PROJECTION)) { + this.projection = (String) pluginSpecifiedTemplates.get(FIND_PROJECTION).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, FIND_LIMIT)) { + this.limit = (String) pluginSpecifiedTemplates.get(FIND_LIMIT).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, FIND_SKIP)) { + this.skip = (String) pluginSpecifiedTemplates.get(FIND_SKIP).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query)) { + return Boolean.TRUE; + } else { + fieldNamesWithNoConfiguration.add("Query"); + } + } + + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("find", this.collection); + + document.put("filter", parseSafely("Query", this.query)); + + if (!StringUtils.isNullOrEmpty(this.sort)) { + document.put("sort", parseSafely("Sort", this.sort)); + } + + if (!StringUtils.isNullOrEmpty(this.projection)) { + document.put("projection", this.projection); + } + + // Default to returning 10 documents if not mentioned + int limit = 10; + if (!StringUtils.isNullOrEmpty(this.limit)) { + limit = Integer.parseInt(this.limit); + } + document.put("limit", limit); + document.put("batchSize", limit); + + if (!StringUtils.isNullOrEmpty(this.skip)) { + document.put("skip", Long.parseLong(this.skip)); + } + + return document; + } + + @Override + public List generateTemplate(Map templateConfiguration) { + String collectionName = (String) templateConfiguration.get("collectionName"); + String filterFieldName = (String) templateConfiguration.get("filterFieldName"); + String filterFieldValue = (String) templateConfiguration.get("filterFieldValue"); + + List templates = new ArrayList<>(); + + templates.add(generateFindTemplate(collectionName, filterFieldName, filterFieldValue)); + + templates.add(generateFindByIdTemplate(collectionName)); + + return templates; + } + + private DatasourceStructure.Template generateFindTemplate(String collectionName, String filterFieldName, String filterFieldValue) { + Map configMap = new HashMap<>(); + + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "FIND"); + configMap.put(COLLECTION, collectionName); + configMap.put(FIND_SORT, "{\"_id\": 1}"); + configMap.put(FIND_LIMIT, "10"); + + String query = filterFieldName == null ? "{}" : + "{ \"" + filterFieldName + "\": \"" + filterFieldValue + "\"}"; + configMap.put(FIND_QUERY, query); + + List pluginSpecifiedTemplates = generateMongoFormConfigTemplates(configMap); + + String rawQuery = "{\n" + + " \"find\": \"" + collectionName + "\",\n" + + ( + filterFieldName == null ? "" : + " \"filter\": {\n" + + " \"" + filterFieldName + "\": \"" + filterFieldValue + "\"\n" + + " },\n" + ) + + " \"sort\": {\n" + + " \"_id\": 1\n" + + " },\n" + + " \"limit\": 10\n" + + "}\n"; + + return new DatasourceStructure.Template( + "Find", + rawQuery, + pluginSpecifiedTemplates + ); + } + + private DatasourceStructure.Template generateFindByIdTemplate(String collectionName) { + Map configMap = new HashMap<>(); + + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "FIND"); + configMap.put(FIND_QUERY, "{\"_id\": ObjectId(\"id_to_query_with\")}"); + configMap.put(COLLECTION, collectionName); + + List pluginSpecifiedTemplates = generateMongoFormConfigTemplates(configMap); + + String rawQuery = "{\n" + + " \"find\": \"" + collectionName + "\",\n" + + " \"filter\": {\n" + + " \"_id\": ObjectId(\"id_to_query_with\")\n" + + " }\n" + + "}\n"; + + return new DatasourceStructure.Template( + "Find by ID", + rawQuery, + pluginSpecifiedTemplates + ); + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java new file mode 100644 index 00000000000..4b46d7d5ec4 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/Insert.java @@ -0,0 +1,126 @@ +package com.external.plugins.commands; + +import com.appsmith.external.constants.DataType; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.helpers.DataTypeStringUtils; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates; +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.BSON; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; +import static com.external.plugins.constants.ConfigurationIndex.INSERT_DOCUMENT; + +@Getter +@Setter +@NoArgsConstructor +public class Insert extends MongoCommand { + String documents; + + public Insert(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, INSERT_DOCUMENT)) { + this.documents = (String) pluginSpecifiedTemplates.get(INSERT_DOCUMENT).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(documents)) { + return Boolean.TRUE; + } else { + fieldNamesWithNoConfiguration.add("Documents"); + } + } + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document commandDocument = new Document(); + + commandDocument.put("insert", this.collection); + + DataType dataType = DataTypeStringUtils.stringToKnownDataTypeConverter(this.documents); + if (dataType.equals(DataType.ARRAY)) { + try { + List arrayListFromInput = objectMapper.readValue(this.documents, List.class); + if (arrayListFromInput.isEmpty()) { + commandDocument.put("documents", "[]"); + } else { + commandDocument.put("documents", arrayListFromInput); + } + } catch (IOException e) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_EXECUTE_ARGUMENT_ERROR, "Documents" + " could not be parsed into expected JSON Array format."); + } + } else { + // The command expects the documents to be sent in an array. Parse and create a single element array + Document document = parseSafely("Documents", this.documents); + ArrayList documentArrayList = new ArrayList<>(); + documentArrayList.add(document); + + commandDocument.put("documents", documentArrayList); + } + + return commandDocument; + } + + @Override + public List generateTemplate(Map templateConfiguration) { + String collectionName = (String) templateConfiguration.get("collectionName"); + Map sampleInsertValues = (Map) templateConfiguration.get("sampleInsertValues"); + + String sampleInsertDocuments = sampleInsertValues.entrySet().stream() + .map(entry -> " \"" + entry.getKey() + "\": " + entry.getValue() + ",\n") + .sorted() + .collect(Collectors.joining("")); + + Map configMap = new HashMap<>(); + + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "INSERT"); + configMap.put(INSERT_DOCUMENT, "[{" + sampleInsertDocuments + "}]"); + configMap.put(COLLECTION, collectionName); + + List pluginSpecifiedTemplates = generateMongoFormConfigTemplates(configMap); + + String rawQuery = "{\n" + + " \"insert\": \"" + collectionName + "\",\n" + + " \"documents\": [\n" + + " {\n" + + sampleInsertDocuments + + " }\n" + + " ]\n" + + "}\n"; + + return Collections.singletonList(new DatasourceStructure.Template( + "Insert", + rawQuery, + pluginSpecifiedTemplates + )); + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/MongoCommand.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/MongoCommand.java new file mode 100644 index 00000000000..2a42d87acce --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/MongoCommand.java @@ -0,0 +1,61 @@ +package com.external.plugins.commands; + +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Property; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; + +/** + * This is the base class which every Mongo Command extends. Common functions across all mongo commands + * are implemented here including reading and validating the collection. This also defines functions which should be + * implemented by all the commands. + */ +@Getter +@Setter +@NoArgsConstructor +public abstract class MongoCommand { + String collection; + List fieldNamesWithNoConfiguration; + protected static final ObjectMapper objectMapper = new ObjectMapper(); + + public MongoCommand(ActionConfiguration actionConfiguration) { + + this.fieldNamesWithNoConfiguration = new ArrayList<>(); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, COLLECTION)) { + this.collection = (String) pluginSpecifiedTemplates.get(COLLECTION).getValue(); + } + } + + public Boolean isValid() { + if (StringUtils.isNullOrEmpty(this.collection)) { + fieldNamesWithNoConfiguration.add("Collection"); + return Boolean.FALSE; + } + return Boolean.TRUE; + } + + public Document parseCommand() { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation : All mongo commands must implement parseCommand"); + } + + public List generateTemplate(Map templateConfiguration) { + throw new AppsmithPluginException(AppsmithPluginError.PLUGIN_ERROR, "Unsupported Operation : All mongo commands must implement generateTemplate"); + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java new file mode 100644 index 00000000000..b9aacf41c39 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateMany.java @@ -0,0 +1,125 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.DatasourceStructure; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates; +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.BSON; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_UPDATE; + +@Getter +@Setter +@NoArgsConstructor +public class UpdateMany extends MongoCommand { + String query; + String update; + + public UpdateMany(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, UPDATE_MANY_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(UPDATE_MANY_QUERY).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, UPDATE_MANY_UPDATE)) { + this.update = (String) pluginSpecifiedTemplates.get(UPDATE_MANY_UPDATE).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query) && !StringUtils.isNullOrEmpty(update)) { + return Boolean.TRUE; + } else { + if (StringUtils.isNullOrEmpty(query)) { + fieldNamesWithNoConfiguration.add("Query"); + } + if (StringUtils.isNullOrEmpty(update)) { + fieldNamesWithNoConfiguration.add("Update"); + } + } + } + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("update", this.collection); + + Document queryDocument = parseSafely("Query", this.query); + + Document updateDocument = parseSafely("Update", this.update); + + Document update = new Document(); + update.put("q", queryDocument); + update.put("u", updateDocument); + + // Set true to update ALL documents meeting the query criteria + update.put("multi", Boolean.TRUE); + + List updates = new ArrayList<>(); + updates.add(update); + + document.put("updates", updates); + + return document; + } + + @Override + public List generateTemplate(Map templateConfiguration) { + String collectionName = (String) templateConfiguration.get("collectionName"); + String filterFieldName = (String) templateConfiguration.get("filterFieldName"); + + Map configMap = new HashMap<>(); + + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "UPDATE_MANY"); + configMap.put(COLLECTION, collectionName); + configMap.put(UPDATE_MANY_QUERY, "{ \"_id\": ObjectId(\"id_of_document_to_update\") }"); + configMap.put(UPDATE_MANY_UPDATE, "{ \"$set\": { \"" + filterFieldName + "\": \"new value\" } }"); + + List pluginSpecifiedTemplates = generateMongoFormConfigTemplates(configMap); + + String rawQuery = "{\n" + + " \"update\": \"" + collectionName + "\",\n" + + " \"updates\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + + " },\n" + + " \"u\": { \"$set\": { \"" + filterFieldName + "\": \"new value\" } }\n" + + " }\n" + + " ]\n" + + "}\n"; + + return Collections.singletonList(new DatasourceStructure.Template( + "Update", + rawQuery, + pluginSpecifiedTemplates + )); + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateOne.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateOne.java new file mode 100644 index 00000000000..8e3529ffec5 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/commands/UpdateOne.java @@ -0,0 +1,79 @@ +package com.external.plugins.commands; + +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.Property; +import lombok.Getter; +import lombok.Setter; +import org.bson.Document; +import org.pf4j.util.StringUtils; + +import java.util.List; + +import static com.external.plugins.MongoPluginUtils.parseSafely; +import static com.external.plugins.MongoPluginUtils.validConfigurationPresent; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_SORT; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_UPDATE; + +@Getter +@Setter +public class UpdateOne extends MongoCommand { + String query; + String sort; + String update; + + public UpdateOne(ActionConfiguration actionConfiguration) { + super(actionConfiguration); + + List pluginSpecifiedTemplates = actionConfiguration.getPluginSpecifiedTemplates(); + + if (validConfigurationPresent(pluginSpecifiedTemplates, UPDATE_ONE_QUERY)) { + this.query = (String) pluginSpecifiedTemplates.get(UPDATE_ONE_QUERY).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, UPDATE_ONE_SORT)) { + this.sort = (String) pluginSpecifiedTemplates.get(UPDATE_ONE_SORT).getValue(); + } + + if (validConfigurationPresent(pluginSpecifiedTemplates, UPDATE_ONE_UPDATE)) { + this.update = (String) pluginSpecifiedTemplates.get(UPDATE_ONE_UPDATE).getValue(); + } + } + + @Override + public Boolean isValid() { + if (super.isValid()) { + if (!StringUtils.isNullOrEmpty(query) && !StringUtils.isNullOrEmpty(update)) { + return Boolean.TRUE; + } else { + if (StringUtils.isNullOrEmpty(query)) { + fieldNamesWithNoConfiguration.add("Query"); + } + if (StringUtils.isNullOrEmpty(update)) { + fieldNamesWithNoConfiguration.add("Update"); + } + } + } + return Boolean.FALSE; + } + + @Override + public Document parseCommand() { + Document document = new Document(); + + document.put("findAndModify", this.collection); + + document.put("query", parseSafely("Query", this.query)); + + if (!StringUtils.isNullOrEmpty(this.sort)) { + document.put("sort", parseSafely("Sort", this.sort)); + } + + document.put("update", parseSafely("Update", this.update)); + + // Return the newly modified document + document.put("new", Boolean.TRUE); + + return document; + } +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java new file mode 100644 index 00000000000..65050c2a309 --- /dev/null +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/java/com/external/plugins/constants/ConfigurationIndex.java @@ -0,0 +1,31 @@ +package com.external.plugins.constants; + +public class ConfigurationIndex { + public static final int BSON = 0; + public static final int INPUT_TYPE = 1; + public static final int COMMAND = 2; + public static final int COLLECTION = 19; + public static final int FIND_QUERY = 3; + public static final int FIND_SORT = 4; + public static final int FIND_PROJECTION = 5; + public static final int FIND_LIMIT = 6; + public static final int FIND_SKIP = 7; + public static final int UPDATE_ONE_QUERY = 8; + public static final int UPDATE_ONE_SORT = 9; + public static final int UPDATE_ONE_UPDATE = 10; + public static final int UPDATE_MANY_QUERY = 11; + public static final int UPDATE_MANY_UPDATE = 12; + public static final int DELETE_QUERY = 13; + public static final int DELETE_LIMIT = 20; + public static final int COUNT_QUERY = 14; + public static final int DISTINCT_QUERY = 15; + public static final int DISTINCT_KEY = 16; + public static final int AGGREGATE_PIPELINE = 17; + public static final int INSERT_DOCUMENT = 18; + + /** + * !!! WARNING !!! + * Please update the size variable below whenever adding a new property in plugin specified templates + */ + public static final int MAX_SIZE = 21; +} diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json index b5ab09ce68c..0857096308d 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/editor.json @@ -4,6 +4,382 @@ "sectionName": "", "id": 1, "children": [ + { + "label": "", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "controlType": "DROP_DOWN", + "initialValue": "FORM", + "options": [ + { + "label": "Form Input", + "value": "FORM" + }, + { + "label": "Raw Input", + "value": "RAW" + } + ] + }, + { + "label": "Command", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "controlType": "DROP_DOWN", + "initialValue": "FIND", + "options": [ + { + "label": "Insert a document", + "value": "INSERT" + }, + { + "label": "Find one or more documents", + "value": "FIND" + }, + { + "label": "Update one document", + "value": "UPDATE_ONE" + }, + { + "label": "Update one or more documents", + "value": "UPDATE_MANY" + }, + { + "label": "Delete one or more documents", + "value": "DELETE" + }, + { + "label": "Count", + "value": "COUNT" + }, + { + "label": "Distinct", + "value": "DISTINCT" + }, + { + "label": "Aggregate", + "value": "AGGREGATE" + } + ], + "hidden": { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "EQUALS", + "value": "RAW" + } + }, + { + "label": "Collection Name", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[19].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "hidden": { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "EQUALS", + "value": "RAW" + } + }, + { + "sectionName": "", + "id": 2, + "serverLabel": "findCommandForm", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "FIND" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[3].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + }, + { + "label": "Sort", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[4].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{name : 1}" + }, + { + "label": "Projection", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[5].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{name : 1}" + }, + { + "label": "Limit", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[6].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "10" + }, + { + "label": "Skip", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[7].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "0" + } + ] + }, + { + "sectionName": "", + "id": 3, + "serverLabel": "updateOne", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "UPDATE_ONE" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[8].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + }, + { + "label": "Sort", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[9].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{name : 1}" + }, + { + "label": "Update", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[10].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{ $inc: { score: 1 } }" + } + ] + }, + { + "sectionName": "", + "id": 4, + "serverLabel": "updateMany", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "UPDATE_MANY" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[11].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + }, + { + "label": "Update", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[12].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{ $inc: { score: 1 } }" + } + ] + }, + { + "sectionName": "", + "id": 5, + "serverLabel": "delete", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "DELETE" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[13].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + }, + { + "label": "Limit", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[20].value", + "controlType": "DROP_DOWN", + "initialValue": "SINGLE", + "options": [ + { + "label": "Single Document", + "value": "SINGLE" + }, + { + "label": "All Matching Documents", + "value": "ALL" + } + ] + } + ] + }, + { + "sectionName": "", + "id": 6, + "serverLabel": "count", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "COUNT" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[14].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + } + ] + }, + { + "sectionName": "", + "id": 7, + "serverLabel": "distinct", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "DISTINCT" + } + ] + }, + "children": [ + { + "label": "Query", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[15].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "{rating : {$gte : 9}}" + }, + { + "label": "Key/Field", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[16].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "name" + } + ] + }, + { + "sectionName": "", + "id": 8, + "serverLabel": "aggregate", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "AGGREGATE" + } + ] + }, + "children": [ + { + "label": "Array of Pipelines", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[17].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "[{ $project: { tags: 1 } }, { $unwind: \"$tags\" }, { $group: { _id: \"$tags\", count: { $sum : 1 } } } ]" + } + ] + }, + { + "sectionName": "", + "id": 9, + "serverLabel": "insert", + "hidden" : { + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "NOT_EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[2].value", + "comparison": "NOT_EQUALS", + "value": "INSERT" + } + ] + }, + "children": [ + { + "label": "Documents", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[18].value", + "controlType": "QUERY_DYNAMIC_INPUT_TEXT", + "evaluationSubstitutionType": "TEMPLATE", + "placeholderText" : "[ { _id: 1, user: \"abc123\", status: \"A\" } ]" + } + ] + }, { "label": "", "internalLabel": "Query", @@ -11,9 +387,19 @@ "controlType": "QUERY_DYNAMIC_TEXT", "evaluationSubstitutionType": "SMART_SUBSTITUTE", "hidden": { - "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "comparison": "EQUALS", - "value": false + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", + "comparison": "EQUALS", + "value": false + } + ] } }, { @@ -23,9 +409,19 @@ "controlType": "QUERY_DYNAMIC_TEXT", "evaluationSubstitutionType": "TEMPLATE", "hidden": { - "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "comparison": "EQUALS", - "value": true + "conditionType": "OR", + "conditions": [ + { + "path": "actionConfiguration.pluginSpecifiedTemplates[1].value", + "comparison": "EQUALS", + "value": "FORM" + }, + { + "path": "actionConfiguration.pluginSpecifiedTemplates[0].value", + "comparison": "EQUALS", + "value": true + } + ] } } ] diff --git a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java index b941ce8e3ce..9e19e4cfd5a 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java +++ b/app/server/appsmith-plugins/mongoPlugin/src/test/java/com/external/plugins/MongoPluginTest.java @@ -1,8 +1,8 @@ package com.external.plugins; import com.appsmith.external.dtos.ExecuteActionDTO; -import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginError; +import com.appsmith.external.exceptions.pluginExceptions.AppsmithPluginException; import com.appsmith.external.models.ActionConfiguration; import com.appsmith.external.models.ActionExecutionRequest; import com.appsmith.external.models.ActionExecutionResult; @@ -38,14 +38,33 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; +import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; import static com.appsmith.external.constants.DisplayDataType.JSON; import static com.appsmith.external.constants.DisplayDataType.RAW; -import static com.appsmith.external.constants.ActionConstants.ACTION_CONFIGURATION_BODY; +import static com.external.plugins.MongoPluginUtils.generateMongoFormConfigTemplates; +import static com.external.plugins.constants.ConfigurationIndex.AGGREGATE_PIPELINE; +import static com.external.plugins.constants.ConfigurationIndex.BSON; +import static com.external.plugins.constants.ConfigurationIndex.COLLECTION; +import static com.external.plugins.constants.ConfigurationIndex.COMMAND; +import static com.external.plugins.constants.ConfigurationIndex.COUNT_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.DELETE_LIMIT; +import static com.external.plugins.constants.ConfigurationIndex.DELETE_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_KEY; +import static com.external.plugins.constants.ConfigurationIndex.DISTINCT_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.FIND_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.FIND_SORT; +import static com.external.plugins.constants.ConfigurationIndex.INPUT_TYPE; +import static com.external.plugins.constants.ConfigurationIndex.INSERT_DOCUMENT; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_MANY_UPDATE; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_QUERY; +import static com.external.plugins.constants.ConfigurationIndex.UPDATE_ONE_UPDATE; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -83,26 +102,27 @@ public static void setUp() { final MongoClient mongoClient = MongoClients.create(uri); Flux.from(mongoClient.getDatabase("test").listCollectionNames()).collectList(). - flatMap(collectionNamesList -> { - final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection( - "users"); - if(collectionNamesList.size() == 0) { - Mono.from(usersCollection.insertMany(List.of( - new Document(Map.of( - "name", "Cierra Vega", - "gender", "F", - "age", 20, - "luckyNumber", 987654321L, - "dob", LocalDate.of(2018, 12, 31), - "netWorth", new BigDecimal("123456.789012") - )), - new Document(Map.of("name", "Alden Cantrell", "gender", "M", "age", 30)), - new Document(Map.of("name", "Kierra Gentry", "gender", "F", "age", 40)) - ))).block(); - } - - return Mono.just(usersCollection); - }).block(); + flatMap(collectionNamesList -> { + final MongoCollection usersCollection = mongoClient.getDatabase("test").getCollection( + "users"); + if (collectionNamesList.size() == 0) { + Mono.from(usersCollection.insertMany(List.of( + new Document(Map.of( + "name", "Cierra Vega", + "gender", "F", + "age", 20, + "luckyNumber", 987654321L, + "dob", LocalDate.of(2018, 12, 31), + "netWorth", new BigDecimal("123456.789012"), + "updatedByCommand", false + )), + new Document(Map.of("name", "Alden Cantrell", "gender", "M", "age", 30)), + new Document(Map.of("name", "Kierra Gentry", "gender", "F", "age", 40)) + ))).block(); + } + + return Mono.just(usersCollection); + }).block(); } private DatasourceConfiguration createDatasourceConfiguration() { @@ -182,7 +202,7 @@ public void testDatasourceWithUnauthorizedException() throws NoSuchFieldExceptio * - On calling testDatasource(...) -> call the real method. * - On calling datasourceCreate(...) -> throw the mock exception defined above. */ - MongoPlugin.MongoPluginExecutor mongoPluginExecutor = new MongoPlugin.MongoPluginExecutor(); + MongoPlugin.MongoPluginExecutor mongoPluginExecutor = new MongoPlugin.MongoPluginExecutor(); MongoPlugin.MongoPluginExecutor spyMongoPluginExecutor = spy(mongoPluginExecutor); /* Please check this out before modifying this line: https://stackoverflow * .com/questions/11620103/mockito-trying-to-spy-on-method-is-calling-the-original-method @@ -308,6 +328,19 @@ public void testExecuteWriteQuery() { ); }) .verifyComplete(); + + // Clean up this newly inserted value + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, "users"); + configMap.put(DELETE_QUERY, "{\"name\": \"John Smith\"}"); + configMap.put(DELETE_LIMIT, "SINGLE"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + // Run the delete command + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); } @Test @@ -390,9 +423,9 @@ public void testStructure() { assertNotNull(structure); assertEquals(1, structure.getTables().size()); - final DatasourceStructure.Table possessionsTable = structure.getTables().get(0); - assertEquals("users", possessionsTable.getName()); - assertEquals(DatasourceStructure.TableType.COLLECTION, possessionsTable.getType()); + final DatasourceStructure.Table usersTable = structure.getTables().get(0); + assertEquals("users", usersTable.getName()); + assertEquals(DatasourceStructure.TableType.COLLECTION, usersTable.getType()); assertArrayEquals( new DatasourceStructure.Column[]{ new DatasourceStructure.Column("_id", "ObjectId", null), @@ -402,72 +435,111 @@ public void testStructure() { new DatasourceStructure.Column("luckyNumber", "Long", null), new DatasourceStructure.Column("name", "String", null), new DatasourceStructure.Column("netWorth", "BigDecimal", null), + new DatasourceStructure.Column("updatedByCommand", "Object", null), }, - possessionsTable.getColumns().toArray() + usersTable.getColumns().toArray() ); assertArrayEquals( new DatasourceStructure.Key[]{}, - possessionsTable.getKeys().toArray() - ); - - assertArrayEquals( - new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("Find", "{\n" + - " \"find\": \"users\",\n" + - " \"filter\": {\n" + - " \"gender\": \"F\"\n" + - " },\n" + - " \"sort\": {\n" + - " \"_id\": 1\n" + - " },\n" + - " \"limit\": 10\n" + - "}\n"), - new DatasourceStructure.Template("Find by ID", "{\n" + - " \"find\": \"users\",\n" + - " \"filter\": {\n" + - " \"_id\": ObjectId(\"id_to_query_with\")\n" + - " }\n" + - "}\n"), - new DatasourceStructure.Template("Insert", "{\n" + - " \"insert\": \"users\",\n" + - " \"documents\": [\n" + - " {\n" + - " \"_id\": ObjectId(\"a_valid_object_id_hex\"),\n" + - " \"age\": 1,\n" + - " \"dob\": new Date(\"2019-07-01\"),\n" + - " \"gender\": \"new value\",\n" + - " \"luckyNumber\": NumberLong(\"1\"),\n" + - " \"name\": \"new value\",\n" + - " \"netWorth\": NumberDecimal(\"1\"),\n" + - " }\n" + - " ]\n" + - "}\n"), - new DatasourceStructure.Template("Update", "{\n" + - " \"update\": \"users\",\n" + - " \"updates\": [\n" + - " {\n" + - " \"q\": {\n" + - " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + - " },\n" + - " \"u\": { \"$set\": { \"gender\": \"new value\" } }\n" + - " }\n" + - " ]\n" + - "}\n"), - new DatasourceStructure.Template("Delete", "{\n" + - " \"delete\": \"users\",\n" + - " \"deletes\": [\n" + - " {\n" + - " \"q\": {\n" + - " \"_id\": \"id_of_document_to_delete\"\n" + - " },\n" + - " \"limit\": 1\n" + - " }\n" + - " ]\n" + - "}\n"), - }, - possessionsTable.getTemplates().toArray() + usersTable.getKeys().toArray() ); + List templates = usersTable.getTemplates(); + + //Assert Find command + DatasourceStructure.Template findTemplate = templates.get(0); + assertEquals(findTemplate.getTitle(), "Find"); + assertEquals(findTemplate.getBody(), "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"gender\": \"F\"\n" + + " },\n" + + " \"sort\": {\n" + + " \"_id\": 1\n" + + " },\n" + + " \"limit\": 10\n" + + "}\n"); + assertEquals(findTemplate.getPluginSpecifiedTemplates().get(COMMAND).getValue(), "FIND"); + assertEquals(findTemplate.getPluginSpecifiedTemplates().get(FIND_QUERY).getValue(), "{ \"gender\": \"F\"}"); + assertEquals(findTemplate.getPluginSpecifiedTemplates().get(FIND_SORT).getValue(), "{\"_id\": 1}"); + + //Assert Find By Id command + DatasourceStructure.Template findByIdTemplate = templates.get(1); + assertEquals(findByIdTemplate.getTitle(), "Find by ID"); + assertEquals(findByIdTemplate.getBody(), "{\n" + + " \"find\": \"users\",\n" + + " \"filter\": {\n" + + " \"_id\": ObjectId(\"id_to_query_with\")\n" + + " }\n" + + "}\n"); + assertEquals(findByIdTemplate.getPluginSpecifiedTemplates().get(COMMAND).getValue(), "FIND"); + assertEquals(findByIdTemplate.getPluginSpecifiedTemplates().get(FIND_QUERY).getValue(), "{\"_id\": ObjectId(\"id_to_query_with\")}"); + + // Assert Insert command + DatasourceStructure.Template insertTemplate = templates.get(2); + assertEquals(insertTemplate.getTitle(), "Insert"); + assertEquals(insertTemplate.getBody(), "{\n" + + " \"insert\": \"users\",\n" + + " \"documents\": [\n" + + " {\n" + + " \"_id\": ObjectId(\"a_valid_object_id_hex\"),\n" + + " \"age\": 1,\n" + + " \"dob\": new Date(\"2019-07-01\"),\n" + + " \"gender\": \"new value\",\n" + + " \"luckyNumber\": NumberLong(\"1\"),\n" + + " \"name\": \"new value\",\n" + + " \"netWorth\": NumberDecimal(\"1\"),\n" + + " \"updatedByCommand\": {},\n" + + " }\n" + + " ]\n" + + "}\n"); + assertEquals(insertTemplate.getPluginSpecifiedTemplates().get(COMMAND).getValue(), "INSERT"); + assertEquals(insertTemplate.getPluginSpecifiedTemplates().get(INSERT_DOCUMENT).getValue(), + "[{ \"_id\": ObjectId(\"a_valid_object_id_hex\"),\n" + + " \"age\": 1,\n" + + " \"dob\": new Date(\"2019-07-01\"),\n" + + " \"gender\": \"new value\",\n" + + " \"luckyNumber\": NumberLong(\"1\"),\n" + + " \"name\": \"new value\",\n" + + " \"netWorth\": NumberDecimal(\"1\"),\n" + + " \"updatedByCommand\": {},\n" + + "}]"); + + // Assert Update command + DatasourceStructure.Template updateTemplate = templates.get(3); + assertEquals(updateTemplate.getTitle(), "Update"); + assertEquals(updateTemplate.getBody(), "{\n" + + " \"update\": \"users\",\n" + + " \"updates\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": ObjectId(\"id_of_document_to_update\")\n" + + " },\n" + + " \"u\": { \"$set\": { \"gender\": \"new value\" } }\n" + + " }\n" + + " ]\n" + + "}\n"); + assertEquals(updateTemplate.getPluginSpecifiedTemplates().get(COMMAND).getValue(), "UPDATE_MANY"); + assertEquals(updateTemplate.getPluginSpecifiedTemplates().get(UPDATE_MANY_QUERY).getValue(), "{ \"_id\": ObjectId(\"id_of_document_to_update\") }"); + assertEquals(updateTemplate.getPluginSpecifiedTemplates().get(UPDATE_MANY_UPDATE).getValue(), "{ \"$set\": { \"gender\": \"new value\" } }"); + + // Assert Delete Command + DatasourceStructure.Template deleteTemplate = templates.get(4); + assertEquals(deleteTemplate.getTitle(), "Delete"); + assertEquals(deleteTemplate.getBody(), "{\n" + + " \"delete\": \"users\",\n" + + " \"deletes\": [\n" + + " {\n" + + " \"q\": {\n" + + " \"_id\": \"id_of_document_to_delete\"\n" + + " },\n" + + " \"limit\": 1\n" + + " }\n" + + " ]\n" + + "}\n"); + assertEquals(deleteTemplate.getPluginSpecifiedTemplates().get(COMMAND).getValue(), "DELETE"); + assertEquals(deleteTemplate.getPluginSpecifiedTemplates().get(DELETE_QUERY).getValue(), "{ \"_id\": ObjectId(\"id_of_document_to_delete\") }"); + assertEquals(deleteTemplate.getPluginSpecifiedTemplates().get(DELETE_LIMIT).getValue(), "SINGLE"); }) .verifyComplete(); } @@ -808,15 +880,15 @@ public void testBsonSmartSubstitution() { assertEquals(parameterEntry.getValue(), "STRING"); parameterEntry = parameters.get(1); - assertEquals(parameterEntry.getKey(),"{ age: { \"$gte\": 30 } }"); + assertEquals(parameterEntry.getKey(), "{ age: { \"$gte\": 30 } }"); assertEquals(parameterEntry.getValue(), "BSON"); parameterEntry = parameters.get(2); - assertEquals(parameterEntry.getKey(),"1"); + assertEquals(parameterEntry.getKey(), "1"); assertEquals(parameterEntry.getValue(), "INTEGER"); parameterEntry = parameters.get(3); - assertEquals(parameterEntry.getKey(),"10"); + assertEquals(parameterEntry.getKey(), "10"); assertEquals(parameterEntry.getValue(), "INTEGER"); assertEquals( @@ -853,10 +925,383 @@ public void testGetStructureReadPermissionError() { StepVerifier.create(structureMono) .verifyErrorSatisfies(error -> { - assertTrue(error instanceof AppsmithPluginException); - String expectedMessage = "Appsmith has failed to get database structure. Please provide read permission on" + - " the database to fix this."; - assertTrue(expectedMessage.equals(error.getMessage())); + assertTrue(error instanceof AppsmithPluginException); + String expectedMessage = "Appsmith has failed to get database structure. Please provide read permission on" + + " the database to fix this."; + assertTrue(expectedMessage.equals(error.getMessage())); }); } + + @Test + public void testFindFormCommand() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "FIND"); + configMap.put(FIND_QUERY, "{ age: { \"$gte\": 30 } }"); + configMap.put(FIND_SORT, "{ id: 1 }"); + configMap.put(COLLECTION, "users"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertEquals(2, ((ArrayNode) result.getBody()).size()); + assertEquals( + List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(), + result.getDataTypes().toString() + ); + }) + .verifyComplete(); + } + + @Test + public void testInsertFormCommandArrayDocuments() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "INSERT"); + configMap.put(COLLECTION, "users"); + configMap.put(INSERT_DOCUMENT, "[{\"name\" : \"ZZZ Insert Form Array Test\", \"gender\" : \"F\", \"age\" : 40, \"tag\" : \"test\"}]"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertEquals( + List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(), + result.getDataTypes().toString() + ); + }) + .verifyComplete(); + + // Clean up this newly inserted value + configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, "users"); + configMap.put(DELETE_QUERY, "{\"tag\" : \"test\"}"); + configMap.put(DELETE_LIMIT, "ALL"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + // Run the delete command + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); + } + + @Test + public void testInsertFormCommandSingleDocument() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "INSERT"); + configMap.put(COLLECTION, "users"); + configMap.put(INSERT_DOCUMENT, "{\"name\" : \"ZZZ Insert Form Single Test\", \"gender\" : \"F\", \"age\" : 40, \"tag\" : \"test\"}"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + assertEquals( + List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(), + result.getDataTypes().toString() + ); + }) + .verifyComplete(); + + // Clean up this newly inserted value + configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, "users"); + configMap.put(DELETE_QUERY, "{\"tag\" : \"test\"}"); + configMap.put(DELETE_LIMIT, "ALL"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + // Run the delete command + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); + } + + @Test + public void testUpdateOneFormCommand() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "UPDATE_ONE"); + configMap.put(COLLECTION, "users"); + configMap.put(UPDATE_ONE_QUERY, "{ name: \"Alden Cantrell\" }"); + configMap.put(UPDATE_ONE_UPDATE, "{ $set: { age: 31 }}}"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + value = ((ObjectNode) result.getBody()).get("value"); + assertNotNull(value); + assertEquals("Alden Cantrell", value.get("name").asText()); + assertEquals(31, value.get("age").asInt()); + assertEquals( + List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(), + result.getDataTypes().toString() + ); + }) + .verifyComplete(); + } + + @Test + public void testUpdateManyFormCommand() { + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "UPDATE_MANY"); + configMap.put(COLLECTION, "users"); + // Query for all the documents in the collection + configMap.put(UPDATE_MANY_QUERY, "{}"); + configMap.put(UPDATE_MANY_UPDATE, "{ $set: { updatedByCommand: true }}}"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode value = ((ObjectNode) result.getBody()).get("nModified"); + assertEquals(value.asText(), "3"); + assertEquals( + List.of(new ParsedDataType(JSON), new ParsedDataType(RAW)).toString(), + result.getDataTypes().toString() + ); + }) + .verifyComplete(); + } + + @Test + public void testDeleteFormCommandSingleDocument() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + // Insert multiple documents which would match the delete criterion + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "INSERT"); + configMap.put(COLLECTION, "users"); + configMap.put(INSERT_DOCUMENT, "[{\"name\" : \"To Delete1\", \"tag\" : \"delete\"}, {\"name\" : \"To Delete2\", \"tag\" : \"delete\"}]"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); + + // Now that the documents have been inserted, lets delete one of them + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, "users"); + configMap.put(DELETE_QUERY, "{tag : \"delete\"}"); + configMap.put(DELETE_LIMIT, "SINGLE"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode value = ((ObjectNode) result.getBody()).get("n"); + //Assert that only one document out of the two gets deleted + assertEquals(value.asInt(), 1); + }) + .verifyComplete(); + + // Run this delete command again to ensure that both the documents added are deleted post this test. + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); + + } + + @Test + public void testDeleteFormCommandMultipleDocument() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + // Insert multiple documents which would match the delete criterion + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "INSERT"); + configMap.put(COLLECTION, "users"); + configMap.put(INSERT_DOCUMENT, "[{\"name\" : \"To Delete1\", \"tag\" : \"delete\"}, {\"name\" : \"To Delete2\", \"tag\" : \"delete\"}]"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)).block(); + + // Now that the documents have been inserted, lets delete both of them + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DELETE"); + configMap.put(COLLECTION, "users"); + configMap.put(DELETE_QUERY, "{tag : \"delete\"}"); + configMap.put(DELETE_LIMIT, "ALL"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode value = ((ObjectNode) result.getBody()).get("n"); + assertEquals(value.asInt(), 2); + }) + .verifyComplete(); + } + + @Test + public void testCountCommand() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "COUNT"); + configMap.put(COLLECTION, "users"); + configMap.put(COUNT_QUERY, "{}"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode value = ((ObjectNode) result.getBody()).get("n"); + assertEquals(value.asInt(), 3); + }) + .verifyComplete(); + } + + @Test + public void testDistinctCommand() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "DISTINCT"); + configMap.put(COLLECTION, "users"); + configMap.put(DISTINCT_QUERY, "{}"); + configMap.put(DISTINCT_KEY, "name"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + int valuesSize = ((ArrayNode) result.getBody()).size(); + assertEquals(valuesSize, 3); + }) + .verifyComplete(); + } + + @Test + public void testAggregateCommand() { + DatasourceConfiguration dsConfig = createDatasourceConfiguration(); + Mono dsConnectionMono = pluginExecutor.datasourceCreate(dsConfig); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + + Map configMap = new HashMap<>(); + configMap.put(BSON, Boolean.FALSE); + configMap.put(INPUT_TYPE, "FORM"); + configMap.put(COMMAND, "AGGREGATE"); + configMap.put(COLLECTION, "users"); + configMap.put(AGGREGATE_PIPELINE, "[{\"$count\": \"userCount\"}]"); + + actionConfiguration.setPluginSpecifiedTemplates(generateMongoFormConfigTemplates(configMap)); + + Mono executeMono = dsConnectionMono.flatMap(conn -> pluginExecutor.executeParameterized(conn, new ExecuteActionDTO(), dsConfig, actionConfiguration)); + StepVerifier.create(executeMono) + .assertNext(obj -> { + System.out.println(obj); + ActionExecutionResult result = (ActionExecutionResult) obj; + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + JsonNode value = ((ArrayNode) result.getBody()).get(0).get("userCount"); + assertEquals(value.asInt(), 3); + }) + .verifyComplete(); + } + } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java index cf15f8fba2f..6d72c48386f 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/main/java/com/external/plugins/MySqlPlugin.java @@ -761,15 +761,15 @@ private void getTemplates(Map tablesByName) { final String tableName = table.getName(); table.getTemplates().addAll(List.of( - new DatasourceStructure.Template("SELECT", "SELECT * FROM " + tableName + " LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM " + tableName + " LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO " + tableName + " (" + String.join(", ", columnNames) + ")\n" - + " VALUES (" + String.join(", ", columnValues) + ");"), + + " VALUES (" + String.join(", ", columnValues) + ");", null), new DatasourceStructure.Template("UPDATE", "UPDATE " + tableName + " SET" + setFragments.toString() + "\n" - + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM " + tableName - + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") + + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null) )); } diff --git a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java index 45768c8808e..9a9469f3aba 100644 --- a/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java +++ b/app/server/appsmith-plugins/mysqlPlugin/src/test/java/com/external/plugins/MySqlPluginTest.java @@ -635,18 +635,18 @@ public void testStructure() { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM possessions LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM possessions LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO possessions (id, title, user_id, username, email)\n" + - " VALUES (1, '', 1, '', '');"), + " VALUES (1, '', 1, '', '');", null), new DatasourceStructure.Template("UPDATE", "UPDATE possessions SET\n" + " id = 1\n" + " title = ''\n" + " user_id = 1\n" + " username = ''\n" + " email = ''\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM possessions\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, possessionsTable.getTemplates().toArray() ); @@ -678,9 +678,9 @@ public void testStructure() { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM users LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM users LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO users (id, username, password, email, spouse_dob, dob, yob, time1, created_on, updated_on)\n" + - " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');"), + " VALUES (1, '', '', '', '2019-07-01', '2019-07-01', '', '', '2019-07-01 10:00:00', '2019-07-01 10:00:00');", null), new DatasourceStructure.Template("UPDATE", "UPDATE users SET\n" + " id = 1\n" + " username = ''\n" + @@ -692,9 +692,9 @@ public void testStructure() { " time1 = ''\n" + " created_on = '2019-07-01 10:00:00'\n" + " updated_on = '2019-07-01 10:00:00'\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM users\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, usersTable.getTemplates().toArray() ); diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java index bab35287cd0..4c58b7cacb4 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/java/com/external/plugins/PostgresPlugin.java @@ -695,15 +695,15 @@ public Mono getStructure(HikariDataSource connection, Datas final String quotedTableName = table.getName().replaceFirst("\\.(\\w+)", ".\"$1\""); table.getTemplates().addAll(List.of( - new DatasourceStructure.Template("SELECT", "SELECT * FROM " + quotedTableName + " LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM " + quotedTableName + " LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO " + quotedTableName + " (" + String.join(", ", columnNames) + ")\n" - + " VALUES (" + String.join(", ", columnValues) + ");"), + + " VALUES (" + String.join(", ", columnValues) + ");", null), new DatasourceStructure.Template("UPDATE", "UPDATE " + quotedTableName + " SET" + setFragments.toString() + "\n" - + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM " + quotedTableName - + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") + + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null) )); } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java index 7e06db46d90..a612400f585 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java +++ b/app/server/appsmith-plugins/postgresPlugin/src/test/java/com/external/plugins/PostgresPluginTest.java @@ -376,15 +376,15 @@ public void testStructure() { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"possessions\" LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"possessions\" LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"possessions\" (\"title\", \"user_id\")\n" + - " VALUES ('', 1);"), + " VALUES ('', 1);", null), new DatasourceStructure.Template("UPDATE", "UPDATE public.\"possessions\" SET\n" + " \"title\" = ''\n" + " \"user_id\" = 1\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"possessions\"\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, possessionsTable.getTemplates().toArray() ); @@ -420,9 +420,9 @@ public void testStructure() { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"users\" LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"users\" LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"users\" (\"username\", \"password\", \"email\", \"spouse_dob\", \"dob\", \"time1\", \"time_tz\", \"created_on\", \"created_on_tz\", \"interval1\", \"numbers\", \"texts\")\n" + - " VALUES ('', '', '', '2019-07-01', '2019-07-01', '18:32:45', '04:05:06 PST', TIMESTAMP '2019-07-01 10:00:00', TIMESTAMP WITH TIME ZONE '2019-07-01 06:30:00 CET', 1, '{1, 2, 3}', '{\"first\", \"second\"}');"), + " VALUES ('', '', '', '2019-07-01', '2019-07-01', '18:32:45', '04:05:06 PST', TIMESTAMP '2019-07-01 10:00:00', TIMESTAMP WITH TIME ZONE '2019-07-01 06:30:00 CET', 1, '{1, 2, 3}', '{\"first\", \"second\"}');", null), new DatasourceStructure.Template("UPDATE", "UPDATE public.\"users\" SET\n" + " \"username\" = ''\n" + " \"password\" = ''\n" + @@ -436,9 +436,9 @@ public void testStructure() { " \"interval1\" = 1\n" + " \"numbers\" = '{1, 2, 3}'\n" + " \"texts\" = '{\"first\", \"second\"}'\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"users\"\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, usersTable.getTemplates().toArray() ); diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java index 05883d7dd54..1cdb5f38bd1 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/main/java/com/external/plugins/RedshiftPlugin.java @@ -602,15 +602,15 @@ private void getTemplates(Map tablesByName) { final String quotedTableName = table.getName().replaceFirst("\\.(\\w+)", ".\"$1\""); table.getTemplates().addAll(List.of( - new DatasourceStructure.Template("SELECT", "SELECT * FROM " + quotedTableName + " LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM " + quotedTableName + " LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO " + quotedTableName + " (" + String.join(", ", columnNames) + ")\n" - + " VALUES (" + String.join(", ", columnValues) + ");"), + + " VALUES (" + String.join(", ", columnValues) + ");", null), new DatasourceStructure.Template("UPDATE", "UPDATE " + quotedTableName + " SET" + setFragments.toString() + "\n" - + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM " + quotedTableName - + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!") + + "\n WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null) )); } } diff --git a/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java index 32207388728..0f5a66fe485 100644 --- a/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java +++ b/app/server/appsmith-plugins/redshiftPlugin/src/test/java/com/external/plugins/RedshiftPluginTest.java @@ -415,16 +415,16 @@ public void testStructure() throws SQLException { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"possessions\" LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"possessions\" LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"possessions\" " + - "(\"id\", \"title\", \"user_id\")\n VALUES (1, '', 1);"), + "(\"id\", \"title\", \"user_id\")\n VALUES (1, '', 1);", null), new DatasourceStructure.Template("UPDATE", "UPDATE public.\"possessions\" SET\n" + " \"id\" = 1\n" + " \"title\" = ''\n" + " \"user_id\" = 1\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"possessions\"\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, possessionsTable.getTemplates().toArray() ); @@ -451,15 +451,15 @@ public void testStructure() throws SQLException { assertArrayEquals( new DatasourceStructure.Template[]{ - new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"users\" LIMIT 10;"), + new DatasourceStructure.Template("SELECT", "SELECT * FROM public.\"users\" LIMIT 10;", null), new DatasourceStructure.Template("INSERT", "INSERT INTO public.\"users\" (\"username\", \"password\")\n" + - " VALUES ('', '');"), + " VALUES ('', '');", null), new DatasourceStructure.Template("UPDATE", "UPDATE public.\"users\" SET\n" + " \"username\" = ''\n" + " \"password\" = ''\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may update every row in the table!", null), new DatasourceStructure.Template("DELETE", "DELETE FROM public.\"users\"\n" + - " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!"), + " WHERE 1 = 0; -- Specify a valid condition here. Removing the condition may delete everything in the table!", null), }, usersTable.getTemplates().toArray() ); 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 c2a6a750e93..2a206bb0ceb 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 @@ -2258,4 +2258,23 @@ public void updateMongoImportFromSrvField(MongoTemplate mongoTemplate) { mongoTemplate.save(datasource); }); } + + @ChangeSet(order = "068", id = "delete-mongo-datasource-structures", author = "") + public void deleteMongoDatasourceStructures(MongoTemplate mongoTemplate, MongoOperations mongoOperations) { + + // Mongo Form requires the query templates to change as well. To ensure this, mongo datasources + // must re-compute the structure. The following deletes all such structures. Whenever getStructure API call is + // made for these datasources, the server would re-compute the structure. + Plugin mongoPlugin = mongoTemplate.findOne(query(where("packageName").is("mongo-plugin")), Plugin.class); + + Query query = query(new Criteria().andOperator( + where(fieldName(QDatasource.datasource.pluginId)).is(mongoPlugin.getId()), + where(fieldName(QDatasource.datasource.structure)).exists(true) + )); + + Update update = new Update().set(fieldName(QDatasource.datasource.structure), null); + + // Delete all the existing mongo datasource structures by setting the key to null. + mongoOperations.updateMulti(query, update, Datasource.class); + } } From 3073bbc5c8bd98bca64943130b6dde170c6c17a8 Mon Sep 17 00:00:00 2001 From: prapullac <71753653+prapullac@users.noreply.github.com> Date: Wed, 19 May 2021 11:46:12 +0530 Subject: [PATCH 35/52] Adding manual test ideas for Text Widget (#3910) --- .../manual_TestSuite/Text_Widget_Spec.js | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 app/client/cypress/manual_TestSuite/Text_Widget_Spec.js diff --git a/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js new file mode 100644 index 00000000000..1aa118d32ac --- /dev/null +++ b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js @@ -0,0 +1,117 @@ +const homePage = require("../../../locators/Textwidget.json"); + +describe("Test Ideas to test different feature of text widget ", function() { + it("Add New Text widget along with BG and text colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property Pane + // Add a text + // Scroll to BG colour and add a colour + // Next add a text colour + // Click on Deploy + } + ) + + it("Enable Scroll feature with text colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Add a long text in the "Label" + // Enable scroll option + // Navigate to Text colour and add a colour + // and ensure it is scrolling + // Click on deploy and check if it scrollable and colour selected is visible + } + ) + + it("Adding text Size to the Text along with BG colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property pane + // Add a medium text in the "Label" + // Increase the area of the Text Widget + // Navigate to BG colour and add a colour + // Naviaget to "Text Size" + // Select Paragarph option + // Ensure the text size varies accordingly + } + ) + + it("Adding Bold Font style and Centre Text Alignment ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property pane + // Add a medium text in the "Label" + // Increase the area of the Text Widget + // Navigate to Font Style + // Make it Bold + // and Navigate to Alignment and make it centre + // Ensure the changes are visible to user + } + ) + + it("Adding Italic Font style and Text Alignment to exsisting text widget ", function() { + // Navigate to already exsisting Text widget + // Ensure the text is added + // Navigate to Property pane + // Navigate to Font Style + // Make it Italic font + // and Navigate to Alignment and make it Right + // Ensure the changes are visible to user + } + ) + + it("Expand and Contract text widget Property pane", function() { + // Navigate to already exsisting Text widget + // Navigate to Property pane + // Click on collapse option + // Observe that the property pane is contracted + // Now click again on the arrow + //and ensure it collapses + } + ) + + it("Copy and paste a text widget", function() { + // Navigate to already exsisting Text widget + // Ensure Clour and font feature exsists + // Copy and paste the widget + // Ensure the new widget retrives the feature exsisting from parent widget + } + ) + + it("Rename and search a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Rename the text widget from the Entity explorer + // Clear the search keyword + // enter the new text widget name + // and observe the user is navigated to same text widget and properties of the widget does not change on renaming + } + ) + + it("Search and delete a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Ensure user is navigated to Text widget + // Click on Delete option + // Ensure the Text widget is delete + // Click on Deploy adn ensure the Widget is delete + } + ) + + it("Search and delete a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Ensure user is navigated to Text widget + // Click on Delete option + // Ensure the Text widget is delete + // Click on Deploy adn ensure the Widget is delete + } + ) +} +) \ No newline at end of file From efca2d0e4783f3a474817d225255e2246e298e0a Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Wed, 19 May 2021 12:14:58 +0530 Subject: [PATCH 36/52] Remove unused socketio dependency --- app/server/appsmith-server/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/server/appsmith-server/pom.xml b/app/server/appsmith-server/pom.xml index dd0ebda2c20..46f27e8d230 100644 --- a/app/server/appsmith-server/pom.xml +++ b/app/server/appsmith-server/pom.xml @@ -223,12 +223,6 @@ 1.8 - - com.corundumstudio.socketio - netty-socketio - 1.7.12 - - From 81f3ccd1635b064f79cdfbf256fa3548a720defe Mon Sep 17 00:00:00 2001 From: prapullac <71753653+prapullac@users.noreply.github.com> Date: Wed, 19 May 2021 19:27:26 +0530 Subject: [PATCH 37/52] Manual test ideas for List Widget (#4090) --- .../manual_TestSuite/List_Widget_Spec.js | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 app/client/cypress/manual_TestSuite/List_Widget_Spec.js diff --git a/app/client/cypress/manual_TestSuite/List_Widget_Spec.js b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js new file mode 100644 index 00000000000..38cd930f041 --- /dev/null +++ b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js @@ -0,0 +1,105 @@ +const dsl = require("../../../fixtures/ListWidgetDsl.json"); + +describe("List Widget test ideas ", function() { + it("List widget background colour and deploy ", function() { + // Drag and drop a List widget + // Open Property pane + // Scroll down to Styles + // Add background colour + // Add item background colour + // Ensure the colour are added appropriately + // Click on Deploy and ensure it is deployed appropriately + } + ) + + it("Adding large item Spacing for item card", function() { + // Drag and drop a List widget + // Open Property pane + // Scroll down to Styles + // Add large item spacing (>100) + // Ensure the cards get spaced appropriately + } + ) + + it("Binding an API data to list widget ", function() { + //Add an API + // Drag and drop a List widget + // Open list Property pane + // Bind the API to list widget + // Add Input widget into the list widget + // Bind the input widgte to the list widget + } + ) + + it("Copy Paste and Delete the List Widget ", function() { + // Drag and drop a List widget + // Click on the property pane + // Click on Copy the widget + // Paste(cmd+v) the list widget + // Click on the delete option of the Parent widget + } + ) + + it("Renaming the widget from Property pane and Entity explorer ", function() { + // Drag and drop a List widget + // Click on the property pane + // Click name of the widget + // Rename the widget + // Navigate to the Entity Explorer + // Click on the Widget expands + // Navigate to List widget (Double Click) + // Rename the widget + // Ensure the name of the widget is possible from both the place + } + ) + + it("Verify the Pagination functionlaity within List Widget", function() { + // Drag and Drop list Widget + // Click on page 2 + // Ensure list widget will be redirected to page 2 + // Click on next button + // Ensure the list widget will be redirected to page 3 + // Click on Previous button + // Ensure the list widget will be redirected to page 2 + // Mouse Hover on the next button + // Ensure the tool tip message is appropriate + // Mouse Hover on the Previous button + // Ensure the tool tip message is appropriate + } + ) + + it("Add new item in the list widget array object", function(){ + //Drag and drop list widget + //Click to open an property pane + //Expand Genearl section + //Add the following new item + //("id": 7,"num": 007",name": Charizard",img": "http://www.serebii.net/pokemongo/pokemon/006.png") + //Ensure the new item gets added to the list widget without any error + //Check for the new page is added upon adding new items + } + ) + + it("Adding apt widget into the List widget", function(){ + //Drag and Drop List widget + //Expand the section 1 size in the list widget + //Ensure by exapdning section inside list widget the page size gets increased + //Drag and Drop button widget inside list widget + //Ensure Button widget can be placed inside list Widget + //Drag and Drop Image widget inside list widget + //Ensure Image widget can be placed inside list widget + // Drag and drop the text widget inside the list widget + // Ensure text widget can be place inside the list widget + } + ) + + it("Adding unapt widget to identify the error message", function(){ + //Drag and Drop List widget + //Expand the section 1 size in the list widget + //Drag and Drop widgets ie: Chart ,Date Picker radio button etc + // Ensure an understandable error message is displayed to user + } + ) + + + + }) \ No newline at end of file From 639cba9095ba844177b096436a05a79e8047d0a8 Mon Sep 17 00:00:00 2001 From: Confidence Okoghenun Date: Wed, 19 May 2021 06:59:08 -0700 Subject: [PATCH 38/52] Improve server setup experience documentation (#4441) * fix: Updates appsmith server docker-compose * fix: Updates server docker-compose * fix: Removes exposed ports * docs: Adds updates for server docker-compose * docs: Adds updates for server docker-compose * docs: Updates app/server/README.md Co-authored-by: Arpit Mohan * chore: Removes deprecated env vars Co-authored-by: Arpit Mohan --- app/server/.gitignore | 1 + app/server/README.md | 38 +++++++++++++++++++++++++++--- app/server/docker-compose.yml | 15 ++++-------- app/server/envs/docker.env.example | 32 ++++++++++++------------- contributions/ServerSetup.md | 2 ++ 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/app/server/.gitignore b/app/server/.gitignore index 6d1ed7069a5..efc8776c803 100644 --- a/app/server/.gitignore +++ b/app/server/.gitignore @@ -11,5 +11,6 @@ node_modules **/.classpath **/.project **/.factorypath +container-volumes *.env dependency-reduced-pom.xml diff --git a/app/server/README.md b/app/server/README.md index 2721304ccca..135bed18332 100644 --- a/app/server/README.md +++ b/app/server/README.md @@ -1,7 +1,39 @@ # Appsmith Server This is the server-side repository for the Appsmith framework. -For details on setting up your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/release/contributions/ServerSetup.md) +For details on setting up your development machine, please refer to the [Setup Guide](../../contributions/ServerSetup.md). Alternatively, you can run the server using docker(see the instructions below). -## Dev Setup -For details on setting up the server on your development machine, please refer to the [Setup Guide](https://github.com/appsmithorg/appsmith/blob/master/contributions/ServerSetup.md) +## Run locally with Docker + +You can run the server codebase in a docker container. This is the easiest way to get the server up and running if all you care about is contributing to the client codebase. + +### What's in the box + +* Appsmith server +* MongoDB +* Redis + +### Pre-requisites + +* [Docker](https://docs.docker.com/get-docker/) + +### Steps for setup + +1. Clone the Appsmith repository and `cd` into it +```sh +git clone https://github.com/appsmithorg/appsmith.git +cd appsmith +``` +2. Change your directory to `app/server` +```sh +cd app/server +``` +3. Create a copy of the `envs/docker.env.example` +```sh +cp envs/docker.env.example envs/docker.env +``` +4. Start up the containers +```sh +docker-compose up -d +``` +5. Have fun! diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml index ad99d51deb2..e6a1e938eef 100644 --- a/app/server/docker-compose.yml +++ b/app/server/docker-compose.yml @@ -2,35 +2,30 @@ version: "3.7" services: appsmith-internal-server: - image: arpitappsmith/appsmith-server:maven + image: appsmith/appsmith-server env_file: envs/docker.env environment: - APPSMITH_MONGODB_URI: "mongodb://mongo:27017/mobtools" APPSMITH_REDIS_URL: "redis://redis:6379" + APPSMITH_MONGODB_URI: "mongodb://mongo:27017/appsmith" ports: - "8080:8080" - links: - - mongo depends_on: - mongo + - redis networks: - appsmith mongo: image: mongo - ports: - - "27017:27017" environment: - - MONGO_INITDB_DATABASE=mobtools + - MONGO_INITDB_DATABASE=appsmith volumes: - - ./mongo-seed/:/docker-entrypoint-initdb.d/ + - ./container-volumes/mongo:/data/db networks: - appsmith redis: image: redis - ports: - - "6379:6379" networks: - appsmith diff --git a/app/server/envs/docker.env.example b/app/server/envs/docker.env.example index 183ed9ff042..a0daf519f57 100644 --- a/app/server/envs/docker.env.example +++ b/app/server/envs/docker.env.example @@ -1,25 +1,23 @@ #!/bin/sh -APPSMITH_OAUTH2_GOOGLE_CLIENT_ID="" -APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET="" -APPSMITH_OAUTH2_GITHUB_CLIENT_ID="" -APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET="" +# APPSMITH_OAUTH2_GOOGLE_CLIENT_ID="" +# APPSMITH_OAUTH2_GOOGLE_CLIENT_SECRET="" +# APPSMITH_OAUTH2_GITHUB_CLIENT_ID="" +# APPSMITH_OAUTH2_GITHUB_CLIENT_SECRET="" -APPSMITH_RAPID_API_KEY_VALUE="" +# APPSMITH_MAIL_ENABLED=true +# APPSMITH_MAIL_HOST=localhost +# APPSMITH_MAIL_PORT=25 +# APPSMITH_MAIL_USERNAME= +# APPSMITH_MAIL_PASSWORD= +# APPSMITH_MAIL_SMTP_AUTH=true +# APPSMITH_MAIL_SMTP_TLS_ENABLED=true -APPSMITH_MAIL_ENABLED=true -APPSMITH_MAIL_HOST=localhost -APPSMITH_MAIL_PORT=25 -APPSMITH_MAIL_USERNAME= -APPSMITH_MAIL_PASSWORD= -APPSMITH_MAIL_SMTP_AUTH=true -APPSMITH_MAIL_SMTP_TLS_ENABLED=true +# APPSMITH_MARKETPLACE_USERNAME="" +# APPSMITH_MARKETPLACE_PASSWORD="" -APPSMITH_MARKETPLACE_USERNAME="" -APPSMITH_MARKETPLACE_PASSWORD="" - -APPSMITH_ENCRYPTION_PASSWORD="" -APPSMITH_ENCRYPTION_SALT="" +APPSMITH_ENCRYPTION_PASSWORD="abcd" +APPSMITH_ENCRYPTION_SALT="abcd" #APPSMITH_RECAPTCHA_SITE_KEY="" #APPSMITH_RECAPTCHA_SECRET_KEY="" diff --git a/contributions/ServerSetup.md b/contributions/ServerSetup.md index 5cbc4d2cff9..16cfb1eaa76 100644 --- a/contributions/ServerSetup.md +++ b/contributions/ServerSetup.md @@ -2,6 +2,8 @@ The server codebase is written in Java and is powered by Spring + WebFlux. This document explains how you can setup a development environment to make changes and test your changes. +>For details on setting up with `Docker`, please refer to the [Setup Guide](../app/server/README.md#run-locally-with-docker) + ## Pre-requisites - Java --- OpenJDK 11. From 7c587f02882214f41fd76ddbe210c4e2eb7a0720 Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Thu, 20 May 2021 11:15:48 +0530 Subject: [PATCH 39/52] binding list widget with Api (#4229) --- .../cypress/fixtures/listwidgetdsl.json | 275 ++++++++++++++++++ .../Binding/Bind_API_with_List_Widget_spec.js | 91 ++++++ 2 files changed, 366 insertions(+) create mode 100644 app/client/cypress/fixtures/listwidgetdsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js diff --git a/app/client/cypress/fixtures/listwidgetdsl.json b/app/client/cypress/fixtures/listwidgetdsl.json new file mode 100644 index 00000000000..cd2e85fa6ae --- /dev/null +++ b/app/client/cypress/fixtures/listwidgetdsl.json @@ -0,0 +1,275 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 966, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 800, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 18, + "minHeight": 240, + "parentColumnSpace": 1, + "dynamicTriggerPathList": [], + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "backgroundColor": "", + "itemBackgroundColor": "white", + "gridType": "vertical", + "enhancements": true, + "gridGap": 0, + "items": "", + "widgetName": "List1", + "children": [ + { + "isVisible": true, + "widgetName": "Canvas1", + "version": 1, + "containerStyle": "none", + "canExtend": false, + "detachFromLayout": true, + "dropDisabled": true, + "noPad": true, + "children": [ + { + "isVisible": true, + "backgroundColor": "white", + "widgetName": "Container1", + "containerStyle": "card", + "children": [ + { + "isVisible": true, + "widgetName": "Canvas2", + "version": 1, + "containerStyle": "none", + "canExtend": false, + "detachFromLayout": true, + "children": [ + { + "isVisible": true, + "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png", + "imageShape": "RECTANGLE", + "maxZoomLevel": 1, + "image": "{{currentItem.img}}", + "widgetName": "Image1", + "version": 1, + "dynamicBindingPathList": [ + { + "key": "image" + } + ], + "dynamicTriggerPathList": [], + "type": "IMAGE_WIDGET", + "isLoading": false, + "leftColumn": 0, + "rightColumn": 4, + "topRow": 0, + "bottomRow": 3, + "parentId": "muh6tmsm1f", + "widgetId": "vr29m4code" + }, + { + "isVisible": true, + "text": "{{currentItem.name}}", + "fontSize": "PARAGRAPH", + "fontStyle": "BOLD", + "textAlign": "LEFT", + "textColor": "#231F20", + "widgetName": "Text1", + "version": 1, + "textStyle": "HEADING", + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "dynamicTriggerPathList": [], + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 4, + "rightColumn": 10, + "topRow": 0, + "bottomRow": 1, + "parentId": "muh6tmsm1f", + "widgetId": "envgv9f2j9" + }, + { + "isVisible": true, + "text": "{{currentItem.num}}", + "fontSize": "PARAGRAPH", + "fontStyle": "BOLD", + "textAlign": "LEFT", + "textColor": "#231F20", + "widgetName": "Text2", + "version": 1, + "textStyle": "BODY", + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "dynamicTriggerPathList": [], + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 4, + "rightColumn": 10, + "topRow": 1, + "bottomRow": 2, + "parentId": "muh6tmsm1f", + "widgetId": "37xbmi0bbz" + } + ], + "minHeight": null, + "type": "CANVAS_WIDGET", + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": null, + "topRow": 0, + "bottomRow": null, + "parentId": "5q9jzp3d17", + "widgetId": "muh6tmsm1f" + } + ], + "version": 1, + "dragDisabled": true, + "isDeletable": false, + "disallowCopy": true, + "disablePropertyPane": true, + "type": "CONTAINER_WIDGET", + "isLoading": false, + "leftColumn": 0, + "rightColumn": 16, + "topRow": 0, + "bottomRow": 4, + "parentId": "qt3cziyljx", + "widgetId": "5q9jzp3d17" + } + ], + "minHeight": 400, + "type": "CANVAS_WIDGET", + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 463, + "topRow": 0, + "bottomRow": 400, + "parentId": "wmuvmnfqm0", + "widgetId": "qt3cziyljx" + } + ], + "type": "LIST_WIDGET", + "isLoading": false, + "parentColumnSpace": 57.875, + "parentRowSpace": 40, + "leftColumn": 4, + "rightColumn": 12, + "topRow": 8, + "bottomRow": 18, + "parentId": "0", + "widgetId": "wmuvmnfqm0", + "dynamicBindingPathList": [ + { + "key": "template.Image1.image" + }, + { + "key": "template.Text1.text" + }, + { + "key": "template.Text2.text" + } + ], + "template": { + "Image1": { + "isVisible": true, + "defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png", + "imageShape": "RECTANGLE", + "maxZoomLevel": 1, + "image": "{{List1.items.map((currentItem) => currentItem.img)}}", + "widgetName": "Image1", + "version": 1, + "dynamicBindingPathList": [ + { + "key": "image" + } + ], + "dynamicTriggerPathList": [], + "type": "IMAGE_WIDGET", + "isLoading": false, + "leftColumn": 0, + "rightColumn": 4, + "topRow": 0, + "bottomRow": 3, + "parentId": "muh6tmsm1f", + "widgetId": "vr29m4code" + }, + "Text1": { + "isVisible": true, + "text": "{{List1.items.map((currentItem) => currentItem.name)}}", + "fontSize": "PARAGRAPH", + "fontStyle": "BOLD", + "textAlign": "LEFT", + "textColor": "#231F20", + "widgetName": "Text1", + "version": 1, + "textStyle": "HEADING", + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "dynamicTriggerPathList": [], + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 4, + "rightColumn": 10, + "topRow": 0, + "bottomRow": 1, + "parentId": "muh6tmsm1f", + "widgetId": "envgv9f2j9" + }, + "Text2": { + "isVisible": true, + "text": "{{List1.items.map((currentItem) => currentItem.num)}}", + "fontSize": "PARAGRAPH", + "fontStyle": "BOLD", + "textAlign": "LEFT", + "textColor": "#231F20", + "widgetName": "Text2", + "version": 1, + "textStyle": "BODY", + "dynamicBindingPathList": [ + { + "key": "text" + } + ], + "dynamicTriggerPathList": [], + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 4, + "rightColumn": 10, + "topRow": 1, + "bottomRow": 2, + "parentId": "muh6tmsm1f", + "widgetId": "37xbmi0bbz" + } + }, + "childAutoComplete": { + "currentItem": {} + }, + "dynamicTriggerPathList": [] + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js new file mode 100644 index 00000000000..e29f52dae68 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_API_with_List_Widget_spec.js @@ -0,0 +1,91 @@ +const commonlocators = require("../../../../locators/commonlocators.json"); +const dsl = require("../../../../fixtures/listwidgetdsl.json"); +const pages = require("../../../../locators/Pages.json"); +const apiPage = require("../../../../locators/ApiEditor.json"); +const publishPage = require("../../../../locators/publishWidgetspage.json"); + +describe("Test Create Api and Bind to Table widget", function() { + let apiData; + before(() => { + cy.addDsl(dsl); + }); + + it("Test_Add users api and execute api", function() { + cy.createAndFillApi(this.data.userApi, "/users"); + cy.RunAPI(); + cy.get(apiPage.responseBody) + .contains("name") + .siblings("span") + .invoke("text") + .then((text) => { + const value = text.match(/"(.*)"/)[0]; + cy.log(value); + + apiData = value; + cy.log("val1:" + value); + }); + }); + + it("Test_Validate the Api data is updated on List widget", function() { + cy.SearchEntityandOpen("List1"); + cy.getCodeMirror().then(($cm) => { + cy.get(".CodeMirror textarea") + .first() + .type(`{{Api1.data.users}}`, { + force: true, + parseSpecialCharSequences: false, + }); + }); + cy.get(commonlocators.editPropCrossButton).click({ force: true }); + cy.get(".t--draggable-textwidget span").should("have.length", 4); + + cy.get(".t--draggable-textwidget span") + .first() + .invoke("text") + .then((text) => { + expect(text).to.equal("Barty Crouch"); + }); + cy.PublishtheApp(); + cy.get(".t--widget-textwidget span").should("have.length", 4); + cy.get(".t--widget-textwidget span") + .first() + .invoke("text") + .then((text) => { + expect(text).to.equal("Barty Crouch"); + }); + }); + + it("Test_Validate the list widget ", function() { + cy.get(publishPage.backToEditor).click({ force: true }); + cy.SearchEntityandOpen("List1"); + + cy.getCodeMirror().then(($cm) => { + cy.get(".CodeMirror textarea") + .last() + .type(`50`, { + force: true, + parseSpecialCharSequences: false, + }); + }); + cy.get(commonlocators.editPropCrossButton).click({ force: true }); + cy.get(".t--draggable-textwidget span").should("have.length", 2); + cy.get(".t--draggable-textwidget span") + .first() + .invoke("text") + .then((text) => { + expect(text).to.equal("Barty Crouch"); + }); + cy.PublishtheApp(); + cy.get(".t--widget-textwidget span").should("have.length", 2); + cy.get(".t--widget-textwidget span") + .first() + .invoke("text") + .then((text) => { + expect(text).to.equal("Barty Crouch"); + }); + }); + + afterEach(() => { + // put your clean up code if any + }); +}); From 7f2b0c6b302b4cdf39cc192e0d3a83f208ec5a58 Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Thu, 20 May 2021 12:22:50 +0530 Subject: [PATCH 40/52] Fix: Mongo datasource and query Cypress test (#4578) * fix for MongoQuery Test * Fixed cypress test * removed unwanted code * updated another test * Commented Query part for stub test --- .../ServerSideTests/Datasources/MongoDataSourceStub_spec.js | 5 ++++- .../ServerSideTests/QueryPane/MongoDatasource_spec.js | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js index 2ce7ec8e3bd..1b63e643197 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/MongoDataSourceStub_spec.js @@ -23,6 +23,7 @@ describe("Create, test, save then delete a mongo datasource", function() { "response.body.responseMeta.status", 200, ); + /* cy.NavigateToQueryEditor(); cy.get("@createDatasource").then((httpResponse) => { @@ -39,7 +40,8 @@ describe("Create, test, save then delete a mongo datasource", function() { "response.body.responseMeta.status", 200, ); - + cy.xpath('//div[contains(text(),"Form Input")]').click({ force: true }); + cy.xpath('//div[contains(text(),"Raw Input")]').click({ force: true }); cy.get(queryLocators.templateMenu).click(); cy.get(".CodeMirror textarea") .first() @@ -94,4 +96,5 @@ describe("Create, test, save then delete a mongo datasource", function() { ); }); */ + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js index d64c91b60cd..d7ff100ec9d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/MongoDatasource_spec.js @@ -35,6 +35,8 @@ describe("Create a query with a mongo datasource, run, save and then delete the 200, ); + cy.xpath('//div[contains(text(),"Form Input")]').click({ force: true }); + cy.xpath('//div[contains(text(),"Raw Input")]').click({ force: true }); cy.get(queryLocators.templateMenu).click(); cy.get(".CodeMirror textarea") .first() From aac5fcb6c752f5b7a0248e4737b8de70ffc2516f Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Thu, 20 May 2021 12:31:10 +0530 Subject: [PATCH 41/52] Use the Function constructor for user script eval (#4446) Co-authored-by: Arpit Mohan --- app/client/src/workers/evaluate.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/client/src/workers/evaluate.ts b/app/client/src/workers/evaluate.ts index 19e9a527061..6e182f1d885 100644 --- a/app/client/src/workers/evaluate.ts +++ b/app/client/src/workers/evaluate.ts @@ -19,11 +19,8 @@ export default function evaluate( ): EvalResult { const unescapedJS = unescapeJS(js).replace(/(\r\n|\n|\r)/gm, ""); const scriptToEvaluate = ` - function closedFunction () { - const result = ${unescapedJS}; - return { result, triggers: self.triggers } - } - closedFunction() + const result = ${unescapedJS}; + return { result, triggers: self.triggers } `; const scriptWithCallback = ` function callback (script) { @@ -31,9 +28,11 @@ export default function evaluate( const result = userFunction.apply(self, CALLBACK_DATA); return { result, triggers: self.triggers }; } - callback(${unescapedJS}); + return callback(${unescapedJS}); `; - const script = callbackData ? scriptWithCallback : scriptToEvaluate; + const script = callbackData + ? Function(scriptWithCallback) + : Function(scriptToEvaluate); const { result, triggers } = (function() { /**** Setting the eval context ****/ const GLOBAL_DATA: Record = {}; @@ -84,7 +83,7 @@ export default function evaluate( self[func] = undefined; }); - const evalResult = eval(script); + const evalResult = script(); // Remove it from self // This is needed so that next eval can have a clean sheet From 63db439183fd23766799272b6405f558bb81781d Mon Sep 17 00:00:00 2001 From: arunvjn <32433245+arunvjn@users.noreply.github.com> Date: Thu, 20 May 2021 16:51:10 +0530 Subject: [PATCH 42/52] Fixed r.match bug (#4593) --- app/client/src/sagas/ApiPaneSagas.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/app/client/src/sagas/ApiPaneSagas.ts b/app/client/src/sagas/ApiPaneSagas.ts index 8ea71706e05..29b8e690be0 100644 --- a/app/client/src/sagas/ApiPaneSagas.ts +++ b/app/client/src/sagas/ApiPaneSagas.ts @@ -2,7 +2,6 @@ * Handles the Api pane ui state. It looks into the routing based on actions too * */ import get from "lodash/get"; -import isObject from "lodash/isObject"; import omit from "lodash/omit"; import cloneDeep from "lodash/cloneDeep"; import { all, select, put, takeEvery, call, take } from "redux-saga/effects"; @@ -79,14 +78,11 @@ function* syncApiParamsSaga( //Payload here contains the path and query params of a typical url like https://{domain}/{path}?{query_params} let value = actionPayload.payload; // Regular expression to find the query params group - if (isObject(value)) { - value = get(value, "datasourceConfiguration.url", ""); - } - const queryParamsRegEx = /(\/[\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/; - value = (value.match(queryParamsRegEx) || [])[2] || ""; const padQueryParams = { key: "", value: "" }; + const queryParamsRegEx = /(\/[\s\S]*?)(\?(?![^{]*})[\s\S]*)?$/; PerformanceTracker.startTracking(PerformanceTransactionName.SYNC_PARAMS_SAGA); if (field === "actionConfiguration.path") { + value = (value.match(queryParamsRegEx) || [])[2] || ""; if (value.indexOf("?") > -1) { const paramsString = value.substr(value.indexOf("?") + 1); const params = paramsString.split("&").map((p) => { @@ -95,7 +91,7 @@ function* syncApiParamsSaga( firstEqualPos > -1 ? [p.substring(0, firstEqualPos), p.substring(firstEqualPos + 1)] : []; - return { key: keyValue[0], value: keyValue[1] || "" }; + return { key: keyValue[0] || "", value: keyValue[1] || "" }; }); if (params.length < 2) { while (params.length < 2) { From 8964aea9df8fa580a6fd9f3043dfb448806acacb Mon Sep 17 00:00:00 2001 From: Rishabh Saxena Date: Thu, 20 May 2021 17:33:08 +0530 Subject: [PATCH 43/52] [Feature] Comments feature updates (#4579) --- .../ApiPaneTests/API_Edit_spec.js | 5 +- .../ProductUpdates/ProductUpdates_spec.js | 2 +- .../manual_TestSuite/List_Widget_Spec.js | 92 ++-- .../manual_TestSuite/Text_Widget_Spec.js | 199 ++++---- app/client/cypress/support/commands.js | 12 +- app/client/package.json | 5 +- app/client/public/index.html | 5 + app/client/src/actions/commentActions.ts | 118 ++++- app/client/src/actions/tourActions.ts | 22 + app/client/src/actions/userActions.ts | 13 + app/client/src/api/CommentsAPI.tsx | 37 +- app/client/src/api/UserApi.tsx | 21 +- app/client/src/assets/icons/comments/chat.svg | 3 + .../comment-mode-unread-indicator.svg | 4 + .../assets/icons/comments/commentCursor.png | Bin 0 -> 1441 bytes .../assets/icons/comments/context-menu.svg | 5 + .../src/assets/icons/comments/down-arrow.svg | 4 + .../src/assets/icons/comments/edit-mode.svg | 3 + .../src/assets/icons/comments/emoji.svg | 4 +- .../src/assets/icons/comments/filter.svg | 3 + app/client/src/assets/icons/comments/link.svg | 4 +- app/client/src/assets/icons/comments/pen.svg | 3 + .../src/assets/icons/comments/pin_3.svg | 3 + .../src/assets/icons/comments/reaction-2.svg | 13 + .../src/assets/icons/comments/reaction.svg | 6 + .../src/assets/icons/comments/read-pin.svg | 3 + .../src/assets/icons/comments/unpin.svg | 3 + .../src/assets/icons/comments/unread-pin.svg | 3 + .../images/comments-onboarding/step-1.png | Bin 0 -> 37202 bytes .../images/comments-onboarding/step-2.png | Bin 0 -> 30390 bytes .../images/comments-onboarding/step-3.png | Bin 0 -> 40249 bytes .../images/comments-onboarding/step-4.png | Bin 0 -> 15261 bytes .../src/assets/images/profile-placeholder.svg | 3 + .../AppComments/AppCommentThreads.tsx | 77 +++- .../src/comments/AppComments/AppComments.tsx | 17 +- .../AppComments/AppCommentsFilterPopover.tsx | 128 ++++++ .../AppComments/AppCommentsHeader.tsx | 55 +-- .../AppComments/AppCommentsPlaceholder.tsx | 34 ++ .../src/comments/AppComments/Container.tsx | 12 +- .../src/comments/CommentCard/CommentCard.tsx | 433 +++++++++++++++--- .../CommentCard/CommentContextMenu.tsx | 106 +++-- .../CommentCard/ResolveCommentButton.tsx | 47 +- .../comments/CommentThread/CommentThread.tsx | 177 ++++--- .../comments/CommentThread/ScrollToLatest.tsx | 30 +- .../CommentsCarouselModal.tsx | 25 + .../FormDisplayImage.tsx | 50 ++ .../CommentsShowcaseCarousel/ProfileForm.tsx | 81 ++++ .../CommentsShowcaseCarousel/index.tsx | 173 +++++++ .../src/comments/ToggleCommentModeButton.tsx | 105 ----- .../inlineComments/AddCommentInput.tsx | 139 ++++-- .../inlineComments/InlineCommentPin.tsx | 260 ++++++++--- .../inlineComments/OverlayCommentsWrapper.tsx | 50 +- .../inlineComments/StyledComponents.tsx | 6 +- .../UnpublishedCommentThread.tsx | 143 +++--- .../comments/tour/AddCommentTourComponent.tsx | 22 + .../src/comments/tour/commentsTourSteps.ts | 25 + app/client/src/comments/utils.ts | 36 ++ app/client/src/components/ads/Checkbox.tsx | 7 +- .../src/components/ads/DisplayImageUpload.tsx | 175 +++++++ app/client/src/components/ads/EmojiPicker.tsx | 44 +- .../src/components/ads/EmojiReactions.tsx | 157 +++++++ app/client/src/components/ads/Icon.tsx | 74 ++- .../src/components/ads/MentionsInput.tsx | 79 +++- app/client/src/components/ads/Radio.tsx | 8 +- .../src/components/ads/ShowcaseCarousel.tsx | 117 +++++ app/client/src/components/ads/Tooltip.tsx | 7 +- .../components/ads/formFields/TextField.tsx | 5 +- .../ads/tour/TourTooltipWrapper.tsx | 57 +++ .../blueprint/ModalComponent.tsx | 24 +- app/client/src/constants/CommentConstants.tsx | 1 + app/client/src/constants/DefaultTheme.tsx | 100 +++- app/client/src/constants/Layers.tsx | 7 +- .../src/constants/ReduxActionConstants.tsx | 21 + app/client/src/constants/TourSteps.tsx | 8 + app/client/src/constants/messages.ts | 17 + .../entities/Comments/CommentsInterfaces.ts | 33 +- app/client/src/entities/Tour/index.ts | 3 + .../src/globalStyles/commentThreadPopovers.ts | 15 +- app/client/src/globalStyles/index.tsx | 4 + app/client/src/globalStyles/popover.ts | 10 +- app/client/src/globalStyles/portals.ts | 20 + app/client/src/globalStyles/uppy.ts | 11 + app/client/src/index.css | 9 - .../AppViewer/viewer/AppViewerHeader.tsx | 2 +- app/client/src/pages/Editor/EditorHeader.tsx | 4 +- app/client/src/pages/Editor/GlobalHotKeys.tsx | 14 + .../src/pages/Editor/ToggleModeButton.tsx | 182 ++++++++ app/client/src/pages/Editor/index.tsx | 4 + app/client/src/pages/common/ProfileImage.tsx | 33 +- app/client/src/pages/common/SubHeader.tsx | 3 +- app/client/src/reducers/index.tsx | 2 + .../commentsReducer/commentsReducer.test.ts | 25 +- .../commentsReducer/commentsReducer.ts | 146 ++++-- .../handleCreateNewCommentThreadSuccess.ts | 2 +- .../handleNewCommentThreadEvent.ts | 17 +- .../handleUpdateCommentEvent.ts | 21 + .../handleUpdateCommentThreadEvent.ts | 5 +- .../handleUpdateCommentThreadSuccess.ts | 2 + .../uiReducers/commentsReducer/interfaces.ts | 7 + .../commentsReducer/testFixtures.ts | 1 - app/client/src/reducers/uiReducers/index.tsx | 2 + .../src/reducers/uiReducers/tourReducer.ts | 40 ++ .../sagas/CommentSagas/handleCommentEvents.ts | 5 + app/client/src/sagas/CommentSagas/index.ts | 172 +++++-- app/client/src/sagas/InitSagas.ts | 13 + app/client/src/sagas/TourSagas.ts | 30 ++ app/client/src/sagas/WebsocketSagas.ts | 22 +- app/client/src/sagas/index.tsx | 2 + app/client/src/sagas/userSagas.tsx | 36 +- app/client/src/selectors/commentsSelectors.ts | 114 ++++- app/client/src/selectors/tourSelectors.tsx | 7 + .../src/utils/hooks/dragResizeHooks.tsx | 10 +- .../src/utils/hooks/useIsScrolledToBottom.tsx | 1 + .../utils/hooks/useProceedToNextTourStep.tsx | 22 + .../src/utils/hooks/useResizeObserver.tsx | 19 + app/client/src/utils/storage.ts | 22 + app/client/start-https.sh | 4 +- app/client/test/setup.ts | 2 + app/client/yarn.lock | 44 +- .../com/appsmith/server/domains/Comment.java | 13 +- .../server/domains/CommentThread.java | 3 +- .../server/services/CommentServiceImpl.java | 17 +- 122 files changed, 3950 insertions(+), 962 deletions(-) create mode 100644 app/client/src/actions/tourActions.ts create mode 100644 app/client/src/assets/icons/comments/chat.svg create mode 100644 app/client/src/assets/icons/comments/comment-mode-unread-indicator.svg create mode 100644 app/client/src/assets/icons/comments/commentCursor.png create mode 100644 app/client/src/assets/icons/comments/context-menu.svg create mode 100644 app/client/src/assets/icons/comments/down-arrow.svg create mode 100644 app/client/src/assets/icons/comments/edit-mode.svg create mode 100644 app/client/src/assets/icons/comments/filter.svg create mode 100644 app/client/src/assets/icons/comments/pen.svg create mode 100644 app/client/src/assets/icons/comments/pin_3.svg create mode 100644 app/client/src/assets/icons/comments/reaction-2.svg create mode 100644 app/client/src/assets/icons/comments/reaction.svg create mode 100644 app/client/src/assets/icons/comments/read-pin.svg create mode 100644 app/client/src/assets/icons/comments/unpin.svg create mode 100644 app/client/src/assets/icons/comments/unread-pin.svg create mode 100644 app/client/src/assets/images/comments-onboarding/step-1.png create mode 100644 app/client/src/assets/images/comments-onboarding/step-2.png create mode 100644 app/client/src/assets/images/comments-onboarding/step-3.png create mode 100644 app/client/src/assets/images/comments-onboarding/step-4.png create mode 100644 app/client/src/assets/images/profile-placeholder.svg create mode 100644 app/client/src/comments/AppComments/AppCommentsFilterPopover.tsx create mode 100644 app/client/src/comments/AppComments/AppCommentsPlaceholder.tsx create mode 100644 app/client/src/comments/CommentsShowcaseCarousel/CommentsCarouselModal.tsx create mode 100644 app/client/src/comments/CommentsShowcaseCarousel/FormDisplayImage.tsx create mode 100644 app/client/src/comments/CommentsShowcaseCarousel/ProfileForm.tsx create mode 100644 app/client/src/comments/CommentsShowcaseCarousel/index.tsx delete mode 100644 app/client/src/comments/ToggleCommentModeButton.tsx create mode 100644 app/client/src/comments/tour/AddCommentTourComponent.tsx create mode 100644 app/client/src/comments/tour/commentsTourSteps.ts create mode 100644 app/client/src/components/ads/DisplayImageUpload.tsx create mode 100644 app/client/src/components/ads/EmojiReactions.tsx create mode 100644 app/client/src/components/ads/ShowcaseCarousel.tsx create mode 100644 app/client/src/components/ads/tour/TourTooltipWrapper.tsx create mode 100644 app/client/src/constants/TourSteps.tsx create mode 100644 app/client/src/entities/Tour/index.ts create mode 100644 app/client/src/globalStyles/portals.ts create mode 100644 app/client/src/globalStyles/uppy.ts create mode 100644 app/client/src/pages/Editor/ToggleModeButton.tsx create mode 100644 app/client/src/reducers/uiReducers/commentsReducer/handleUpdateCommentEvent.ts create mode 100644 app/client/src/reducers/uiReducers/tourReducer.ts create mode 100644 app/client/src/sagas/TourSagas.ts create mode 100644 app/client/src/selectors/tourSelectors.tsx create mode 100644 app/client/src/utils/hooks/useProceedToNextTourStep.tsx create mode 100644 app/client/src/utils/hooks/useResizeObserver.tsx diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js index 918c4592da5..94a026cb444 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js @@ -49,6 +49,9 @@ describe("API Panel Test Functionality", function() { "https://mock-api.appsmith.com/users", ); cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methodWithQueryParam); - cy.ValidateQueryParams({ key: "q", value:"mimeType='application/vnd.google-apps.spreadsheet'" }); + cy.ValidateQueryParams({ + key: "q", + value: "mimeType='application/vnd.google-apps.spreadsheet'", + }); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js index 338970ff7bf..ef124c354f8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ProductUpdates/ProductUpdates_spec.js @@ -10,7 +10,7 @@ describe("Check for product updates button and modal", function() { .its("store") .invoke("getState") .then((state) => { - const { releaseItems, newReleasesCount } = state.ui.releases; + const { newReleasesCount, releaseItems } = state.ui.releases; if (Array.isArray(releaseItems) && releaseItems.length > 0) { cy.get("[data-cy=t--product-updates-btn]") .contains("What's New?") diff --git a/app/client/cypress/manual_TestSuite/List_Widget_Spec.js b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js index 38cd930f041..720bd2ab87c 100644 --- a/app/client/cypress/manual_TestSuite/List_Widget_Spec.js +++ b/app/client/cypress/manual_TestSuite/List_Widget_Spec.js @@ -2,104 +2,92 @@ const dsl = require("../../../fixtures/ListWidgetDsl.json"); describe("List Widget test ideas ", function() { it("List widget background colour and deploy ", function() { - // Drag and drop a List widget - // Open Property pane - // Scroll down to Styles - // Add background colour - // Add item background colour - // Ensure the colour are added appropriately - // Click on Deploy and ensure it is deployed appropriately - } - ) + // Drag and drop a List widget + // Open Property pane + // Scroll down to Styles + // Add background colour + // Add item background colour + // Ensure the colour are added appropriately + // Click on Deploy and ensure it is deployed appropriately + }); it("Adding large item Spacing for item card", function() { // Drag and drop a List widget // Open Property pane // Scroll down to Styles // Add large item spacing (>100) - // Ensure the cards get spaced appropriately - } - ) + // Ensure the cards get spaced appropriately + }); - it("Binding an API data to list widget ", function() { - //Add an API + it("Binding an API data to list widget ", function() { + //Add an API // Drag and drop a List widget // Open list Property pane // Bind the API to list widget // Add Input widget into the list widget // Bind the input widgte to the list widget - } - ) + }); - it("Copy Paste and Delete the List Widget ", function() { + it("Copy Paste and Delete the List Widget ", function() { // Drag and drop a List widget // Click on the property pane // Click on Copy the widget // Paste(cmd+v) the list widget // Click on the delete option of the Parent widget - } - ) + }); it("Renaming the widget from Property pane and Entity explorer ", function() { // Drag and drop a List widget // Click on the property pane // Click name of the widget // Rename the widget - // Navigate to the Entity Explorer + // Navigate to the Entity Explorer // Click on the Widget expands // Navigate to List widget (Double Click) - // Rename the widget - // Ensure the name of the widget is possible from both the place - } - ) + // Rename the widget + // Ensure the name of the widget is possible from both the place + }); - it("Verify the Pagination functionlaity within List Widget", function() { - // Drag and Drop list Widget + it("Verify the Pagination functionlaity within List Widget", function() { + // Drag and Drop list Widget // Click on page 2 // Ensure list widget will be redirected to page 2 - // Click on next button + // Click on next button // Ensure the list widget will be redirected to page 3 // Click on Previous button // Ensure the list widget will be redirected to page 2 // Mouse Hover on the next button // Ensure the tool tip message is appropriate - // Mouse Hover on the Previous button + // Mouse Hover on the Previous button // Ensure the tool tip message is appropriate - } - ) + }); - it("Add new item in the list widget array object", function(){ + it("Add new item in the list widget array object", function() { //Drag and drop list widget - //Click to open an property pane + //Click to open an property pane //Expand Genearl section - //Add the following new item + //Add the following new item //("id": 7,"num": 007",name": Charizard",img": "http://www.serebii.net/pokemongo/pokemon/006.png") //Ensure the new item gets added to the list widget without any error - //Check for the new page is added upon adding new items - } - ) + //Check for the new page is added upon adding new items + }); - it("Adding apt widget into the List widget", function(){ - //Drag and Drop List widget - //Expand the section 1 size in the list widget + it("Adding apt widget into the List widget", function() { + //Drag and Drop List widget + //Expand the section 1 size in the list widget //Ensure by exapdning section inside list widget the page size gets increased - //Drag and Drop button widget inside list widget + //Drag and Drop button widget inside list widget //Ensure Button widget can be placed inside list Widget //Drag and Drop Image widget inside list widget //Ensure Image widget can be placed inside list widget // Drag and drop the text widget inside the list widget // Ensure text widget can be place inside the list widget - } - ) + }); - it("Adding unapt widget to identify the error message", function(){ - //Drag and Drop List widget - //Expand the section 1 size in the list widget + it("Adding unapt widget to identify the error message", function() { + //Drag and Drop List widget + //Expand the section 1 size in the list widget //Drag and Drop widgets ie: Chart ,Date Picker radio button etc - // Ensure an understandable error message is displayed to user - } - ) - - - - }) \ No newline at end of file + // Ensure an understandable error message is displayed to user + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js index 1aa118d32ac..d180857d68f 100644 --- a/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js +++ b/app/client/cypress/manual_TestSuite/Text_Widget_Spec.js @@ -1,117 +1,106 @@ const homePage = require("../../../locators/Textwidget.json"); describe("Test Ideas to test different feature of text widget ", function() { - it("Add New Text widget along with BG and text colour ", function() { - // Navigate to application - // Drag and drop a Text Widget - // Navigate to Property Pane - // Add a text - // Scroll to BG colour and add a colour - // Next add a text colour - // Click on Deploy - } - ) + it("Add New Text widget along with BG and text colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property Pane + // Add a text + // Scroll to BG colour and add a colour + // Next add a text colour + // Click on Deploy + }); - it("Enable Scroll feature with text colour ", function() { - // Navigate to application - // Drag and drop a Text Widget - // Add a long text in the "Label" - // Enable scroll option - // Navigate to Text colour and add a colour - // and ensure it is scrolling - // Click on deploy and check if it scrollable and colour selected is visible - } - ) + it("Enable Scroll feature with text colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Add a long text in the "Label" + // Enable scroll option + // Navigate to Text colour and add a colour + // and ensure it is scrolling + // Click on deploy and check if it scrollable and colour selected is visible + }); - it("Adding text Size to the Text along with BG colour ", function() { - // Navigate to application - // Drag and drop a Text Widget - // Navigate to Property pane - // Add a medium text in the "Label" - // Increase the area of the Text Widget - // Navigate to BG colour and add a colour - // Naviaget to "Text Size" - // Select Paragarph option - // Ensure the text size varies accordingly - } - ) + it("Adding text Size to the Text along with BG colour ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property pane + // Add a medium text in the "Label" + // Increase the area of the Text Widget + // Navigate to BG colour and add a colour + // Naviaget to "Text Size" + // Select Paragarph option + // Ensure the text size varies accordingly + }); - it("Adding Bold Font style and Centre Text Alignment ", function() { - // Navigate to application - // Drag and drop a Text Widget - // Navigate to Property pane - // Add a medium text in the "Label" - // Increase the area of the Text Widget - // Navigate to Font Style - // Make it Bold - // and Navigate to Alignment and make it centre - // Ensure the changes are visible to user - } - ) + it("Adding Bold Font style and Centre Text Alignment ", function() { + // Navigate to application + // Drag and drop a Text Widget + // Navigate to Property pane + // Add a medium text in the "Label" + // Increase the area of the Text Widget + // Navigate to Font Style + // Make it Bold + // and Navigate to Alignment and make it centre + // Ensure the changes are visible to user + }); - it("Adding Italic Font style and Text Alignment to exsisting text widget ", function() { - // Navigate to already exsisting Text widget - // Ensure the text is added - // Navigate to Property pane - // Navigate to Font Style - // Make it Italic font - // and Navigate to Alignment and make it Right - // Ensure the changes are visible to user - } - ) + it("Adding Italic Font style and Text Alignment to exsisting text widget ", function() { + // Navigate to already exsisting Text widget + // Ensure the text is added + // Navigate to Property pane + // Navigate to Font Style + // Make it Italic font + // and Navigate to Alignment and make it Right + // Ensure the changes are visible to user + }); - it("Expand and Contract text widget Property pane", function() { - // Navigate to already exsisting Text widget - // Navigate to Property pane - // Click on collapse option - // Observe that the property pane is contracted - // Now click again on the arrow - //and ensure it collapses - } - ) + it("Expand and Contract text widget Property pane", function() { + // Navigate to already exsisting Text widget + // Navigate to Property pane + // Click on collapse option + // Observe that the property pane is contracted + // Now click again on the arrow + //and ensure it collapses + }); - it("Copy and paste a text widget", function() { - // Navigate to already exsisting Text widget - // Ensure Clour and font feature exsists - // Copy and paste the widget - // Ensure the new widget retrives the feature exsisting from parent widget - } - ) + it("Copy and paste a text widget", function() { + // Navigate to already exsisting Text widget + // Ensure Clour and font feature exsists + // Copy and paste the widget + // Ensure the new widget retrives the feature exsisting from parent widget + }); - it("Rename and search a text widget", function() { - // Ensure there are multiple Text widget - // Navigate to Entity Explorer - // Search for "Text" keyword - // Click on one of the text widget - // Rename the text widget from the Entity explorer - // Clear the search keyword - // enter the new text widget name - // and observe the user is navigated to same text widget and properties of the widget does not change on renaming - } - ) + it("Rename and search a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Rename the text widget from the Entity explorer + // Clear the search keyword + // enter the new text widget name + // and observe the user is navigated to same text widget and properties of the widget does not change on renaming + }); - it("Search and delete a text widget", function() { - // Ensure there are multiple Text widget - // Navigate to Entity Explorer - // Search for "Text" keyword - // Click on one of the text widget - // Ensure user is navigated to Text widget - // Click on Delete option - // Ensure the Text widget is delete - // Click on Deploy adn ensure the Widget is delete - } - ) + it("Search and delete a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Ensure user is navigated to Text widget + // Click on Delete option + // Ensure the Text widget is delete + // Click on Deploy adn ensure the Widget is delete + }); - it("Search and delete a text widget", function() { - // Ensure there are multiple Text widget - // Navigate to Entity Explorer - // Search for "Text" keyword - // Click on one of the text widget - // Ensure user is navigated to Text widget - // Click on Delete option - // Ensure the Text widget is delete - // Click on Deploy adn ensure the Widget is delete - } - ) -} -) \ No newline at end of file + it("Search and delete a text widget", function() { + // Ensure there are multiple Text widget + // Navigate to Entity Explorer + // Search for "Text" keyword + // Click on one of the text widget + // Ensure user is navigated to Text widget + // Click on Delete option + // Ensure the Text widget is delete + // Click on Deploy adn ensure the Widget is delete + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 5b8e20e9333..48b6b2af7d7 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -2347,8 +2347,12 @@ Cypress.Commands.add("assertPageSave", () => { Cypress.Commands.add("ValidateQueryParams", (param) => { cy.xpath(apiwidget.paramsTab) - .should("be.visible") - .click({ force: true }); - cy.xpath(apiwidget.paramKey).first().contains(param.key); - cy.xpath(apiwidget.paramValue).first().contains(param.value); + .should("be.visible") + .click({ force: true }); + cy.xpath(apiwidget.paramKey) + .first() + .contains(param.key); + cy.xpath(apiwidget.paramValue) + .first() + .contains(param.value); }); diff --git a/app/client/package.json b/app/client/package.json index 347bbb8a6a5..9ba69034289 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -45,6 +45,7 @@ "@uppy/dashboard": "^1.16.0", "@uppy/file-input": "^1.4.22", "@uppy/google-drive": "^1.5.22", + "@uppy/image-editor": "^0.2.4", "@uppy/onedrive": "^1.1.22", "@uppy/react": "^1.11.2", "@uppy/url": "^1.5.16", @@ -62,7 +63,7 @@ "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "draft-js": "^0.11.7", - "emoji-picker-react": "^3.4.2", + "emoji-mart": "^3.0.1", "eslint": "^7.11.0", "fast-deep-equal": "^3.1.1", "fast-xml-parser": "^3.17.5", @@ -197,6 +198,7 @@ "@types/deep-diff": "^1.0.0", "@types/downloadjs": "^1.4.2", "@types/draft-js": "^0.11.1", + "@types/emoji-mart": "^3.0.4", "@types/jest": "^24.0.22", "@types/marked": "^1.2.2", "@types/react-beautiful-dnd": "^11.0.4", @@ -206,6 +208,7 @@ "@types/react-window": "^1.8.2", "@types/redux-form": "^8.1.9", "@types/redux-mock-store": "^1.0.2", + "@types/resize-observer-browser": "^0.1.5", "@types/styled-system": "^5.1.9", "@types/tern": "0.22.0", "@types/toposort": "^2.0.3", diff --git a/app/client/public/index.html b/app/client/public/index.html index 70250975eb9..8079bbba087 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -33,6 +33,11 @@
+ +