From d1822a9dee05620c4faf173d50802a824224af9b Mon Sep 17 00:00:00 2001 From: Ashok Kumar M <35134347+marks0351@users.noreply.github.com> Date: Tue, 23 Feb 2021 09:48:23 +0530 Subject: [PATCH 01/17] Feature: Modal Widget (#3092) * Feature: Modal Widget(DIP) * Adding cypress tests. --- app/client/cypress/fixtures/ModalDsl.json | 129 ++++++++++++++++++ .../DisplayWidgets/Modal_spec.js | 25 ++++ .../src/constants/ReduxActionConstants.tsx | 1 + .../mockResponses/WidgetConfigResponse.tsx | 4 +- .../mockResponses/WidgetSidebarResponse.tsx | 5 + app/client/src/sagas/ModalSagas.ts | 12 ++ app/client/src/sagas/WidgetOperationSagas.tsx | 8 +- app/client/src/utils/WidgetRegistry.tsx | 15 ++ 8 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 app/client/cypress/fixtures/ModalDsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Modal_spec.js diff --git a/app/client/cypress/fixtures/ModalDsl.json b/app/client/cypress/fixtures/ModalDsl.json new file mode 100644 index 0000000000..b7f974d981 --- /dev/null +++ b/app/client/cypress/fixtures/ModalDsl.json @@ -0,0 +1,129 @@ + +{ + "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": 10, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "size": "MODAL_SMALL", + "canEscapeKeyClose": true, + "detachFromLayout": true, + "canOutsideClickClose": true, + "shouldScrollContents": true, + "widgetName": "Modal1", + "children": [ + { + "isVisible": true, + "widgetName": "Canvas1", + "detachFromLayout": true, + "canExtend": true, + "isDisabled": false, + "shouldScrollContents": false, + "children": [ + { + "isVisible": true, + "widgetName": "Icon1", + "iconName": "cross", + "iconSize": 24, + "color": "#040627", + "type": "ICON_WIDGET", + "isLoading": false, + "leftColumn": 14, + "rightColumn": 16, + "topRow": 0, + "bottomRow": 1, + "parentId": "yyyrxs383y", + "widgetId": "kxdvolusyp", + "onClick": "{{closeModal('Modal1')}}" + }, + { + "isVisible": true, + "text": "Modal Title", + "textStyle": "HEADING", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "leftColumn": 0, + "rightColumn": 10, + "topRow": 0, + "bottomRow": 1, + "parentId": "yyyrxs383y", + "widgetId": "3fugqdtg8g" + }, + { + "isVisible": true, + "text": "Cancel", + "buttonStyle": "SECONDARY_BUTTON", + "widgetName": "Button1", + "isDisabled": false, + "isDefaultClickDisabled": true, + "type": "BUTTON_WIDGET", + "isLoading": false, + "leftColumn": 9, + "rightColumn": 12, + "topRow": 4, + "bottomRow": 5, + "parentId": "yyyrxs383y", + "widgetId": "6cjfbnjfa4" + }, + { + "isVisible": true, + "text": "Confirm", + "buttonStyle": "PRIMARY_BUTTON", + "widgetName": "Button2", + "isDisabled": false, + "isDefaultClickDisabled": true, + "type": "BUTTON_WIDGET", + "isLoading": false, + "leftColumn": 12, + "rightColumn": 16, + "topRow": 4, + "bottomRow": 5, + "parentId": "yyyrxs383y", + "widgetId": "3tmnukkm7m" + } + ], + "minHeight": 240, + "type": "CANVAS_WIDGET", + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 444, + "topRow": 0, + "bottomRow": 240, + "parentId": "287u6pannr", + "widgetId": "yyyrxs383y" + } + ], + "type": "MODAL_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 5, + "rightColumn": 11, + "topRow": 15, + "bottomRow": 21, + "parentId": "0", + "widgetId": "287u6pannr" + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Modal_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Modal_spec.js new file mode 100644 index 0000000000..0fe1a010e7 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/DisplayWidgets/Modal_spec.js @@ -0,0 +1,25 @@ +const dsl = require("../../../fixtures/ModalDsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); +const explorer = require("../../../locators/explorerlocators.json"); + +describe("Modal Widget Functionality", function() { + beforeEach(() => { + cy.addDsl(dsl); + }); + + it("Add new Modal", () => { + cy.get(explorer.addWidget).click(); + cy.dragAndDropToCanvas("modalwidget", { x: 300, y: -300 }); + cy.get(".t--modal-widget").should("exist"); + }); + + it("Open Existing Modal from created Widgets list", () => { + cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({ + multiple: true, + }); + cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Modal1)").click({ + multiple: true, + }); + cy.get(".t--modal-widget").should("exist"); + }); +}); diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 950e327b52..b2ebcb5599 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -47,6 +47,7 @@ export const ReduxActionTypes: { [key: string]: string } = { SAVE_PAGE_SUCCESS: "SAVE_PAGE_SUCCESS", UPDATE_LAYOUT: "UPDATE_LAYOUT", WIDGET_ADD_CHILD: "WIDGET_ADD_CHILD", + WIDGET_CHILD_ADDED: "WIDGET_CHILD_ADDED", WIDGET_REMOVE_CHILD: "WIDGET_REMOVE_CHILD", WIDGET_MOVE: "WIDGET_MOVE", WIDGET_RESIZE: "WIDGET_RESIZE", diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index 1862f086f7..f2d841b45b 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -226,8 +226,8 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, }, MODAL_WIDGET: { - rows: 456, - columns: 456, + rows: 6, + columns: 6, size: "MODAL_SMALL", canEscapeKeyClose: true, detachFromLayout: true, diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx index 2275ac3686..dc8e13878f 100644 --- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx +++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx @@ -93,6 +93,11 @@ const WidgetSidebarResponse: WidgetCardProps[] = [ widgetCardName: "Video", key: generateReactKey(), }, + { + type: "MODAL_WIDGET", + widgetCardName: "Modal", + key: generateReactKey(), + }, ]; export default WidgetSidebarResponse; diff --git a/app/client/src/sagas/ModalSagas.ts b/app/client/src/sagas/ModalSagas.ts index e8eaf21c23..cc9802b5be 100644 --- a/app/client/src/sagas/ModalSagas.ts +++ b/app/client/src/sagas/ModalSagas.ts @@ -89,6 +89,17 @@ export function* showModalByNameSaga( } } +export function* showIfModalSaga( + action: ReduxAction<{ widgetId: string; type: string }>, +) { + if (action.payload.type === "MODAL_WIDGET") { + yield put({ + type: ReduxActionTypes.SHOW_MODAL, + payload: { modalId: action.payload.widgetId }, + }); + } +} + export function* showModalSaga(action: ReduxAction<{ modalId: string }>) { // First we close the currently open modals (if any) // Notice the empty payload. @@ -184,5 +195,6 @@ export default function* modalSagas() { takeLatest(ReduxActionTypes.CREATE_MODAL_INIT, createModalSaga), takeLatest(ReduxActionTypes.SHOW_MODAL, showModalSaga), takeLatest(ReduxActionTypes.SHOW_MODAL_BY_NAME, showModalByNameSaga), + takeLatest(ReduxActionTypes.WIDGET_CHILD_ADDED, showIfModalSaga), ]); } diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 1310719231..333bc1b9e9 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -239,7 +239,6 @@ export function* addChildSaga(addChildAction: ReduxAction) { const start = performance.now(); Toaster.clear(); const { widgetId } = addChildAction.payload; - // Get the current parent widget whose child will be the new widget. const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId); // const parent = Object.assign({}, stateParent); @@ -262,6 +261,13 @@ export function* addChildSaga(addChildAction: ReduxAction) { widgets[parent.widgetId] = parent; log.debug("add child computations took", performance.now() - start, "ms"); + yield put({ + type: ReduxActionTypes.WIDGET_CHILD_ADDED, + payload: { + widgetId: childWidgetPayload.widgetId, + type: addChildAction.payload.type, + }, + }); yield put(updateAndSaveLayout(widgets)); } catch (error) { yield put({ diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index d6d4828bae..2fced6e1e0 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -435,5 +435,20 @@ export default class WidgetBuilderRegistry { SkeletonWidget.getDefaultPropertiesMap(), SkeletonWidget.getMetaPropertiesMap(), ); + + WidgetFactory.registerWidgetBuilder( + WidgetTypes.MODAL_WIDGET, + { + buildWidget(widgetData: ModalWidgetProps): JSX.Element { + return ; + }, + }, + ModalWidget.getPropertyValidationMap(), + ModalWidget.getDerivedPropertiesMap(), + ModalWidget.getTriggerPropertyMap(), + ModalWidget.getDefaultPropertiesMap(), + ModalWidget.getMetaPropertiesMap(), + ModalWidget.getPropertyPaneConfig(), + ); } } From 4f6a48bf40f6e4a003a44d6399e8b5aa8c552a74 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 23 Feb 2021 09:51:23 +0530 Subject: [PATCH 02/17] Fix BatchGetItem & TransactGetItems operations failing on the DynamoDB plugin (#3120) * Fix batch operations APIs failing with DynamoDB * Fixed TransactGetItems operation on DynamoDB --- .../com/external/plugins/DynamoPlugin.java | 76 ++++++++++++++----- .../external/plugins/DynamoPluginTest.java | 69 +++++++++++++++++ 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java index 883db8e1f2..2c3b53d669 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java @@ -36,6 +36,8 @@ import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -115,7 +117,8 @@ public class DynamoPlugin extends BasePlugin { toLowerCamelCase(action), requestClass ); - final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, plainToSdk(parameters, requestClass)); + final Object sdkValue = plainToSdk(parameters, requestClass); + final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, sdkValue); result.setBody(sdkToPlain(response)); } catch (AppsmithPluginException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { final String message = "Error executing the DynamoDB Action: " + (e.getCause() == null ? e : e.getCause()).getMessage(); @@ -290,34 +293,43 @@ public class DynamoPlugin extends BasePlugin { // For maps, we go recursive, applying this transformation to each value, and replacing with the // result in the map. Generic types in the setter method's signature are used to convert the values. final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName)); - final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0]; - final Map transformedMap = new HashMap<>(); - for (final Map.Entry innerEntry : ((Map) value).entrySet()) { - Object innerValue = innerEntry.getValue(); - if (innerValue instanceof Map) { - innerValue = plainToSdk((Map) innerValue, (Class) valueType.getActualTypeArguments()[1]); + final Type parameterType = setterMethod.getGenericParameterTypes()[0]; + if (parameterType instanceof ParameterizedType) { + final ParameterizedType valueType = (ParameterizedType) parameterType; + final Map transformedMap = new HashMap<>(); + for (final Map.Entry innerEntry : ((Map) value).entrySet()) { + Object innerValue = innerEntry.getValue(); + if (innerValue instanceof Map) { + innerValue = plainToSdk((Map) innerValue, (Class) valueType.getActualTypeArguments()[1]); + } + transformedMap.put(innerEntry.getKey(), innerValue); } - transformedMap.put(innerEntry.getKey(), innerValue); + value = transformedMap; + if (!Map.class.isAssignableFrom((Class) valueType.getRawType())) { + // Some setters don't take a plain map. For example, some require an `AttributeValue` instance + // for objects that are just maps in JSON. So, we make that conversion here. + value = plainToSdk((Map) value, (Class) valueType.getRawType()); + } + setterMethod.invoke(builder, value); + } else if (parameterType instanceof Class) { + setterMethod.invoke(builder, plainToSdk((Map) value, (Class) parameterType)); } - value = transformedMap; - if (!Map.class.isAssignableFrom((Class) valueType.getRawType())) { - // Some setters don't take a plain map. For example, some require an `AttributeValue` instance - // for objects that are just maps in JSON. So, we make that conversion here. - value = plainToSdk((Map) value, (Class) valueType.getRawType()); - } - setterMethod.invoke(builder, value); } else if (value instanceof Collection) { // For linear collections, the process is similar to that of maps. final Collection valueAsCollection = (Collection) value; // Find method by name and exclude the varargs version of the method. final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName) && !m.getParameterTypes()[0].getName().startsWith("[L")); - final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0]; + Type valueType = ((ParameterizedType) setterMethod.getGenericParameterTypes()[0]).getActualTypeArguments()[0]; + if (valueType instanceof WildcardType) { + // This occurs when the method's parameter is typed as `Collection>`. Example op: `BatchGetItem`. + valueType = ((WildcardType) valueType).getUpperBounds()[0]; + } final Collection reTypedList = new ArrayList<>(); for (final Object innerValue : valueAsCollection) { if (innerValue instanceof Map) { - reTypedList.add(plainToSdk((Map) innerValue, (Class) valueType.getActualTypeArguments()[0])); - } else if (innerValue instanceof String && SdkBytes.class.isAssignableFrom((Class) valueType.getActualTypeArguments()[0])) { + reTypedList.add(plainToSdk((Map) innerValue, valueType)); + } else if (innerValue instanceof String && SdkBytes.class.isAssignableFrom((Class) valueType)) { reTypedList.add(SdkBytes.fromUtf8String((String) innerValue)); } else { reTypedList.add(innerValue); @@ -338,6 +350,34 @@ public class DynamoPlugin extends BasePlugin { return (T) builderType.getMethod("build").invoke(builder); } + public static Object plainToSdk(Map mapping, Type type) + throws InvocationTargetException, NoSuchMethodException, ClassNotFoundException, AppsmithPluginException, + IllegalAccessException { + + if (mapping == null) { + return null; + } + + if (!(type instanceof ParameterizedType)) { + return plainToSdk(mapping, (Class) type); + } + + final ParameterizedType ptype = (ParameterizedType) type; + + if (Map.class.equals(ptype.getRawType())) { + final Map convertedMap = new HashMap<>(); + for (final Map.Entry entry : mapping.entrySet()) { + convertedMap.put(entry.getKey(), plainToSdk((Map) entry.getValue(), (Class) ptype.getActualTypeArguments()[1])); + } + return convertedMap; + } + + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Unknown type to convert to SDK style " + type.getTypeName() + ); + } + private static Method findMethod(Class builderType, Predicate predicate) { return Arrays.stream(builderType.getMethods()) .filter(predicate) diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java index 39b6c03dae..375c39af43 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import java.net.URI; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -238,6 +239,74 @@ public class DynamoPluginTest { .verifyComplete(); } + @Test + public void testBatchGetItem() { + final String body = "{\n" + + " \"RequestItems\": {\n" + + " \"cities\": {\n" + + " \"Keys\": [\n" + + " {\n" + + " \"Id\": {\n" + + " \"S\": \"1\"\n" + + " }\n" + + " },\n" + + " {\n" + + " \"Id\": {\n" + + " \"S\": \"2\"\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"ProjectionExpression\":\"City\"\n" + + " }\n" + + " },\n" + + " \"ReturnConsumedCapacity\": \"TOTAL\"\n" + + "}"; + + StepVerifier.create(execute("BatchGetItem", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + final Map response = (Map) result.getBody(); + assertEquals( + Collections.emptyMap(), + response.remove("UnprocessedKeys") + ); + final List>> items = (List>>) ((Map) response.get("Responses")).get("cities"); + assertEquals("New Delhi", items.get(0).get("City").get("S")); + assertEquals("Bengaluru", items.get(1).get("City").get("S")); + }) + .verifyComplete(); + } + + @Test + public void testTransactGetItems() { + final String body = + "{\n" + + " \"ReturnConsumedCapacity\": \"NONE\",\n" + + " \"TransactItems\": [\n" + + " {\n" + + " \"Get\": {\n" + + " \"Key\": {\n" + + " \"Id\": {\n" + + " \"S\": \"1\"\n" + + " }\n" + + " },\n" + + " \"TableName\": \"cities\"\n" + + " }\n" + + " }\n" + + " ]\n" + + "}"; + + StepVerifier.create(execute("TransactGetItems", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + final Map response = (Map) result.getBody(); + assertEquals("New Delhi", ((List>>>) response.get("Responses")).get(0).get("Item").get("City").get("S")); + }) + .verifyComplete(); + } + @Test public void testStructure() { final Mono structureMono = pluginExecutor From e2509ae3253ec05123f9ced26ec483812e12c4c0 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 23 Feb 2021 09:52:45 +0530 Subject: [PATCH 03/17] Add placeholder for MongoDB datasource config's host inputs (#3145) * Remove example indicator --- .../appsmith-plugins/mongoPlugin/src/main/resources/form.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json index f2b6162dd4..a14eca0dac 100644 --- a/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json +++ b/app/server/appsmith-plugins/mongoPlugin/src/main/resources/form.json @@ -43,7 +43,8 @@ "configProperty": "datasourceConfiguration.endpoints[*].host", "controlType": "KEYVALUE_ARRAY", "validationMessage": "Please enter a valid host", - "validationRegex": "^((?![/:]).)*$" + "validationRegex": "^((?![/:]).)*$", + "placeholder": "myapp.abcde.mongodb.net" }, { "label": "Port", From 725043fde303f887b642570113604dbb0976b4ba Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Tue, 23 Feb 2021 09:56:02 +0530 Subject: [PATCH 04/17] Replace test api with mockApi in Cypress tests (#2975) Co-authored-by: nandan.anantharamu --- app/client/cypress/fixtures/testdata.json | 10 ++++----- .../BindApi_withPageload_Input_spec.js | 10 ++++----- .../ActionExecution/Action_PageOnLoad_spec.js | 3 ++- .../ApiPaneTests/API_All_Verb_spec.js | 22 +++++++++---------- .../ApiPaneTests/API_CurlPOSTImport_spec.js | 2 +- .../ApiPaneTests/API_Mustache_spec.js | 5 +++-- .../Datasources/DatasourceForm_spec.js | 4 +++- .../Datasources/RestApiDatasource_spec.js | 6 +++-- 8 files changed, 33 insertions(+), 29 deletions(-) diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index cd8432c8da..84d2a402ce 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -24,10 +24,10 @@ "Post": "POST", "Delete": "DELETE", "Patch": "PATCH", - "methodput": "api/users/2", - "methodpost": "api/users", - "methodpatch": "api/users/2", - "methoddelete": "1", + "methodput": "echo/put", + "methodpost": "echo/post", + "methodpatch": "echo/patch", + "methoddelete": "echo/delete", "putAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'PUT')]", "postAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'POST')]", "patchAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'PATCH')]", @@ -97,7 +97,7 @@ "btoaInput": "{{btoa('A')", "defaultInputBinding": "{{Input2.text", "tabBinding": "{{Tabs1.selectedTab", - "pageloadBinding": "{{PageLoadApi.data.data[1].id}}{{Input1.text}}", + "pageloadBinding": "{{PageLoadApi.data.users[1].id}}{{Input1.text}}", "currentRowEmail": "{{currentRow.email}}", "currentRowOrderAmt": "{{currentRow.orderAmount}}", "momentDate": "{{moment()}}", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js index bd2f3eb8d1..4e38c8ccee 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js @@ -15,7 +15,7 @@ describe("Binding the API with pageOnLoad and input Widgets", function() { it("Will load an api on load", function() { cy.NavigateToAPI_Panel(); cy.CreateAPI("PageLoadApi"); - cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); cy.WaitAutoSave(); cy.get(apiwidget.settings).click({ force: true }); cy.get(apiwidget.onPageLoad).click({ force: true }); @@ -25,9 +25,7 @@ describe("Binding the API with pageOnLoad and input Widgets", function() { it("Input widget updated with deafult data", function() { cy.SearchEntityandOpen("Input1"); - cy.get(widgetsPage.defaultInput) - .type(testdata.command) - .type("3"); + cy.get(widgetsPage.defaultInput).type("3"); cy.get(commonlocators.editPropCrossButton).click(); cy.wait("@updateLayout").should( "have.nested.property", @@ -53,13 +51,13 @@ describe("Binding the API with pageOnLoad and input Widgets", function() { ); cy.PublishtheApp(); cy.get(publish.inputWidget + " " + "input") - .last() + .first() .invoke("attr", "value") .should("contain", "3"); cy.get(publish.inputWidget + " " + "input") .last() .invoke("attr", "value") - .should("contain", "2"); + .should("contain", "23"); cy.get(publish.backToEditor) .first() .click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js index 2dbc253e37..e8ae36ec3d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js @@ -1,5 +1,6 @@ const dsl = require("../../../../fixtures/tableWidgetDsl.json"); const commonlocators = require("../../../../locators/commonlocators.json"); +const testdata = require("../../../../fixtures/testdata.json"); describe("API Panel Test Functionality", function() { before(() => { @@ -8,7 +9,7 @@ describe("API Panel Test Functionality", function() { it("Will load an api on load", function() { cy.NavigateToAPI_Panel(); cy.CreateAPI("PageLoadApi"); - cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); cy.WaitAutoSave(); cy.get("li:contains('Settings')").click({ force: true }); cy.get("[data-cy=executeOnLoad]").click({ force: true }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js index 6e3cb4edd5..84030d93bb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_All_Verb_spec.js @@ -21,7 +21,7 @@ describe("API Panel Test Functionality", function() { cy.log("Creation of FirstAPI Action successful"); cy.SelectAction(testdata.putAction); cy.EnterSourceDetailsWithbody( - testdata.baseUrl2, + testdata.baseUrl, testdata.methodput, testdata.headerKey, testdata.headerValue, @@ -35,7 +35,7 @@ describe("API Panel Test Functionality", function() { .type(json, { force: true }); cy.WaitAutoSave(); cy.RunAPI(); - cy.validateRequest(testdata.baseUrl2, testdata.methodput, testdata.Put); + cy.validateRequest(testdata.baseUrl, testdata.methodput, testdata.Put); }); cy.ResponseStatusCheck("200 OK"); cy.log("Response code check successful"); @@ -48,7 +48,7 @@ describe("API Panel Test Functionality", function() { cy.log("Creation of FirstAPI Action successful"); cy.SelectAction(testdata.postAction); cy.EnterSourceDetailsWithbody( - testdata.baseUrl2, + testdata.baseUrl, testdata.methodpost, testdata.headerKey, testdata.headerValue, @@ -62,7 +62,7 @@ describe("API Panel Test Functionality", function() { .type(json, { force: true }); cy.WaitAutoSave(); cy.RunAPI(); - cy.validateRequest(testdata.baseUrl2, testdata.methodpost, testdata.Post); + cy.validateRequest(testdata.baseUrl, testdata.methodpost, testdata.Post); }); cy.ResponseStatusCheck("201 CREATED"); cy.log("Response code check successful"); @@ -75,7 +75,7 @@ describe("API Panel Test Functionality", function() { cy.log("Creation of FirstAPI Action successful"); cy.SelectAction(testdata.patchAction); cy.EnterSourceDetailsWithbody( - testdata.baseUrl2, + testdata.baseUrl, testdata.methodpatch, testdata.headerKey, testdata.headerValue, @@ -90,7 +90,7 @@ describe("API Panel Test Functionality", function() { cy.WaitAutoSave(); cy.RunAPI(); cy.validateRequest( - testdata.baseUrl2, + testdata.baseUrl, testdata.methodpatch, testdata.Patch, ); @@ -106,19 +106,19 @@ describe("API Panel Test Functionality", function() { cy.log("Creation of FirstAPI Action successful"); cy.SelectAction(testdata.deleteAction); cy.EnterSourceDetailsWithbody( - testdata.baseUrl2, - testdata.methodpatch, + testdata.baseUrl, + testdata.methoddelete, testdata.headerKey, testdata.headerValue, ); cy.WaitAutoSave(); cy.RunAPI(); cy.validateRequest( - testdata.baseUrl2, - testdata.methodpatch, + testdata.baseUrl, + testdata.methoddelete, testdata.Delete, ); - cy.ResponseStatusCheck("204 NO_CONTENT"); + cy.ResponseStatusCheck("200"); cy.log("Response code check successful"); }); 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 58c96969a1..b23b1b8fdb 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 @@ -7,7 +7,7 @@ describe("Test curl import flow", function() { cy.NavigateToApiEditor(); cy.get(ApiEditor.curlImage).click({ force: true }); cy.get("textarea").type( - 'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://reqres.in/api/users', + 'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://mock-api.appsmith.com/echo/post', { force: true, parseSpecialCharSequences: false, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_Mustache_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_Mustache_spec.js index 4a246430fb..429a50a55d 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_Mustache_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ApiPaneTests/API_Mustache_spec.js @@ -11,12 +11,13 @@ describe("Moustache test Functionality", function() { it("Moustache test Functionality", function() { cy.openPropertyPane("textwidget"); cy.widgetText("Api", widgetsPage.textWidget, widgetsPage.textInputval); - cy.testCodeMirror("/api/users/2"); + cy.testCodeMirror("users"); cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("TestAPINew"); cy.log("Creation of API Action successful"); - cy.enterDatasourceAndPath(testdata.baseUrl2, testdata.moustacheMethod); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.moustacheMethod); + cy.wait(300); cy.RunAPI(); cy.ResponseStatusCheck(testdata.successStatusCode); cy.log("Response code check successful"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceForm_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceForm_spec.js index 2a1486967d..778c6091c6 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceForm_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/DatasourceForm_spec.js @@ -1,8 +1,10 @@ +const testdata = require("../../../../fixtures/testdata.json"); + describe("Datasource form related tests", function() { it("Check whether the delete button has the right color", function() { cy.NavigateToAPI_Panel(); cy.CreateAPI("Testapi"); - cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); cy.get(".t--store-as-datasource").click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js index b9d29746f3..b33c76418a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/Datasources/RestApiDatasource_spec.js @@ -1,13 +1,15 @@ +const testdata = require("../../../../fixtures/testdata.json"); + describe("Create a rest datasource", function() { it("Create a rest datasource", function() { cy.NavigateToAPI_Panel(); cy.CreateAPI("Testapi"); - cy.enterDatasourceAndPath("https://reqres.in/api/", "users"); + cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods); cy.get(".t--store-as-datasource").click(); cy.saveDatasource(); - cy.contains(".datasource-highlight", "https://reqres.in"); + cy.contains(".datasource-highlight", "https://mock-api.appsmith.com"); cy.SaveAndRunAPI(); }); From e5e5af204d479aef4d5cd52a9bc2389b1cc3f4a4 Mon Sep 17 00:00:00 2001 From: Young Yoo Date: Mon, 22 Feb 2021 23:49:14 -0500 Subject: [PATCH 05/17] Fix #2342 Un-select row feature (#3074) Co-authored-by: iheaven0129@gmail.com <*Dbtldud135> --- .../appsmith/TableComponent/index.tsx | 2 +- app/client/src/widgets/TableWidget/index.tsx | 27 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx index e78bc71dfd..dbcf1239cf 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/index.tsx @@ -163,7 +163,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => { row: { original: Record; index: number }, isSelected: boolean, ) => { - if (!isSelected || !!props.multiRowSelection) { + if (!isSelected || props.multiRowSelection || !props.multiRowSelection) { props.onRowClick(row.original, row.index); } }; diff --git a/app/client/src/widgets/TableWidget/index.tsx b/app/client/src/widgets/TableWidget/index.tsx index 84bd674b97..f43bd21d1e 100644 --- a/app/client/src/widgets/TableWidget/index.tsx +++ b/app/client/src/widgets/TableWidget/index.tsx @@ -950,17 +950,24 @@ class TableWidget extends BaseWidget { ), ); } else { - this.props.updateWidgetMetaProperty("selectedRowIndex", index); - this.props.updateWidgetMetaProperty( - "selectedRow", - this.props.filteredTableData[index], - { - dynamicString: this.props.onRowSelected, - event: { - type: EventType.ON_ROW_SELECTED, + const selectedRowIndex = isNumber(this.props.selectedRowIndex) + ? this.props.selectedRowIndex + : -1; + if (selectedRowIndex === index) { + index = -1; + } else { + this.props.updateWidgetMetaProperty( + "selectedRow", + this.props.filteredTableData[index], + { + dynamicString: this.props.onRowSelected, + event: { + type: EventType.ON_ROW_SELECTED, + }, }, - }, - ); + ); + } + this.props.updateWidgetMetaProperty("selectedRowIndex", index); } }; From 61af3061105c85347e976700fd9ca5a4663f9957 Mon Sep 17 00:00:00 2001 From: Nidhi Date: Tue, 23 Feb 2021 11:10:47 +0530 Subject: [PATCH 06/17] Added null check for OAuth2 scopes(#3153) --- .../com/appsmith/server/solutions/AuthenticationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/AuthenticationService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/AuthenticationService.java index 9b5bc62f7c..45f4e77e68 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/AuthenticationService.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/AuthenticationService.java @@ -78,7 +78,7 @@ public class AuthenticationService { // The state is used internally to calculate the redirect url when returning control to the client .queryParam(STATE, String.join(",", pageId, datasourceId, redirectUri)); // Adding optional scope parameter - if (!oAuth2.getScope().isEmpty()) { + if (oAuth2.getScope() != null && !oAuth2.getScope().isEmpty()) { uriComponentsBuilder .queryParam(SCOPE, String.join(",", oAuth2.getScope())); } From a090d56093e870590b0463c2ec1cc7fd6057af7f Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 23 Feb 2021 11:30:12 +0530 Subject: [PATCH 07/17] docs: add Devedunkey as a contributor (#3151) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index e479120379..1e9d14dca4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -297,6 +297,15 @@ "contributions": [ "code" ] + }, + { + "login": "Devedunkey", + "name": "Young Yoo", + "avatar_url": "https://avatars.githubusercontent.com/u/29448764?v=4", + "profile": "https://github.com/Devedunkey", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 0cf7af5197..d1e3730241 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap
vicky-primathon

💻
devrk96

💻
Tim Dillon

💻 +
Young Yoo

💻 From 9137adaece3ffaaf0ae07ec110d9d51c0d261651 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 23 Feb 2021 12:10:25 +0530 Subject: [PATCH 08/17] docs: add zegerhoogeboom as a contributor (#3152) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Abhinav Jha --- .all-contributorsrc | 8 ++++++++ README.md | 1 + 2 files changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1e9d14dca4..d8d34bb89a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -299,6 +299,14 @@ ] }, { + "login": "zegerhoogeboom", + "name": "Zeger Hoogeboom", + "avatar_url": "https://avatars.githubusercontent.com/u/5371096?v=4", + "profile": "https://github.com/zegerhoogeboom", + "contributions": [ + "code" + ] + }, "login": "Devedunkey", "name": "Young Yoo", "avatar_url": "https://avatars.githubusercontent.com/u/29448764?v=4", diff --git a/README.md b/README.md index d1e3730241..44e181457f 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap
vicky-primathon

💻
devrk96

💻
Tim Dillon

💻 +
Zeger Hoogeboom

💻
Young Yoo

💻 From 9b58e683b565dfeaab3958a8efe421952ca64cb4 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 23 Feb 2021 12:32:55 +0530 Subject: [PATCH 09/17] Fix Spring deserialization routines failing at appLayout (#3155) --- .../src/main/java/com/appsmith/server/domains/Application.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java index 4ae1f24dee..9a5e4a2989 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/domains/Application.java @@ -74,6 +74,7 @@ public class Application extends BaseDomain { } @Data + @NoArgsConstructor @AllArgsConstructor public static class AppLayout implements Serializable { Type type; From faea2f36ffcd24c9f5a679bf0de357e50ca85234 Mon Sep 17 00:00:00 2001 From: Piyush Date: Tue, 23 Feb 2021 13:57:37 +0530 Subject: [PATCH 10/17] Add support for setting.json for pluginType DB (#3156) --- .../src/pages/Editor/QueryEditor/Form.tsx | 4 +-- .../src/pages/Editor/QueryEditor/index.tsx | 16 ++++++++- .../reducers/entityReducers/pluginsReducer.ts | 7 ++++ app/client/src/sagas/ActionSagas.ts | 22 +++++++++--- app/client/src/sagas/QueryPaneSagas.ts | 10 ++++++ app/client/src/selectors/entitiesSelector.ts | 4 +++ .../src/main/resources/editor.json | 9 +---- .../src/main/resources/setting.json | 36 +++++++++++++++++++ .../server/services/PluginServiceImpl.java | 11 +++++- .../server/services/PluginServiceTest.java | 11 ++++++ 10 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json diff --git a/app/client/src/pages/Editor/QueryEditor/Form.tsx b/app/client/src/pages/Editor/QueryEditor/Form.tsx index a07fa4128a..78965d52b2 100644 --- a/app/client/src/pages/Editor/QueryEditor/Form.tsx +++ b/app/client/src/pages/Editor/QueryEditor/Form.tsx @@ -35,7 +35,6 @@ import { import { ControlProps } from "components/formControls/BaseControl"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import ActionSettings from "pages/Editor/ActionSettings"; -import { queryActionSettingsConfig } from "mockResponses/ActionSettings"; import { addTableWidgetFromQuery } from "actions/widgetActions"; import { OnboardingStep } from "constants/OnboardingConstants"; import Boxed from "components/editorComponents/Onboarding/Boxed"; @@ -279,6 +278,7 @@ type QueryFormProps = { state: any; }; editorConfig?: any; + settingConfig: any; loadingFormConfigs: boolean; }; @@ -579,7 +579,7 @@ const QueryEditorForm: React.FC = (props: Props) => { panelComponent: ( diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx index 9d05287713..6baac137dc 100644 --- a/app/client/src/pages/Editor/QueryEditor/index.tsx +++ b/app/client/src/pages/Editor/QueryEditor/index.tsx @@ -29,6 +29,7 @@ import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; import AnalyticsUtil from "utils/AnalyticsUtil"; +import { queryActionSettingsConfig } from "mockResponses/ActionSettings"; const EmptyStateContainer = styled.div` display: flex; @@ -58,6 +59,7 @@ type ReduxStateProps = { isCreating: boolean; pluginImages: Record; editorConfig: any; + settingConfig: any; loadingFormConfigs: boolean; isEditorInitialized: boolean; }; @@ -118,6 +120,7 @@ class QueryEditor extends React.Component { runErrorMessage, loadingFormConfigs, editorConfig, + settingConfig, isEditorInitialized, } = this.props; const { applicationId, pageId } = this.props.match.params; @@ -154,6 +157,7 @@ class QueryEditor extends React.Component { onRunClick={this.handleRunClick} dataSources={dataSources} editorConfig={editorConfig} + settingConfig={settingConfig} loadingFormConfigs={loadingFormConfigs} DATASOURCES_OPTIONS={DATASOURCES_OPTIONS} executedQueryData={responses[queryId]} @@ -178,7 +182,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const { runErrorMessage } = state.ui.queryPane; const { plugins } = state.entities; - const { editorConfigs, loadingFormConfigs } = plugins; + const { editorConfigs, loadingFormConfigs, settingConfigs } = plugins; const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as QueryAction; const queryAction = getAction( state, @@ -194,6 +198,15 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { editorConfig = editorConfigs[pluginId]; } + let settingConfig: any; + + if (settingConfigs && pluginId) { + settingConfig = settingConfigs[pluginId]; + } + if (!settingConfig) { + settingConfig = queryActionSettingsConfig; + } + return { pluginImages: getPluginImages(state), plugins: getPlugins(state), @@ -205,6 +218,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { isDeleting: state.ui.queryPane.isDeleting[props.match.params.queryId], formData, editorConfig, + settingConfig, loadingFormConfigs, isCreating: state.ui.apiPane.isCreating, isEditorInitialized: getIsEditorInitialized(state), diff --git a/app/client/src/reducers/entityReducers/pluginsReducer.ts b/app/client/src/reducers/entityReducers/pluginsReducer.ts index 480a141ccf..f4dcb55f49 100644 --- a/app/client/src/reducers/entityReducers/pluginsReducer.ts +++ b/app/client/src/reducers/entityReducers/pluginsReducer.ts @@ -10,6 +10,7 @@ export interface PluginFormPayload { id: string; form: any[]; editor: any[]; + setting: any[]; } export interface PluginDataState { @@ -17,6 +18,7 @@ export interface PluginDataState { loading: boolean; formConfigs: Record; editorConfigs: Record; + settingConfigs: Record; loadingFormConfigs: boolean; loadingDBFormConfigs: boolean; } @@ -26,6 +28,7 @@ const initialState: PluginDataState = { loading: false, formConfigs: {}, editorConfigs: {}, + settingConfigs: {}, loadingFormConfigs: false, loadingDBFormConfigs: false, }; @@ -71,6 +74,10 @@ const pluginsReducer = createReducer(initialState, { ...state.editorConfigs, [action.payload.id]: action.payload.editor, }, + settingConfigs: { + ...state.settingConfigs, + [action.payload.id]: action.payload.setting, + }, }; }, [ReduxActionErrorTypes.FETCH_PLUGIN_FORM_ERROR]: (state: PluginDataState) => { diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index f6a192311c..9e338cd55c 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -49,6 +49,7 @@ import { getAction, getCurrentPageNameByActionId, getPageNameByPageId, + getSettingConfig, } from "selectors/entitiesSelector"; import { getDataSources } from "selectors/editorSelectors"; import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants"; @@ -77,13 +78,13 @@ export function* createActionSaga( try { let payload = actionPayload.payload; if (actionPayload.payload.pluginId) { - let formConfig; - formConfig = yield select( + let editorConfig; + editorConfig = yield select( getEditorConfig, actionPayload.payload.pluginId, ); - if (!formConfig) { + if (!editorConfig) { const formConfigResponse: GenericApiResponse = yield PluginsApi.fetchFormConfig( actionPayload.payload.pluginId, ); @@ -96,13 +97,24 @@ export function* createActionSaga( }, }); - formConfig = yield select( + editorConfig = yield select( getEditorConfig, actionPayload.payload.pluginId, ); } + const settingConfig = yield select( + getSettingConfig, + actionPayload.payload.pluginId, + ); - const initialValues = yield call(getConfigInitialValues, formConfig); + let initialValues = yield call(getConfigInitialValues, editorConfig); + if (settingConfig) { + const settingInitialValues = yield call( + getConfigInitialValues, + settingConfig, + ); + initialValues = merge(initialValues, settingInitialValues); + } payload = merge(initialValues, actionPayload.payload); } diff --git a/app/client/src/sagas/QueryPaneSagas.ts b/app/client/src/sagas/QueryPaneSagas.ts index c64bb74d9f..913914824d 100644 --- a/app/client/src/sagas/QueryPaneSagas.ts +++ b/app/client/src/sagas/QueryPaneSagas.ts @@ -48,6 +48,7 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { const { id } = actionPayload.payload; const state = yield select(); const editorConfigs = state.entities.plugins.editorConfigs; + const settingConfigs = state.entities.plugins.settingConfigs; let configInitialValues = {}; // // Typescript says Element does not have blur function but it does; // document.activeElement && @@ -82,6 +83,7 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { currentEditorConfig = success.payload.editor; } } + const currentSettingConfig = settingConfigs[action.datasource.pluginId]; // If config exists if (currentEditorConfig) { @@ -92,6 +94,14 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) { ); } + if (currentSettingConfig) { + const settingInitialValues = yield call( + getConfigInitialValues, + currentSettingConfig, + ); + configInitialValues = merge(configInitialValues, settingInitialValues); + } + // Merge the initial values and action. const formInitialValues = merge(configInitialValues, action); diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index c4e1534008..69178c3aff 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -101,6 +101,10 @@ export const getEditorConfig = (state: AppState, pluginId: string): any[] => { return state.entities.plugins.editorConfigs[pluginId]; }; +export const getSettingConfig = (state: AppState, pluginId: string): any[] => { + return state.entities.plugins.settingConfigs[pluginId]; +}; + export const getActions = (state: AppState): ActionDataState => state.entities.actions; diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json index e832453323..b3ba061b6e 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/editor.json @@ -4,13 +4,6 @@ "sectionName": "", "id": 1, "children": [ - { - "label": "Use Prepared Statement", - "configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value", - "controlType": "SWITCH", - "isRequired": true, - "initialValue": true - }, { "label": "", "configProperty": "actionConfiguration.body", @@ -19,4 +12,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json new file mode 100644 index 0000000000..c774b147d5 --- /dev/null +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json @@ -0,0 +1,36 @@ +{ + "setting": [ + { + "sectionName": "", + "id": 1, + "children": [ + { + "label": "Run query on page load", + "configProperty": "executeOnLoad", + "controlType": "SWITCH", + "info": "Will refresh data each time the page is loaded" + }, + { + "label": "Request confirmation before running query", + "configProperty": "confirmBeforeExecute", + "controlType": "SWITCH", + "info": "Ask confirmation from the user each time before refreshing data" + }, + { + "label": "Use Prepared Statement", + "configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value", + "controlType": "SWITCH", + "isRequired": true, + "initialValue": true + }, + { + "label": "Query timeout (in milliseconds)", + "info": "Maximum time after which the query will return", + "configProperty": "actionConfiguration.timeoutInMillisecond", + "controlType": "INPUT_TEXT", + "dataType": "NUMBER" + } + ] + } + ] +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PluginServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PluginServiceImpl.java index 4a3d685625..8454e7a48a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PluginServiceImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/PluginServiceImpl.java @@ -313,12 +313,21 @@ public class PluginServiceImpl extends BaseService settingMono = loadPluginResource(pluginId, "setting.json") + .doOnError(throwable -> + // Remove this pluginId from the cache so it is tried again next time. + formCache.remove(pluginId) + ) + .onErrorReturn(new HashMap()) + .cache(); - Mono resourceMono = Mono.zip(formMono, editorMono) + Mono resourceMono = Mono.zip(formMono, editorMono, settingMono) .map(tuple -> { Map formMap = tuple.getT1(); Map editorMap = tuple.getT2(); + Map settingMap = tuple.getT3(); formMap.putAll(editorMap); + formMap.putAll(settingMap); return formMap; }); diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java index 3f2d2918b4..ff5f835562 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PluginServiceTest.java @@ -68,6 +68,8 @@ public class PluginServiceTest { .thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL))); Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json"))) .thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL))); + Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json"))) + .thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL))); Mono formConfig = pluginService.getFormConfig("random-plugin-id"); @@ -86,6 +88,8 @@ public class PluginServiceTest { .thenReturn(Mono.just(formMap)); Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json"))) .thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL))); + Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json"))) + .thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL))); Mono formConfig = pluginService.getFormConfig("random-plugin-id"); StepVerifier.create(formConfig) @@ -93,6 +97,7 @@ public class PluginServiceTest { assertThat(form).isNotNull(); assertThat(form.get("form")).isNotNull(); assertThat(form.get("editor")).isNull(); + assertThat(form.get("setting")).isNull(); }) .verifyComplete(); } @@ -105,10 +110,15 @@ public class PluginServiceTest { Map editorMap = new HashMap(); editorMap.put("editor", new Object()); + Map settingMap = new HashMap(); + settingMap.put("setting", new Object()); + Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("form.json"))) .thenReturn(Mono.just(formMap)); Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json"))) .thenReturn(Mono.just(editorMap)); + Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json"))) + .thenReturn(Mono.just(settingMap)); Mono formConfig = pluginService.getFormConfig("random-plugin-id"); StepVerifier.create(formConfig) @@ -116,6 +126,7 @@ public class PluginServiceTest { assertThat(form).isNotNull(); assertThat(form.get("form")).isNotNull(); assertThat(form.get("editor")).isNotNull(); + assertThat(form.get("setting")).isNotNull(); }) .verifyComplete(); } From d20d013d69f794f7c1bdedbfd8f88460fd49b05f Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:08:31 +0530 Subject: [PATCH 11/17] Cypress test for Tab rename from entity explorer (#2780) * Bug automation * Test flow completed * Another bug automation * Updated tests * uncommented delete tab test * Uncommented a test * updated tests Co-authored-by: nandan.anantharamu --- .../cypress/fixtures/basicTabledsl.json | 42 +++++++++ app/client/cypress/fixtures/tabdsl.json | 91 +++++++++++++++++++ ...ty_Explorer_Entity_Naming_conflict_spec.js | 28 ++++++ .../Entity_Explorer_Tab_rename_Delete_spec.js | 70 ++++++++++++++ .../cypress/locators/apiWidgetslocator.json | 3 +- .../cypress/locators/explorerlocators.json | 3 +- app/client/cypress/support/commands.js | 34 +++++++ 7 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 app/client/cypress/fixtures/basicTabledsl.json create mode 100644 app/client/cypress/fixtures/tabdsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Entity_Naming_conflict_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js diff --git a/app/client/cypress/fixtures/basicTabledsl.json b/app/client/cypress/fixtures/basicTabledsl.json new file mode 100644 index 0000000000..7af5dc7a88 --- /dev/null +++ b/app/client/cypress/fixtures/basicTabledsl.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": "[\n {\n \"id\": 2381224,\n \"email\": \"michael.lawson@reqres.in\",\n \"userName\": \"Michael Lawson\",\n \"productName\": \"Chicken Sandwich\",\n \"orderAmount\": 4.99\n },\n {\n \"id\": 2736212,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"userName\": \"Lindsay Ferguson\",\n \"productName\": \"Tuna Salad\",\n \"orderAmount\": 9.99\n },\n {\n \"id\": 6788734,\n \"email\": \"tobias.funke@reqres.in\",\n \"userName\": \"Tobias Funke\",\n \"productName\": \"Beef steak\",\n \"orderAmount\": 19.99\n }\n]", + "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/fixtures/tabdsl.json b/app/client/cypress/fixtures/tabdsl.json new file mode 100644 index 0000000000..83795bf2fd --- /dev/null +++ b/app/client/cypress/fixtures/tabdsl.json @@ -0,0 +1,91 @@ +{ + "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": 8, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "isVisible": true, + "shouldScrollContents": false, + "widgetName": "Tabs1", + "tabs": [ + { + "label": "Tab 1", + "id": "tab1", + "widgetId": "uxle0mrg8t" + }, + { + "label": "Tab 2", + "id": "tab2", + "widgetId": "9hy1rmqb2f" + } + ], + "shouldShowTabs": true, + "defaultTab": "Tab 1", + "type": "TABS_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 2, + "rightColumn": 10, + "topRow": 8, + "bottomRow": 15, + "parentId": "0", + "widgetId": "y6rla3dsd6", + "children": [ + { + "type": "CANVAS_WIDGET", + "tabId": "tab1", + "tabName": "Tab 1", + "widgetId": "uxle0mrg8t", + "parentId": "y6rla3dsd6", + "detachFromLayout": true, + "children": [], + "parentRowSpace": 1, + "parentColumnSpace": 1, + "leftColumn": 0, + "rightColumn": 592, + "topRow": 0, + "bottomRow": 280, + "isLoading": false, + "widgetName": "Canvas1", + "renderMode": "CANVAS" + }, + { + "type": "CANVAS_WIDGET", + "tabId": "tab2", + "tabName": "Tab 2", + "widgetId": "9hy1rmqb2f", + "parentId": "y6rla3dsd6", + "detachFromLayout": true, + "children": [], + "parentRowSpace": 1, + "parentColumnSpace": 1, + "leftColumn": 0, + "rightColumn": 592, + "topRow": 0, + "bottomRow": 280, + "isLoading": false, + "widgetName": "Canvas2", + "renderMode": "CANVAS" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Entity_Naming_conflict_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Entity_Naming_conflict_spec.js new file mode 100644 index 0000000000..2bae6e5108 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Entity_Naming_conflict_spec.js @@ -0,0 +1,28 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const Layoutpage = require("../../../locators/Layout.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const dsl = require("../../../fixtures/basicTabledsl.json"); +const pages = require("../../../locators/Pages.json"); +const tabname = "UpdatedTab"; + +describe("Tab widget test", function() { + const apiName = "Table1"; + const tableName = "Table"; + before(() => { + cy.addDsl(dsl); + }); + + it("Rename API with table widget name validation test", function() { + cy.log("Login Successful"); + cy.NavigateToAPI_Panel(); + cy.log("Navigation to API Panel screen successful"); + cy.CreateApiAndValidateUniqueEntityName(apiName); + }); + + it("Rename Table widget with api name validation test", function() { + cy.GlobalSearchEntity("Table1"); + cy.RenameEntity(tableName); + cy.validateMessage(tableName); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js new file mode 100644 index 0000000000..1180651253 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ExplorerTests/Entity_Explorer_Tab_rename_Delete_spec.js @@ -0,0 +1,70 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const Layoutpage = require("../../../locators/Layout.json"); +const widgetsPage = require("../../../locators/Widgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const dsl = require("../../../fixtures/tabdsl.json"); +const pages = require("../../../locators/Pages.json"); +const tabname = "UpdatedTab"; + +describe("Tab widget test", function() { + const tabname = "UpdatedTab"; + before(() => { + cy.addDsl(dsl); + }); + + it("Tab Widget Functionality To rename Tabs from entity explorer", function() { + cy.GlobalSearchEntity("Tab 1"); + cy.RenameEntity(tabname); + }); + + it("Tab name validation in properties and widget ", function() { + cy.openPropertyPane("tabswidget"); + cy.closePropertyPane(); + cy.get(Layoutpage.tabWidget) + .contains(tabname) + .click({ force: true }) + .should("be.visible"); + }); + + it("Tab Widget Functionality To delete Tabs from entity explorer", function() { + cy.GlobalSearchEntity("Tab 2"); + cy.RenameEntity(tabname); + cy.validateMessage(tabname); + cy.deleteEntity(); + cy.get(commonlocators.entityExplorersearch).should("be.visible"); + cy.get(commonlocators.entityExplorersearch) + .clear() + .type("Tab 2"); + cy.get( + commonlocators.entitySearchResult.concat("Tab 2").concat("')"), + ).should("not.exist"); + }); + + /* To be enabled once the bug is fixed + it("Publish app and check for the widget name", function() { + cy.PublishtheApp(); + cy.get(publish.tabWidget) + .contains(tabname) + .click({ force: true }) + .should("be.selected"); + cy.get(publish.tabWidget) + .contains("Tab 2") + .click({ force: true }) + .should("be.selected"); + }); + + it("Tab Widget Functionality To Unchecked Visible Widget", function() { + cy.get(publish.backToEditor).click(); + cy.openPropertyPane("tabswidget"); + cy.closePropertyPane(); + cy.get(Layoutpage.tabWidget) + .contains("Tab 2") + .click({ force: true }) + .should("not.be.visible"); + }); + */ +}); + +afterEach(() => { + // put your clean up code if any +}); diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index f83ddfd316..2727f72efe 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -51,5 +51,6 @@ "propertyList": ".t--entity-property", "actionlist": ".action div div", "settings": "li:contains('Settings')", - "onPageLoad": "[data-cy=executeOnLoad]" + "onPageLoad": "[data-cy=executeOnLoad]", + "renameEntity": ".single-select >div:contains('Edit Name')" } diff --git a/app/client/cypress/locators/explorerlocators.json b/app/client/cypress/locators/explorerlocators.json index 23c42bc461..47b927f915 100644 --- a/app/client/cypress/locators/explorerlocators.json +++ b/app/client/cypress/locators/explorerlocators.json @@ -25,5 +25,6 @@ "addWidget":".widgets .t--entity-add-btn", "dropHere":".appsmith_widget_0", "closeWidgets":".t--close-widgets-sidebar", - "addDBQueryEntity": ".dbqueries .t--entity-add-btn" + "addDBQueryEntity": ".dbqueries .t--entity-add-btn", + "editEntity": ".t--entity-name input" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 4350a27197..3bc45ff9f0 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -825,6 +825,40 @@ Cypress.Commands.add("CopyAPIToHome", () => { ); }); +Cypress.Commands.add("RenameEntity", (value) => { + cy.xpath(apiwidget.popover) + .last() + .click({ force: true }); + cy.get(apiwidget.renameEntity).click({ force: true }); + cy.wait(2000); + cy.get(explorer.editEntity) + .last() + .type(value, { force: true }); + cy.wait(3000); +}); + +Cypress.Commands.add("CreateApiAndValidateUniqueEntityName", (apiname) => { + cy.get(apiwidget.createapi).click({ force: true }); + cy.wait("@createNewApi"); + cy.get(apiwidget.resourceUrl).should("be.visible"); + cy.get(apiwidget.ApiName).click({ force: true }); + cy.get(apiwidget.apiTxt) + .clear() + .type(apiname, { force: true }) + .should("have.value", apiname); + cy.get(".t--nameOfApi .error-message").should(($x) => { + console.log($x); + expect($x).contain(apiname.concat(" is already being used.")); + }); +}); + +Cypress.Commands.add("validateMessage", (value) => { + cy.get(".bp3-popover-content").should(($x) => { + console.log($x); + expect($x).contain(value.concat(" is already being used.")); + }); +}); + Cypress.Commands.add("DeleteAPIFromSideBar", () => { cy.deleteEntity(); cy.wait("@deleteAction").should( From 3a5e0b30bb8a5e550dc99e9ea2da8b7f15d4cb4a Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Tue, 23 Feb 2021 15:37:13 +0530 Subject: [PATCH 12/17] Force light theme on the whole editor except the header (#3143) --- .../src/pages/Editor/APIEditor/Form.tsx | 8 +- .../src/pages/Editor/APIEditor/index.tsx | 96 +++++++++---------- app/client/src/pages/Editor/index.tsx | 37 ++++--- 3 files changed, 72 insertions(+), 69 deletions(-) diff --git a/app/client/src/pages/Editor/APIEditor/Form.tsx b/app/client/src/pages/Editor/APIEditor/Form.tsx index a93cf35ce2..687dacb4c6 100644 --- a/app/client/src/pages/Editor/APIEditor/Form.tsx +++ b/app/client/src/pages/Editor/APIEditor/Form.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { connect, useSelector } from "react-redux"; -import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form"; +import { formValueSelector, InjectedFormProps, reduxForm } from "redux-form"; import { HTTP_METHOD_OPTIONS, HTTP_METHODS, @@ -11,8 +11,8 @@ import FormRow from "components/editorComponents/FormRow"; import { PaginationField } from "api/ActionAPI"; import { API_EDITOR_FORM_NAME } from "constants/forms"; import Pagination from "./Pagination"; -import { PaginationType, Action } from "entities/Action"; -import { HelpMap, HelpBaseURL } from "constants/HelpConstants"; +import { Action, PaginationType } from "entities/Action"; +import { HelpBaseURL, HelpMap } from "constants/HelpConstants"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import PostBodyData from "./PostBodyData"; import ApiResponseView from "components/editorComponents/ApiResponseView"; @@ -255,8 +255,6 @@ const ApiEditorForm: React.FC = (props: Props) => { e.stopPropagation(); history.replace(BUILDER_PAGE_URL(applicationId, pageId)); }; - - // Enforcing the light theme const theme = EditorTheme.LIGHT; return ( diff --git a/app/client/src/pages/Editor/APIEditor/index.tsx b/app/client/src/pages/Editor/APIEditor/index.tsx index a69225578e..d3cf8c57ed 100644 --- a/app/client/src/pages/Editor/APIEditor/index.tsx +++ b/app/client/src/pages/Editor/APIEditor/index.tsx @@ -25,7 +25,7 @@ import { Plugin } from "api/PluginApi"; import { Action, PaginationType, RapidApiAction } from "entities/Action"; import { getApiName } from "selectors/formSelectors"; import Spinner from "components/editorComponents/Spinner"; -import styled, { ThemeProvider } from "styled-components"; +import styled from "styled-components"; import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper"; import { changeApi } from "actions/apiPaneActions"; import PerformanceTracker, { @@ -178,55 +178,53 @@ class ApiEditor extends React.Component { /> ); return ( - -
- {apiId ? ( - <> - {formUiComponent === "ApiEditorForm" && ( - - )} +
+ {apiId ? ( + <> + {formUiComponent === "ApiEditorForm" && ( + + )} - {formUiComponent === "RapidApiEditorForm" && ( - - )} - - ) : ( - apiHomeScreen - )} -
- + {formUiComponent === "RapidApiEditorForm" && ( + + )} + + ) : ( + apiHomeScreen + )} +
); } } diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index b7eef9f369..f120c3a49f 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -38,6 +38,9 @@ import { isMac } from "utils/helpers"; import { getSelectedWidget } from "selectors/ui"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import Welcome from "./Welcome"; +import { getThemeDetails, ThemeMode } from "selectors/themeSelectors"; +import { ThemeProvider } from "styled-components"; +import { Theme } from "constants/DefaultTheme"; type EditorProps = { currentApplicationId?: string; @@ -55,6 +58,7 @@ type EditorProps = { cutSelectedWidget: () => void; user?: User; selectedWidget?: string; + lightTheme: Theme; }; type Props = EditorProps & RouteComponentProps; @@ -200,21 +204,23 @@ class Editor extends Component { ); } return ( - -
- - - Editor | Appsmith - - -
- -
+ + +
+ + + Editor | Appsmith + + +
+ +
+
); } } @@ -229,6 +235,7 @@ const mapStateToProps = (state: AppState) => ({ user: getCurrentUser(state), selectedWidget: getSelectedWidget(state), creatingOnboardingDatabase: state.ui.onBoarding.showOnboardingLoader, + lightTheme: getThemeDetails(state, ThemeMode.LIGHT), }); const mapDispatchToProps = (dispatch: any) => { From 139d870c17b235c755b13efc0a08c1c6a42ce202 Mon Sep 17 00:00:00 2001 From: Trisha Anand Date: Tue, 23 Feb 2021 15:39:53 +0530 Subject: [PATCH 13/17] Marking Prepared Statement in Postgres plugin as a beta feature (#3161) --- .../java/com/external/plugins/PostgresPlugin.java | 11 +++++++---- .../postgresPlugin/src/main/resources/setting.json | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) 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 5a8dfda659..079f82ef71 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 @@ -150,10 +150,13 @@ public class PostgresPlugin extends BasePlugin { Boolean isPreparedStatement; final List properties = actionConfiguration.getPluginSpecifiedTemplates(); - if (properties.get(PREPARED_STATEMENT_INDEX) == null) { - // If the configuration does not exist, default to true - // Note this is not possible today since the query editor sets a default value for this field. - isPreparedStatement = true; + if (properties == null || properties.get(PREPARED_STATEMENT_INDEX) == null) { + /** + * TODO : + * In case the prepared statement configuration is missing, default to true once PreparedStatement + * is no longer in beta. + */ + isPreparedStatement = false; } else { isPreparedStatement = Boolean.parseBoolean(properties.get(PREPARED_STATEMENT_INDEX).getValue()); } diff --git a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json index c774b147d5..91d7d6e00a 100644 --- a/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json +++ b/app/server/appsmith-plugins/postgresPlugin/src/main/resources/setting.json @@ -17,11 +17,11 @@ "info": "Ask confirmation from the user each time before refreshing data" }, { - "label": "Use Prepared Statement", + "label": "[Beta]Use Prepared Statement", + "info": "Turning on Prepared Statement makes the query parametrized. This in turn makes it resilient against SQL injections", "configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value", "controlType": "SWITCH", - "isRequired": true, - "initialValue": true + "initialValue": false }, { "label": "Query timeout (in milliseconds)", From 4b09b2c3795ad15aff51e4dfa72162dcf268e32b Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Tue, 23 Feb 2021 16:24:06 +0530 Subject: [PATCH 14/17] Transform Dynamodb response for easy consumption (#3050) - add support to handle dnyamoDb Binary type - transform raw response to show extracted response and raw response for: GetItem BatchGetItem DeleteItem PutItem Scan TransactGetItems UpdateItem - modify TC. --- .../com/external/plugins/DynamoPlugin.java | 216 +++++++++++++++++- .../external/plugins/DynamoPluginTest.java | 203 ++++++++++++++-- 2 files changed, 396 insertions(+), 23 deletions(-) diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java index 2c3b53d669..b1aed8864c 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/main/java/com/external/plugins/DynamoPlugin.java @@ -49,9 +49,33 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; public class DynamoPlugin extends BasePlugin { + private static final String SCAN_ACTION_VALUE = "Scan"; + private static final String GET_ITEM_ACTION_VALUE = "GetItem"; + private static final String BATCH_GET_ITEM_ACTION_VALUE = "BatchGetItem"; + private static final String TRANSACT_GET_ITEMS_ACTION_VALUE = "TransactGetItems"; + private static final String PUT_ITEM_ACTION_VALUE = "PutItem"; + private static final String UPDATE_ITEM_ACTION_VALUE = "UpdateItem"; + private static final String DELETE_ITEM_ACTION_VALUE = "DeleteItem"; + private static final String ITEMS_KEY = "Items"; + private static final String ITEM_KEY = "Item"; + private static final String ATTRIBUTES_KEY = "Attributes"; + private static final String RESPONSES_KEY = "Responses"; + private static final String DYNAMO_TYPE_STRING_LABEL = "S"; + private static final String DYNAMO_TYPE_NUMBER_LABEL = "N"; + private static final String DYNAMO_TYPE_BINARY_LABEL = "B"; + private static final String DYNAMO_TYPE_BOOLEAN_LABEL = "BOOL"; + private static final String DYNAMO_TYPE_NULL_LABEL = "NUL"; + private static final String DYNAMO_TYPE_STRING_SET_LABEL = "SS"; + private static final String DYNAMO_TYPE_NUMBER_SET_LABEL = "NS"; + private static final String DYNAMO_TYPE_BINARY_SET_LABEL = "BS"; + private static final String DYNAMO_TYPE_MAP_LABEL = "M"; + private static final String DYNAMO_TYPE_LIST_LABEL = "L"; + private static final String RAW_RESPONSE_LABEL = "raw"; + public DynamoPlugin(PluginWrapper wrapper) { super(wrapper); } @@ -73,6 +97,187 @@ public class DynamoPlugin extends BasePlugin { private final Scheduler scheduler = Schedulers.elastic(); + public Object extractValue(Object rawItem) { + + if(!(rawItem instanceof List) + && !(rawItem instanceof Map)) { + return rawItem; + } + + if (rawItem instanceof List) { + return ((List) rawItem) + .stream() + .map(item -> extractValue(item)) + .collect(Collectors.toList()); + } + else { /* map type */ + Map extractedValueMap = new HashMap<>(); + Map rawItemAsMap = (Map) rawItem; + for (Map.Entry entry: rawItemAsMap.entrySet()) { + switch (entry.getKey()) { + case DYNAMO_TYPE_NUMBER_LABEL: + case DYNAMO_TYPE_STRING_LABEL: + case DYNAMO_TYPE_BINARY_LABEL: + case DYNAMO_TYPE_BOOLEAN_LABEL: + case DYNAMO_TYPE_NULL_LABEL: + if (entry.getValue() != null) { + return entry.getValue(); + } + + break; + case DYNAMO_TYPE_STRING_SET_LABEL: + case DYNAMO_TYPE_NUMBER_SET_LABEL: + case DYNAMO_TYPE_BINARY_SET_LABEL: + if (entry.getValue() != null && ((List)entry.getValue()).size() > 0) { + return entry.getValue(); + } + + break; + case DYNAMO_TYPE_LIST_LABEL: + /* + * - If size of rawValueAsList is 0, then we don't want to return. + */ + List rawValueAsList = (List) entry.getValue(); + if (rawValueAsList.size() > 0) { + return rawValueAsList + .stream() + .map(listItem -> extractValue(listItem)) + .collect(Collectors.toList()); + } + + break; + case DYNAMO_TYPE_MAP_LABEL: + /* + * - If size of rawValueAsMap is 0, then we don't want to return. + */ + Map rawValueAsMap = (Map) entry.getValue(); + if (rawValueAsMap.size() > 0) { + return rawValueAsMap + .entrySet() + .stream() + .map(item -> Map.entry(item.getKey(), extractValue(item.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + break; + default: + extractedValueMap.put(entry.getKey(), extractValue(entry.getValue())); + } + } + + return extractedValueMap; + } + } + /* + * - Transform response for easy consumption. For details please visit + * https://github.com/appsmithorg/appsmith/issues/3010 + */ + public Object getTransformedResponse(Map rawResponse, + String action) throws AppsmithPluginException { + + Map transformedResponse = new HashMap<>(); + + /* + * - Any action other than the following do not return transformed response: + * - Scan + * - GetItem + * - PutItem + * - UpdateItem + * - DeleteItem + * - BatchGetItem + * - TransactGetItem + */ + if (!SCAN_ACTION_VALUE.equals(action) + && !GET_ITEM_ACTION_VALUE.equals(action) + && !PUT_ITEM_ACTION_VALUE.equals(action) + && !UPDATE_ITEM_ACTION_VALUE.equals(action) + && !DELETE_ITEM_ACTION_VALUE.equals(action) + && !BATCH_GET_ITEM_ACTION_VALUE.equals(action) + && !TRANSACT_GET_ITEMS_ACTION_VALUE.equals(action)) { + return rawResponse; + } + + /* + * - Transformed response has section "raw", under which raw response appears. + */ + transformedResponse.put(RAW_RESPONSE_LABEL, rawResponse); + + /* + * - Transform response based on action. + */ + String topLevelKey; + switch (action) { + case SCAN_ACTION_VALUE: + topLevelKey = ITEMS_KEY; + break; + case GET_ITEM_ACTION_VALUE: + topLevelKey = ITEM_KEY; + break; + case PUT_ITEM_ACTION_VALUE: + case UPDATE_ITEM_ACTION_VALUE: + case DELETE_ITEM_ACTION_VALUE: + topLevelKey = ATTRIBUTES_KEY; + break; + case BATCH_GET_ITEM_ACTION_VALUE: + case TRANSACT_GET_ITEMS_ACTION_VALUE: + topLevelKey = RESPONSES_KEY; + break; + default: + throw new AppsmithPluginException( + AppsmithPluginError.PLUGIN_ERROR, + "Appsmith has encountered an unexpected error when transforming raw DynamoDb response" + + ". Please reach out to Appsmith customer support to resolve this." + ); + } + + for (Map.Entry responseEntry: rawResponse.entrySet()) { + if (!responseEntry.getKey().equals(topLevelKey)) { + transformedResponse.put(responseEntry.getKey(), responseEntry.getValue()); + } + else { + if (rawResponse.get(topLevelKey) instanceof Collection) { + /* + * - Need to have an empty list if rawItems is null. + */ + List extractedResponse = new ArrayList<>(); + Collection rawItems = (Collection) (rawResponse.get(topLevelKey)); + if (rawItems != null) { + /* + * - Insert transformed values into extractedResponse list. + */ + extractedResponse = rawItems + .stream() + .map(item -> extractValue(item)) + .collect(Collectors.toList()); + } + + transformedResponse.put(topLevelKey, extractedResponse); + } + else if (rawResponse.get(topLevelKey) instanceof Map) { + /* + * - Need to have an empty map if rawItems is null. + */ + Map extractedResponse = new HashMap<>(); + HashMap rawItems = (HashMap) rawResponse.get(topLevelKey); + if (rawItems != null) { + /* + * - Insert transformed values into extractedResponse map. + */ + extractedResponse = rawItems + .entrySet() + .stream() + .map(item -> Map.entry(item.getKey(), extractValue(item.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + transformedResponse.put(topLevelKey, extractedResponse); + } + } + } + + return transformedResponse; + } + @Override public Mono execute(DynamoDbClient ddb, DatasourceConfiguration datasourceConfiguration, @@ -119,7 +324,9 @@ public class DynamoPlugin extends BasePlugin { ); final Object sdkValue = plainToSdk(parameters, requestClass); final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, sdkValue); - result.setBody(sdkToPlain(response)); + Object rawResponse = sdkToPlain(response); + Object transformedResponse = getTransformedResponse((Map)rawResponse, action); + result.setBody(transformedResponse); } catch (AppsmithPluginException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) { final String message = "Error executing the DynamoDB Action: " + (e.getCause() == null ? e : e.getCause()).getMessage(); log.warn(message, e); @@ -403,7 +610,7 @@ public class DynamoPlugin extends BasePlugin { private static Object sdkToPlain(Object valueObj) { if (valueObj instanceof SdkPojo) { - final SdkPojo response = (SdkPojo) valueObj; + SdkPojo response = (SdkPojo) valueObj; final Map plain = new HashMap<>(); for (final SdkField field : response.sdkFields()) { @@ -413,6 +620,10 @@ public class DynamoPlugin extends BasePlugin { return plain; + } else if (valueObj instanceof SdkBytes) { + SdkBytes response = (SdkBytes) valueObj; + + return new String(response.asByteArray()); } else if (valueObj instanceof Map) { final Map valueAsMap = (Map) valueObj; final Map plainMap = new HashMap<>(); @@ -432,7 +643,6 @@ public class DynamoPlugin extends BasePlugin { } return plainList; - } return valueObj; diff --git a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java index 375c39af43..131ae1e8d6 100644 --- a/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java +++ b/app/server/appsmith-plugins/dynamoPlugin/src/test/java/com/external/plugins/DynamoPluginTest.java @@ -15,6 +15,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition; @@ -27,11 +28,13 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -94,6 +97,42 @@ public class DynamoPluginTest { )) .build()); + ddb.createTable(CreateTableRequest.builder() + .tableName("allTypes") + .attributeDefinitions( + AttributeDefinition.builder().attributeName("Id").attributeType(ScalarAttributeType.N).build() + ) + .keySchema( + KeySchemaElement.builder().attributeName("Id").keyType(KeyType.HASH).build() + ) + .provisionedThroughput( + ProvisionedThroughput.builder().readCapacityUnits(5L).writeCapacityUnits(5L).build() + ) + .build()); + + String testPayload1 = "payload1"; + SdkBytes bytesValue1 = SdkBytes.fromByteArray(testPayload1.getBytes()); + String testPayload2 = "payload2"; + SdkBytes bytesValue2 = SdkBytes.fromByteArray(testPayload2.getBytes()); + AttributeValue mapValue = AttributeValue.builder().s("mapValue").build(); + AttributeValue listValue1 = AttributeValue.builder().s("listValue1").build(); + AttributeValue listValue2 = AttributeValue.builder().s("listValue2").build(); + ddb.putItem(PutItemRequest.builder() + .tableName("allTypes") + .item(Map.of( + "Id", AttributeValue.builder().n("1").build(), + "StringType", AttributeValue.builder().s("str").build(), + "BooleanType", AttributeValue.builder().bool(true).build(), + "BinaryType", AttributeValue.builder().b(bytesValue1).build(), + "NullType", AttributeValue.builder().nul(true).build(), + "StringSetType", AttributeValue.builder().ss("str1", "str2").build(), + "NumberSetType", AttributeValue.builder().ns("1", "2").build(), + "BinarySetType", AttributeValue.builder().bs(bytesValue1, bytesValue2).build(), + "MapType", AttributeValue.builder().m(Map.of("mapKey", mapValue)).build(), + "ListType", AttributeValue.builder().l(listValue1, listValue2).build() + )) + .build()); + Endpoint endpoint = new Endpoint(); endpoint.setHost(host); endpoint.setPort(port.longValue()); @@ -122,10 +161,17 @@ public class DynamoPluginTest { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); - assertArrayEquals( - new String[]{"cities"}, - ((Map>) result.getBody()).get("TableNames").toArray() - ); + + HashSet expectedTables = new HashSet<>(); + expectedTables.add("cities"); + expectedTables.add("allTypes"); + + HashSet actualTables = new HashSet<>(); + actualTables.add(((Map>) result.getBody()).get("TableNames").get(0)); + actualTables.add(((Map>) result.getBody()).get("TableNames").get(1)); + + assertTrue(expectedTables.equals(actualTables)); + }) .verifyComplete(); } @@ -163,8 +209,13 @@ public class DynamoPluginTest { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); - final Map> item = ((Map>>) result.getBody()).get("Item"); - assertEquals("New Delhi", item.get("City").get("S")); + Map resultBody = (Map) result.getBody(); + Map rawResponse = (Map) resultBody.get("raw"); + Map> rawItem = (Map>) rawResponse.get("Item"); + assertEquals("New Delhi", rawItem.get("City").get("S")); + + Map transformedItem = (Map) resultBody.get("Item"); + assertEquals("New Delhi", transformedItem.get("City")); }) .verifyComplete(); } @@ -216,8 +267,14 @@ public class DynamoPluginTest { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); - final Map> attributes = ((Map>>) result.getBody()).get("Attributes"); - assertEquals("Bengaluru", attributes.get("City").get("S")); + Map resultBody = (Map) result.getBody(); + Map rawResponse = (Map) resultBody.get("raw"); + Map> rawItem = (Map>) rawResponse.get( + "Attributes"); + assertEquals("Bengaluru", rawItem.get("City").get("S")); + + Map transformedItem = (Map) resultBody.get("Attributes"); + assertEquals("Bengaluru", transformedItem.get("City")); }) .verifyComplete(); } @@ -233,8 +290,21 @@ public class DynamoPluginTest { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); assertNotNull(result.getBody()); - final List items = (List) ((Map) result.getBody()).get("Items"); + + List> items = (List>) + ((Map)((Map) result.getBody()).get("raw")).get("Items"); assertEquals(2, items.size()); + + for (int i=0; i item = items.get(i); + for (Map.Entry entry: item.entrySet()) { + Object rawValue = ((Map) entry.getValue()).get("S"); + Object transformedValue = + ((List>)((Map) result.getBody()).get("Items")) + .get(i).get(entry.getKey()); + assertTrue(rawValue.equals(transformedValue)); + } + } }) .verifyComplete(); } @@ -271,9 +341,18 @@ public class DynamoPluginTest { Collections.emptyMap(), response.remove("UnprocessedKeys") ); - final List>> items = (List>>) ((Map) response.get("Responses")).get("cities"); - assertEquals("New Delhi", items.get(0).get("City").get("S")); - assertEquals("Bengaluru", items.get(1).get("City").get("S")); + + // Test raw response + Map rawResponse = + (Map) ((Map) response.get("raw")).get( + "Responses"); + ArrayList> rawCitiesList = (ArrayList>) rawResponse.get("cities"); + assertEquals("New Delhi", ((Map)(rawCitiesList.get(0)).get("City")).get("S")); + + // Test transformed response + Map transformedResponse = (Map) response.get("Responses"); + ArrayList> transformedCitiesList = (ArrayList>) transformedResponse.get("cities"); + assertEquals("New Delhi", transformedCitiesList.get(0).get("City")); }) .verifyComplete(); } @@ -301,8 +380,21 @@ public class DynamoPluginTest { .assertNext(result -> { assertNotNull(result); assertTrue(result.getIsExecutionSuccess()); + final Map response = (Map) result.getBody(); - assertEquals("New Delhi", ((List>>>) response.get("Responses")).get(0).get("Item").get("City").get("S")); + + // Test raw response + ArrayList> rawResponse = + (ArrayList>) ((Map) response.get("raw")).get( + "Responses"); + assertEquals("New Delhi", + ((Map>)rawResponse.get(0).get("Item")).get("City").get("S")); + + // Test transformed response + ArrayList> transformedResponse = (ArrayList>) response.get("Responses"); + assertEquals("New Delhi", + ((Map)transformedResponse.get(0).get("Item")).get("City")); + }) .verifyComplete(); } @@ -317,14 +409,85 @@ public class DynamoPluginTest { .assertNext(structure -> { assertNotNull(structure); assertNotNull(structure.getTables()); - assertEquals( - List.of("cities"), - structure.getTables().stream() - .map(DatasourceStructure.Table::getName) - .collect(Collectors.toList()) - ); + + HashSet expectedTables = new HashSet<>(); + expectedTables.add("cities"); + expectedTables.add("allTypes"); + + HashSet actualTables = new HashSet<>(); + actualTables.add(structure.getTables().get(0).getName()); + actualTables.add(structure.getTables().get(1).getName()); + + assertTrue(expectedTables.equals(actualTables)); }) .verifyComplete(); } + /* + * - "allTypes" table contains data of all type supported by DynamoDb. + * - This test aims to test the data type handling capability of the plugin. + */ + @Test + public void testParsingCapabilityForAllTypes() { + final String body = "{\n" + + " \"TableName\": \"allTypes\"\n" + + "}\n"; + + StepVerifier.create(execute("Scan", body)) + .assertNext(result -> { + assertNotNull(result); + assertTrue(result.getIsExecutionSuccess()); + assertNotNull(result.getBody()); + + Map resultBody = (Map) result.getBody(); + Map rawResponse = (Map) resultBody.get("raw"); + + /* + * - Check if data under the "raw" section i.e. non-transformed data is correct. + */ + ArrayList> rawItems = (ArrayList>) rawResponse.get( + "Items"); + Map rawItemMap = rawItems.get(0); + assertEquals("1", ((Map)rawItemMap.get("Id")).get("N")); + assertEquals("str", ((Map)rawItemMap.get("StringType")).get("S")); + assertEquals("true", ((Map)rawItemMap.get("BooleanType")).get("BOOL").toString()); + assertEquals("payload1", ((Map)rawItemMap.get("BinaryType")).get("B")); + assertEquals("true", ((Map)rawItemMap.get("NullType")).get("NUL").toString()); + assertArrayEquals(new String[]{"str1", "str2"}, + ((ArrayList)((Map)rawItemMap.get("StringSetType")).get("SS")).toArray()); + assertArrayEquals(new String[]{"payload1", "payload2"}, + ((ArrayList)((Map)rawItemMap.get("BinarySetType")).get("BS")).toArray()); + assertArrayEquals(new String[]{"1", "2"}, + ((ArrayList)((Map)rawItemMap.get("NumberSetType")).get("NS")).toArray()); + assertEquals("mapValue", + ((Map>>)rawItemMap.get("MapType")).get("M") + .get("mapKey").get("S").toString()); + assertEquals("listValue1", + ((HashMap>>)rawItemMap.get("ListType")).get("L").get(0).get("S")); + assertEquals("listValue2", + ((HashMap>>)rawItemMap.get("ListType")).get("L").get(1).get("S")); + + /* + * - Check if the transformed data is correct. + */ + ArrayList> transformedItems = (ArrayList>) resultBody.get("Items"); + Map transformedItemMap = transformedItems.get(0); + assertEquals("1", transformedItemMap.get("Id")); + assertEquals("str", transformedItemMap.get("StringType")); + assertEquals("true", transformedItemMap.get("BooleanType").toString()); + assertEquals("payload1", transformedItemMap.get("BinaryType")); + assertEquals("true", transformedItemMap.get("NullType").toString()); + assertArrayEquals(new String[]{"str1", "str2"}, + ((ArrayList)transformedItemMap.get("StringSetType")).toArray()); + assertArrayEquals(new String[]{"payload1", "payload2"}, + ((ArrayList)transformedItemMap.get("BinarySetType")).toArray()); + assertArrayEquals(new String[]{"1", "2"}, + ((ArrayList)transformedItemMap.get("NumberSetType")).toArray()); + assertEquals("mapValue", + ((Map)transformedItemMap.get("MapType")).get("mapKey").toString()); + assertEquals("listValue1", ((ArrayList)transformedItemMap.get("ListType")).get(0)); + assertEquals("listValue2", ((ArrayList)transformedItemMap.get("ListType")).get(1)); + }) + .verifyComplete(); + } } From 17dd5345a83c3b0ff1954244d43c673ed689232f Mon Sep 17 00:00:00 2001 From: Hetu Nandu Date: Tue, 23 Feb 2021 16:25:22 +0530 Subject: [PATCH 15/17] Fix code editor theme colours on the property pane (#3168) --- .../CodeEditor/styledComponents.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index dce5b987ae..c239aa3cca 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -28,7 +28,7 @@ export const HintStyles = createGlobalStyle<{ max-height: 20em; overflow-y: auto; background: ${(props) => - props.theme.colors.codeMirror.background.defaultState}; + props.editorTheme === EditorTheme.LIGHT ? "#FAFAFA" : "#262626"}; box-shadow: 0px 12px 28px -6px rgba(0, 0, 0, 0.32); border-radius: 0px; ${(props) => @@ -37,7 +37,8 @@ export const HintStyles = createGlobalStyle<{ .CodeMirror-hint { height: 24px; - color: ${(props) => props.theme.colors.codeMirror.text}; + color: ${(props) => + props.editorTheme === EditorTheme.LIGHT ? "#090707" : "#FFFFFF"}; cursor: pointer; display: flex; width: 220px; @@ -47,7 +48,7 @@ export const HintStyles = createGlobalStyle<{ letter-spacing: -0.24px; &:hover { background: ${(props) => - props.theme.colors.codeMirror.background.hoverState}; + props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"}; border-radius: 0px; color: #fff; &:after { @@ -69,7 +70,7 @@ export const HintStyles = createGlobalStyle<{ padding-left: ${(props) => props.theme.spaces[11]}px !important; &:hover{ background: ${(props) => - props.theme.colors.codeMirror.background.hoverState}; + props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"}; } } .CodeMirror-Tern-completion:before { @@ -281,9 +282,7 @@ export const EditorWrapper = styled.div<{ ? `border-bottom: 1px solid ${Colors.MERCURY}` : `border: 1px solid ${Colors.MERCURY}`}; background: ${(props) => - props.isFocused || props.fill - ? Colors.MERCURY - : props.theme.colors.codeMirror.background.defaultState}; + props.isFocused || props.fill ? Colors.MERCURY : "#FAFAFA"}; color: ${Colors.CHARCOAL}; & { span.cm-operator { @@ -303,9 +302,7 @@ export const EditorWrapper = styled.div<{ ? `border-bottom: 1px solid ${Colors.NERO}` : `border: 1px solid ${Colors.NERO}`}; background: ${(props) => - props.isFocused || props.fill - ? Colors.NERO - : props.theme.colors.codeMirror.background.defaultState}; + props.isFocused || props.fill ? Colors.NERO : "#262626"}; color: ${Colors.LIGHT_GREY}; } .cm-s-duotone-light .CodeMirror-linenumber, From 72df6fb299066d91bf10eda1fb1ba1f9c832f0b6 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Kandula Date: Tue, 23 Feb 2021 16:54:27 +0530 Subject: [PATCH 16/17] Allow MongoDB datasources without username/password (#3149) --- .../com/external/plugins/MongoPlugin.java | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) 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 73d0f7e756..d395fbde94 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 @@ -242,11 +242,17 @@ public class MongoPlugin extends BasePlugin { DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); if (authentication != null) { - builder - .append(urlEncode(authentication.getUsername())) - .append(':') - .append(urlEncode(authentication.getPassword())) - .append('@'); + final boolean hasUsername = StringUtils.hasText(authentication.getUsername()); + final boolean hasPassword = StringUtils.hasText(authentication.getPassword()); + if (hasUsername) { + builder.append(urlEncode(authentication.getUsername())); + } + if (hasPassword) { + builder.append(':').append(urlEncode(authentication.getPassword())); + } + if (hasUsername || hasPassword) { + builder.append('@'); + } } for (Endpoint endpoint : endpoints) { @@ -313,25 +319,11 @@ public class MongoPlugin extends BasePlugin { } DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication(); - if (authentication == null) { - invalids.add("Missing authentication details."); - - } else { + if (authentication != null) { DBAuth.Type authType = authentication.getAuthType(); - if (authType != null && VALID_AUTH_TYPES.contains(authType)) { - - if (StringUtils.isEmpty(authentication.getUsername())) { - invalids.add("Missing username for authentication. Needed because authType is " + authType + "."); - } - - if (StringUtils.isEmpty(authentication.getPassword())) { - invalids.add("Missing password for authentication. Needed because authType is " + authType + "."); - } - - } else { + if (authType == null || !VALID_AUTH_TYPES.contains(authType)) { invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR); - } if (StringUtils.isEmpty(authentication.getDatabaseName())) { From 6c80f23201db50ddefc62b98593d07e3ba2cb783 Mon Sep 17 00:00:00 2001 From: NandanAnantharamu <67676905+NandanAnantharamu@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:05:09 +0530 Subject: [PATCH 17/17] Feature: DatePicker V2 (#2889) Co-authored-by: vicky-primathon.in Co-authored-by: nandan.anantharamu Co-authored-by: Abhinav Jha --- .../cypress/fixtures/datePicker2dsl.json | 80 ++++++ .../Applications/DuplicateApplication_spec.js | 1 + .../Applications/UpdateApplication_spec.js | 5 + .../Binding/Bind_TableTextPagination_spec.js | 1 + ...uttonWidgets_NavigateTo_validation_spec.js | 2 + .../ClientSideTests/Binding/JS_Toggle_spec.js | 2 +- ...TableWidgets_NavigateTo_Validation_spec.js | 1 + ..._Explorer_Widgets_Copy_Delete_Undo_spec.js | 3 + ...y_Paste_Delete_Undo_Keyboard_Event_spec.js | 1 + .../FormWidgets/DatePicker_spec.js | 66 ++--- .../FormWidgets/FormReset_spec.js | 2 +- .../ClientSideTests/LayoutWidgets/Tab_spec.js | 1 + .../FormWidgets/DatePicker_2_Default_spec.js | 39 +++ .../FormWidgets/DatePicker_2_spec.js | 79 ++++++ .../FormWidgets/DatePicker_Toggle_js_spec.js | 47 ++++ .../OrganisationTests/UpdateOrgTests_spec.js | 1 + .../QueryPane/AddWidgetTableAndBind_spec.js | 1 + .../QueryPane/AddWidget_spec.js | 1 + .../UnitTest/LoginFromUIApp_spec.js | 2 + app/client/cypress/locators/FormWidgets.json | 71 +++--- app/client/cypress/support/commands.js | 31 +++ .../appsmith/TableComponent/CascadeFields.tsx | 2 +- .../blueprint/DatePickerComponent.tsx | 95 +++++-- .../blueprint/DatePickerComponent2.tsx | 236 ++++++++++++++++++ .../propertyControls/DatePickerControl.tsx | 116 ++------- .../src/constants/FieldExpectedValue.ts | 7 + app/client/src/constants/HelpConstants.ts | 4 + app/client/src/constants/WidgetConstants.tsx | 1 + app/client/src/constants/WidgetValidation.ts | 2 +- app/client/src/constants/messages.ts | 2 + app/client/src/entities/Widget/utils.test.ts | 1 + app/client/src/icons/WidgetIcons.tsx | 2 +- .../mockResponses/WidgetConfigResponse.tsx | 44 +++- .../mockResponses/WidgetSidebarResponse.tsx | 2 +- .../entityReducers/widgetConfigReducer.tsx | 2 + app/client/src/sagas/WidgetOperationSagas.tsx | 2 + app/client/src/utils/WidgetPropsUtils.tsx | 19 ++ app/client/src/utils/WidgetRegistry.tsx | 18 ++ .../utils/autocomplete/EntityDefinitions.ts | 9 + .../dataTreeTypeDefCreator.test.ts | 1 + .../src/utils/migrations/TableWidget.test.ts | 6 + app/client/src/widgets/BaseWidget.tsx | 1 + app/client/src/widgets/DatePickerWidget2.tsx | 223 +++++++++++++++++ app/client/src/workers/evaluation.test.ts | 28 +++ app/client/src/workers/validations.ts | 54 ++-- 45 files changed, 1091 insertions(+), 223 deletions(-) create mode 100644 app/client/cypress/fixtures/datePicker2dsl.json create mode 100644 app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_Default_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_Toggle_js_spec.js create mode 100644 app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx create mode 100644 app/client/src/widgets/DatePickerWidget2.tsx diff --git a/app/client/cypress/fixtures/datePicker2dsl.json b/app/client/cypress/fixtures/datePicker2dsl.json new file mode 100644 index 0000000000..ceb13dfb82 --- /dev/null +++ b/app/client/cypress/fixtures/datePicker2dsl.json @@ -0,0 +1,80 @@ +{ + "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, + "isDisabled": false, + "datePickerType": "DATE_PICKER", + "label": "", + "dateFormat": "DD/MM/YYYY HH:mm", + "widgetName": "DatePicker1", + "defaultDate": "2021-02-05T10:53:12.791Z", + "version": 2, + "type": "DATE_PICKER_WIDGET2", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 5, + "rightColumn": 10, + "topRow": 0, + "bottomRow": 1, + "parentId": "0", + "widgetId": "w4htilgv5t" + }, + { + "isVisible": true, + "text": "Label", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text1", + "version": 1, + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 1, + "rightColumn": 5, + "topRow": 3, + "bottomRow": 4, + "parentId": "0", + "widgetId": "voohxsv4t2" + }, + { + "isVisible": true, + "text": "Label", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text2", + "version": 1, + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 74, + "parentRowSpace": 40, + "leftColumn": 8, + "rightColumn": 12, + "topRow": 3, + "bottomRow": 4, + "parentId": "0", + "widgetId": "xif8wugzjv" + } + ] + } +} \ No newline at end of file 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 580a401dac..e824916c33 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 @@ -13,6 +13,7 @@ describe("Duplicate application", function() { cy.get(commonlocators.homeIcon).click({ force: true }); const appname = localStorage.getItem("AppName"); cy.get(homePage.searchInput).type(appname); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationCard) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js index 1dd97677df..b28651d94f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Applications/UpdateApplication_spec.js @@ -14,6 +14,7 @@ describe("Update Application", function() { cy.get(commonlocators.homeIcon).click({ force: true }); appname = localStorage.getItem("AppName"); cy.get(homePage.searchInput).type(appname); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationCard) @@ -53,6 +54,7 @@ describe("Update Application", function() { it("Check for errors in updating application name", function() { cy.get(commonlocators.homeIcon).click({ force: true }); cy.get(homePage.searchInput).type(appname); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationCard) .first() @@ -61,6 +63,7 @@ describe("Update Application", function() { .first() .click({ force: true }); cy.get("#loading").should("not.exist"); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationName).type(" "); cy.get(homePage.toastMessage).should( @@ -79,6 +82,7 @@ describe("Update Application", function() { it("Updates the name of first application to very long name and checks whether update is reflected in the application card with a popover", function() { cy.get(commonlocators.homeIcon).click({ force: true }); cy.get(homePage.searchInput).clear(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationCard) @@ -94,6 +98,7 @@ describe("Update Application", function() { "response.body.responseMeta.status", 200, ); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.applicationCard) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js index 8746e2b84d..accae859d4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js @@ -31,6 +31,7 @@ describe("Test Create Api and Bind to Table widget", function() { /**Validate Table data on current page(page1) */ cy.ValidateTableData("1"); cy.get(commonlocators.tableNextPage).click({ force: true }); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(5000); /* cy.wait("@postExecute").should( diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/ButtonWidgets_NavigateTo_validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/ButtonWidgets_NavigateTo_validation_spec.js index a5846c60af..caffe2c865 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/ButtonWidgets_NavigateTo_validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/ButtonWidgets_NavigateTo_validation_spec.js @@ -22,12 +22,14 @@ describe("Binding the button Widgets and validating NavigateTo Page functionalit .click(); cy.enterNavigatePageName(testdata.externalPage); cy.get(commonlocators.editPropCrossButton).click({ force: true }); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(300); }); it("Button click should take the control to page link validation", function() { cy.PublishtheApp(); cy.get(publish.buttonWidget).click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(publish.buttonWidget).should("not.exist"); cy.go("back"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/JS_Toggle_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/JS_Toggle_spec.js index fe767d9967..c5ceb1981b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/JS_Toggle_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/JS_Toggle_spec.js @@ -16,7 +16,7 @@ describe("JS Toggle tests", () => { .should("have.class", "is-active"); cy.testJsontext("visible", "false"); - + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(1000); cy.get(".t--property-control-visible") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/TableWidgets_NavigateTo_Validation_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/TableWidgets_NavigateTo_Validation_spec.js index 762cfe72af..08aa7c30b9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/TableWidgets_NavigateTo_Validation_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/TableWidgets_NavigateTo_Validation_spec.js @@ -29,6 +29,7 @@ describe("Table Widget and Navigate to functionality validation", function() { it("Create MyPage and valdiate if its successfully created", function() { cy.Createpage(pageid); cy.addDsl(dsl2); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(`.t--entity-name:contains("${pageid}")`).should("be.visible"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js index 2ccdb51220..f15307d536 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_Widgets_Copy_Delete_Undo_spec.js @@ -21,6 +21,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { formWidgetsPage.formInner, ); cy.get(commonlocators.copyWidget).click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(commonlocators.toastBody) .first() @@ -36,6 +37,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { expect($lis.eq(1)).to.contain("{{FormTest.data}}"); }); cy.DeleteWidgetFromSideBar(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(apiwidget.propertyList).should("not.exist"); /* @@ -53,6 +55,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { "response.body.responseMeta.status", 200, ); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(apiwidget.propertyList).then(function($lis) { expect($lis).to.have.length(2); 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 bf695ad477..c6627d91bf 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 @@ -24,6 +24,7 @@ describe("Test Suite to validate copy/delete/undo functionalites", function() { ); cy.get("body").click(); cy.get("body").type(`{${modifierKey}}c`); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(commonlocators.toastBody) .first() diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_spec.js index e6111d4d4c..3a1a83a28e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_spec.js @@ -63,43 +63,45 @@ describe("DatePicker Widget Functionality", function() { ); }); - it("Datepicker min/max date validation", function() { - cy.get(formWidgetsPage.defaultDate).click({ force: true }); - cy.SetDateToToday(); + // it("Datepicker min/max date validation", function() { + // cy.get(formWidgetsPage.defaultDate).click({ force: true }); + // cy.SetDateToToday(); - cy.get(formWidgetsPage.minDate) - .first() - .click(); - cy.wait(1000); - cy.setDate(-1, "ddd MMM DD YYYY"); + // cy.get(formWidgetsPage.minDate) + // .first() + // .click(); + // // eslint-disable-next-line cypress/no-unnecessary-waiting + // cy.wait(1000); + // cy.setDate(-1, "ddd MMM DD YYYY"); - cy.get(formWidgetsPage.maxDate) - .first() - .click(); - cy.wait(1000); - cy.setDate(1, "ddd MMM DD YYYY"); + // cy.get(formWidgetsPage.maxDate) + // .first() + // .click(); + // // eslint-disable-next-line cypress/no-unnecessary-waiting + // cy.wait(1000); + // cy.setDate(1, "ddd MMM DD YYYY"); - cy.PublishtheApp(); - cy.get(publishPage.datepickerWidget + " .bp3-input").click(); + // cy.PublishtheApp(); + // cy.get(publishPage.datepickerWidget + " .bp3-input").click(); - const minDate = Cypress.moment() - .add(2, "days") - .format("ddd MMM DD YYYY"); - const maxDate = Cypress.moment() - .add(2, "days") - .format("ddd MMM DD YYYY"); + // const minDate = Cypress.moment() + // .add(2, "days") + // .format("ddd MMM DD YYYY"); + // const maxDate = Cypress.moment() + // .add(2, "days") + // .format("ddd MMM DD YYYY"); - cy.get(`.DayPicker-Day[aria-label=\"${minDate}\"]`).should( - "have.attr", - "aria-disabled", - "true", - ); - cy.get(`.DayPicker-Day[aria-label=\"${maxDate}\"]`).should( - "have.attr", - "aria-disabled", - "true", - ); - }); + // cy.get(`.DayPicker-Day[aria-label=\"${minDate}\"]`).should( + // "have.attr", + // "aria-disabled", + // "true", + // ); + // cy.get(`.DayPicker-Day[aria-label=\"${maxDate}\"]`).should( + // "have.attr", + // "aria-disabled", + // "true", + // ); + // }); // it("Datepicker default date validation", function() { // cy.get(formWidgetsPage.defaultDate).click(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormReset_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormReset_spec.js index 1de82fb833..541586379a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormReset_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormReset_spec.js @@ -19,7 +19,7 @@ describe("Form reset functionality", function() { cy.get(widgetsPage.formButtonWidget) .contains("Reset") .click(); - + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(".tr") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js index 3d35ed8020..8afda73a11 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_spec.js @@ -32,6 +32,7 @@ describe("Tab widget test", function() { .click({ force: true }) .should("be.visible"); cy.get(Layoutpage.tabButton).click({ force: true }); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(200); cy.tabVerify(2, "Day"); cy.get(Layoutpage.tabDelete) diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_Default_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_Default_spec.js new file mode 100644 index 0000000000..765b040a33 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_Default_spec.js @@ -0,0 +1,39 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/datePicker2dsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); + +describe("DatePicker Widget Property pane tests with js bindings", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Datepicker default date validation with js binding and default date", function() { + cy.openPropertyPane("datepickerwidget2"); + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.testJsontext("defaultdate", "{{ moment().add(-1,'days') }}"); + }); + + it("Text widgets binding with datepicker", function() { + cy.SearchEntityandOpen("Text1"); + cy.testJsontext("text", "{{DatePicker1.formattedDate}}"); + cy.closePropertyPane(); + cy.SearchEntityandOpen("Text2"); + cy.testJsontext("text", "{{DatePicker1.selectedDate}}"); + cy.closePropertyPane(); + }); + + it("Text widgets binding with datepicker", function() { + cy.openPropertyPane("datepickerwidget2"); + cy.selectDateFormat("DD/MM/YYYY"); + cy.assertDateFormat(); + cy.closePropertyPane(); + cy.assertDateFormat(); + }); + it("Datepicker default date validation with js binding", function() { + cy.PublishtheApp(); + cy.wait(10000); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_spec.js new file mode 100644 index 0000000000..c74fd97c46 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_2_spec.js @@ -0,0 +1,79 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/datePicker2dsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); + +describe("DatePicker Widget Property pane tests with js bindings", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Datepicker default date validation with js binding", function() { + cy.openPropertyPane("datepickerwidget2"); + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.testJsontext("defaultdate", "{{moment().toISOString()}}"); + cy.get(formWidgetsPage.toggleJsMinDate).click(); + cy.testJsontext( + "mindate", + "{{moment().subtract(10, 'days').toISOString()}}", + ); + cy.get(formWidgetsPage.toggleJsMaxDate).click(); + cy.testJsontext("maxdate", "{{moment().add(10, 'days').toISOString()}}"); + /* + cy.get(formWidgetsPage.datepickerWidget + " .bp3-input").should( + "contain.value", + "14/02/2021", + ); + cy.PublishtheApp(); + cy.get(publishPage.datepickerWidget + " .bp3-input").should( + "contain.value", + "14/02/2021", + ); + */ + }); + + it("Text widgets binding with datepicker", function() { + cy.SearchEntityandOpen("Text1"); + cy.testJsontext("text", "{{DatePicker1.formattedDate}}"); + cy.closePropertyPane(); + cy.SearchEntityandOpen("Text2"); + cy.testJsontext("text", "{{DatePicker1.selectedDate}}"); + cy.closePropertyPane(); + }); + + it("Text widgets binding with datepicker", function() { + cy.openPropertyPane("datepickerwidget2"); + cy.selectDateFormat("YYYY-MM-DD"); + cy.assertDateFormat(); + cy.selectDateFormat("YYYY-MM-DD HH:mm"); + cy.assertDateFormat(); + cy.selectDateFormat("YYYY-MM-DDTHH:mm:ss.sssZ"); + cy.assertDateFormat(); + cy.selectDateFormat("DD/MM/YYYY"); + cy.assertDateFormat(); + cy.selectDateFormat("DD/MM/YYYY HH:mm"); + cy.closePropertyPane(); + cy.assertDateFormat(); + }); + + it("Datepicker default date validation with strings", function() { + cy.openPropertyPane("datepickerwidget2"); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(".t--property-control-defaultdate .bp3-input").type("2020-02-01"); + cy.closePropertyPane(); + cy.openPropertyPane("datepickerwidget2"); + cy.get(formWidgetsPage.toggleJsMinDate).click(); + cy.get(".t--property-control-mindate .bp3-input").type("2020-01-01"); + cy.get(formWidgetsPage.toggleJsMaxDate).click(); + cy.get(".t--property-control-maxdate .bp3-input").type("2020-02-10"); + cy.closePropertyPane(); + }); + + it("Datepicker default date validation with js binding", function() { + cy.PublishtheApp(); + cy.wait(10000); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_Toggle_js_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_Toggle_js_spec.js new file mode 100644 index 0000000000..a581ad7d17 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_Toggle_js_spec.js @@ -0,0 +1,47 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const formWidgetsPage = require("../../../locators/FormWidgets.json"); +const dsl = require("../../../fixtures/newFormDsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); +const pages = require("../../../locators/Pages.json"); + +describe("DatePicker Widget Property pane tests with js bindings", function() { + before(() => { + cy.addDsl(dsl); + }); + + beforeEach(() => { + cy.openPropertyPane("datepickerwidget"); + }); + + it("Datepicker default date validation with js binding", function() { + cy.get(".t--property-control-defaultdate .bp3-input").clear(); + cy.get(formWidgetsPage.toggleJsDefaultDate).click(); + cy.testJsontext( + "defaultdate", + "{{moment('14/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}", + ); + cy.get(formWidgetsPage.toggleJsMinDate).click(); + cy.testJsontext( + "mindate", + "{{moment('12/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}", + ); + cy.get(formWidgetsPage.toggleJsMaxDate).click(); + cy.testJsontext( + "maxdate", + "{{moment('17/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}", + ); + cy.get(formWidgetsPage.datepickerWidget + " .bp3-input").should( + "contain.value", + "14/02/2021", + ); + cy.PublishtheApp(); + cy.get(publishPage.datepickerWidget + " .bp3-input").should( + "contain.value", + "14/02/2021", + ); + }); + + afterEach(() => { + cy.get(publishPage.backToEditor).click({ force: true }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js index 9edcd9f157..fc37647369 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/OrganisationTests/UpdateOrgTests_spec.js @@ -24,6 +24,7 @@ describe("Update Organization", function() { localStorage.setItem("OrgName", orgid); cy.get(homePage.orgNameInput).clear(); cy.get(homePage.orgNameInput).type(orgid); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(2000); cy.get(homePage.orgHeaderName).should("have.text", orgid); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidgetTableAndBind_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidgetTableAndBind_spec.js index cbf1143f16..11b923bc27 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidgetTableAndBind_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidgetTableAndBind_spec.js @@ -29,6 +29,7 @@ describe("Addwidget from Query and bind with other widgets", function() { .first() .focus() .type("SELECT * FROM configs LIMIT 10;"); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(queryEditor.runQuery).click(); cy.wait("@postExecute").should( diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidget_spec.js index 18f5a407ef..d579b4eab6 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/AddWidget_spec.js @@ -22,6 +22,7 @@ describe("Add widget", function() { .first() .focus() .type("select * from configs"); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(queryEditor.runQuery).click(); cy.wait("@postExecute").should( diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/UnitTest/LoginFromUIApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/UnitTest/LoginFromUIApp_spec.js index 23d7769e1a..54d3485a4f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/UnitTest/LoginFromUIApp_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/UnitTest/LoginFromUIApp_spec.js @@ -26,8 +26,10 @@ describe("Login from UI and check the functionality", function() { cy.LogintoApp(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.get(homePage.profileMenu).click(); cy.get(homePage.signOutIcon).click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.get(homePage.headerAppSmithLogo).click(); + // eslint-disable-next-line cypress/no-unnecessary-waiting cy.wait(500); cy.url().should("include", "user/login"); }); diff --git a/app/client/cypress/locators/FormWidgets.json b/app/client/cypress/locators/FormWidgets.json index 673ae44d1a..84d613b30e 100644 --- a/app/client/cypress/locators/FormWidgets.json +++ b/app/client/cypress/locators/FormWidgets.json @@ -1,37 +1,36 @@ { - "checkboxWidget": ".t--draggable-checkboxwidget", - "dropdownWidget": ".t--draggable-dropdownwidget", - "dropdownSelectionType": ".t--property-control-selectiontype", - "radioWidget": ".t--draggable-radiogroupwidget", - "radioOnSelectionChangeDropdown": ".t--property-control-onselectionchange", - "nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day", - "datepickerWidget": ".t--draggable-datepickerwidget", - "defaultDate": ".t--property-control-defaultdate input", - "minDate": ".t--property-control-mindate input", - "maxDate": ".t--property-control-maxdate input", - "filepickerWidget": ".t--draggable-filepickerwidget", - "formWidget": ".t--draggable-formwidget", - "richTextEditorWidget": ".t--draggable-richtexteditorwidget", - "richEditorOnTextChange": ".t--property-control-ontextchange", - "optionvalue": ".t--property-control-defaultselectedvalue .kDwnRc", - "defselected": ".CodeMirror textarea", - "dropdowninner": ".bp3-button > .bp3-button-text", - "Textinput": ".t--property-control-options .CodeMirror-code", - "labelvalue": ".t--draggable-dropdownwidget label", - "dropdownInput": ".bp3-tag-input-values", - "labelradio": ".t--draggable-radiogroupwidget label", - "deleteradiovalue": ".t--property-control-options mask", - "inputRadio": ".t--draggable-radiogroupwidget input", - "defaultSelect": ".t--property-control-defaultselectedvalue .CodeMirror-code", - "formInner": ".t--draggable-formwidget span.t--widget-name", - "radioInput": ".t--draggable-radiogroupwidget span.t--widget-name", - "radioAddButton": ".t--property-control-options button", - "formD": "div[type='FORM_WIDGET']", - "datepickerFooter": ".bp3-datepicker-footer span", - "disableJs":".t--property-control-disable input[type='checkbox']", - "switchWidget": ".t--draggable-switchwidget", - "toggleJsDefaultDate": ".t--property-control-defaultdate .t--js-toggle", - "toggleJsMinDate": ".t--property-control-mindate .t--js-toggle", - "toggleJsMaxDate": ".t--property-control-maxdate .t--js-toggle" - - } + "checkboxWidget": ".t--draggable-checkboxwidget", + "dropdownWidget": ".t--draggable-dropdownwidget", + "dropdownSelectionType": ".t--property-control-selectiontype", + "radioWidget": ".t--draggable-radiogroupwidget", + "radioOnSelectionChangeDropdown": ".t--property-control-onselectionchange", + "nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day", + "datepickerWidget": ".t--draggable-datepickerwidget", + "defaultDate": ".t--property-control-defaultdate input", + "minDate": ".t--property-control-mindate input", + "maxDate": ".t--property-control-maxdate input", + "filepickerWidget": ".t--draggable-filepickerwidget", + "formWidget": ".t--draggable-formwidget", + "richTextEditorWidget": ".t--draggable-richtexteditorwidget", + "richEditorOnTextChange": ".t--property-control-ontextchange", + "optionvalue": ".t--property-control-defaultselectedvalue .kDwnRc", + "defselected": ".CodeMirror textarea", + "dropdowninner": ".bp3-button > .bp3-button-text", + "Textinput": ".t--property-control-options .CodeMirror-code", + "labelvalue": ".t--draggable-dropdownwidget label", + "dropdownInput": ".bp3-tag-input-values", + "labelradio": ".t--draggable-radiogroupwidget label", + "deleteradiovalue": ".t--property-control-options mask", + "inputRadio": ".t--draggable-radiogroupwidget input", + "defaultSelect": ".t--property-control-defaultselectedvalue .CodeMirror-code", + "formInner": ".t--draggable-formwidget span.t--widget-name", + "radioInput": ".t--draggable-radiogroupwidget span.t--widget-name", + "radioAddButton": ".t--property-control-options button", + "formD": "div[type='FORM_WIDGET']", + "datepickerFooter": ".bp3-datepicker-footer span", + "disableJs": ".t--property-control-disable input[type='checkbox']", + "switchWidget": ".t--draggable-switchwidget", + "toggleJsDefaultDate": ".t--property-control-defaultdate .t--js-toggle", + "toggleJsMinDate": ".t--property-control-mindate .t--js-toggle", + "toggleJsMaxDate": ".t--property-control-maxdate .t--js-toggle" +} diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 3bc45ff9f0..3b193b59ba 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -690,6 +690,37 @@ Cypress.Commands.add("switchToAPIInputTab", () => { .click({ force: true }); }); +Cypress.Commands.add("selectDateFormat", (value) => { + cy.get(".t--property-control-dateformat button") + .first() + .click({ force: true }); + cy.get("ul.bp3-menu") + .children() + .contains(value) + .click(); +}); + +Cypress.Commands.add("assertDateFormat", () => { + cy.get(".t--draggable-datepickerwidget2 input") + .first() + .invoke("attr", "value") + .then((text) => { + const firstTxt = text; + cy.log("date time : ", firstTxt); + cy.get(commonlocators.labelTextStyle) + .first() + .should("contain", firstTxt); + cy.get(commonlocators.labelTextStyle) + .last() + .invoke("text") + .then((text) => { + const secondText = text; + cy.log("date time : ", secondText); + expect(firstTxt).not.to.equal(secondText); + }); + }); +}); + Cypress.Commands.add("selectPaginationType", (option) => { cy.xpath(option).click({ force: true }); }); diff --git a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx index 755573d9bc..5770579ccc 100644 --- a/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx +++ b/app/client/src/components/designSystems/appsmith/TableComponent/CascadeFields.tsx @@ -8,7 +8,7 @@ import { ControlIcons } from "icons/ControlIcons"; import { AnyStyledComponent } from "styled-components"; import { Skin } from "constants/DefaultTheme"; import AutoToolTipComponent from "components/designSystems/appsmith/TableComponent/AutoToolTipComponent"; -import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent"; +import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent2"; import { OperatorTypes, Condition, diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx index 4d1e95c631..e0fac3e121 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx @@ -1,6 +1,10 @@ import React from "react"; import styled from "styled-components"; -import { getBorderCSSShorthand, labelStyle } from "constants/DefaultTheme"; +import { + getBorderCSSShorthand, + labelStyle, + IntentColors, +} from "constants/DefaultTheme"; import { ControlGroup, Classes, Label } from "@blueprintjs/core"; import { ComponentProps } from "components/designSystems/appsmith/BaseComponent"; import { DateInput } from "@blueprintjs/datetime"; @@ -11,24 +15,30 @@ import { WIDGET_PADDING } from "constants/WidgetConstants"; import { TimePrecision } from "@blueprintjs/datetime"; import { Colors } from "constants/Colors"; import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; +import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +import { DATE_WIDGET_DEFAULT_VALIDATION_ERROR } from "constants/messages"; -const StyledControlGroup = styled(ControlGroup)` +const StyledControlGroup = styled(ControlGroup)<{ isValid: boolean }>` &&& { .${Classes.INPUT} { box-shadow: none; color: ${Colors.OXFORD_BLUE}; font-size: ${(props) => props.theme.fontSizes[3]}px; border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])}; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.GEYSER_LIGHT}; border-radius: 0; width: 100%; height: inherit; align-items: center; &:active { - border-color: ${Colors.HIT_GRAY}; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.HIT_GRAY}; } &:focus { border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])}; - border-color: #80bdff; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : "#80bdff"}; outline: 0; box-shadow: 0 0 0 0.1rem rgba(0, 123, 255, 0.25); } @@ -95,10 +105,17 @@ class DatePickerComponent extends React.Component< .clone() .set({ month: 11, date: 31, year: year + 20 }) .toDate(); - + const isValid = this.state.selectedDate + ? this.isValidDate(this.parseDate(this.state.selectedDate)) + : true; + const value = + isValid && this.state.selectedDate + ? this.parseDate(this.state.selectedDate) + : null; return ( { e.stopPropagation(); }} @@ -115,29 +132,58 @@ class DatePickerComponent extends React.Component< )} { - + + + } ); } + isValidDate = (date: Date): boolean => { + let isValid = true; + const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT; + const parsedCurrentDate = moment(date); + if (this.props.minDate) { + const parsedMinDate = moment(this.props.minDate, dateFormat); + if ( + this.props.minDate && + parsedMinDate.isValid() && + parsedCurrentDate.isBefore(parsedMinDate) + ) { + isValid = false; + } + } + if (this.props.maxDate) { + const parsedMaxDate = moment(this.props.maxDate, dateFormat); + if ( + isValid && + this.props.maxDate && + parsedMaxDate.isValid() && + parsedCurrentDate.isAfter(parsedMaxDate) + ) { + isValid = false; + } + } + return isValid; + }; + formatDate = (date: Date): string => { const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT; return moment(date).format(dateFormat); @@ -165,9 +211,6 @@ class DatePickerComponent extends React.Component< const date = selectedDate ? this.formatDate(selectedDate) : ""; this.setState({ selectedDate: date }); - // if date is null ( if date is cleared ), don't call onDateSelected - if (!selectedDate) return false; - onDateSelected(date); } }; diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx new file mode 100644 index 0000000000..a3d2cb7178 --- /dev/null +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent2.tsx @@ -0,0 +1,236 @@ +import React from "react"; +import styled from "styled-components"; +import { labelStyle, IntentColors } from "constants/DefaultTheme"; +import { ControlGroup, Classes, Label } from "@blueprintjs/core"; +import { ComponentProps } from "components/designSystems/appsmith/BaseComponent"; +import { DateInput } from "@blueprintjs/datetime"; +import moment from "moment-timezone"; +import "../../../../node_modules/@blueprintjs/datetime/lib/css/blueprint-datetime.css"; +import { DatePickerType } from "widgets/DatePickerWidget"; +import { WIDGET_PADDING } from "constants/WidgetConstants"; +import { TimePrecision } from "@blueprintjs/datetime"; +import { Colors } from "constants/Colors"; +import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; +import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +import { DATE_WIDGET_DEFAULT_VALIDATION_ERROR } from "constants/messages"; + +const StyledControlGroup = styled(ControlGroup)<{ isValid: boolean }>` + &&& { + .${Classes.INPUT} { + box-shadow: none; + border: 1px solid; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.GEYSER_LIGHT}; + border-radius: ${(props) => props.theme.radii[1]}px; + width: 100%; + height: inherit; + align-items: center; + &:active { + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.HIT_GRAY}; + } + &:focus { + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.MYSTIC}; + } + } + .${Classes.INPUT_GROUP} { + display: block; + margin: 0; + } + .${Classes.CONTROL_GROUP} { + justify-content: flex-start; + } + label { + ${labelStyle} + flex: 0 1 30%; + margin: 7px ${WIDGET_PADDING * 2}px 0 0; + text-align: right; + align-self: flex-start; + max-width: calc(30% - ${WIDGET_PADDING}px); + } + } + &&& { + input { + border: 1px solid; + border-color: ${(props) => + !props.isValid ? IntentColors.danger : Colors.HIT_GRAY}; + border-radius: ${(props) => props.theme.radii[1]}px; + box-shadow: none; + color: ${Colors.OXFORD_BLUE}; + font-size: ${(props) => props.theme.fontSizes[3]}px; + } + } +`; + +class DatePickerComponent extends React.Component< + DatePickerComponentProps, + DatePickerComponentState +> { + constructor(props: DatePickerComponentProps) { + super(props); + this.state = { + selectedDate: props.selectedDate, + }; + } + + componentDidUpdate(prevProps: DatePickerComponentProps) { + if ( + this.props.selectedDate !== this.state.selectedDate && + !moment(this.props.selectedDate).isSame( + moment(prevProps.selectedDate), + "seconds", + ) + ) { + this.setState({ selectedDate: this.props.selectedDate }); + } + } + + getValidDate = (date: string, format: string) => { + const _date = moment(date, format); + return _date.isValid() ? _date.toDate() : undefined; + }; + + render() { + const now = moment(); + const year = now.get("year"); + const minDate = this.props.minDate + ? new Date(this.props.minDate) + : now + .clone() + .set({ month: 0, date: 1, year: year - 100 }) + .toDate(); + const maxDate = this.props.maxDate + ? new Date(this.props.maxDate) + : now + .clone() + .set({ month: 11, date: 31, year: year + 20 }) + .toDate(); + const isValid = this.state.selectedDate + ? this.isValidDate(new Date(this.state.selectedDate)) + : true; + const value = + isValid && this.state.selectedDate + ? new Date(this.state.selectedDate) + : null; + return ( + { + e.stopPropagation(); + }} + > + {this.props.label && ( + + )} + { + + + + } + + ); + } + + isValidDate = (date: Date): boolean => { + let isValid = true; + const parsedCurrentDate = moment(date); + if (this.props.minDate) { + const parsedMinDate = moment(this.props.minDate); + if ( + this.props.minDate && + parsedMinDate.isValid() && + parsedCurrentDate.isBefore(parsedMinDate) + ) { + isValid = false; + } + } + if (this.props.maxDate) { + const parsedMaxDate = moment(this.props.maxDate); + if ( + isValid && + this.props.maxDate && + parsedMaxDate.isValid() && + parsedCurrentDate.isAfter(parsedMaxDate) + ) { + isValid = false; + } + } + return isValid; + }; + + formatDate = (date: Date): string => { + const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT; + return moment(date).format(dateFormat); + }; + + parseDate = (dateStr: string): Date => { + const date = moment(dateStr); + + if (date.isValid()) return moment(dateStr).toDate(); + else return moment().toDate(); + }; + + /** + * checks if selelectedDate is null or not, + * sets state and calls props onDateSelected + * if its null, don't call onDateSelected + * + * @param selectedDate + */ + onDateSelected = (selectedDate: Date, isUserChange: boolean) => { + if (isUserChange) { + const { onDateSelected } = this.props; + + const date = selectedDate ? selectedDate.toISOString() : ""; + this.setState({ selectedDate: date }); + + onDateSelected(date); + } + }; +} + +interface DatePickerComponentProps extends ComponentProps { + label: string; + dateFormat: string; + enableTimePicker?: boolean; + selectedDate?: string; + minDate?: string; + maxDate?: string; + timezone?: string; + datePickerType: DatePickerType; + isDisabled: boolean; + onDateSelected: (selectedDate: string) => void; + isLoading: boolean; +} + +interface DatePickerComponentState { + selectedDate?: string; +} + +export default DatePickerComponent; diff --git a/app/client/src/components/propertyControls/DatePickerControl.tsx b/app/client/src/components/propertyControls/DatePickerControl.tsx index 6796c3e736..259417ffa6 100644 --- a/app/client/src/components/propertyControls/DatePickerControl.tsx +++ b/app/client/src/components/propertyControls/DatePickerControl.tsx @@ -5,8 +5,6 @@ import moment from "moment-timezone"; import styled from "styled-components"; import { TimePrecision } from "@blueprintjs/datetime"; import { WidgetProps } from "widgets/BaseWidget"; -import { Toaster } from "components/ads/Toast"; -import { Variant } from "components/ads/common"; import { ISO_DATE_FORMAT } from "constants/WidgetValidation"; const DatePickerControlWrapper = styled.div<{ isValid: boolean }>` @@ -63,18 +61,22 @@ class DatePickerControl extends BaseControl< } render() { + const version = this.props.widgetProperties.version; const dateFormat = - this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; + version === 2 + ? ISO_DATE_FORMAT + : this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; const isValid = this.state.selectedDate ? this.validateDate(moment(this.state.selectedDate, dateFormat).toDate()) : true; - const maxDate = - this.props.widgetProperties?.evaluatedValues?.maxDate ?? this.maxDate; - const minDate = - this.props.widgetProperties?.evaluatedValues?.minDate ?? this.minDate; - + const value = + this.props.propertyValue && isValid + ? version === 2 + ? new Date(this.props.propertyValue) + : this.parseDate(this.props.propertyValue) + : null; return ( - + ); @@ -117,10 +105,13 @@ class DatePickerControl extends BaseControl< */ onDateSelected = (date: Date, isUserChange: boolean): void => { if (isUserChange) { - const selectedDate = date ? this.formatDate(date) : undefined; + const selectedDate = date + ? this.props.widgetProperties.version === 2 + ? date.toISOString() + : this.formatDate(date) + : undefined; const isValid = this.validateDate(date); if (!isValid) return; - // if everything is ok, put date in state this.setState({ selectedDate: selectedDate }); this.updateProperty(this.props.propertyName, selectedDate); @@ -128,73 +119,14 @@ class DatePickerControl extends BaseControl< }; /** - * checks: - * 1. if max date is greater than the default date - * 2. if default date is in range of min and max date + * checks if date is of valid date format */ validateDate = (date: Date): boolean => { const dateFormat = - this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; - const parsedSelectedDate = moment(date, dateFormat); - //validate defaultDate if both minDate and maxDate is already selected - if (this.props.propertyName === "defaultDate") { - if ( - parsedSelectedDate.isValid() && - this.props.widgetProperties?.evaluatedValues?.minDate && - this.props.widgetProperties?.evaluatedValues?.maxDate - ) { - const parsedMinDate = moment( - this.props.widgetProperties.evaluatedValues.minDate, - dateFormat, - ); - const parsedMaxDate = moment( - this.props.widgetProperties.evaluatedValues.maxDate, - dateFormat, - ); - if ( - parsedSelectedDate.isBefore(parsedMinDate) || - parsedSelectedDate.isAfter(parsedMaxDate) - ) { - return false; - } - } - } - if (this.props.widgetProperties?.evaluatedValues?.value) { - const parsedWidgetDate = moment( - this.props.widgetProperties.evaluatedValues.value, - dateFormat, - ); - // checking if widget date is after min date - if (this.props.propertyName === "minDate") { - if ( - parsedSelectedDate.isValid() && - parsedWidgetDate.isBefore(parsedSelectedDate) - ) { - Toaster.show({ - text: "Min date cannot be greater than current widget value.", - variant: Variant.danger, - }); - - return false; - } - } - - // checking if widget date is before max date - if (this.props.propertyName === "maxDate") { - if ( - parsedSelectedDate.isValid() && - parsedWidgetDate.isAfter(parsedSelectedDate) - ) { - Toaster.show({ - text: "Max date cannot be less than current widget value.", - variant: Variant.danger, - }); - - return false; - } - } - } - return true; + this.props.widgetProperties.version === 2 + ? ISO_DATE_FORMAT + : this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; + return date ? moment(date, dateFormat).isValid() : true; }; formatDate = (date: Date): string => { @@ -205,7 +137,9 @@ class DatePickerControl extends BaseControl< parseDate = (dateStr: string): Date => { const dateFormat = - this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; + this.props.widgetProperties.version === 2 + ? ISO_DATE_FORMAT + : this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT; const date = moment(dateStr, dateFormat); if (date.isValid()) return moment(dateStr, dateFormat).toDate(); diff --git a/app/client/src/constants/FieldExpectedValue.ts b/app/client/src/constants/FieldExpectedValue.ts index 99580f40c3..eed49ad87e 100644 --- a/app/client/src/constants/FieldExpectedValue.ts +++ b/app/client/src/constants/FieldExpectedValue.ts @@ -24,6 +24,13 @@ const FIELD_VALUES: Record< isDisabled: "boolean", // onDateSelected: "Function Call", }, + DATE_PICKER_WIDGET2: { + defaultDate: "string", //TODO:Vicky validate this property + isRequired: "boolean", + isVisible: "boolean", + isDisabled: "boolean", + // onDateSelected: "Function Call", + }, TABLE_WIDGET: { tableData: "Array", serverSidePaginationEnabled: "boolean", diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts index 6e1020b27f..43fe688253 100644 --- a/app/client/src/constants/HelpConstants.ts +++ b/app/client/src/constants/HelpConstants.ts @@ -27,6 +27,10 @@ export const HelpMap = { path: "/widget-reference/datepicker", searchKey: "DatePicker", }, + DATE_PICKER_WIDGET2: { + path: "/widget-reference/datepicker", + searchKey: "DatePicker", + }, TABLE_WIDGET: { path: "/widget-reference/table", searchKey: "Table", diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index 40f8e4b1d9..5c3123eddf 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -5,6 +5,7 @@ export enum WidgetTypes { INPUT_WIDGET = "INPUT_WIDGET", CONTAINER_WIDGET = "CONTAINER_WIDGET", DATE_PICKER_WIDGET = "DATE_PICKER_WIDGET", + DATE_PICKER_WIDGET2 = "DATE_PICKER_WIDGET2", TABLE_WIDGET = "TABLE_WIDGET", DROP_DOWN_WIDGET = "DROP_DOWN_WIDGET", CHECKBOX_WIDGET = "CHECKBOX_WIDGET", diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index 1b5a26bc16..d22315b7dc 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -42,7 +42,7 @@ export type Validator = ( dataTree?: DataTree, ) => ValidationResponse; -export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.Z"; +export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.sssZ"; export const JAVASCRIPT_KEYWORDS = { true: "true", diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index 9ca3f8cf96..5f95ddc1fb 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -138,6 +138,8 @@ export const FORGOT_PASSWORD_PAGE_LOGIN_LINK = "Back to Sign In"; export const ADD_API_TO_PAGE_SUCCESS_MESSAGE = "Api added to page."; export const INPUT_WIDGET_DEFAULT_VALIDATION_ERROR = "Invalid input"; +export const DATE_WIDGET_DEFAULT_VALIDATION_ERROR = "Date out of range"; + export const AUTOFIT_ALL_COLUMNS = "Autofit all columns"; export const AUTOFIT_THIS_COLUMN = "Autofit this column"; export const AUTOFIT_COLUMN = "Autofit column"; diff --git a/app/client/src/entities/Widget/utils.test.ts b/app/client/src/entities/Widget/utils.test.ts index 8e89f01311..d7402546db 100644 --- a/app/client/src/entities/Widget/utils.test.ts +++ b/app/client/src/entities/Widget/utils.test.ts @@ -29,6 +29,7 @@ describe("getAllPathsFromPropertyConfig", () => { isLoading: false, horizontalAlignment: "LEFT", parentColumnSpace: 74, + version: 1, dynamicTriggerPathList: [ { key: "primaryColumns.status.onClick", diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx index 7eaa30f489..c95ff4fdaa 100644 --- a/app/client/src/icons/WidgetIcons.tsx +++ b/app/client/src/icons/WidgetIcons.tsx @@ -51,7 +51,7 @@ export const WidgetIcons: { ), - DATE_PICKER_WIDGET: (props: IconProps) => ( + DATE_PICKER_WIDGET2: (props: IconProps) => ( diff --git a/app/client/src/mockResponses/WidgetConfigResponse.tsx b/app/client/src/mockResponses/WidgetConfigResponse.tsx index f2d841b45b..1e88df7137 100644 --- a/app/client/src/mockResponses/WidgetConfigResponse.tsx +++ b/app/client/src/mockResponses/WidgetConfigResponse.tsx @@ -14,6 +14,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isDisabled: false, isVisible: true, isDefaultClickDisabled: true, + version: 1, }, TEXT_WIDGET: { text: "Label", @@ -22,6 +23,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { rows: 1, columns: 4, widgetName: "Text", + version: 1, }, RICH_TEXT_EDITOR_WIDGET: { defaultText: "This is the initial content of the editor", @@ -31,6 +33,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isVisible: true, widgetName: "RichTextEditor", isDefaultClickDisabled: true, + version: 1, }, IMAGE_WIDGET: { defaultImage: @@ -41,6 +44,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { rows: 3, columns: 4, widgetName: "Image", + version: 1, }, INPUT_WIDGET: { inputType: "TEXT", @@ -48,6 +52,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { label: "", columns: 5, widgetName: "Input", + version: 1, resetOnSubmit: true, }, SWITCH_WIDGET: { @@ -57,11 +62,13 @@ const WidgetConfigResponse: WidgetConfigReducerState = { defaultSwitchState: true, widgetName: "Switch", alignWidget: "LEFT", + version: 1, }, ICON_WIDGET: { widgetName: "Icon", rows: 1, columns: 1, + version: 1, }, CONTAINER_WIDGET: { backgroundColor: "#FFFFFF", @@ -84,6 +91,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, ], }, + version: 1, }, DATE_PICKER_WIDGET: { isDisabled: false, @@ -94,14 +102,26 @@ const WidgetConfigResponse: WidgetConfigReducerState = { columns: 5, widgetName: "DatePicker", defaultDate: moment().format("DD/MM/YYYY HH:mm"), + version: 1, + }, + DATE_PICKER_WIDGET2: { + isDisabled: false, + datePickerType: "DATE_PICKER", + rows: 1, + label: "", + dateFormat: "DD/MM/YYYY HH:mm", + columns: 5, + widgetName: "DatePicker", + defaultDate: moment().toISOString(), + version: 2, }, - VIDEO_WIDGET: { rows: 7, columns: 7, widgetName: "Video", url: "https://www.youtube.com/watch?v=mzqK0QIZRLs", autoPlay: false, + version: 1, }, TABLE_WIDGET: { rows: 7, @@ -137,6 +157,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { orderAmount: 19.99, }, ], + version: 1, }, DROP_DOWN_WIDGET: { rows: 1, @@ -150,6 +171,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { ], widgetName: "Dropdown", defaultOptionValue: "VEG", + version: 1, }, CHECKBOX_WIDGET: { rows: 1, @@ -157,6 +179,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { label: "Label", defaultCheckedState: true, widgetName: "Checkbox", + version: 1, alignWidget: "LEFT", }, RADIO_GROUP_WIDGET: { @@ -169,6 +192,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { ], defaultOptionValue: "M", widgetName: "RadioGroup", + version: 1, }, ALERT_WIDGET: { alertType: "NOTIFICATION", @@ -178,6 +202,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { header: "", message: "", widgetName: "Alert", + version: 1, }, FILE_PICKER_WIDGET: { rows: 1, @@ -188,6 +213,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { maxFileSize: 5, widgetName: "FilePicker", isDefaultClickDisabled: true, + version: 1, }, TABS_WIDGET: { rows: 7, @@ -224,6 +250,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { }, ], }, + version: 1, }, MODAL_WIDGET: { rows: 6, @@ -235,6 +262,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { shouldScrollContents: true, widgetName: "Modal", children: [], + version: 1, blueprint: { view: [ { @@ -247,6 +275,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { isDisabled: false, shouldScrollContents: false, children: [], + version: 1, blueprint: { view: [ { @@ -257,6 +286,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { iconName: "cross", iconSize: 24, color: "#040627", + version: 1, }, }, { @@ -266,6 +296,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { props: { text: "Modal Title", textStyle: "HEADING", + version: 1, }, }, { @@ -275,6 +306,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { props: { text: "Cancel", buttonStyle: "SECONDARY_BUTTON", + version: 1, }, }, { @@ -284,6 +316,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { props: { text: "Confirm", buttonStyle: "PRIMARY_BUTTON", + version: 1, }, }, ], @@ -322,6 +355,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { rows: 0, columns: 0, widgetName: "Canvas", + version: 1, }, CHART_WIDGET: { rows: 8, @@ -330,6 +364,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { chartType: "LINE_CHART", chartName: "Sales on working days", allowHorizontalScroll: false, + version: 1, chartData: [ { seriesName: "Sales", @@ -374,6 +409,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { widgetName: "FormButton", text: "Submit", isDefaultClickDisabled: true, + version: 1, }, FORM_WIDGET: { rows: 13, @@ -391,6 +427,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { canExtend: false, detachFromLayout: true, children: [], + version: 1, blueprint: { view: [ { @@ -400,6 +437,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { props: { text: "Form", textStyle: "HEADING", + version: 1, }, }, { @@ -411,6 +449,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { buttonStyle: "PRIMARY_BUTTON", disabledWhenInvalid: true, resetFormOnClick: true, + version: 1, }, }, { @@ -422,6 +461,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = { buttonStyle: "SECONDARY_BUTTON", disabledWhenInvalid: false, resetFormOnClick: true, + version: 1, }, }, ], @@ -443,12 +483,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = { allowZoom: true, mapCenter: { lat: -34.397, long: 150.644 }, defaultMarkers: [{ lat: -34.397, long: 150.644, title: "Test A" }], + version: 1, }, SKELETON_WIDGET: { isLoading: true, rows: 1, columns: 1, widgetName: "Skeleton", + version: 1, }, }, configVersion: 1, diff --git a/app/client/src/mockResponses/WidgetSidebarResponse.tsx b/app/client/src/mockResponses/WidgetSidebarResponse.tsx index dc8e13878f..205bb38231 100644 --- a/app/client/src/mockResponses/WidgetSidebarResponse.tsx +++ b/app/client/src/mockResponses/WidgetSidebarResponse.tsx @@ -29,7 +29,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [ key: generateReactKey(), }, { - type: "DATE_PICKER_WIDGET", + type: "DATE_PICKER_WIDGET2", widgetCardName: "DatePicker", key: generateReactKey(), }, diff --git a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx index 4a8d29f747..6b327b2db9 100644 --- a/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx +++ b/app/client/src/reducers/entityReducers/widgetConfigReducer.tsx @@ -9,6 +9,7 @@ import { ImageWidgetProps } from "widgets/ImageWidget"; import { InputWidgetProps } from "widgets/InputWidget"; import { RichTextEditorWidgetProps } from "widgets/RichTextEditorWidget"; import { DatePickerWidgetProps } from "../../widgets/DatePickerWidget"; +import { DatePickerWidget2Props } from "../../widgets/DatePickerWidget2"; import { TableWidgetProps } from "../../widgets/TableWidget/TableWidgetConstants"; import { DropdownWidgetProps } from "../../widgets/DropdownWidget"; import { CheckboxWidgetProps } from "../../widgets/CheckboxWidget"; @@ -59,6 +60,7 @@ export interface WidgetConfigReducerState { CONTAINER_WIDGET: Partial> & WidgetConfigProps; DATE_PICKER_WIDGET: Partial & WidgetConfigProps; + DATE_PICKER_WIDGET2: Partial & WidgetConfigProps; TABLE_WIDGET: Partial & WidgetConfigProps; VIDEO_WIDGET: Partial & WidgetConfigProps; DROP_DOWN_WIDGET: Partial & WidgetConfigProps; diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 333bc1b9e9..e99f01529f 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -143,6 +143,7 @@ function* getChildWidgetProps( parentColumnSpace, widgetName, widgetProps, + restDefaultConfig.version, ); widget.widgetId = newWidgetId; @@ -1411,6 +1412,7 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction) { parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT, parentColumnSpace: 1, isLoading: false, + version: 1, props: { tableData: `{{${queryName}.data}}`, dynamicBindingPathList: [{ key: "tableData" }], diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index b490956241..f2a28a3717 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -259,6 +259,18 @@ const dynamicPathListMigration = ( return currentDSL; }; +const addVersionNumberMigration = ( + currentDSL: ContainerWidgetProps, +) => { + if (currentDSL.children && currentDSL.children.length) { + currentDSL.children = currentDSL.children.map(addVersionNumberMigration); + } + if (currentDSL.version === undefined) { + currentDSL.version = 1; + } + return currentDSL; +}; + const canvasNameConflictMigration = ( currentDSL: ContainerWidgetProps, props = { counter: 1 }, @@ -367,6 +379,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps) => { currentDSL.version = 10; } + if (currentDSL.version === 10) { + currentDSL = addVersionNumberMigration(currentDSL); + currentDSL.version = 11; + } + return currentDSL; }; @@ -590,6 +607,7 @@ export const generateWidgetProps = ( widgetId: string; renderMode: RenderMode; } & Partial, + version: number, ): ContainerWidgetProps => { if (parent) { const sizes = { @@ -611,6 +629,7 @@ export const generateWidgetProps = ( ...sizes, ...others, parentId: parent.widgetId, + version, }; delete props.rows; delete props.columns; diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index 2fced6e1e0..5e09d10a2a 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -71,6 +71,10 @@ import DatePickerWidget, { DatePickerWidgetProps, ProfiledDatePickerWidget, } from "widgets/DatePickerWidget"; +import DatePickerWidget2, { + DatePickerWidget2Props, + ProfiledDatePickerWidget2, +} from "widgets/DatePickerWidget2"; import FormWidget, { ProfiledFormWidget } from "widgets/FormWidget"; import FormButtonWidget, { FormButtonWidgetProps, @@ -286,6 +290,20 @@ export default class WidgetBuilderRegistry { DatePickerWidget.getMetaPropertiesMap(), DatePickerWidget.getPropertyPaneConfig(), ); + WidgetFactory.registerWidgetBuilder( + "DATE_PICKER_WIDGET2", + { + buildWidget(widgetData: DatePickerWidget2Props): JSX.Element { + return ; + }, + }, + DatePickerWidget2.getPropertyValidationMap(), + DatePickerWidget2.getDerivedPropertiesMap(), + DatePickerWidget2.getTriggerPropertyMap(), + DatePickerWidget2.getDefaultPropertiesMap(), + DatePickerWidget2.getMetaPropertiesMap(), + DatePickerWidget.getPropertyPaneConfig(), + ); WidgetFactory.registerWidgetBuilder( "TABS_WIDGET", { diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts index 0dd1e5dc84..cb25204611 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -124,6 +124,15 @@ export const entityDefinitions = { selectedDate: "string", isDisabled: "bool", }, + DATE_PICKER_WIDGET2: { + "!doc": + "Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth", + "!url": "https://docs.appsmith.com/widget-reference/datepicker", + isVisible: isVisible, + selectedDate: "string", + formattedDate: "string", + isDisabled: "bool", + }, CHECKBOX_WIDGET: { "!doc": "Checkbox is a simple UI widget you can use when you want users to make a binary choice", diff --git a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts index ea0eda774a..853bb76e67 100644 --- a/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts +++ b/app/client/src/utils/autocomplete/dataTreeTypeDefCreator.test.ts @@ -24,6 +24,7 @@ describe("dataTreeTypeDefCreator", () => { topRow: 1, bottomRow: 2, isLoading: false, + version: 1, bindingPaths: { defaultText: true, }, diff --git a/app/client/src/utils/migrations/TableWidget.test.ts b/app/client/src/utils/migrations/TableWidget.test.ts index ed4d0524c3..57291b80a7 100644 --- a/app/client/src/utils/migrations/TableWidget.test.ts +++ b/app/client/src/utils/migrations/TableWidget.test.ts @@ -44,6 +44,7 @@ const input1: ContainerWidgetProps = { widgetId: "fs785w9gcy", dynamicBindingPathList: [], renderMode: "CANVAS", + version: 1, }, ], }; @@ -105,6 +106,7 @@ const input2: ContainerWidgetProps = { }, ], renderMode: "CANVAS", + version: 1, }, ], }; @@ -157,6 +159,7 @@ const input3: ContainerWidgetProps = { onRowSelected: "{{showAlert('test','success')}}", onSearchTextChanged: "{{showAlert('fail','error')}}", renderMode: "CANVAS", + version: 1, }, ], }; @@ -293,6 +296,7 @@ const output1 = { horizontalAlignment: "LEFT", verticalAlignment: "CENTER", renderMode: "CANVAS", + version: 1, }, ], }; @@ -474,6 +478,7 @@ const output2 = { horizontalAlignment: "LEFT", verticalAlignment: "CENTER", renderMode: "CANVAS", + version: 1, }, ], }; @@ -616,6 +621,7 @@ const output3 = { horizontalAlignment: "LEFT", verticalAlignment: "CENTER", renderMode: "CANVAS", + version: 1, }, ], }; diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index e497898ad0..2ae2da6702 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -319,6 +319,7 @@ export interface WidgetBaseProps { widgetName: string; parentId: string; renderMode: RenderMode; + version: number; } export type WidgetRowCols = { diff --git a/app/client/src/widgets/DatePickerWidget2.tsx b/app/client/src/widgets/DatePickerWidget2.tsx new file mode 100644 index 0000000000..3ba8f2fe6d --- /dev/null +++ b/app/client/src/widgets/DatePickerWidget2.tsx @@ -0,0 +1,223 @@ +import React from "react"; +import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; +import { WidgetType } from "constants/WidgetConstants"; +import { EventType } from "constants/ActionConstants"; +import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent2"; +import { + WidgetPropertyValidationType, + BASE_WIDGET_VALIDATION, +} from "utils/WidgetValidation"; +import { VALIDATION_TYPES } from "constants/WidgetValidation"; +import { + DerivedPropertiesMap, + TriggerPropertiesMap, +} from "utils/WidgetFactory"; +import * as Sentry from "@sentry/react"; +import withMeta, { WithMeta } from "./MetaHOC"; + +class DatePickerWidget extends BaseWidget { + static getPropertyPaneConfig() { + return [ + { + sectionName: "General", + children: [ + { + propertyName: "defaultDate", + label: "Default Date", + helpText: + "Sets the default date of the widget. The date is updated if the default date changes", + controlType: "DATE_PICKER", + placeholderText: "Enter Default Date", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + { + helpText: "Sets the format of the selected date", + propertyName: "dateFormat", + label: "Date Format", + controlType: "DROP_DOWN", + isJSConvertible: true, + options: [ + { + label: "YYYY-MM-DD", + value: "YYYY-MM-DD", + }, + { + label: "YYYY-MM-DD HH:mm", + value: "YYYY-MM-DD HH:mm", + }, + { + label: "YYYY-MM-DDTHH:mm:ss.sssZ", + value: "YYYY-MM-DDTHH:mm:ss.sssZ", + }, + { + label: "DD/MM/YYYY", + value: "DD/MM/YYYY", + }, + { + label: "DD/MM/YYYY HH:mm", + value: "DD/MM/YYYY HH:mm", + }, + ], + isBindProperty: true, + isTriggerProperty: false, + }, + { + propertyName: "isRequired", + label: "Required", + helpText: "Makes input to the widget mandatory", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + { + propertyName: "isVisible", + label: "Visible", + helpText: "Controls the visibility of the widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + { + propertyName: "isDisabled", + label: "Disabled", + helpText: "Disables input to this widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + { + propertyName: "minDate", + label: "Min Date", + helpText: "Defines the min date for this widget", + controlType: "DATE_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + { + propertyName: "maxDate", + label: "Max Date", + helpText: "Defines the max date for this widget", + controlType: "DATE_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + }, + ], + }, + { + sectionName: "Actions", + children: [ + { + propertyName: "onDateSelected", + label: "onDateSelected", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, + ]; + } + + static getPropertyValidationMap(): WidgetPropertyValidationType { + return { + ...BASE_WIDGET_VALIDATION, + defaultDate: VALIDATION_TYPES.DEFAULT_DATE, + timezone: VALIDATION_TYPES.TEXT, + enableTimePicker: VALIDATION_TYPES.BOOLEAN, + dateFormat: VALIDATION_TYPES.TEXT, + label: VALIDATION_TYPES.TEXT, + datePickerType: VALIDATION_TYPES.TEXT, + maxDate: VALIDATION_TYPES.DATE, + minDate: VALIDATION_TYPES.DATE, + isRequired: VALIDATION_TYPES.BOOLEAN, + // onDateSelected: VALIDATION_TYPES.ACTION_SELECTOR, + // onDateRangeSelected: VALIDATION_TYPES.ACTION_SELECTOR, + }; + } + + static getDerivedPropertiesMap(): DerivedPropertiesMap { + return { + isValid: `{{ this.isRequired ? !!this.selectedDate : true }}`, + selectedDate: `{{ this.value ? moment(this.value).toISOString() : (this.defaultDate ? moment(this.defaultDate).toISOString() : "")}}`, + formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : (this.defaultDate ? moment(this.defaultDate).format(this.dateFormat) : "")}}`, + }; + } + + static getTriggerPropertyMap(): TriggerPropertiesMap { + return { + onDateSelected: true, + }; + } + + static getDefaultPropertiesMap(): Record { + return { + value: "defaultDate", + }; + } + + static getMetaPropertiesMap(): Record { + return { + value: undefined, + }; + } + + getPageView() { + return ( + + ); + } + + onDateSelected = (selectedDate: string) => { + this.props.updateWidgetMetaProperty("value", selectedDate, { + dynamicString: this.props.onDateSelected, + event: { + type: EventType.ON_DATE_SELECTED, + }, + }); + }; + + getWidgetType(): WidgetType { + return "DATE_PICKER_WIDGET2"; + } +} + +export type DatePickerType = "DATE_PICKER" | "DATE_RANGE_PICKER"; + +export interface DatePickerWidget2Props extends WidgetProps, WithMeta { + defaultDate: string; + selectedDate: string; + formattedDate: string; + isDisabled: boolean; + dateFormat: string; + label: string; + datePickerType: DatePickerType; + onDateSelected?: string; + onDateRangeSelected?: string; + maxDate: string; + minDate: string; + isRequired?: boolean; +} + +export default DatePickerWidget; +export const ProfiledDatePickerWidget2 = Sentry.withProfiler( + withMeta(DatePickerWidget), +); diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts index 00d3b4d9f6..64265180d9 100644 --- a/app/client/src/workers/evaluation.test.ts +++ b/app/client/src/workers/evaluation.test.ts @@ -289,6 +289,33 @@ const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { }, metaProperties: {}, }, + DATE_PICKER_WIDGET2: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + defaultDate: "DATE", + timezone: "TEXT", + enableTimePicker: "BOOLEAN", + dateFormat: "TEXT", + label: "TEXT", + datePickerType: "TEXT", + maxDate: "DATE", + minDate: "DATE", + isRequired: "BOOLEAN", + }, + defaultProperties: { + selectedDate: "defaultDate", + }, + derivedProperties: { + isValid: "{{ this.isRequired ? !!this.selectedDate : true }}", + value: "{{ this.selectedDate }}", + }, + triggerProperties: { + onDateSelected: true, + }, + metaProperties: {}, + }, TABS_WIDGET: { validations: { tabs: "TABS_DATA", @@ -447,6 +474,7 @@ const BASE_WIDGET: DataTreeWidget = { topRow: 0, type: WidgetTypes.SKELETON_WIDGET, parentId: "0", + version: 1, bindingPaths: {}, triggerPaths: {}, ENTITY_TYPE: ENTITY_TYPE.WIDGET, diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index af27b932ea..6a595e7075 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -391,7 +391,10 @@ export const VALIDATORS: Record = { dateString: string, props: WidgetProps, ): ValidationResponse => { - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + const dateFormat = + props.version === 2 + ? ISO_DATE_FORMAT + : props.dateFormat || ISO_DATE_FORMAT; if (dateString === undefined) { return { @@ -421,39 +424,22 @@ export const VALIDATORS: Record = { dateString: string, props: WidgetProps, ): ValidationResponse => { - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + const dateFormat = + props.version === 2 + ? ISO_DATE_FORMAT + : props.dateFormat || ISO_DATE_FORMAT; if (dateString === undefined) { return { isValid: false, parsed: "", message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat + ? dateFormat : "", }; } const parsedCurrentDate = moment(dateString, dateFormat); - let isValid = parsedCurrentDate.isValid(); - const parsedMinDate = moment(props.minDate, dateFormat); - const parsedMaxDate = moment(props.maxDate, dateFormat); - - // checking for max/min date range - if (isValid) { - if ( - parsedMinDate.isValid() && - parsedCurrentDate.isBefore(parsedMinDate) - ) { - isValid = false; - } - - if ( - isValid && - parsedMaxDate.isValid() && - parsedCurrentDate.isAfter(parsedMaxDate) - ) { - isValid = false; - } - } + const isValid = parsedCurrentDate.isValid(); if (!isValid) { return { isValid: isValid, @@ -471,14 +457,17 @@ export const VALIDATORS: Record = { dateString: string, props: WidgetProps, ): ValidationResponse => { - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + const dateFormat = + props.version === 2 + ? ISO_DATE_FORMAT + : props.dateFormat || ISO_DATE_FORMAT; if (dateString === undefined) { return { isValid: false, parsed: "", message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat + ? dateFormat : "", }; } @@ -517,14 +506,17 @@ export const VALIDATORS: Record = { dateString: string, props: WidgetProps, ): ValidationResponse => { - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + const dateFormat = + props.version === 2 + ? ISO_DATE_FORMAT + : props.dateFormat || ISO_DATE_FORMAT; if (dateString === undefined) { return { isValid: false, parsed: "", message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat + ? dateFormat : "", }; }