diff --git a/app/client/.eslintrc.json b/app/client/.eslintrc.json index ba5f880ccc..8e7feb5f34 100644 --- a/app/client/.eslintrc.json +++ b/app/client/.eslintrc.json @@ -32,12 +32,13 @@ "react": { "pragma": "React", // Tells eslint-plugin-react to automatically detect the version of React to use - "version": "detect" + "version": "detect" } }, "env": { "browser": true, "node": true, - "cypress/globals": true + "cypress/globals": true, + "worker": true } } diff --git a/app/client/cypress/fixtures/newFormDsl.json b/app/client/cypress/fixtures/newFormDsl.json index ee06ab89bb..f321674a52 100644 --- a/app/client/cypress/fixtures/newFormDsl.json +++ b/app/client/cypress/fixtures/newFormDsl.json @@ -126,14 +126,12 @@ "label": "", "options": [ { - "id": "1", - "label": "Male", - "value": "M" + "label":"Male", + "value":"M" }, { - "id": "2", - "label": "Female", - "value": "F" + "label":"Female", + "value":"F" } ], "defaultOptionValue": "1", diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index f62b32c3a6..6bc87d181f 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -87,90 +87,6 @@ "userName": "Tobias Funke", "productName": "Beef steak", "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 - }, - { - "id": 2381224, - "email": "michael.lawson@reqres.in", - "userName": "Michael Lawson", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 2736212, - "email": "lindsay.ferguson@reqres.in", - "userName": "Lindsay Ferguson", - "productName": "Tuna Salad", - "orderAmount": 9.99 - }, - { - "id": 6788734, - "email": "tobias.funke@reqres.in", - "userName": "Tobias Funke", - "productName": "Beef steak", - "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 - }, - { - "id": 2381224, - "email": "michael.lawson@reqres.in", - "userName": "Michael Lawson", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 2736212, - "email": "lindsay.ferguson@reqres.in", - "userName": "Lindsay Ferguson", - "productName": "Tuna Salad", - "orderAmount": 9.99 - }, - { - "id": 6788734, - "email": "tobias.funke@reqres.in", - "userName": "Tobias Funke", - "productName": "Beef steak", - "orderAmount": 19.99 - }, - { - "id": 7434532, - "email": "byron.fields@reqres.in", - "userName": "Byron Fields", - "productName": "Chicken Sandwich", - "orderAmount": 4.99 - }, - { - "id": 7434532, - "email": "ryan.holmes@reqres.in", - "userName": "Ryan Holmes", - "productName": "Avocado Panini", - "orderAmount": 7.99 } ], "addInputWidgetBinding": "{{Table1.selectedRow.id", diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js index 16b98f57c5..62536d7ea3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/Binding_Table_Widget_DefaultSearch_Input_widget_spec.js @@ -27,7 +27,7 @@ describe("Binding the Table and input Widget", function() { .type("2736212", { force: true }); cy.get(commonlocators.editPropCrossButton).click(); cy.wait("@updateLayout").isSelectRow(0); - cy.readTabledataPublish("0", "0").then(tabData => { + cy.readTabledataPublish("0", "0").then((tabData) => { const tabValue = tabData; expect(tabValue).to.be.equal("2736212"); cy.log("the value is" + tabValue); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js index 7d0404578c..c6f78dbbad 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateAppWithSameName_spec.js @@ -6,7 +6,7 @@ describe("Create org and a new app / delete and recreate app", function() { it("create app within an org and delete and re-create another app with same name", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js index 9b906e31a2..3ae3b48dd8 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateDuplicateAppWithinOrg_spec.js @@ -6,7 +6,7 @@ describe("Create new org and an app within the same", function() { it("create multiple apps and validate", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); diff --git a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js index 06956c5dd4..62a13a40a9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/OrganisationTests/CreateOrgTests_spec.js @@ -2,13 +2,13 @@ const homePage = require("../../../locators/HomePage.json"); -describe("Create new org and share with a user", function () { +describe("Create new org and share with a user", function() { let orgid; let appid; - it("create org and then share with a user from UI", function () { + it("create org and then share with a user from UI", function() { cy.NavigateToHome(); - cy.generateUUID().then(uid => { + cy.generateUUID().then((uid) => { orgid = uid; appid = uid; localStorage.setItem("OrgName", orgid); @@ -25,7 +25,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate viewer privilage", function () { + it("login as invited user and then validate viewer privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -39,7 +39,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and update the invited user role to developer", function () { + it("login as Org owner and update the invited user role to developer", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -58,7 +58,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate developer privilage", function () { + it("login as invited user and then validate developer privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -76,7 +76,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and update the invited user role to administrator", function () { + it("login as Org owner and update the invited user role to administrator", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -95,7 +95,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as invited user and then validate administrator privilage", function () { + it("login as invited user and then validate administrator privilage", function() { cy.LogintoApp(Cypress.env("TESTUSERNAME1"), Cypress.env("TESTPASSWORD1")); cy.get(homePage.searchInput).type(appid); cy.wait(2000); @@ -108,7 +108,7 @@ describe("Create new org and share with a user", function () { cy.LogOut(); }); - it("login as Org owner and delete App ", function () { + it("login as Org owner and delete App ", function() { cy.LoginFromAPI(Cypress.env("USERNAME"), Cypress.env("PASSWORD")); cy.visit("/applications"); cy.wait("@applications").should( @@ -119,7 +119,7 @@ describe("Create new org and share with a user", function () { cy.get(homePage.searchInput).type(appid); cy.wait(2000); cy.navigateToOrgSettings(orgid); - cy.get(homePage.emailList).then(function ($list) { + cy.get(homePage.emailList).then(function($list) { expect($list).to.have.length(3); expect($list.eq(0)).to.contain(Cypress.env("USERNAME")); expect($list.eq(1)).to.contain(Cypress.env("TESTUSERNAME1")); diff --git a/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js index bc2aa2edfa..f9ddcc04c8 100644 --- a/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js +++ b/app/client/cypress/manual_TestSuite/Deletion _of_Duplicate_App.js @@ -1,19 +1,15 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Click on "Appsmith" to navigate to homepage - // Click on action icon - // Click on Delete option - // Click on "Are You Sure?" option - // Ensure the App gets deleted - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App.js b/app/client/cypress/manual_TestSuite/Duplicate_App.js index 46134860f6..8b70a6bbb3 100644 --- a/app/client/cypress/manual_TestSuite/Duplicate_App.js +++ b/app/client/cypress/manual_TestSuite/Duplicate_App.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Ensure the name is appended with the word "Copy" - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js index c724d651c8..1b6eb8240a 100644 --- a/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js +++ b/app/client/cypress/manual_TestSuite/Duplicate_App_Spec.js @@ -1,28 +1,22 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { - it("Duplicating an application", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Ensure the name is appended with the word "Copy" - } - ) - it("Deleting the duplicated Application ", function() - { - // Navigate to home Page - // Click on any application action icon (Three dots) - // Click on "Duplicate" option - // Ensure the application gets copied - // Click on "Appsmith" to navigate to homepage - // Click on action icon - // Click on Delete option - // Click on "Are You Sure?" option - // Ensure the App gets deleted - } - ) -} -) \ No newline at end of file +describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { + it("Duplicating an application", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Ensure the name is appended with the word "Copy" + }); + it("Deleting the duplicated Application ", function() { + // Navigate to home Page + // Click on any application action icon (Three dots) + // Click on "Duplicate" option + // Ensure the application gets copied + // Click on "Appsmith" to navigate to homepage + // Click on action icon + // Click on Delete option + // Click on "Are You Sure?" option + // Ensure the App gets deleted + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name.js b/app/client/cypress/manual_TestSuite/Organisation_Name.js index f4314ed760..4babed2641 100644 --- a/app/client/cypress/manual_TestSuite/Organisation_Name.js +++ b/app/client/cypress/manual_TestSuite/Organisation_Name.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Checking for error message on Organisation Name ", function() { - it("Ensure of Inactive Submit button ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Type "Space" as first character - // Ensure "Submit" button does not get Active - // Now click on "X" (Close icon) ensure the pop up closes - } - ) -} -) \ No newline at end of file +describe("Checking for error message on Organisation Name ", function() { + it("Ensure of Inactive Submit button ", function() { + // Navigate to home Page + // Click on Create Organisation + // Type "Space" as first character + // Ensure "Submit" button does not get Active + // Now click on "X" (Close icon) ensure the pop up closes + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js index 88ca89850f..a12574edbd 100644 --- a/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js +++ b/app/client/cypress/manual_TestSuite/Organisation_Name_Spec.js @@ -1,36 +1,28 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Checking for error message on Organisation Name ", function() { - it("Ensure of Inactive Submit button ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Type "Space" as first character - // Ensure "Submit" button does not get Active - // Now click on "X" (Close icon) ensure the pop up closes - } - ) - it("Reuse the name of the deleted application name ", function() - { - // Navigate to home Page - // Create an Application by name "XYZ" - // Add some widgets - // Navigate back to the application - // Delete the Application - // Click on "Create New" option under samee organisation - // Enter the name "XYZ" - // Ensure the application can be created with the same name - } - ) - it("Adding Special Character ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Add special as first character - // Ensure "Submit" get Active - // Now click outside and ensure the pop up closes - } - ) -} -) \ No newline at end of file +describe("Checking for error message on Organisation Name ", function() { + it("Ensure of Inactive Submit button ", function() { + // Navigate to home Page + // Click on Create Organisation + // Type "Space" as first character + // Ensure "Submit" button does not get Active + // Now click on "X" (Close icon) ensure the pop up closes + }); + it("Reuse the name of the deleted application name ", function() { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under samee organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + }); + it("Adding Special Character ", function() { + // Navigate to home Page + // Click on Create Organisation + // Add special as first character + // Ensure "Submit" get Active + // Now click outside and ensure the pop up closes + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js index 186ba3b586..879abc8b3b 100644 --- a/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js +++ b/app/client/cypress/manual_TestSuite/Reusing_Name_of_Deleted_App.js @@ -1,18 +1,14 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Reuse the name of the deleted application name inside the same organisation", function() { - it("Reuse the name of the deleted application name ", function() - { - // Navigate to home Page - // Create an Application by name "XYZ" - // Add some widgets - // Navigate back to the application - // Delete the Application - // Click on "Create New" option under samee organisation - // Enter the name "XYZ" - // Ensure the application can be created with the same name - } - ) -} -) \ No newline at end of file +describe("Reuse the name of the deleted application name inside the same organisation", function() { + it("Reuse the name of the deleted application name ", function() { + // Navigate to home Page + // Create an Application by name "XYZ" + // Add some widgets + // Navigate back to the application + // Delete the Application + // Click on "Create New" option under samee organisation + // Enter the name "XYZ" + // Ensure the application can be created with the same name + }); +}); diff --git a/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js index 195fc06229..39093565c6 100644 --- a/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js +++ b/app/client/cypress/manual_TestSuite/Spl_Chracter_Org_Name.js @@ -1,15 +1,11 @@ const homePage = require("../../../locators/HomePage.json"); - -describe("Adding Special Character ", function() { - it("Adding Special Character ", function() - { - // Navigate to home Page - // Click on Create Organisation - // Add special as first character - // Ensure "Submit" get Active - // Now click outside and ensure the pop up closes - } - ) -} -) \ No newline at end of file +describe("Adding Special Character ", function() { + it("Adding Special Character ", function() { + // Navigate to home Page + // Click on Create Organisation + // Add special as first character + // Ensure "Submit" get Active + // Now click outside and ensure the pop up closes + }); +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index ae358d6ec5..e5af4678fb 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -20,7 +20,7 @@ const explorer = require("../locators/explorerlocators.json"); let pageidcopy = " "; -Cypress.Commands.add("createOrg", orgName => { +Cypress.Commands.add("createOrg", (orgName) => { cy.get(homePage.createOrg) .should("be.visible") .first() @@ -45,7 +45,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("navigateToOrgSettings", orgName => { +Cypress.Commands.add("navigateToOrgSettings", (orgName) => { cy.get(homePage.orgList.concat(orgName).concat(")")) .scrollIntoView() .should("be.visible"); @@ -207,7 +207,7 @@ Cypress.Commands.add("updateUserRoleForOrg", (orgName, email, role) => { ); }); -Cypress.Commands.add("launchApp", appName => { +Cypress.Commands.add("launchApp", (appName) => { cy.get(homePage.appView) .should("be.visible") .first() @@ -239,7 +239,7 @@ Cypress.Commands.add("CreateAppForOrg", (orgName, appname) => { ); }); -Cypress.Commands.add("CreateApp", appname => { +Cypress.Commands.add("CreateApp", (appname) => { cy.get(homePage.createNew) .first() .click({ force: true }); @@ -259,7 +259,7 @@ Cypress.Commands.add("CreateApp", appname => { ); }); -Cypress.Commands.add("DeleteApp", appName => { +Cypress.Commands.add("DeleteApp", (appName) => { cy.get(commonlocators.homeIcon).click({ force: true }); cy.wait("@applications").should( "have.nested.property", @@ -312,13 +312,13 @@ Cypress.Commands.add("LoginFromAPI", (uname, pword) => { username: uname, password: pword, }, - }).then(response => { + }).then((response) => { expect(response.status).equal(302); cy.log(response.body); }); }); -Cypress.Commands.add("DeleteApp", appName => { +Cypress.Commands.add("DeleteApp", (appName) => { cy.get(commonlocators.homeIcon).click({ force: true }); cy.get(homePage.searchInput).type(appName); cy.wait(2000); @@ -360,7 +360,7 @@ Cypress.Commands.add("NavigateToHome", () => { ); }); -Cypress.Commands.add("NavigateToWidgets", pageName => { +Cypress.Commands.add("NavigateToWidgets", (pageName) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(".t--page-sidebar-" + pageName + "") .find(">div") @@ -371,7 +371,7 @@ Cypress.Commands.add("NavigateToWidgets", pageName => { cy.get("#loading").should("not.exist"); }); -Cypress.Commands.add("SearchApp", appname => { +Cypress.Commands.add("SearchApp", (appname) => { cy.get(homePage.searchInput).type(appname); cy.wait(2000); cy.get(homePage.applicationCard) @@ -398,7 +398,7 @@ Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => { ).should("not.be.visible"); }); -Cypress.Commands.add("GlobalSearchEntity", apiname1 => { +Cypress.Commands.add("GlobalSearchEntity", (apiname1) => { cy.get(commonlocators.entityExplorersearch).should("be.visible"); cy.get(commonlocators.entityExplorersearch) .clear() @@ -409,12 +409,12 @@ Cypress.Commands.add("GlobalSearchEntity", apiname1 => { ).should("be.visible"); }); -Cypress.Commands.add("ResponseStatusCheck", statusCode => { +Cypress.Commands.add("ResponseStatusCheck", (statusCode) => { cy.xpath(apiwidget.responseStatus).should("be.visible"); cy.xpath(apiwidget.responseStatus).contains(statusCode); }); -Cypress.Commands.add("ResponseCheck", textTocheck => { +Cypress.Commands.add("ResponseCheck", (textTocheck) => { //Explicit assert cy.get(apiwidget.responseText).should("be.visible"); }); @@ -433,7 +433,7 @@ Cypress.Commands.add("NavigateToEntityExplorer", () => { cy.get("#loading").should("not.exist"); }); -Cypress.Commands.add("CreateAPI", apiname => { +Cypress.Commands.add("CreateAPI", (apiname) => { cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); cy.get(apiwidget.resourceUrl).should("be.visible"); @@ -449,7 +449,7 @@ Cypress.Commands.add("CreateAPI", apiname => { cy.wait(2000); }); -Cypress.Commands.add("CreateSubsequentAPI", apiname => { +Cypress.Commands.add("CreateSubsequentAPI", (apiname) => { cy.get(apiwidget.createApiOnSideBar) .first() .click({ force: true }); @@ -462,7 +462,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => { cy.WaitAutoSave(); }); -Cypress.Commands.add("EditApiName", apiname => { +Cypress.Commands.add("EditApiName", (apiname) => { cy.get(apiwidget.ApiName).click({ force: true }); cy.get(apiwidget.apiTxt) .clear() @@ -471,7 +471,7 @@ Cypress.Commands.add("EditApiName", apiname => { cy.WaitAutoSave(); }); -Cypress.Commands.add("EditApiNameFromExplorer", apiname => { +Cypress.Commands.add("EditApiNameFromExplorer", (apiname) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -530,7 +530,7 @@ Cypress.Commands.add("validateRequest", (baseurl, path, verb) => { .click({ force: true }); }); -Cypress.Commands.add("SelectAction", action => { +Cypress.Commands.add("SelectAction", (action) => { cy.get(ApiEditor.ApiVerb) .first() .click({ force: true }); @@ -565,7 +565,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("SearchEntityandOpen", apiname1 => { +Cypress.Commands.add("SearchEntityandOpen", (apiname1) => { cy.get(commonlocators.entityExplorersearch).should("be.visible"); cy.get(commonlocators.entityExplorersearch) .clear() @@ -595,7 +595,7 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { .type(path, { parseSpecialCharSequences: false }); }); -Cypress.Commands.add("changeZoomLevel", zoomValue => { +Cypress.Commands.add("changeZoomLevel", (zoomValue) => { cy.get(commonlocators.changeZoomlevel).click(); cy.get("ul.bp3-menu") .children() @@ -609,7 +609,7 @@ Cypress.Commands.add("changeZoomLevel", zoomValue => { cy.get(commonlocators.selectedZoomlevel) .first() .invoke("text") - .then(text => { + .then((text) => { const someText = text; expect(someText).to.equal(zoomValue); }); @@ -663,14 +663,14 @@ Cypress.Commands.add("switchToAPIInputTab", () => { .click({ force: true }); }); -Cypress.Commands.add("selectPaginationType", option => { +Cypress.Commands.add("selectPaginationType", (option) => { cy.get(apiwidget.paginationOption) .first() .click({ force: true }); cy.xpath(option).click({ force: true }); }); -Cypress.Commands.add("clickTest", testbutton => { +Cypress.Commands.add("clickTest", (testbutton) => { cy.wait(2000); cy.wait("@saveAction"); cy.get(testbutton) @@ -726,7 +726,7 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { +Cypress.Commands.add("CreationOfUniqueAPIcheck", (apiname) => { cy.get(pages.addEntityAPI).click(); cy.get(apiwidget.createapi).click({ force: true }); cy.wait("@createNewApi"); @@ -738,13 +738,13 @@ Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { .type(apiname, { force: true }) .should("have.value", apiname) .focus(); - cy.get(".bp3-popover-content").should($x => { + cy.get(".bp3-popover-content").should(($x) => { console.log($x); expect($x).contain(apiname.concat(" is already being used.")); }); }); -Cypress.Commands.add("MoveAPIToHome", apiname => { +Cypress.Commands.add("MoveAPIToHome", (apiname) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -757,7 +757,7 @@ Cypress.Commands.add("MoveAPIToHome", apiname => { ); }); -Cypress.Commands.add("MoveAPIToPage", pageName => { +Cypress.Commands.add("MoveAPIToPage", (pageName) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -772,7 +772,7 @@ Cypress.Commands.add("MoveAPIToPage", pageName => { ); }); -Cypress.Commands.add("copyEntityToPage", pageName => { +Cypress.Commands.add("copyEntityToPage", (pageName) => { cy.xpath(apiwidget.popover) .last() .click({ force: true }); @@ -825,7 +825,7 @@ Cypress.Commands.add("deleteEntity", () => { cy.get(apiwidget.delete).click({ force: true }); }); -Cypress.Commands.add("DeleteAPI", apiname => { +Cypress.Commands.add("DeleteAPI", (apiname) => { cy.get(apiwidget.deleteAPI) .first() .click({ force: true }); @@ -896,14 +896,14 @@ Cypress.Commands.add("createModal", (modalType, ModalName) => { cy.get(".bp3-overlay-backdrop").click({ force: true }); }); -Cypress.Commands.add("CheckWidgetProperties", checkboxCss => { +Cypress.Commands.add("CheckWidgetProperties", (checkboxCss) => { cy.get(checkboxCss).check({ force: true, }); cy.assertPageSave(); }); -Cypress.Commands.add("UncheckWidgetProperties", checkboxCss => { +Cypress.Commands.add("UncheckWidgetProperties", (checkboxCss) => { cy.get(checkboxCss).uncheck({ force: true, }); @@ -933,13 +933,13 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(innercss).should("have.text", text); }); -Cypress.Commands.add("EvaluateDataType", dataType => { +Cypress.Commands.add("EvaluateDataType", (dataType) => { cy.get(commonlocators.evaluatedType) .should("be.visible") .contains(dataType); }); -Cypress.Commands.add("EvaluateCurrentValue", currentValue => { +Cypress.Commands.add("EvaluateCurrentValue", (currentValue) => { cy.wait(2000); cy.get(commonlocators.evaluatedCurrentValue) .should("be.visible") @@ -954,8 +954,8 @@ Cypress.Commands.add("PublishtheApp", () => { cy.assertPageSave(); // Stubbing window.open to open in the same tab - cy.window().then(window => { - cy.stub(window, "open").callsFake(url => { + cy.window().then((window) => { + cy.stub(window, "open").callsFake((url) => { window.location.href = Cypress.config().baseUrl + url.substring(1); window.location.target = "_self"; }); @@ -976,12 +976,12 @@ Cypress.Commands.add("getCodeMirror", () => { .type("{ctrl}{shift}{downarrow}"); }); -Cypress.Commands.add("testCodeMirror", value => { +Cypress.Commands.add("testCodeMirror", (value) => { cy.get(".CodeMirror textarea") .first() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .first() @@ -1009,7 +1009,7 @@ Cypress.Commands.add("testJsontext", (endp, value) => { .focus({ force: true }) .type("{uparrow}", { force: true }) .type("{ctrl}{shift}{downarrow}", { force: true }); - cy.focused().then($cm => { + cy.focused().then(($cm) => { if ($cm.contents != "") { cy.log("The field is empty"); cy.get(".t--property-control-" + endp + " .CodeMirror textarea") @@ -1028,14 +1028,14 @@ Cypress.Commands.add("testJsontext", (endp, value) => { cy.wait(200); }); -Cypress.Commands.add("selectShowMsg", value => { +Cypress.Commands.add("selectShowMsg", (value) => { cy.get(commonlocators.chooseAction) .children() .contains("Show Message") .click(); }); -Cypress.Commands.add("addSuccessMessage", value => { +Cypress.Commands.add("addSuccessMessage", (value) => { cy.get(commonlocators.chooseMsgType) .last() .click(); @@ -1052,12 +1052,12 @@ Cypress.Commands.add("SetDateToToday", () => { cy.assertPageSave(); }); -Cypress.Commands.add("enterActionValue", value => { +Cypress.Commands.add("enterActionValue", (value) => { cy.get(".CodeMirror textarea") .last() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .last() @@ -1079,7 +1079,7 @@ Cypress.Commands.add("enterActionValue", value => { }); }); -Cypress.Commands.add("enterNavigatePageName", value => { +Cypress.Commands.add("enterNavigatePageName", (value) => { cy.get("ul.tree") .children() .first() @@ -1088,7 +1088,7 @@ Cypress.Commands.add("enterNavigatePageName", value => { .first() .focus() .type("{ctrl}{shift}{downarrow}") - .then($cm => { + .then(($cm) => { if ($cm.val() !== "") { cy.get(".CodeMirror textarea") .first() @@ -1125,7 +1125,7 @@ Cypress.Commands.add("DeleteModal", () => { .click({ force: true }); }); -Cypress.Commands.add("Createpage", Pagename => { +Cypress.Commands.add("Createpage", (Pagename) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(pages.AddPage) .first() @@ -1148,7 +1148,7 @@ Cypress.Commands.add("Createpage", Pagename => { cy.wait(2000); }); -Cypress.Commands.add("Deletepage", Pagename => { +Cypress.Commands.add("Deletepage", (Pagename) => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(".t--page-sidebar-" + Pagename + ""); cy.get( @@ -1167,11 +1167,11 @@ Cypress.Commands.add("generateUUID", () => { return id.split("-")[0]; }); -Cypress.Commands.add("addDsl", dsl => { +Cypress.Commands.add("addDsl", (dsl) => { let currentURL; let pageid; let layoutId; - cy.url().then(url => { + cy.url().then((url) => { currentURL = url; const myRegexp = /pages(.*)/; const match = myRegexp.exec(currentURL); @@ -1180,7 +1180,7 @@ Cypress.Commands.add("addDsl", dsl => { cy.log(pageid + "page id"); //Fetch the layout id cy.server(); - cy.request("GET", "api/v1/pages/" + pageid).then(response => { + cy.request("GET", "api/v1/pages/" + pageid).then((response) => { const len = JSON.stringify(response.body); cy.log(len); layoutId = JSON.parse(len).data.layouts[0].id; @@ -1189,7 +1189,7 @@ Cypress.Commands.add("addDsl", dsl => { "PUT", "api/v1/layouts/" + layoutId + "/pages/" + pageid, dsl, - ).then(response => { + ).then((response) => { expect(response.status).equal(200); cy.reload(); }); @@ -1200,7 +1200,7 @@ Cypress.Commands.add("addDsl", dsl => { Cypress.Commands.add("DeleteAppByApi", () => { let currentURL; let appId; - cy.url().then(url => { + cy.url().then((url) => { currentURL = url; const myRegexp = /applications(.*)/; const match = myRegexp.exec(currentURL); @@ -1212,7 +1212,7 @@ Cypress.Commands.add("DeleteAppByApi", () => { } }); }); -Cypress.Commands.add("togglebar", value => { +Cypress.Commands.add("togglebar", (value) => { cy.get(value) .check({ force: true }) .should("be.checked"); @@ -1229,7 +1229,7 @@ Cypress.Commands.add("optionValue", (value, value2) => { .clear() .type(value2); }); -Cypress.Commands.add("dropdownDynamic", text => { +Cypress.Commands.add("dropdownDynamic", (text) => { cy.wait(2000); cy.get("ul[class='bp3-menu']") .first() @@ -1238,7 +1238,7 @@ Cypress.Commands.add("dropdownDynamic", text => { .should("have.text", text); }); -Cypress.Commands.add("getAlert", alertcss => { +Cypress.Commands.add("getAlert", (alertcss) => { cy.get(commonlocators.dropdownSelectButton).click({ force: true }); cy.get(widgetsPage.menubar) .contains("Show Alert") @@ -1268,18 +1268,18 @@ Cypress.Commands.add("tabVerify", (index, text) => { .should("be.visible"); }); -Cypress.Commands.add("togglebar", value => { +Cypress.Commands.add("togglebar", (value) => { cy.get(value) .check({ force: true }) .should("be.checked"); }); -Cypress.Commands.add("togglebarDisable", value => { +Cypress.Commands.add("togglebarDisable", (value) => { cy.get(value) .uncheck({ force: true }) .should("not.checked"); }); -Cypress.Commands.add("getAlert", alertcss => { +Cypress.Commands.add("getAlert", (alertcss) => { cy.get(commonlocators.dropdownSelectButton).click({ force: true }); cy.get(widgetsPage.menubar) .contains("Show Message") @@ -1473,7 +1473,7 @@ Cypress.Commands.add("createPostgresDatasource", () => { cy.testSaveDatasource(); }); -Cypress.Commands.add("deleteDatasource", datasourceName => { +Cypress.Commands.add("deleteDatasource", (datasourceName) => { cy.NavigateToQueryEditor(); cy.contains(".t--datasource-name", datasourceName) @@ -1553,7 +1553,7 @@ Cypress.Commands.add("dragAndDropToCanvas", (widgetType, { x, y }) => { .trigger("mouseup", { force: true }); }); -Cypress.Commands.add("executeDbQuery", queryName => { +Cypress.Commands.add("executeDbQuery", (queryName) => { cy.get(widgetsPage.buttonOnClick) .get(commonlocators.dropdownSelectButton) .click({ force: true }) @@ -1567,7 +1567,7 @@ Cypress.Commands.add("executeDbQuery", queryName => { .click({ force: true }); }); -Cypress.Commands.add("openPropertyPane", widgetType => { +Cypress.Commands.add("openPropertyPane", (widgetType) => { const selector = `.t--draggable-${widgetType}`; cy.get(selector) .first() @@ -1585,13 +1585,13 @@ Cypress.Commands.add("closePropertyPane", () => { Cypress.Commands.add("createAndFillApi", (url, parameters) => { cy.NavigateToApiEditor(); cy.testCreateApiButton(); - cy.get("@createNewApi").then(response => { + cy.get("@createNewApi").then((response) => { cy.get(ApiEditor.ApiNameField).should("be.visible"); cy.expect(response.response.body.responseMeta.success).to.eq(true); cy.get(ApiEditor.ApiNameField) .click() .invoke("text") - .then(text => { + .then((text) => { const someText = text; expect(someText).to.equal(response.response.body.data.name); }); @@ -1612,7 +1612,7 @@ Cypress.Commands.add("createAndFillApi", (url, parameters) => { cy.get(ApiEditor.ApiRunBtn).should("not.be.disabled"); }); -Cypress.Commands.add("isSelectRow", index => { +Cypress.Commands.add("isSelectRow", (index) => { cy.get( '.tbody .td[data-rowindex="' + index + '"][data-colindex="' + 0 + '"]', ).click({ force: true }); @@ -1640,13 +1640,13 @@ Cypress.Commands.add("setDate", (date, dateFormate) => { cy.get(sel).click(); }); -Cypress.Commands.add("pageNo", index => { +Cypress.Commands.add("pageNo", (index) => { cy.get(".page-item") .first() .click({ force: true }); }); -Cypress.Commands.add("pageNoValidate", index => { +Cypress.Commands.add("pageNoValidate", (index) => { const data = '.e-numericcontainer a[index="' + index + '"]'; const pageVal = cy.get(data); return pageVal; @@ -1661,7 +1661,7 @@ Cypress.Commands.add("validateEnableWidget", (widgetCss, disableCss) => { }); Cypress.Commands.add("validateHTMLText", (widgetCss, htmlTag, value) => { - cy.get(widgetCss + " iframe").then($iframe => { + cy.get(widgetCss + " iframe").then(($iframe) => { const $body = $iframe.contents().find("body"); cy.wrap($body) .find(htmlTag) @@ -1747,7 +1747,7 @@ Cypress.Commands.add("startServerAndRoutes", () => { cy.route("DELETE", "/api/v1/organizations/*/logo").as("deleteLogo"); }); -Cypress.Commands.add("alertValidate", text => { +Cypress.Commands.add("alertValidate", (text) => { cy.get(commonlocators.success) .should("be.visible") .and("have.text", text); @@ -1777,7 +1777,7 @@ Cypress.Commands.add("scrollTabledataPublish", (rowNum, colNum) => { return tabVal; }); -Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { +Cypress.Commands.add("assertEvaluatedValuePopup", (expectedType) => { cy.get(dynamicInputLocators.evaluatedValue) .should("be.visible") .children("p") @@ -1787,7 +1787,7 @@ Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { .should("have.text", expectedType); }); -Cypress.Commands.add("validateToastMessage", value => { +Cypress.Commands.add("validateToastMessage", (value) => { cy.get(commonlocators.toastMsg).should("have.text", value); }); @@ -1802,23 +1802,23 @@ Cypress.Commands.add("NavigateToPaginationTab", () => { .type("{enter}"); }); -Cypress.Commands.add("ValidateTableData", value => { +Cypress.Commands.add("ValidateTableData", (value) => { // cy.isSelectRow(0); - cy.readTabledata("0", "0").then(tabData => { + cy.readTabledata("0", "0").then((tabData) => { const tableData = tabData; expect(tableData).to.equal(value.toString()); }); }); -Cypress.Commands.add("ValidatePublishTableData", value => { +Cypress.Commands.add("ValidatePublishTableData", (value) => { cy.isSelectRow(0); - cy.readTabledataPublish("0", "0").then(tabData => { + cy.readTabledataPublish("0", "0").then((tabData) => { const tableData = tabData; expect(tableData).to.equal(value); }); }); -Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { +Cypress.Commands.add("ValidatePaginateResponseUrlData", (runTestCss) => { cy.SearchEntityandOpen("Api2"); cy.NavigateToPaginationTab(); cy.RunAPI(); @@ -1832,7 +1832,7 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { .contains("name") .siblings("span") .invoke("text") - .then(tabData => { + .then((tabData) => { const respBody = tabData.match(/"(.*)"/)[0]; localStorage.setItem("respBody", respBody); cy.log(respBody); @@ -1840,7 +1840,7 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { // cy.openPropertyPane("tablewidget"); // cy.testJsontext("tabledata", "{{Api2.data.results}}"); cy.isSelectRow(0); - cy.readTabledata("0", "1").then(tabData => { + cy.readTabledata("0", "1").then((tabData) => { const tableData = tabData; expect(`\"${tableData}\"`).to.equal(respBody); }); @@ -1849,13 +1849,13 @@ Cypress.Commands.add("ValidatePaginateResponseUrlData", runTestCss => { Cypress.Commands.add("ValidatePaginationInputData", () => { cy.isSelectRow(0); - cy.readTabledataPublish("0", "1").then(tabData => { + cy.readTabledataPublish("0", "1").then((tabData) => { const tableData = tabData; expect(`\"${tableData}\"`).to.equal(localStorage.getItem("respBody")); }); }); -Cypress.Commands.add("callApi", apiname => { +Cypress.Commands.add("callApi", (apiname) => { cy.get(commonlocators.callApi) .first() .click(); diff --git a/app/client/package.json b/app/client/package.json index 0b99883e20..e0c5331ab9 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -50,6 +50,7 @@ "copy-to-clipboard": "^3.3.1", "craco-alias": "^2.1.1", "cypress-log-to-output": "^1.1.2", + "deep-diff": "^1.0.2", "downloadjs": "^1.4.7", "eslint": "^7.11.0", "fast-deep-equal": "^3.1.1", @@ -166,6 +167,7 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.0.4", "@types/codemirror": "^0.0.96", + "@types/deep-diff": "^1.0.0", "@types/downloadjs": "^1.4.2", "@types/jest": "^24.0.22", "@types/react-beautiful-dnd": "^11.0.4", diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 6dff6c434b..0bda645322 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -78,7 +78,7 @@ export interface ActionApiResponse { } export interface ActionResponse { - body: Record; + body: unknown; headers: Record; request?: ActionApiResponseReq; statusCode: string; diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index faa39bfbc4..ec67a234e0 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -50,7 +50,6 @@ export const ReduxActionTypes: { [key: string]: string } = { WIDGET_MOVE: "WIDGET_MOVE", WIDGET_RESIZE: "WIDGET_RESIZE", WIDGET_DELETE: "WIDGET_DELETE", - WIDGETS_LOADING: "WIDGETS_LOADING", SHOW_PROPERTY_PANE: "SHOW_PROPERTY_PANE", UPDATE_WIDGET_PROPERTY_REQUEST: "UPDATE_WIDGET_PROPERTY_REQUEST", UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY", @@ -307,11 +306,13 @@ export const ReduxActionTypes: { [key: string]: string } = { CUT_SELECTED_WIDGET: "CUT_SELECTED_WIDGET", WIDGET_ADD_CHILDREN: "WIDGET_ADD_CHILDREN", SET_EVALUATED_TREE: "SET_EVALUATED_TREE", + SET_EVALUATION_DEPENDENCY_MAP: "SET_EVALUATION_DEPENDENCY_MAP", BATCH_UPDATES_SUCCESS: "BATCH_UPDATES_SUCCESS", UPDATE_CANVAS_STRUCTURE: "UPDATE_CANVAS_STRUCTURE", SET_SELECTED_WIDGET_ANCESTORY: "SET_SELECTED_WIDGET_ANCESTORY", START_EVALUATION: "START_EVALUATION", CURRENT_APPLICATION_NAME_UPDATE: "CURRENT_APPLICATION_NAME_UPDATE", + SET_WIDGET_LOADING: "SET_WIDGET_LOADING", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/entities/DataTree/dataTreeFactory.ts b/app/client/src/entities/DataTree/dataTreeFactory.ts index 81c87c7551..8364620b71 100644 --- a/app/client/src/entities/DataTree/dataTreeFactory.ts +++ b/app/client/src/entities/DataTree/dataTreeFactory.ts @@ -126,6 +126,9 @@ export class DataTreeFactory { const derivedPropertyMap = WidgetFactory.getWidgetDerivedPropertiesMap( widget.type, ); + const defaultProps = WidgetFactory.getWidgetDefaultPropertiesMap( + widget.type, + ); const derivedProps: any = {}; const dynamicBindingPathList = getEntityDynamicBindingPathList(widget); dynamicBindingPathList.forEach((dynamicPath) => { @@ -137,6 +140,7 @@ export class DataTreeFactory { } }); Object.keys(derivedPropertyMap).forEach((propertyName) => { + // TODO regex is too greedy derivedProps[propertyName] = derivedPropertyMap[propertyName].replace( /this./g, `${widget.widgetName}.`, @@ -145,11 +149,18 @@ export class DataTreeFactory { key: propertyName, }); }); + const unInitializedDefaultProps: Record = {}; + Object.values(defaultProps).forEach((propertyName) => { + if (!(propertyName in widget)) { + unInitializedDefaultProps[propertyName] = undefined; + } + }); dataTree[widget.widgetName] = { ...widget, ...defaultMetaProps, ...widgetMetaProps, ...derivedProps, + ...unInitializedDefaultProps, dynamicBindingPathList, ENTITY_TYPE: ENTITY_TYPE.WIDGET, }; diff --git a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx index 2588ca7dcc..fa01e3eb7f 100644 --- a/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx +++ b/app/client/src/reducers/entityReducers/canvasWidgetsReducer.tsx @@ -14,6 +14,7 @@ export type FlattenedWidgetProps = WidgetProps & { }; const canvasWidgetsReducer = createImmerReducer(initialState, { + // TODO Rename to INIT_LAYOUT [ReduxActionTypes.UPDATE_CANVAS]: ( state: CanvasWidgetsReduxState, action: ReduxAction, diff --git a/app/client/src/reducers/evaluationReducers/dependencyReducer.ts b/app/client/src/reducers/evaluationReducers/dependencyReducer.ts index 8800ec8dbb..92cbc5e543 100644 --- a/app/client/src/reducers/evaluationReducers/dependencyReducer.ts +++ b/app/client/src/reducers/evaluationReducers/dependencyReducer.ts @@ -1,21 +1,30 @@ import { createReducer } from "utils/AppsmithUtils"; import { ReduxAction, ReduxActionTypes } from "constants/ReduxActionConstants"; +import { DependencyMap } from "../../utils/DynamicBindingUtils"; export type EvaluationDependencyState = { - dependencyMap: Record>; + dependencyMap: DependencyMap; + inverseDependencyMap: DependencyMap; dependencyTree: Array<[string, string]>; }; const initialState: EvaluationDependencyState = { dependencyMap: {}, + inverseDependencyMap: {}, dependencyTree: [], }; const evaluationDependencyReducer = createReducer(initialState, { - [ReduxActionTypes.SET_EVALUATION_DEPENDENCIES]: ( + [ReduxActionTypes.SET_EVALUATION_DEPENDENCY_MAP]: ( state: EvaluationDependencyState, - action: ReduxAction, - ) => action.payload, + action: ReduxAction<{ + dependencyMap: DependencyMap; + inverseDependencyMap: DependencyMap; + }>, + ): EvaluationDependencyState => ({ + ...state, + ...action.payload, + }), }); export default evaluationDependencyReducer; diff --git a/app/client/src/reducers/evaluationReducers/treeReducer.ts b/app/client/src/reducers/evaluationReducers/treeReducer.ts index 5c95524c7e..79e2ae1f2a 100644 --- a/app/client/src/reducers/evaluationReducers/treeReducer.ts +++ b/app/client/src/reducers/evaluationReducers/treeReducer.ts @@ -11,6 +11,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, { state: EvaluatedTreeState, action: ReduxAction, ) => action.payload, + [ReduxActionTypes.FETCH_PAGE_INIT]: () => initialState, }); export default evaluatedTreeReducer; diff --git a/app/client/src/sagas/ActionExecutionSagas.ts b/app/client/src/sagas/ActionExecutionSagas.ts index 5598a95cc4..e02ddb60f4 100644 --- a/app/client/src/sagas/ActionExecutionSagas.ts +++ b/app/client/src/sagas/ActionExecutionSagas.ts @@ -81,7 +81,7 @@ import { getAppMode, getCurrentApplication, } from "selectors/applicationSelectors"; -import { evaluateDynamicTrigger, evaluateSingleValue } from "./evaluationsSaga"; +import { evaluateDynamicTrigger, evaluateSingleValue } from "./EvaluationsSaga"; function* navigateActionSaga( action: { pageNameOrUrl: string; params: Record }, diff --git a/app/client/src/sagas/evaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts similarity index 83% rename from app/client/src/sagas/evaluationsSaga.ts rename to app/client/src/sagas/EvaluationsSaga.ts index a4c3aeb317..211fe074ae 100644 --- a/app/client/src/sagas/evaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -1,4 +1,11 @@ -import { actionChannel, call, put, select, take } from "redux-saga/effects"; +import { + actionChannel, + call, + fork, + put, + select, + take, +} from "redux-saga/effects"; import { EvaluationReduxAction, @@ -19,7 +26,6 @@ import { EvalErrorTypes, } from "../utils/DynamicBindingUtils"; import log from "loglevel"; -import _ from "lodash"; import { WidgetType } from "../constants/WidgetConstants"; import { WidgetProps } from "../widgets/BaseWidget"; import PerformanceTracker, { @@ -70,26 +76,29 @@ function* evaluateTreeSaga(postEvalActions?: ReduxAction[]) { PerformanceTracker.startAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, ); - const unEvalTree = yield select(getUnevaluatedDataTree); - log.debug({ unEvalTree }); + const unevalTree = yield select(getUnevaluatedDataTree); + log.debug({ unevalTree }); const workerResponse = yield call( worker.request, EVAL_WORKER_ACTIONS.EVAL_TREE, { - dataTree: unEvalTree, + unevalTree, widgetTypeConfigMap, }, ); - const { errors, dataTree, logs } = workerResponse; - const parsedDataTree = JSON.parse(dataTree); + const { errors, dataTree, dependencies, logs } = workerResponse; + log.debug({ dataTree: dataTree }); logs.forEach((evalLog: any) => log.debug(evalLog)); - log.debug({ dataTree: parsedDataTree }); evalErrorHandler(errors); yield put({ type: ReduxActionTypes.SET_EVALUATED_TREE, - payload: parsedDataTree, + payload: dataTree, + }); + yield put({ + type: ReduxActionTypes.SET_EVALUATION_DEPENDENCY_MAP, + payload: dependencies, }); PerformanceTracker.stopAsyncTracking( PerformanceTransactionName.DATA_TREE_EVALUATION, @@ -173,6 +182,7 @@ export function* validateProperty( props: WidgetProps, ) { return yield call(worker.request, EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY, { + widgetTypeConfigMap, widgetType, property, value, @@ -223,19 +233,18 @@ const EVALUATE_REDUX_ACTIONS = [ ]; function evalQueueBuffer() { - let initialised = false; - let takable = false; + let canTake = false; let postEvalActions: any = []; const take = () => { - if (takable) { + if (canTake) { const resp = postEvalActions; postEvalActions = []; - takable = false; - return { postEvalActions: resp, type: "FAKE_ACTION" }; + canTake = false; + return { postEvalActions: resp, type: "BUFFERED_ACTION" }; } }; const flush = () => { - if (takable) { + if (canTake) { return [take() as Action]; } @@ -243,34 +252,7 @@ function evalQueueBuffer() { }; const put = (action: EvaluationReduxAction) => { - if (!initialised) { - if ( - ![ - ReduxActionTypes.FETCH_PAGE_SUCCESS, - ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS, - ].includes(action.type) - ) { - return; - } - initialised = true; - } - // When batching success action happens, we need to only evaluate - // if the batch had any action we need to evaluate properties for - if ( - action.type === ReduxActionTypes.BATCH_UPDATES_SUCCESS && - Array.isArray(action.payload) - ) { - const batchedActionTypes = action.payload.map( - (batchedAction: ReduxAction) => batchedAction.type, - ); - if ( - _.intersection(EVALUATE_REDUX_ACTIONS, batchedActionTypes).length === 0 - ) { - return; - } - } - - takable = true; + canTake = true; // TODO: If the action is the same as before, we can send only one and ignore duplicates. if (action.postEvalActions) { postEvalActions.push(...action.postEvalActions); @@ -281,7 +263,7 @@ function evalQueueBuffer() { take, put, isEmpty: () => { - return !takable; + return !canTake; }, flush, }; @@ -292,6 +274,8 @@ function* evaluationChangeListenerSaga() { yield call(worker.shutdown); yield call(worker.start); widgetTypeConfigMap = WidgetFactory.getWidgetTypeConfigMap(); + const initAction = yield take(FIRST_EVAL_REDUX_ACTIONS); + yield fork(evaluateTreeSaga, initAction.postEvalActions); const evtActionChannel = yield actionChannel( EVALUATE_REDUX_ACTIONS, evalQueueBuffer(), diff --git a/app/client/src/sagas/OnboardingSagas.ts b/app/client/src/sagas/OnboardingSagas.ts index f84da19c70..fe5396a5b3 100644 --- a/app/client/src/sagas/OnboardingSagas.ts +++ b/app/client/src/sagas/OnboardingSagas.ts @@ -128,7 +128,11 @@ function* listenForSuccessfullBinding() { if (widgetProperties.invalidProps) { bindSuccessfull = - bindSuccessfull && !("tableData" in widgetProperties.invalidProps); + bindSuccessfull && + !( + "tableData" in widgetProperties.invalidProps && + widgetProperties.invalidProps.tableData + ); } if (bindSuccessfull) { diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index a859fca89e..ea5d0a7837 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -70,7 +70,7 @@ import { setActionsToExecuteOnPageLoad, } from "actions/actionActions"; import { APP_MODE, UrlDataState } from "reducers/entityReducers/appReducer"; -import { clearEvalCache } from "./evaluationsSaga"; +import { clearEvalCache } from "./EvaluationsSaga"; import { getQueryParams } from "utils/AppsmithUtils"; import PerformanceTracker, { PerformanceTransactionName, diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 3fdba1060a..25c3340997 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -49,7 +49,7 @@ import { isPathADynamicTrigger, } from "utils/DynamicBindingUtils"; import { WidgetProps } from "widgets/BaseWidget"; -import _, { isString } from "lodash"; +import _ from "lodash"; import WidgetFactory from "utils/WidgetFactory"; import { buildWidgetBlueprint, @@ -88,7 +88,7 @@ import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { validateProperty, clearEvalPropertyCacheOfWidget, -} from "./evaluationsSaga"; +} from "./EvaluationsSaga"; import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer"; import { Toaster } from "components/ads/Toast"; import { Variant } from "components/ads/common"; diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index b3a1ef6c63..74df3751f6 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -19,9 +19,8 @@ import queryPaneSagas from "./QueryPaneSagas"; import modalSagas from "./ModalSagas"; import batchSagas from "./BatchSagas"; import themeSagas from "./ThemeSaga"; -import evaluationsSaga from "./evaluationsSaga"; +import evaluationsSaga from "./EvaluationsSaga"; import onboardingSaga from "./OnboardingSagas"; - import log from "loglevel"; import * as sentry from "@sentry/react"; diff --git a/app/client/src/selectors/dataTreeSelectors.ts b/app/client/src/selectors/dataTreeSelectors.ts index be6b42dac4..fd9d722303 100644 --- a/app/client/src/selectors/dataTreeSelectors.ts +++ b/app/client/src/selectors/dataTreeSelectors.ts @@ -33,6 +33,12 @@ export const getUnevaluatedDataTree = createSelector( }, ); +export const getEvaluationDependencyMap = (state: AppState) => + state.evaluations.dependencies.dependencyMap; + +export const getEvaluationInverseDependencyMap = (state: AppState) => + state.evaluations.dependencies.inverseDependencyMap; + /** * returns evaluation tree object * diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 8178cced84..f5596faa0f 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -123,9 +123,9 @@ export const getCanvasWidgetDsl = createSelector( const widgets: Record = {}; Object.keys(canvasWidgets).forEach((widgetKey) => { const canvasWidget = canvasWidgets[widgetKey]; - const evaluatedWidget = evaluatedDataTree[ - canvasWidget.widgetName - ] as DataTreeWidget; + const evaluatedWidget = _.find(evaluatedDataTree, { + widgetId: widgetKey, + }) as DataTreeWidget; if (evaluatedWidget) { widgets[widgetKey] = createCanvasWidget(canvasWidget, evaluatedWidget); } else { diff --git a/app/client/src/utils/DynamicBindingUtils.ts b/app/client/src/utils/DynamicBindingUtils.ts index 3e26969a1a..398fc852c8 100644 --- a/app/client/src/utils/DynamicBindingUtils.ts +++ b/app/client/src/utils/DynamicBindingUtils.ts @@ -8,6 +8,8 @@ import moment from "moment-timezone"; import { WidgetProps } from "../widgets/BaseWidget"; import parser from "fast-xml-parser"; +export type DependencyMap = Record>; + export const removeBindingsFromActionObject = (obj: Action) => { const string = JSON.stringify(obj); const withBindings = string.replace(DATA_BIND_REGEX_GLOBAL, "{{ }}"); @@ -87,6 +89,7 @@ export enum EvalErrorTypes { EVAL_TREE_ERROR = "EVAL_TREE_ERROR", UNESCAPE_STRING_ERROR = "UNESCAPE_STRING_ERROR", EVAL_ERROR = "EVAL_ERROR", + UNKNOWN_ERROR = "UNKNOWN_ERROR", BAD_UNEVAL_TREE_ERROR = "BAD_UNEVAL_TREE_ERROR", } diff --git a/app/client/src/utils/DynamicBindingsUtil.test.ts b/app/client/src/utils/DynamicBindingsUtil.test.ts index 05fcfefe32..ef0bea5619 100644 --- a/app/client/src/utils/DynamicBindingsUtil.test.ts +++ b/app/client/src/utils/DynamicBindingsUtil.test.ts @@ -1,213 +1,32 @@ -// import { -// mockExecute, -// mockRegisterLibrary, -// } from "../../test/__mocks__/RealmExecutorMock"; -// import { -// dependencySortedEvaluateDataTree, -// getDynamicValue, -// getEntityDependencies, -// parseDynamicString, -// } from "./DynamicBindingUtils"; -// import { DataTree, ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -// import { RenderModes, WidgetTypes } from "constants/WidgetConstants"; -// -// beforeAll(() => { -// mockRegisterLibrary.mockClear(); -// mockExecute.mockClear(); -// }); -// -// it("Gets the value from the data tree", () => { -// const dynamicBinding = "{{GetUsers.data}}"; -// const nameBindingsWithData: DataTree = { -// GetUsers: { -// data: { text: "correct data" }, -// config: { -// pluginId: "", -// id: "id", -// name: "text", -// actionConfiguration: {}, -// pageId: "", -// jsonPathKeys: [], -// datasource: { id: "" }, -// pluginType: "1", -// }, -// isLoading: false, -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// run: jest.fn(), -// }, -// }; -// const actualValue = { result: { text: "correct data" } }; -// -// const value = getDynamicValue(dynamicBinding, nameBindingsWithData); -// -// expect(value).toEqual(actualValue); -// }); -// -// describe.each([ -// ["{{A}}", ["{{A}}"]], -// ["A {{B}}", ["A ", "{{B}}"]], -// [ -// "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", -// [ -// "Hello ", -// "{{Customer.Name}}", -// ", the status for your order id ", -// "{{orderId}}", -// " is ", -// "{{status}}", -// ], -// ], -// [ -// "{{data.map(datum => {return {id: datum}})}}", -// ["{{data.map(datum => {return {id: datum}})}}"], -// ], -// ["{{}}{{}}}", ["{{}}", "{{}}", "}"]], -// ["{{{}}", ["{{{}}"]], -// ["{{ {{", ["{{ {{"]], -// ["}} }}", ["}} }}"]], -// ["}} {{", ["}} {{"]], -// ])("Parse the dynamic string(%s, %j)", (dynamicString, expected) => { -// test(`returns ${expected}`, () => { -// expect(parseDynamicString(dynamicString as string)).toStrictEqual(expected); -// }); -// }); -// -// const baseWidgetProps = { -// parentColumnSpace: 0, -// parentRowSpace: 0, -// parentId: "0", -// type: WidgetTypes.BUTTON_WIDGET, -// renderMode: RenderModes.CANVAS, -// leftColumn: 0, -// rightColumn: 0, -// topRow: 0, -// bottomRow: 0, -// isLoading: false, -// }; -// -// it("evaluates the data tree", () => { -// const input: DataTree = { -// widget1: { -// ...baseWidgetProps, -// widgetId: "1", -// widgetName: "widget1", -// displayValue: "{{widget2.computedProperty}}", -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// widget2: { -// ...baseWidgetProps, -// widgetId: "2", -// widgetName: "widget2", -// computedProperty: "{{ widget2.data[widget2.index] }}", -// data: "{{ apiData.data }}", -// index: 2, -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// apiData: { -// config: { -// id: "123", -// pageId: "1234", -// datasource: {}, -// name: "api", -// actionConfiguration: {}, -// jsonPathKeys: [], -// pluginId: "plugin", -// }, -// run: (onSuccess, onError) => ({ -// type: "RUN_ACTION", -// payload: { -// actionId: "", -// onSuccess: "", -// onError: "", -// }, -// }), -// isLoading: false, -// data: ["wrong value", "still wrong", "correct"], -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// }, -// }; -// -// const dynamicBindings = { -// "widget1.displayValue": ["widget2.computedProperty"], -// "widget2.computedProperty": ["widget2.data", "widget2.index"], -// "widget2.data": ["apiData.data"], -// }; -// -// const sortedDeps = [ -// "apiData.data", -// "widget2.data", -// "widget2.index", -// "widget2.computedProperty", -// "widget1.displayValue", -// ]; -// -// const output: DataTree = { -// widget1: { -// ...baseWidgetProps, -// widgetId: "1", -// widgetName: "widget1", -// displayValue: "correct", -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// widget2: { -// ...baseWidgetProps, -// widgetId: "2", -// widgetName: "widget2", -// computedProperty: "correct", -// data: ["wrong value", "still wrong", "correct"], -// index: 2, -// ENTITY_TYPE: ENTITY_TYPE.WIDGET, -// }, -// apiData: { -// config: { -// id: "123", -// pageId: "1234", -// datasource: {}, -// name: "api", -// actionConfiguration: {}, -// jsonPathKeys: [], -// pluginId: "plugin", -// }, -// run: (onSuccess, onError) => ({ -// type: "RUN_ACTION", -// payload: { -// actionId: "", -// onSuccess: "", -// onError: "", -// }, -// }), -// isLoading: false, -// data: ["wrong value", "still wrong", "correct"], -// ENTITY_TYPE: ENTITY_TYPE.ACTION, -// }, -// }; -// -// const result = dependencySortedEvaluateDataTree( -// input, -// dynamicBindings, -// sortedDeps, -// ); -// expect(result).toEqual(output); -// }); -// -// it("finds dependencies of a entity", () => { -// const depMap: Array<[string, string]> = [ -// ["Widget5.text", "Widget2.data.visible"], -// ["Widget1.options", "Action1.data"], -// ["Widget2.text", "Widget1.selectedOption"], -// ["Widget3.text", "Widget4.selectedRow.name"], -// ["Widget6.label", "Action1.data.label"], -// ]; -// const entity = "Action1"; -// const result = ["Widget1", "Widget2", "Widget5", "Widget6"]; -// -// const actual = getEntityDependencies(depMap, entity); -// -// expect(actual).toEqual(result); -// }); +import { getDynamicStringSegments } from "./DynamicBindingUtils"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: No types available -it("does nothing. needs implementing", () => { - expect(1 + 1).toEqual(2); +describe.each([ + ["{{A}}", ["{{A}}"]], + ["A {{B}}", ["A ", "{{B}}"]], + [ + "Hello {{Customer.Name}}, the status for your order id {{orderId}} is {{status}}", + [ + "Hello ", + "{{Customer.Name}}", + ", the status for your order id ", + "{{orderId}}", + " is ", + "{{status}}", + ], + ], + [ + "{{data.map(datum => {return {id: datum}})}}", + ["{{data.map(datum => {return {id: datum}})}}"], + ], + ["{{}}{{}}}", ["{{}}", "{{}}", "}"]], + ["{{{}}", ["{{{}}"]], + ["{{ {{", ["{{ {{"]], + ["}} }}", ["}} }}"]], + ["}} {{", ["}} {{"]], +])("Parse the dynamic string(%s, %j)", (dynamicString, expected) => { + test(`returns ${expected}`, () => { + expect(getDynamicStringSegments(dynamicString as string)).toStrictEqual( + expected, + ); + }); }); diff --git a/app/client/src/utils/PerformanceTracker.ts b/app/client/src/utils/PerformanceTracker.ts index b305332ebb..7aa9216b4b 100644 --- a/app/client/src/utils/PerformanceTracker.ts +++ b/app/client/src/utils/PerformanceTracker.ts @@ -7,6 +7,8 @@ import * as log from "loglevel"; export enum PerformanceTransactionName { DEPLOY_APPLICATION = "DEPLOY_APPLICATION", DATA_TREE_EVALUATION = "DATA_TREE_EVALUATION", + DATA_TREE_WORKER_EVALUATION = "DATA_TREE_WORKER_EVALUATION", + EVAL_REDUX_UPDATE = "EVAL_REDUX_UPDATE", CONSTRUCT_UNEVAL_TREE = "CONSTRUCT_UNEVAL_TREE", CONSTRUCT_CANVAS_DSL = "CONSTRUCT_CANVAS_DSL", CREATE_DEPENDENCIES = "CREATE_DEPENDENCIES", @@ -38,6 +40,7 @@ export enum PerformanceTransactionName { USER_ME_API = "USER_ME_API", SIGN_UP = "SIGN_UP", LOGIN_CLICK = "LOGIN_CLICK", + SET_EVALUATED = "SET_EVALUATED", } export enum PerformanceTagNames { diff --git a/app/client/src/widgets/BaseWidget.tsx b/app/client/src/widgets/BaseWidget.tsx index a6efc38aae..c0d95ea54c 100644 --- a/app/client/src/widgets/BaseWidget.tsx +++ b/app/client/src/widgets/BaseWidget.tsx @@ -72,7 +72,7 @@ abstract class BaseWidget< static getDefaultPropertiesMap(): Record { return {}; } - + // TODO Find a way to enforce this, (dont let it be set) static getMetaPropertiesMap(): Record { return {}; } diff --git a/app/client/src/widgets/ImageWidget.test.tsx b/app/client/src/widgets/ImageWidget.test.tsx index d58fef38f8..e4dd57c812 100644 --- a/app/client/src/widgets/ImageWidget.test.tsx +++ b/app/client/src/widgets/ImageWidget.test.tsx @@ -123,6 +123,8 @@ // ); // }); // }); +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore it("does nothing. needs implementing", () => { expect(1 + 1).toEqual(2); }); diff --git a/app/client/src/widgets/InputWidget.tsx b/app/client/src/widgets/InputWidget.tsx index 3c94422144..f186283381 100644 --- a/app/client/src/widgets/InputWidget.tsx +++ b/app/client/src/widgets/InputWidget.tsx @@ -55,7 +55,7 @@ class InputWidget extends BaseWidget { if (this.regex) { /* * break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags - * Example /appsmith/i will be split into ["/appsmith/gi", "/", "appsmith", "gi"] + * Example /test/i will be split into ["/test/gi", "/", "test", "gi"] */ const regexParts = this.regex.match(/(\\/?)(.+)\\1([a-z]*)/i); diff --git a/app/client/src/widgets/MetaHOC.tsx b/app/client/src/widgets/MetaHOC.tsx index ececa772e8..a248aa4ecb 100644 --- a/app/client/src/widgets/MetaHOC.tsx +++ b/app/client/src/widgets/MetaHOC.tsx @@ -2,7 +2,7 @@ import React from "react"; import BaseWidget, { WidgetProps } from "./BaseWidget"; import _ from "lodash"; import { EditorContext } from "../components/editorComponents/EditorContextProvider"; -import { clearEvalPropertyCache } from "sagas/evaluationsSaga"; +import { clearEvalPropertyCache } from "sagas/EvaluationsSaga"; import { ExecuteActionPayload } from "../constants/ActionConstants"; type DebouncedExecuteActionPayload = Omit< diff --git a/app/client/src/widgets/TabsWidget.tsx b/app/client/src/widgets/TabsWidget.tsx index 3fee66d9bf..def8801780 100644 --- a/app/client/src/widgets/TabsWidget.tsx +++ b/app/client/src/widgets/TabsWidget.tsx @@ -38,6 +38,12 @@ class TabsWidget extends BaseWidget< }; } + static getMetaPropertiesMap() { + return { + selectedTabWidgetId: undefined, + }; + } + static getDefaultPropertiesMap(): Record { return {}; } diff --git a/app/client/src/workers/evaluation.test.ts b/app/client/src/workers/evaluation.test.ts new file mode 100644 index 0000000000..06718f2ac7 --- /dev/null +++ b/app/client/src/workers/evaluation.test.ts @@ -0,0 +1,767 @@ +import { DataTreeEvaluator } from "./evaluation.worker"; +import { + DataTreeAction, + DataTreeWidget, + ENTITY_TYPE, +} from "../entities/DataTree/dataTreeFactory"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import { RenderModes, WidgetTypes } from "../constants/WidgetConstants"; +import { PluginType } from "../entities/Action"; + +const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = { + CONTAINER_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + TEXT_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + textStyle: "TEXT", + shouldScroll: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: { + value: "{{ this.text }}", + }, + triggerProperties: {}, + metaProperties: {}, + }, + BUTTON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + buttonStyle: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + INPUT_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + inputType: "TEXT", + defaultText: "TEXT", + text: "TEXT", + regex: "REGEX", + errorMessage: "TEXT", + placeholderText: "TEXT", + maxChars: "NUMBER", + minNum: "NUMBER", + maxNum: "NUMBER", + label: "TEXT", + inputValidators: "ARRAY", + focusIndex: "NUMBER", + isAutoFocusEnabled: "BOOLEAN", + isRequired: "BOOLEAN", + isValid: "BOOLEAN", + }, + defaultProperties: { + text: "defaultText", + }, + derivedProperties: { + isValid: + '{{\n function(){\n let parsedRegex = null;\n if (this.regex) {\n /*\n * break up the regexp pattern into 4 parts: given regex, regex prefix , regex pattern, regex flags\n * Example /test/i will be split into ["/test/gi", "/", "test", "gi"]\n */\n const regexParts = this.regex.match(/(\\/?)(.+)\\1([a-z]*)/i);\n if (!regexParts) {\n parsedRegex = new RegExp(this.regex);\n } else {\n /*\n * if we don\'t have a regex flags (gmisuy), convert provided string into regexp directly\n /*\n if (regexParts[3] && !/^(?!.*?(.).*?\\1)[gmisuy]+$/.test(regexParts[3])) {\n parsedRegex = RegExp(this.regex);\n }\n /*\n * if we have a regex flags, use it to form regexp\n */\n parsedRegex = new RegExp(regexParts[2], regexParts[3]);\n }\n }\n if (this.inputType === "EMAIL") {\n const emailRegex = new RegExp(/^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$/);\n return emailRegex.test(this.text);\n }\n else if (this.inputType === "NUMBER") {\n return !isNaN(this.text)\n }\n else if (this.isRequired) {\n if(this.text && this.text.length) {\n if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n } else {\n return false;\n }\n } if (parsedRegex) {\n return parsedRegex.test(this.text)\n } else {\n return true;\n }\n }()\n }}', + value: "{{this.text}}", + }, + triggerProperties: { + onTextChanged: true, + }, + metaProperties: { + isFocused: false, + isDirty: false, + }, + }, + CHECKBOX_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + defaultCheckedState: "BOOLEAN", + }, + defaultProperties: { + isChecked: "defaultCheckedState", + }, + derivedProperties: { + value: "{{this.isChecked}}", + }, + triggerProperties: { + onCheckChange: true, + }, + metaProperties: {}, + }, + DROP_DOWN_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + placeholderText: "TEXT", + label: "TEXT", + options: "OPTIONS_DATA", + selectionType: "TEXT", + isRequired: "BOOLEAN", + selectedOptionValues: "ARRAY", + defaultOptionValue: "DEFAULT_OPTION_VALUE", + }, + defaultProperties: { + selectedOptionValue: "defaultOptionValue", + selectedOptionValueArr: "defaultOptionValue", + }, + derivedProperties: { + isValid: + "{{this.isRequired ? this.selectionType === 'SINGLE_SELECT' ? !!this.selectedOption : !!this.selectedIndexArr && this.selectedIndexArr.length > 0 : true}}", + selectedOption: + "{{ this.selectionType === 'SINGLE_SELECT' ? _.find(this.options, { value: this.selectedOptionValue }) : undefined}}", + selectedOptionArr: + '{{this.selectionType === "MULTI_SELECT" ? this.options.filter(opt => _.includes(this.selectedOptionValueArr, opt.value)) : undefined}}', + selectedIndex: + "{{ _.findIndex(this.options, { value: this.selectedOption.value } ) }}", + selectedIndexArr: + "{{ this.selectedOptionValueArr.map(o => _.findIndex(this.options, { value: o })) }}", + value: + "{{ this.selectionType === 'SINGLE_SELECT' ? this.selectedOptionValue : this.selectedOptionValueArr }}", + selectedOptionValues: "{{ this.selectedOptionValueArr }}", + }, + triggerProperties: { + onOptionChange: true, + }, + metaProperties: {}, + }, + RADIO_GROUP_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + options: "OPTIONS_DATA", + selectedOptionValue: "TEXT", + defaultOptionValue: "TEXT", + isRequired: "BOOLEAN", + }, + defaultProperties: { + selectedOptionValue: "defaultOptionValue", + }, + derivedProperties: { + selectedOption: + "{{_.find(this.options, { value: this.selectedOptionValue })}}", + isValid: "{{ this.isRequired ? !!this.selectedOptionValue : true }}", + value: "{{this.selectedOptionValue}}", + }, + triggerProperties: { + onSelectionChange: true, + }, + metaProperties: {}, + }, + IMAGE_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + image: "TEXT", + imageShape: "TEXT", + defaultImage: "TEXT", + maxZoomLevel: "NUMBER", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + TABLE_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + tableData: "TABLE_DATA", + nextPageKey: "TEXT", + prevPageKey: "TEXT", + label: "TEXT", + searchText: "TEXT", + defaultSearchText: "TEXT", + defaultSelectedRow: "DEFAULT_SELECTED_ROW", + }, + defaultProperties: { + searchText: "defaultSearchText", + selectedRowIndex: "defaultSelectedRow", + selectedRowIndices: "defaultSelectedRow", + }, + derivedProperties: { + selectedRow: `{{ _.get(this.filteredTableData, this.selectedRowIndex, _.mapValues(this.filteredTableData[0], () => undefined)) }}`, + selectedRows: `{{ this.filteredTableData.filter((item, i) => selectedRowIndices.includes(i) }); }}`, + }, + triggerProperties: { + onRowSelected: true, + onPageChange: true, + onSearchTextChanged: true, + columnActions: true, + }, + metaProperties: { + pageNo: 1, + selectedRow: {}, + selectedRows: [], + }, + }, + VIDEO_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + url: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onEnd: true, + onPlay: true, + onPause: true, + }, + metaProperties: { + playState: "NOT_STARTED", + }, + }, + FILE_PICKER_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + label: "TEXT", + maxNumFiles: "NUMBER", + allowedFileTypes: "ARRAY", + files: "ARRAY", + isRequired: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: { + isValid: "{{ this.isRequired ? this.files.length > 0 : true }}", + value: "{{this.files}}", + }, + triggerProperties: { + onFilesSelected: true, + }, + metaProperties: { + files: [], + uploadedFileData: {}, + }, + }, + DATE_PICKER_WIDGET: { + 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", + defaultTab: "SELECTED_TAB", + }, + defaultProperties: {}, + derivedProperties: { + selectedTab: + "{{_.find(this.tabs, { widgetId: this.selectedTabWidgetId }).label}}", + }, + triggerProperties: { + onTabSelected: true, + }, + metaProperties: {}, + }, + MODAL_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + RICH_TEXT_EDITOR_WIDGET: { + validations: { + text: "TEXT", + placeholder: "TEXT", + defaultValue: "TEXT", + isDisabled: "BOOLEAN", + isVisible: "BOOLEAN", + }, + defaultProperties: { + text: "defaultText", + }, + derivedProperties: { + value: "{{this.text}}", + }, + triggerProperties: { + onTextChange: true, + }, + metaProperties: {}, + }, + CHART_WIDGET: { + validations: { + xAxisName: "TEXT", + yAxisName: "TEXT", + chartName: "TEXT", + isVisible: "BOOLEAN", + chartData: "CHART_DATA", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + FORM_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + FORM_BUTTON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + text: "TEXT", + disabledWhenInvalid: "BOOLEAN", + buttonStyle: "TEXT", + buttonType: "TEXT", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + MAP_WIDGET: { + validations: { + defaultMarkers: "MARKERS", + isDisabled: "BOOLEAN", + isVisible: "BOOLEAN", + enableSearch: "BOOLEAN", + enablePickLocation: "BOOLEAN", + allowZoom: "BOOLEAN", + zoomLevel: "NUMBER", + mapCenter: "OBJECT", + }, + defaultProperties: { + markers: "defaultMarkers", + center: "mapCenter", + }, + derivedProperties: {}, + triggerProperties: { + onMarkerClick: true, + onCreateMarker: true, + }, + metaProperties: {}, + }, + CANVAS_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, + ICON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: { + onClick: true, + }, + metaProperties: {}, + }, + SKELETON_WIDGET: { + validations: { + isLoading: "BOOLEAN", + isVisible: "BOOLEAN", + isDisabled: "BOOLEAN", + }, + defaultProperties: {}, + derivedProperties: {}, + triggerProperties: {}, + metaProperties: {}, + }, +}; + +const BASE_WIDGET: DataTreeWidget = { + widgetId: "randomID", + widgetName: "randomName", + bottomRow: 0, + isLoading: false, + leftColumn: 0, + parentColumnSpace: 0, + parentRowSpace: 0, + renderMode: RenderModes.CANVAS, + rightColumn: 0, + topRow: 0, + type: WidgetTypes.SKELETON_WIDGET, + parentId: "0", + ENTITY_TYPE: ENTITY_TYPE.WIDGET, +}; + +const BASE_ACTION: DataTreeAction = { + actionId: "randomId", + name: "randomName", + config: { + timeoutInMillisecond: 10, + }, + dynamicBindingPathList: [], + isLoading: false, + pluginType: PluginType.API, + run: {}, + data: {}, + ENTITY_TYPE: ENTITY_TYPE.ACTION, +}; + +describe("DataTreeEvaluator", () => { + const unEvalTree = { + Text1: { + ...BASE_WIDGET, + widgetName: "Text1", + text: "Label", + type: WidgetTypes.TEXT_WIDGET, + }, + Text2: { + ...BASE_WIDGET, + widgetName: "Text2", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + Text3: { + ...BASE_WIDGET, + widgetName: "Text3", + text: "{{Text1.text}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + Dropdown1: { + ...BASE_WIDGET, + options: [ + { + label: "test", + value: "valueTest", + }, + { + label: "test2", + value: "valueTest2", + }, + ], + type: WidgetTypes.DROP_DOWN_WIDGET, + }, + Table1: { + ...BASE_WIDGET, + tableData: "{{Api1.data.map(datum => ({ ...datum, raw: Text1.text }) )}}", + dynamicBindingPathList: [{ key: "tableData" }], + type: WidgetTypes.TABLE_WIDGET, + }, + Text4: { + ...BASE_WIDGET, + text: "{{Table1.selectedRow.test}}", + dynamicBindingPathList: [{ key: "text" }], + type: WidgetTypes.TEXT_WIDGET, + }, + }; + const evaluator = new DataTreeEvaluator(WIDGET_CONFIG_MAP); + evaluator.createFirstTree(unEvalTree); + it("Evaluates a binding in first run", () => { + const evaluation = evaluator.evalTree; + const dependencyMap = evaluator.dependencyMap; + + expect(evaluation).toHaveProperty("Text2.text", "Label"); + expect(evaluation).toHaveProperty("Text3.text", "Label"); + expect(dependencyMap).toStrictEqual({ + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + }); + }); + + it("Evaluates a value change in update run", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Text1: { + ...unEvalTree.Text1, + text: "Hey there", + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty("Text2.text", "Hey there"); + expect(updatedEvalTree).toHaveProperty("Text3.text", "Hey there"); + }); + + it("Evaluates a dependency change in update run", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Text3: { + ...unEvalTree.Text3, + text: "Label 3", + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Text2.text", "Label"); + expect(updatedEvalTree).toHaveProperty("Text3.text", "Label 3"); + expect(updatedDependencyMap).toStrictEqual({ + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + }); + }); + + it("Overrides with default value", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Input1: { + ...BASE_WIDGET, + text: undefined, + defaultText: "Default value", + widgetName: "Input1", + type: WidgetTypes.INPUT_WIDGET, + }, + }; + + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty("Input1.text", "Default value"); + }); + + it("Evaluates for value changes in nested diff paths", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Dropdown1: { + ...BASE_WIDGET, + options: [ + { + label: "newValue", + value: "valueTest", + }, + { + label: "test2", + value: "valueTest2", + }, + ], + type: WidgetTypes.DROP_DOWN_WIDGET, + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + expect(updatedEvalTree).toHaveProperty( + "Dropdown1.options.0.label", + "newValue", + ); + }); + + it("Adds an entity with a complicated binding", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Api1: { + ...BASE_ACTION, + data: [ + { + test: "Hey", + }, + { + test: "Ho", + }, + ], + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + { + test: "Hey", + raw: "Label", + }, + { + test: "Ho", + raw: "Label", + }, + ]); + expect(updatedDependencyMap).toStrictEqual({ + Api1: ["Api1.data"], + Input1: ["Input1.text"], + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.searchText", + "Table1.selectedRowIndex", + "Table1.selectedRowIndices", + ], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Api1.data", "Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": [], + "Input1.text": [], + }); + }); + + it("Selects a row", () => { + const updatedUnEvalTree = { + ...unEvalTree, + Table1: { + ...unEvalTree.Table1, + selectedRowIndex: 0, + selectedRow: { + test: "Hey", + raw: "Label", + }, + }, + Api1: { + ...BASE_ACTION, + data: [ + { + test: "Hey", + }, + { + test: "Ho", + }, + ], + }, + }; + const updatedEvalTree = evaluator.updateDataTree(updatedUnEvalTree); + const updatedDependencyMap = evaluator.dependencyMap; + expect(updatedEvalTree).toHaveProperty("Table1.tableData", [ + { + test: "Hey", + raw: "Label", + }, + { + test: "Ho", + raw: "Label", + }, + ]); + expect(updatedEvalTree).toHaveProperty("Text4.text", "Hey"); + expect(updatedDependencyMap).toStrictEqual({ + Api1: ["Api1.data"], + Text1: ["Text1.text"], + Text2: ["Text2.text"], + Text3: ["Text3.text"], + Text4: ["Text4.text"], + Table1: [ + "Table1.tableData", + "Table1.selectedRowIndex", + "Table1.searchText", + "Table1.selectedRowIndices", + "Table1.selectedRow", + ], + "Table1.selectedRow": ["Table1.selectedRow.test"], + Dropdown1: [ + "Dropdown1.selectedOptionValue", + "Dropdown1.selectedOptionValueArr", + ], + Input1: ["Input1.text"], + "Text2.text": ["Text1.text"], + "Text3.text": ["Text1.text"], + "Dropdown1.selectedOptionValue": [], + "Dropdown1.selectedOptionValueArr": [], + "Table1.tableData": ["Api1.data", "Text1.text"], + "Table1.searchText": [], + "Table1.selectedRowIndex": [], + "Table1.selectedRowIndices": [], + "Text4.text": ["Table1.selectedRow.test"], + "Input1.text": [], + }); + }); +}); diff --git a/app/client/src/workers/evaluation.worker.ts b/app/client/src/workers/evaluation.worker.ts index 5f6cbf5c91..c986c16cf3 100644 --- a/app/client/src/workers/evaluation.worker.ts +++ b/app/client/src/workers/evaluation.worker.ts @@ -1,11 +1,3 @@ -/* eslint no-restricted-globals: 0 */ -import { - ISO_DATE_FORMAT, - VALIDATION_TYPES, - ValidationResponse, - ValidationType, - Validator, -} from "../constants/WidgetValidation"; import { ActionDescription, DataTree, @@ -14,43 +6,49 @@ import { DataTreeObjectEntity, DataTreeWidget, ENTITY_TYPE, -} from "../entities/DataTree/dataTreeFactory"; -import equal from "fast-deep-equal/es6"; -import _, { - every, - isBoolean, - isNumber, - isObject, - isPlainObject, - isString, - isUndefined, - toNumber, - toString, -} from "lodash"; -import toposort from "toposort"; -import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; -import unescapeJS from "unescape-js"; -import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; -import { WidgetType } from "../constants/WidgetConstants"; -import { WidgetProps } from "../widgets/BaseWidget"; -import { WIDGET_TYPE_VALIDATION_ERROR } from "../constants/messages"; -import moment from "moment"; +} from "entities/DataTree/dataTreeFactory"; import { + DependencyMap, EVAL_WORKER_ACTIONS, EvalError, EvalErrorTypes, extraLibraries, + getDynamicBindings, getEntityDynamicBindingPathList, getWidgetDynamicTriggerPathList, isPathADynamicTrigger, unsafeFunctionForEval, } from "../utils/DynamicBindingUtils"; +import _ from "lodash"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import toposort from "toposort"; +import { DATA_BIND_REGEX } from "../constants/BindingsConstants"; +import equal from "fast-deep-equal/es6"; +import unescapeJS from "unescape-js"; + +import { applyChange, diff, Diff } from "deep-diff"; +import { + addDependantsOfNestedPropertyPaths, + convertPathToString, + CrashingError, + DataTreeDiffEvent, + getValidatedTree, + makeParentsDependOnChildren, + removeFunctions, + removeFunctionsFromDataTree, + translateDiffEventToDataTreeDiffEvent, + validateWidgetProperty, +} from "./evaluationUtils"; const ctx: Worker = self as any; -let ERRORS: EvalError[] = []; +let dataTreeEvaluator: DataTreeEvaluator | undefined; let LOGS: any[] = []; -let WIDGET_TYPE_CONFIG_MAP: WidgetTypeConfigMap = {}; + +type EvalResult = { + result: any; + triggers?: ActionDescription[]; +}; //TODO: Create a more complete RPC setup in the subtree-eval branch. function messageEventListener( @@ -66,7 +64,6 @@ function messageEventListener( responseData, timeTaken: (endTime - startTime).toFixed(2), }); - ERRORS = []; LOGS = []; }; } @@ -76,313 +73,1132 @@ ctx.addEventListener( messageEventListener((method, requestData: any) => { switch (method) { case EVAL_WORKER_ACTIONS.EVAL_TREE: { - const { widgetTypeConfigMap, dataTree } = requestData; - WIDGET_TYPE_CONFIG_MAP = widgetTypeConfigMap; + const { widgetTypeConfigMap, unevalTree } = requestData; + let dataTree: DataTree = unevalTree; + let errors: EvalError[] = []; + let dependencies: DependencyMap = {}; try { - const response = getEvaluatedDataTree(dataTree); + if (!dataTreeEvaluator) { + dataTreeEvaluator = new DataTreeEvaluator(widgetTypeConfigMap); + dataTreeEvaluator.createFirstTree(unevalTree); + dataTree = dataTreeEvaluator.evalTree; + } else { + dataTree = dataTreeEvaluator.updateDataTree(unevalTree); + } + // We need to clean it to remove any possible functions inside the tree. // If functions exist, it will crash the web worker - const cleanDataTree = JSON.stringify(response); - return { dataTree: cleanDataTree, errors: ERRORS, logs: LOGS }; + dataTree = JSON.parse(JSON.stringify(dataTree)); + dependencies = dataTreeEvaluator.dependencyMap; + errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); } catch (e) { - const cleanDataTree = JSON.stringify(getValidatedTree(dataTree)); - return { dataTree: cleanDataTree, errors: ERRORS, logs: LOGS }; + if (dataTreeEvaluator !== undefined) { + errors = dataTreeEvaluator.errors; + } + if (!(e instanceof CrashingError)) { + errors.push({ + type: EvalErrorTypes.UNKNOWN_ERROR, + message: e.message, + }); + console.error(e); + } + dataTree = getValidatedTree(widgetTypeConfigMap, unevalTree); + dataTreeEvaluator = undefined; } + return { + dataTree, + dependencies, + errors, + logs: LOGS, + }; } case EVAL_WORKER_ACTIONS.EVAL_SINGLE: { const { binding, dataTree } = requestData; const withFunctions = addFunctions(dataTree); - const value = getDynamicValue(binding, withFunctions, false); - const cleanedResponse = removeFunctions(value); - return { value: cleanedResponse, errors: ERRORS }; + if (!dataTreeEvaluator) { + return { value: undefined, errors: [] }; + } + const value = dataTreeEvaluator.getDynamicValue( + binding, + withFunctions, + false, + ); + const errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); + return { value, errors }; } case EVAL_WORKER_ACTIONS.EVAL_TRIGGER: { const { dynamicTrigger, callbackData, dataTree } = requestData; - const evalTree = getEvaluatedDataTree(dataTree); + if (!dataTreeEvaluator) { + return { triggers: [], errors: [] }; + } + const evalTree = dataTreeEvaluator.updateDataTree(dataTree); const withFunctions = addFunctions(evalTree); - const triggers = getDynamicValue( + const triggers = dataTreeEvaluator.getDynamicValue( dynamicTrigger, withFunctions, true, callbackData, ); - const cleanedResponse = removeFunctions(triggers); - return { triggers: cleanedResponse, errors: ERRORS }; + const errors = dataTreeEvaluator.errors; + dataTreeEvaluator.clearErrors(); + return { triggers, errors }; } case EVAL_WORKER_ACTIONS.CLEAR_CACHE: { - clearCaches(); + dataTreeEvaluator = undefined; return true; } case EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE: { const { propertyPath } = requestData; - clearPropertyCache(propertyPath); + if (!dataTreeEvaluator) { + return true; + } + dataTreeEvaluator.clearPropertyCache(propertyPath); return true; } case EVAL_WORKER_ACTIONS.CLEAR_PROPERTY_CACHE_OF_WIDGET: { const { widgetName } = requestData; - clearPropertyCacheOfWidget(widgetName); + if (!dataTreeEvaluator) { + return true; + } + dataTreeEvaluator.clearPropertyCacheOfWidget(widgetName); return true; } case EVAL_WORKER_ACTIONS.VALIDATE_PROPERTY: { - const { widgetType, property, value, props } = requestData; - const result = validateWidgetProperty( + const { + widgetType, + widgetTypeConfigMap, + property, + value, + props, + } = requestData; + return validateWidgetProperty( + widgetTypeConfigMap, widgetType, property, value, props, ); - const cleanedResponse = removeFunctions(result); - return cleanedResponse; } default: { - console.error("Action not registered on worker", method, requestData); + console.error("Action not registered on worker", method); } } }), ); -let dependencyTreeCache: any = {}; -let cachedDataTreeString = ""; - -function getEvaluatedDataTree(dataTree: DataTree): DataTree { - const totalStart = performance.now(); - // Add functions to the tre - const withFunctions = addFunctions(dataTree); - // Create Dependencies DAG - const createDepsStart = performance.now(); - const dataTreeString = JSON.stringify(dataTree); - // Stringify before doing a fast equals because the data tree has functions and fast equal will always treat those as changed values - // Better solve will be to prune functions - if (!equal(dataTreeString, cachedDataTreeString)) { - cachedDataTreeString = dataTreeString; - dependencyTreeCache = createDependencyTree(withFunctions); - } - const createDepsEnd = performance.now(); - const { - dependencyMap, - sortedDependencies, - dependencyTree, - } = dependencyTreeCache; - - // Evaluate Tree - const evaluatedTreeStart = performance.now(); - const evaluatedTree = dependencySortedEvaluateDataTree( - dataTree, - dependencyMap, - sortedDependencies, - ); - const evaluatedTreeEnd = performance.now(); - - // Set Loading Widgets - const loadingTreeStart = performance.now(); - const treeWithLoading = setTreeLoading(evaluatedTree, dependencyTree); - const loadingTreeEnd = performance.now(); - - // Validate Widgets - const validateTreeStart = performance.now(); - const validated = getValidatedTree(treeWithLoading); - const validateTreeEnd = performance.now(); - const withoutFunctions = removeFunctionsFromDataTree(validated); - - // End counting total time - const endStart = performance.now(); - - // Log time taken and count - const timeTaken = { - total: (endStart - totalStart).toFixed(2), - createDeps: (createDepsEnd - createDepsStart).toFixed(2), - evaluate: (evaluatedTreeEnd - evaluatedTreeStart).toFixed(2), - loading: (loadingTreeEnd - loadingTreeStart).toFixed(2), - validate: (validateTreeEnd - validateTreeStart).toFixed(2), - }; - LOGS.push({ timeTaken }); - // dataTreeCache = validated; - return withoutFunctions; -} - -const addFunctions = (dataTree: DataTree): DataTree => { - dataTree.actionPaths = []; - Object.keys(dataTree).forEach((entityName) => { - const entity = dataTree[entityName]; - if (isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION) { - const runFunction = function( - this: DataTreeAction, - onSuccess: string, - onError: string, - params = "", - ) { - return { - type: "RUN_ACTION", - payload: { - actionId: this.actionId, - onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", - onError: onError ? `{{${onError.toString()}}}` : "", - params, - }, - }; - }; - _.set(dataTree, `${entityName}.run`, runFunction); - dataTree.actionPaths && dataTree.actionPaths.push(`${entityName}.run`); +export class DataTreeEvaluator { + dependencyMap: DependencyMap = {}; + sortedDependencies: Array = []; + inverseDependencyMap: DependencyMap = {}; + widgetConfigMap: WidgetTypeConfigMap = {}; + evalTree: DataTree = {}; + allKeys: Record = {}; + oldUnEvalTree: DataTree = {}; + errors: EvalError[] = []; + parsedValueCache: Map< + string, + { + value: any; + version: number; } - }); - dataTree.navigateTo = function( - pageNameOrUrl: string, - params: Record, - ) { - return { - type: "NAVIGATE_TO", - payload: { pageNameOrUrl, params }, - }; - }; - dataTree.actionPaths.push("navigateTo"); + > = new Map(); - dataTree.showAlert = function(message: string, style: string) { - return { - type: "SHOW_ALERT", - payload: { message, style }, - }; - }; - dataTree.actionPaths.push("showAlert"); - - dataTree.showModal = function(modalName: string) { - return { - type: "SHOW_MODAL_BY_NAME", - payload: { modalName }, - }; - }; - dataTree.actionPaths.push("showModal"); - - dataTree.closeModal = function(modalName: string) { - return { - type: "CLOSE_MODAL", - payload: { modalName }, - }; - }; - dataTree.actionPaths.push("closeModal"); - - dataTree.storeValue = function(key: string, value: string) { - return { - type: "STORE_VALUE", - payload: { key, value }, - }; - }; - dataTree.actionPaths.push("storeValue"); - - dataTree.download = function(data: string, name: string, type: string) { - return { - type: "DOWNLOAD", - payload: { data, name, type }, - }; - }; - dataTree.actionPaths.push("download"); - return dataTree; -}; - -const removeFunctionsFromDataTree = (dataTree: DataTree) => { - dataTree.actionPaths?.forEach((functionPath) => { - _.set(dataTree, functionPath, {}); - }); - delete dataTree.actionPaths; - return dataTree; -}; - -// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing -// Check issue https://github.com/appsmithorg/appsmith/issues/719 -const removeFunctions = (value: any) => { - if (_.isFunction(value)) { - return "Function call"; - } else if (_.isObject(value)) { - return JSON.parse(JSON.stringify(value)); - } else { - return value; + constructor(widgetConfigMap: WidgetTypeConfigMap) { + this.widgetConfigMap = widgetConfigMap; } -}; -type DynamicDependencyMap = Record>; -const createDependencyTree = ( - dataTree: DataTree, -): { - sortedDependencies: Array; - dependencyTree: Array<[string, string]>; - dependencyMap: DynamicDependencyMap; -} => { - let dependencyMap: DynamicDependencyMap = {}; - const allKeys = getAllPaths(dataTree); - Object.keys(dataTree).forEach((entityKey) => { - const entity = dataTree[entityKey]; - if (isValidEntity(entity)) { - if ( - entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET || - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION - ) { - const dynamicBindingPathList = getEntityDynamicBindingPathList(entity); - if (dynamicBindingPathList.length) { - dynamicBindingPathList.forEach((dynamicPath) => { - const propertyPath = dynamicPath.key; - const unevalPropValue = _.get(entity, propertyPath); - const { jsSnippets } = getDynamicBindings(unevalPropValue); - const existingDeps = - dependencyMap[`${entityKey}.${propertyPath}`] || []; - dependencyMap[`${entityKey}.${propertyPath}`] = existingDeps.concat( - jsSnippets.filter((jsSnippet) => !!jsSnippet), - ); - }); - } - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - // Set default property dependency - const defaultProperties = - WIDGET_TYPE_CONFIG_MAP[entity.type].defaultProperties; - Object.keys(defaultProperties).forEach((property) => { - dependencyMap[`${entityKey}.${property}`] = [ - `${entityKey}.${defaultProperties[property]}`, - ]; - }); - const dynamicTriggerPathList = getWidgetDynamicTriggerPathList( - entity, - ); - if (dynamicTriggerPathList.length) { - dynamicTriggerPathList.forEach((dynamicPath) => { - dependencyMap[`${entityKey}.${dynamicPath.key}`] = []; - }); + createFirstTree(unEvalTree: DataTree) { + const totalStart = performance.now(); + // Add functions to the tree + const withFunctions = addFunctions(unEvalTree); + // Create dependency map + const createDependencyStart = performance.now(); + this.dependencyMap = this.createDependencyMap(withFunctions); + const createDependencyEnd = performance.now(); + // Sort + const sortDependenciesStart = performance.now(); + this.sortedDependencies = this.sortDependencies(this.dependencyMap); + const sortDependenciesEnd = performance.now(); + // Inverse + this.inverseDependencyMap = this.getInverseDependencyTree(); + // Evaluate + const evaluateStart = performance.now(); + const evaluatedTree = this.evaluateTree( + withFunctions, + this.sortedDependencies, + ); + const evaluateEnd = performance.now(); + // Validate Widgets + const validateStart = performance.now(); + const validated = getValidatedTree(this.widgetConfigMap, evaluatedTree); + const validateEnd = performance.now(); + // Remove functions + this.evalTree = removeFunctionsFromDataTree(validated); + this.oldUnEvalTree = unEvalTree; + const totalEnd = performance.now(); + const timeTakenForFirstTree = { + total: (totalEnd - totalStart).toFixed(2), + createDependencies: (createDependencyEnd - createDependencyStart).toFixed( + 2, + ), + sortDependencies: (sortDependenciesEnd - sortDependenciesStart).toFixed( + 2, + ), + evaluate: (evaluateEnd - evaluateStart).toFixed(2), + validate: (validateEnd - validateStart).toFixed(2), + }; + LOGS.push({ timeTakenForFirstTree }); + } + + updateDataTree(unEvalTree: DataTree) { + const totalStart = performance.now(); + // Add appsmith internal functions to the tree ex. navigateTo / showModal + const unEvalTreeWithFunctions = addFunctions(unEvalTree); + // Calculate diff + const diffCheckTimeStart = performance.now(); + const differences = diff(this.oldUnEvalTree, unEvalTree) || []; + // Since eval tree is listening to possible events that dont cause differences + // We want to check if no diffs are present and bail out early + if (differences.length === 0) { + return this.evalTree; + } + const diffCheckTimeStop = performance.now(); + // Check if dependencies have changed + const updateDependenciesStart = performance.now(); + + // Find all the paths that have changed as part of the difference and update the + // global dependency map if an existing dynamic binding has now become legal + const removedDependencyNodes = this.updateDependencyMap( + differences, + unEvalTreeWithFunctions, + ); + const updateDependenciesStop = performance.now(); + + const calculateSortOrderStart = performance.now(); + + const subTreeSortOrder: string[] = this.calculateSubTreeSortOrder( + differences, + removedDependencyNodes, + ); + + const calculateSortOrderStop = performance.now(); + + LOGS.push({ + differences, + subTreeSortOrder, + sortedDependencies: this.sortedDependencies, + inverse: this.inverseDependencyMap, + updatedDependencyMap: this.dependencyMap, + }); + + // Evaluate + const evalStart = performance.now(); + // We are setting all values from our uneval tree to the old eval tree we have + // this way we can get away with just evaluating the sort order and nothing else + subTreeSortOrder.forEach((propertyPath) => { + const lastIndexOfDot = propertyPath.lastIndexOf("."); + // Only do this for property paths and not the entity themselves + if (lastIndexOfDot !== -1) { + const unEvalPropValue = _.get(unEvalTree, propertyPath); + _.set(this.evalTree, propertyPath, unEvalPropValue); + } + }); + const evaluatedTree = this.evaluateTree(this.evalTree, subTreeSortOrder); + const evalStop = performance.now(); + + // Set widgets loading + const loadingStart = performance.now(); + const loadingSetTree = this.updateWidgetLoadingStateSaga(evaluatedTree); + const loadingStop = performance.now(); + + const validateStart = performance.now(); + // Validate and parse updated widgets + const updatedWidgets = new Set( + subTreeSortOrder.map((path) => path.split(".")[0]), + ); + + const validatedTree = getValidatedTree( + this.widgetConfigMap, + loadingSetTree, + updatedWidgets, + ); + const validateEnd = performance.now(); + + // Remove functions + this.evalTree = removeFunctionsFromDataTree(validatedTree); + const totalEnd = performance.now(); + this.oldUnEvalTree = unEvalTree; + const timeTakenForSubTreeEval = { + total: (totalEnd - totalStart).toFixed(2), + findDifferences: (diffCheckTimeStop - diffCheckTimeStart).toFixed(2), + updateDependencies: ( + updateDependenciesStop - updateDependenciesStart + ).toFixed(2), + calculateSortOrder: ( + calculateSortOrderStop - calculateSortOrderStart + ).toFixed(2), + evaluate: (evalStop - evalStart).toFixed(2), + setLoading: (loadingStop - loadingStart).toFixed(2), + validate: (validateEnd - validateStart).toFixed(2), + }; + LOGS.push({ timeTakenForSubTreeEval }); + return this.evalTree; + } + + getCompleteSortOrder( + changes: Array, + inverseMap: DependencyMap, + ): Array { + let finalSortOrder: Array = []; + let computeSortOrder = true; + // Initialize parents with the current sent of property paths that need to be evaluated + let parents = changes; + let subSortOrderArray: Array; + while (computeSortOrder) { + // Get all the nodes that would be impacted by the evaluation of the nodes in parents array in sorted order + subSortOrderArray = this.getEvaluationSortOrder(parents, inverseMap); + + // Add all the sorted nodes in the final list + finalSortOrder = [...finalSortOrder, ...subSortOrderArray]; + + parents = this.getImmediateParentsOfPropertyPaths(subSortOrderArray); + // If we find parents of the property paths in the sorted array, we should continue finding all the nodes dependent + // on the parents + computeSortOrder = parents.length > 0; + } + + // Remove duplicates from this list. Since we explicitly walk down the tree and implicitly (by fetching parents) walk + // up the tree, there are bound to be many duplicates. + const uniqueKeysInSortOrder = [...new Set(finalSortOrder)]; + + return Array.from(uniqueKeysInSortOrder); + } + + // The idea is to find the immediate parents of the property paths + // e.g. For Table1.selectedRow.email, the parent is Table1.selectedRow + getImmediateParentsOfPropertyPaths( + propertyPaths: Array, + ): Array { + // Use a set to ensure that we dont have duplicates + const parents: Set = new Set(); + + propertyPaths.forEach((path) => { + const parentProperty = path.substr(0, path.lastIndexOf(".")); + + if (parentProperty.length != 0) { + parents.add(parentProperty); + } else { + // We have reached the top of the path. No parent exists + } + }); + + return Array.from(parents); + } + + getEvaluationSortOrder( + changes: Array, + inverseMap: DependencyMap, + ): Array { + const sortOrder: Array = [...changes]; + let iterator = 0; + while (iterator < sortOrder.length) { + // Find all the nodes who are to be evaluated when sortOrder[iterator] changes + const newNodes = inverseMap[sortOrder[iterator]]; + + // If we find more nodes that would be impacted by the evaluation of the node being investigated + // we add these to the sort order. + if (newNodes) { + newNodes.forEach((toBeEvaluatedNode) => { + // Only add the nodes if they haven't been already added for evaluation in the list. Since we are doing + // breadth first traversal, we should be safe in not changing the evaluation order and adding this now at this + // point instead of the previous index found. + if (!sortOrder.includes(toBeEvaluatedNode)) { + sortOrder.push(toBeEvaluatedNode); } - } + }); + } + iterator++; + } + return sortOrder; + } + + createDependencyMap(unEvalTree: DataTree): DependencyMap { + let dependencyMap: DependencyMap = {}; + this.allKeys = getAllPaths(unEvalTree); + Object.keys(unEvalTree).forEach((entityName) => { + const entity = unEvalTree[entityName]; + if (isAction(entity) || isWidget(entity)) { + const entityListedDependencies = this.listEntityDependencies( + entity, + entityName, + ); + dependencyMap = { ...dependencyMap, ...entityListedDependencies }; + } + }); + Object.keys(dependencyMap).forEach((key) => { + dependencyMap[key] = _.flatten( + dependencyMap[key].map((path) => + extractReferencesFromBinding(path, this.allKeys), + ), + ); + }); + dependencyMap = makeParentsDependOnChildren(dependencyMap); + return dependencyMap; + } + + listEntityDependencies( + entity: DataTreeWidget | DataTreeAction, + entityName: string, + ): DependencyMap { + const dependencies: DependencyMap = {}; + const dynamicBindingPathList = getEntityDynamicBindingPathList(entity); + if (dynamicBindingPathList.length) { + dynamicBindingPathList.forEach((dynamicPath) => { + const propertyPath = dynamicPath.key; + const unevalPropValue = _.get(entity, propertyPath); + const { jsSnippets } = getDynamicBindings(unevalPropValue); + const existingDeps = + dependencies[`${entityName}.${propertyPath}`] || []; + dependencies[`${entityName}.${propertyPath}`] = existingDeps.concat( + jsSnippets.filter((jsSnippet) => !!jsSnippet), + ); + }); + } + if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { + // Set default property dependency + const defaultProperties = this.widgetConfigMap[entity.type] + .defaultProperties; + Object.keys(defaultProperties).forEach((property) => { + dependencies[`${entityName}.${property}`] = [ + `${entityName}.${defaultProperties[property]}`, + ]; + }); + // Set triggers. TODO check if needed + const dynamicTriggerPathList = getWidgetDynamicTriggerPathList(entity); + if (dynamicTriggerPathList.length) { + dynamicTriggerPathList.forEach((dynamicPath) => { + dependencies[`${entityName}.${dynamicPath.key}`] = []; + }); } } - }); - Object.keys(dependencyMap).forEach((key) => { - dependencyMap[key] = _.flatten( - dependencyMap[key].map((path) => calculateSubDependencies(path, allKeys)), + return dependencies; + } + + evaluateTree( + oldUnevalTree: DataTree, + sortedDependencies: Array, + ): DataTree { + const tree = _.cloneDeep(oldUnevalTree); + try { + return sortedDependencies.reduce( + (currentTree: DataTree, propertyPath: string) => { + LOGS.push(`evaluating ${propertyPath}`); + const entityName = propertyPath.split(".")[0]; + const entity: DataTreeEntity = currentTree[entityName]; + const unEvalPropertyValue = _.get(currentTree as any, propertyPath); + let evalPropertyValue; + const requiresEval = isDynamicValue(unEvalPropertyValue); + if (requiresEval) { + try { + evalPropertyValue = this.evaluateDynamicProperty( + propertyPath, + currentTree, + unEvalPropertyValue, + ); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_PROPERTY_ERROR, + message: e.message, + context: { + propertyPath, + }, + }); + evalPropertyValue = undefined; + } + } else { + evalPropertyValue = unEvalPropertyValue; + } + if (isWidget(entity)) { + const widgetEntity = entity; + // TODO fix for nested properties + const propertyName = propertyPath.split(".")[1]; + if (propertyName) { + let parsedValue = this.validateAndParseWidgetProperty( + propertyPath, + widgetEntity, + currentTree, + evalPropertyValue, + unEvalPropertyValue, + ); + const defaultPropertyMap = this.widgetConfigMap[widgetEntity.type] + .defaultProperties; + const hasDefaultProperty = propertyName in defaultPropertyMap; + if (hasDefaultProperty) { + const defaultProperty = defaultPropertyMap[propertyName]; + parsedValue = this.overwriteDefaultDependentProps( + defaultProperty, + parsedValue, + propertyPath, + widgetEntity, + ); + } + return _.set(currentTree, propertyPath, parsedValue); + } + return _.set(currentTree, propertyPath, evalPropertyValue); + } else { + return _.set(currentTree, propertyPath, evalPropertyValue); + } + }, + tree, + ); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_TREE_ERROR, + message: e.message, + }); + return tree; + } + } + + sortDependencies(dependencyMap: DependencyMap): Array { + const dependencyTree: Array<[string, string]> = []; + Object.keys(dependencyMap).forEach((key: string) => { + if (dependencyMap[key].length) { + dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep])); + } else { + // Set no dependency + dependencyTree.push([key, ""]); + } + }); + + try { + // sort dependencies and remove empty dependencies + return toposort(dependencyTree) + .reverse() + .filter((d) => !!d); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.DEPENDENCY_ERROR, + message: e.message, + }); + throw new CrashingError(e.message); + } + } + + getParsedValueCache(propertyPath: string) { + return ( + this.parsedValueCache.get(propertyPath) || { + value: undefined, + version: 0, + } ); - }); - dependencyMap = makeParentsDependOnChildren(dependencyMap); - const dependencyTree: Array<[string, string]> = []; - Object.keys(dependencyMap).forEach((key: string) => { - if (dependencyMap[key].length) { - dependencyMap[key].forEach((dep) => dependencyTree.push([key, dep])); + } + + clearPropertyCache(propertyPath: string) { + this.parsedValueCache.delete(propertyPath); + } + + clearPropertyCacheOfWidget(widgetName: string) { + // TODO check if this loop mutating itself is safe + this.parsedValueCache.forEach((value, key) => { + const match = key.match(`${widgetName}.`); + if (match) { + this.parsedValueCache.delete(key); + } + }); + } + + clearAllCaches() { + this.parsedValueCache.clear(); + this.clearErrors(); + this.dependencyMap = {}; + this.allKeys = {}; + this.inverseDependencyMap = {}; + this.sortedDependencies = []; + this.evalTree = {}; + this.oldUnEvalTree = {}; + } + + getDynamicValue( + dynamicBinding: string, + data: DataTree, + returnTriggers: boolean, + callBackData?: Array, + ) { + // Get the {{binding}} bound values + const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); + if (returnTriggers) { + const result = this.evaluateDynamicBoundValue( + data, + jsSnippets[0], + callBackData, + ); + return result.triggers; + } + if (stringSegments.length) { + // Get the Data Tree value of those "binding "paths + const values = jsSnippets.map((jsSnippet, index) => { + if (jsSnippet) { + const result = this.evaluateDynamicBoundValue( + data, + jsSnippet, + callBackData, + ); + return result.result; + } else { + return stringSegments[index]; + } + }); + + // if it is just one binding, no need to create template string + if (stringSegments.length === 1) return values[0]; + // else return a string template with bindings + return createDynamicValueString(dynamicBinding, stringSegments, values); + } + return undefined; + } + + // Paths are expected to have "{name}.{path}" signature + // Also returns any action triggers found after evaluating value + evaluateDynamicBoundValue( + data: DataTree, + path: string, + callbackData?: Array, + ): EvalResult { + try { + const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); + return this.evaluate(unescapedJS, data, callbackData); + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.UNESCAPE_STRING_ERROR, + message: e.message, + context: { + path, + }, + }); + return { result: undefined, triggers: [] }; + } + } + + evaluate(js: string, data: DataTree, callbackData?: Array): EvalResult { + const scriptToEvaluate = ` + function closedFunction () { + const result = ${js}; + return { result, triggers: self.triggers } + } + closedFunction() + `; + const scriptWithCallback = ` + function callback (script) { + const userFunction = script; + const result = userFunction.apply(self, CALLBACK_DATA); + return { result, triggers: self.triggers }; + } + callback(${js}); + `; + const script = callbackData ? scriptWithCallback : scriptToEvaluate; + try { + const { result, triggers } = (function() { + /**** Setting the eval context ****/ + const GLOBAL_DATA: Record = {}; + ///// Adding callback data + GLOBAL_DATA.CALLBACK_DATA = callbackData; + ///// Adding Data tree + Object.keys(data).forEach((datum) => { + GLOBAL_DATA[datum] = data[datum]; + }); + ///// Fixing action paths and capturing their execution response + if (data.actionPaths) { + GLOBAL_DATA.triggers = []; + const pusher = function( + this: DataTree, + action: any, + ...payload: any[] + ) { + const actionPayload = action(...payload); + GLOBAL_DATA.triggers.push(actionPayload); + }; + GLOBAL_DATA.actionPaths.forEach((path: string) => { + const action = _.get(GLOBAL_DATA, path); + const entity = _.get(GLOBAL_DATA, path.split(".")[0]); + if (action) { + _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity))); + } + }); + } + + // Set it to self + Object.keys(GLOBAL_DATA).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[key] = GLOBAL_DATA[key]; + }); + + ///// Adding extra libraries separately + extraLibraries.forEach((library) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[library.accessor] = library.lib; + }); + + ///// Remove all unsafe functions + unsafeFunctionForEval.forEach((func) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + self[func] = undefined; + }); + + const evalResult = eval(script); + + // Remove it from self + // This is needed so that next eval can have a clean sheet + Object.keys(GLOBAL_DATA).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: No types available + delete self[key]; + }); + + return evalResult; + })(); + return { result, triggers }; + } catch (e) { + this.errors.push({ + type: EvalErrorTypes.EVAL_ERROR, + message: e.message, + context: { + binding: js, + }, + }); + return { result: undefined, triggers: [] }; + } + } + + evaluateDynamicProperty( + propertyPath: string, + currentTree: DataTree, + unEvalPropertyValue: any, + ): any { + return this.getDynamicValue(unEvalPropertyValue, currentTree, false); + } + + validateAndParseWidgetProperty( + propertyPath: string, + widget: DataTreeWidget, + currentTree: DataTree, + evalPropertyValue: any, + unEvalPropertyValue: string, + ): any { + const entityPropertyName = _.drop(propertyPath.split(".")).join("."); + let valueToValidate = evalPropertyValue; + if (isPathADynamicTrigger(widget, propertyPath)) { + const { triggers } = this.getDynamicValue( + unEvalPropertyValue, + currentTree, + true, + undefined, + ); + valueToValidate = triggers; + } + const { parsed, isValid, message, transformed } = validateWidgetProperty( + this.widgetConfigMap, + widget.type, + entityPropertyName, + valueToValidate, + widget, + currentTree, + ); + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? evalPropertyValue + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue); + if (!isValid) { + _.set(widget, `invalidProps.${entityPropertyName}`, true); + _.set(widget, `validationMessages.${entityPropertyName}`, message); } else { - // Set no dependency - dependencyTree.push([key, ""]); + _.set(widget, `invalidProps.${entityPropertyName}`, false); + _.set(widget, `validationMessages.${entityPropertyName}`, ""); + } + + if (isPathADynamicTrigger(widget, entityPropertyName)) { + return unEvalPropertyValue; + } else { + const parsedCache = this.getParsedValueCache(propertyPath); + if (!equal(parsedCache.value, parsed)) { + this.parsedValueCache.set(propertyPath, { + value: parsed, + version: Date.now(), + }); + } + return parsed; + } + } + + overwriteDefaultDependentProps( + defaultProperty: string, + propertyValue: any, + propertyPath: string, + entity: DataTreeWidget, + ) { + const defaultPropertyCache = this.getParsedValueCache( + `${entity.widgetName}.${defaultProperty}`, + ); + const propertyCache = this.getParsedValueCache(propertyPath); + if ( + propertyValue === undefined || + propertyCache.version < defaultPropertyCache.version + ) { + return defaultPropertyCache.value; + } + return propertyValue; + } + + updateDependencyMap( + differences: Array>, + unEvalDataTree: DataTree, + ): Array { + const diffCalcStart = performance.now(); + let didUpdateDependencyMap = false; + const removedNodes: Array = []; + + // This is needed for NEW and DELETE events below. + // In worst case, it tends to take ~12.5% of entire diffCalc (8 ms out of 67ms for 132 array of NEW) + // TODO: Optimise by only getting paths of changed node + this.allKeys = getAllPaths(unEvalDataTree); + // Transform the diff library events to Appsmith evaluator events + differences + .map(translateDiffEventToDataTreeDiffEvent) + .forEach((dataTreeDiff) => { + const entityName = dataTreeDiff.payload.propertyPath.split(".")[0]; + const entity = unEvalDataTree[entityName]; + const entityType = isValidEntity(entity) ? entity.ENTITY_TYPE : "noop"; + + if (entityType !== "noop") { + switch (dataTreeDiff.event) { + case DataTreeDiffEvent.NEW: { + // If a new widget was added, add all the internal bindings for this widget to the global dependency map + if ( + isWidget(entity) && + dataTreeDiff.payload.propertyPath === entityName + ) { + const widgetDependencyMap: DependencyMap = this.listEntityDependencies( + entity as DataTreeWidget, + entityName, + ); + if (Object.keys(widgetDependencyMap).length) { + didUpdateDependencyMap = true; + Object.assign(this.dependencyMap, widgetDependencyMap); + } + } + // Either a new entity or a new property path has been added. Go through existing dynamic bindings and + // find out if a new dependency has to be created because the property path used in the binding just became + // eligible + const possibleReferencesInOldBindings: DependencyMap = this.getPropertyPathReferencesInExistingBindings( + unEvalDataTree, + dataTreeDiff.payload.propertyPath, + ); + // We have found some bindings which are related to the new property path and hence should be added to the + // global dependency map + if (Object.keys(possibleReferencesInOldBindings).length) { + didUpdateDependencyMap = true; + Object.assign( + this.dependencyMap, + possibleReferencesInOldBindings, + ); + } + break; + } + case DataTreeDiffEvent.DELETE: { + // If an existing widget was deleted, remove all the bindings from the global dependency map + if ( + entityType === ENTITY_TYPE.WIDGET && + dataTreeDiff.payload.propertyPath === entityName + ) { + const entity: DataTreeWidget = unEvalDataTree[ + entityName + ] as DataTreeWidget; + + const widgetBindings = this.listEntityDependencies( + entity, + entityName, + ); + Object.keys(widgetBindings).forEach((widgetDep) => { + didUpdateDependencyMap = true; + delete this.dependencyMap[widgetDep]; + }); + } + // Either an existing entity or an existing property path has been deleted. Update the global dependency map + // by removing the bindings from the same. + Object.keys(this.dependencyMap).forEach((dependencyPath) => { + didUpdateDependencyMap = true; + // TODO delete via regex + if ( + dependencyPath.includes(dataTreeDiff.payload.propertyPath) + ) { + delete this.dependencyMap[dependencyPath]; + } else { + const toRemove: Array = []; + this.dependencyMap[dependencyPath].forEach( + (dependantPath) => { + if ( + dependantPath.includes( + dataTreeDiff.payload.propertyPath, + ) + ) { + removedNodes.push(dependencyPath); + toRemove.push(dependantPath); + } + }, + ); + this.dependencyMap[dependencyPath] = _.difference( + this.dependencyMap[dependencyPath], + toRemove, + ); + } + }); + break; + } + + case DataTreeDiffEvent.EDIT: { + // We only care about dependencies for a widget. This is because in case a dependency of an action changes, + // that shouldn't trigger an evaluation. + // Also for a widget, we only care if the difference is in dynamic bindings since static values do not need + // an evaluation. + if ( + entityType === ENTITY_TYPE.WIDGET && + typeof dataTreeDiff.payload.value === "string" + ) { + didUpdateDependencyMap = true; + + const { jsSnippets } = getDynamicBindings( + dataTreeDiff.payload.value, + ); + const correctSnippets = jsSnippets.filter( + (jsSnippet) => !!jsSnippet, + ); + // We found a new dynamic binding for this property path. We update the dependency map by overwriting the + // dependencies for this property path with the newly found dependencies + if (correctSnippets.length) { + this.dependencyMap[ + dataTreeDiff.payload.propertyPath + ] = correctSnippets; + } else { + // The dependency on this property path has been removed. Delete this property path from the global + // dependency map + delete this.dependencyMap[dataTreeDiff.payload.propertyPath]; + } + } + break; + } + default: { + break; + } + } + } + }); + const diffCalcEnd = performance.now(); + const subDepCalcStart = performance.now(); + if (didUpdateDependencyMap) { + // TODO Optimise + Object.keys(this.dependencyMap).forEach((key) => { + this.dependencyMap[key] = _.flatten( + this.dependencyMap[key].map((path) => + extractReferencesFromBinding(path, this.allKeys), + ), + ); + }); + this.dependencyMap = makeParentsDependOnChildren(this.dependencyMap); + } + const subDepCalcEnd = performance.now(); + const updateChangedDependenciesStart = performance.now(); + // If the global dependency map has changed, re-calculate the sort order for all entities and the + // global inverse dependency map + if (didUpdateDependencyMap) { + // This is being called purely to test for new circular dependencies that might have been added + this.sortedDependencies = this.sortDependencies(this.dependencyMap); + this.inverseDependencyMap = this.getInverseDependencyTree(); + } + + const updateChangedDependenciesStop = performance.now(); + LOGS.push({ + diffCalcDeps: (diffCalcEnd - diffCalcStart).toFixed(2), + subDepCalc: (subDepCalcEnd - subDepCalcStart).toFixed(2), + updateChangedDependencies: ( + updateChangedDependenciesStop - updateChangedDependenciesStart + ).toFixed(2), + }); + + return removedNodes; + } + + calculateSubTreeSortOrder( + differences: Diff[], + removedDependencyNodes: Array, + ) { + const changePaths: Set = new Set(removedDependencyNodes); + differences.forEach((d) => { + if (d.path) { + // Apply the changes into the oldEvalTree so that it can be evaluated + applyChange(this.evalTree, undefined, d); + + // If this is a property path change, simply add for evaluation + if (d.path.length > 1) { + const propertyPath = convertPathToString(d.path); + changePaths.add(propertyPath); + + // If this is an array update, trim the array index and add it to the change paths for evaluation + // This is because sometimes inside an object of array time, if only a particular entry changes, the + // difference comes as propertyPath[0].fieldChanged. Another entity could depend on propertyPath and not + // propertyPath[0]. The said entity must be evaluated. + // To do this, we are trimming the array index + if (propertyPath.lastIndexOf("[") > 0) { + changePaths.add( + propertyPath.substr(0, propertyPath.lastIndexOf("[")), + ); + } + } else if (d.path.length === 1) { + /* + When we see a new widget has been added or or delete an old widget ( d.path.length === 1) + We want to add all the dependencies in the sorted order to make + sure all the bindings are evaluated. + */ + this.sortedDependencies.forEach((dependency) => { + if (d.path && dependency.split(".")[0] === d.path[0]) { + changePaths.add(dependency); + } + }); + } + } + }); + + // If a nested property path has changed and someone (say x) is dependent on the parent of the said property, + // x must also be evaluated. For example, the following relationship exists in dependency map: + // < "Input1.defaultText" : ["Table1.selectedRow.email"] > + // If Table1.selectedRow has changed, then Input1.defaultText must also be evaluated because Table1.selectedRow.email + // is a nested property of Table1.selectedRow + const changePathsWithNestedDependants = addDependantsOfNestedPropertyPaths( + Array.from(changePaths), + this.inverseDependencyMap, + ); + + // Now that we have all the root nodes which have to be evaluated, recursively find all the other paths which + // would get impacted because they are dependent on the said root nodes and add them in order + return this.getCompleteSortOrder( + changePathsWithNestedDependants, + this.inverseDependencyMap, + ); + } + + getInverseDependencyTree(): DependencyMap { + const inverseDag: DependencyMap = {}; + this.sortedDependencies.forEach((propertyPath) => { + const incomingEdges: Array = this.dependencyMap[propertyPath]; + if (incomingEdges) { + incomingEdges.forEach((edge) => { + const node = inverseDag[edge]; + if (node) { + node.push(propertyPath); + } else { + inverseDag[edge] = [propertyPath]; + } + }); + } + }); + return inverseDag; + } + + // TODO: create the lookup dictionary once + // Response from listEntityDependencies only needs to change if the entity itself changed. + // Check if it is possible to make a flat structure with O(1) or at least O(m) lookup instead of O(n*m) + getPropertyPathReferencesInExistingBindings( + dataTree: DataTree, + propertyPath: string, + ) { + const possibleRefs: DependencyMap = {}; + Object.keys(dataTree).forEach((entityName) => { + const entity = dataTree[entityName]; + if ( + isValidEntity(entity) && + (entity.ENTITY_TYPE === ENTITY_TYPE.ACTION || + entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) + ) { + const depPaths = this.listEntityDependencies(entity, entityName); + Object.keys(depPaths).forEach((path) => { + const values = depPaths[path]; + values.forEach((value) => { + // TODO Do regex here. + if (value.includes(propertyPath)) { + possibleRefs[path] = values; + } + }); + }); + } + }); + return possibleRefs; + } + + updateWidgetLoadingStateSaga(dataTree: DataTree) { + const entityDependencyMap = createEntityDependencyMap( + this.inverseDependencyMap, + ); + const isLoadingActions: string[] = []; + const widgetNames: string[] = []; + + Object.entries(dataTree).forEach(([entityName, entity]) => { + if (isWidget(entity)) { + widgetNames.push(entityName); + } else if (isAction(entity) && entity.isLoading) { + isLoadingActions.push(entityName); + } + }); + const loadingEntities = getEntityDependencies( + isLoadingActions, + entityDependencyMap, + new Set(), + ); + + widgetNames.forEach((widgetName) => { + _.set( + dataTree, + [widgetName, "isLoading"], + loadingEntities.has(widgetName), + ); + }); + + return dataTree; + } + + clearErrors() { + this.errors = []; + } +} + +const getAllPaths = ( + tree: Record, + prefix = "", + result: Record = {}, +): Record => { + Object.keys(tree).forEach((el) => { + if (Array.isArray(tree[el])) { + const key = `${prefix}${el}`; + result[key] = true; + } else if (typeof tree[el] === "object" && tree[el] !== null) { + const key = `${prefix}${el}`; + result[key] = true; + getAllPaths(tree[el], `${key}.`, result); + } else { + const key = `${prefix}${el}`; + result[key] = true; } }); - - try { - // sort dependencies and remove empty dependencies - const sortedDependencies = toposort(dependencyTree) - .reverse() - .filter((d) => !!d); - - return { sortedDependencies, dependencyMap, dependencyTree }; - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.DEPENDENCY_ERROR, - message: e.message, - }); - throw new Error("Dependency Error"); - //return { sortedDependencies: [], dependencyMap: {}, dependencyTree: [] }; - } + return result; }; -const calculateSubDependencies = ( +const extractReferencesFromBinding = ( path: string, all: Record, ): Array => { @@ -409,636 +1225,63 @@ const calculateSubDependencies = ( return _.uniq(subDeps); }; -const setTreeLoading = ( - dataTree: DataTree, - dependencyMap: Array<[string, string]>, -) => { - const widgets: string[] = []; - const isLoadingActions: string[] = []; - - // Fetch all actions that are in loading state - Object.keys(dataTree).forEach((e) => { - const entity = dataTree[e]; - if (isValidEntity(entity)) { - if (entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET) { - widgets.push(e); - } else if ( - entity.ENTITY_TYPE === ENTITY_TYPE.ACTION && - entity.isLoading - ) { - isLoadingActions.push(e); - } - } - }); - - // get all widget dependencies of those actions - isLoadingActions - .reduce( - (allEntities: string[], curr) => - allEntities.concat(getEntityDependencies(dependencyMap, curr, widgets)), - [], - ) - // set loading to true for those widgets - .forEach((w) => { - const entity = dataTree[w] as DataTreeWidget; - entity.isLoading = true; - }); - return dataTree; -}; - const getEntityDependencies = ( - dependencyMap: Array<[string, string]>, - entity: string, - entities: string[], -): Array => { - const entityDeps: Record = dependencyMap - .map((d) => [d[1].split(".")[0], d[0].split(".")[0]]) - .filter((d) => d[0] !== d[1]) - .reduce((deps: Record, dep) => { - const key: string = dep[0]; - const value: string = dep[1]; - return { - ...deps, - [key]: deps[key] ? deps[key].concat(value) : [value], - }; - }, {}); - - if (entity in entityDeps) { - const visited = new Set(); - const recFind = ( - keys: Array, - deps: Record, - ): Array => { - let allDeps: string[] = []; - keys - .filter((k) => entities.includes(k)) - .forEach((e) => { - if (visited.has(e)) { - return; - } - visited.add(e); - allDeps = allDeps.concat([e]); - if (e in deps) { - allDeps = allDeps.concat([...recFind(deps[e], deps)]); - } + entityNames: string[], + inverseMap: DependencyMap, + visited: Set, +): Set => { + const dependantsEntities: Set = new Set(); + entityNames.forEach((entityName) => { + if (entityName in inverseMap) { + inverseMap[entityName].forEach((dependency) => { + const dependantEntityName = dependency.split(".")[0]; + // Example: For a dependency chain that looks like Dropdown1.selectedOptionValue -> Table1.tableData -> Text1.text -> Dropdown1.options + // Here we're operating on + // Dropdown1 -> Table1 -> Text1 -> Dropdown1 + // It looks like a circle, but isn't + // So we need to mark the visited nodes and avoid infinite recursion in case we've already visited a node once. + if (visited.has(dependantEntityName)) { + return; + } + visited.add(dependantEntityName); + dependantsEntities.add(dependantEntityName); + const childDependencies = getEntityDependencies( + Array.from(dependantsEntities), + inverseMap, + visited, + ); + childDependencies.forEach((entityName) => { + dependantsEntities.add(entityName); }); - return allDeps; - }; - return recFind(entityDeps[entity], entityDeps); - } - return []; -}; - -function dependencySortedEvaluateDataTree( - dataTree: DataTree, - dependencyMap: DynamicDependencyMap, - sortedDependencies: Array, -): DataTree { - const tree = _.cloneDeep(dataTree); - try { - return sortedDependencies.reduce( - (currentTree: DataTree, propertyPath: string) => { - const entityName = propertyPath.split(".")[0]; - const entity: DataTreeEntity = currentTree[entityName]; - const unEvalPropertyValue = _.get(currentTree as any, propertyPath); - let evalPropertyValue; - const propertyDependencies = dependencyMap[propertyPath]; - const currentDependencyValues = getCurrentDependencyValues( - propertyDependencies, - currentTree, - propertyPath, - ); - const cachedDependencyValues = dependencyCache.get(propertyPath); - const requiresEval = isDynamicValue(unEvalPropertyValue); - if (requiresEval) { - try { - evalPropertyValue = evaluateDynamicProperty( - propertyPath, - currentTree, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_PROPERTY_ERROR, - message: e.message, - context: { - propertyPath, - }, - }); - evalPropertyValue = undefined; - } - } else { - evalPropertyValue = unEvalPropertyValue; - // If we have stored any previous dependency cache, clear it - // since it is no longer a binding - if (cachedDependencyValues && cachedDependencyValues.length) { - dependencyCache.set(propertyPath, []); - } - } - if (isWidget(entity)) { - const widgetEntity: DataTreeWidget = entity as DataTreeWidget; - const propertyName = propertyPath.split(".")[1]; - if (propertyName) { - let parsedValue = validateAndParseWidgetProperty( - propertyPath, - widgetEntity, - currentTree, - evalPropertyValue, - unEvalPropertyValue, - currentDependencyValues, - cachedDependencyValues, - ); - const defaultPropertyMap = - WIDGET_TYPE_CONFIG_MAP[widgetEntity.type].defaultProperties; - const hasDefaultProperty = propertyName in defaultPropertyMap; - if (hasDefaultProperty) { - const defaultProperty = defaultPropertyMap[propertyName]; - parsedValue = overwriteDefaultDependentProps( - defaultProperty, - parsedValue, - propertyPath, - widgetEntity, - ); - } - return _.set(currentTree, propertyPath, parsedValue); - } - return _.set(currentTree, propertyPath, evalPropertyValue); - } else { - return _.set(currentTree, propertyPath, evalPropertyValue); - } - }, - tree, - ); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_TREE_ERROR, - message: e.message, - }); - return tree; - } -} - -const overwriteDefaultDependentProps = ( - defaultProperty: string, - propertyValue: any, - propertyPath: string, - entity: DataTreeWidget, -) => { - const defaultPropertyCache = getParsedValueCache( - `${entity.widgetName}.${defaultProperty}`, - ); - const propertyCache = getParsedValueCache(propertyPath); - - if ( - propertyValue === undefined || - propertyCache.version < defaultPropertyCache.version - ) { - return defaultPropertyCache.value; - } - return propertyValue; -}; - -const getValidatedTree = (tree: any) => { - return Object.keys(tree).reduce((tree, entityKey: string) => { - const entity = tree[entityKey]; - if (entity && entity.type) { - const parsedEntity = { ...entity }; - Object.keys(entity).forEach((property: string) => { - const hasEvaluatedValue = _.has( - parsedEntity, - `evaluatedValues.${property}`, - ); - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - const isSpecialField = [ - "dynamicBindingPathList", - "dynamicTriggerPathList", - "dynamicPropertyPathList", - "evaluatedValues", - "invalidProps", - "validationMessages", - ].includes(property); - - if (!isSpecialField && (!hasValidation || !hasEvaluatedValue)) { - const value = entity[property]; - // Pass it through parse - const { - parsed, - isValid, - message, - transformed, - } = validateWidgetProperty( - entity.type, - property, - value, - entity, - tree, - ); - parsedEntity[property] = parsed; - if (!hasEvaluatedValue) { - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? value - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set( - parsedEntity, - `evaluatedValues.${property}`, - safeEvaluatedValue, - ); - } - - const hasValidation = _.has(parsedEntity, `invalidProps.${property}`); - if (!hasValidation && !isValid) { - _.set(parsedEntity, `invalidProps.${property}`, true); - _.set(parsedEntity, `validationMessages.${property}`, message); - } - } }); - return { ...tree, [entityKey]: parsedEntity }; } - return tree; - }, tree); -}; - -const getAllPaths = ( - tree: Record, - prefix = "", -): Record => { - return Object.keys(tree).reduce((res: Record, el): Record< - string, - true - > => { - if (Array.isArray(tree[el])) { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } else if (typeof tree[el] === "object" && tree[el] !== null) { - const key = `${prefix}${el}`; - return { ...res, [key]: true, ...getAllPaths(tree[el], `${key}.`) }; - } else { - const key = `${prefix}${el}`; - return { ...res, [key]: true }; - } - }, {}); -}; - -const getDynamicBindings = ( - dynamicString: string, -): { stringSegments: string[]; jsSnippets: string[] } => { - // Protect against bad string parse - if (!dynamicString || !_.isString(dynamicString)) { - return { stringSegments: [], jsSnippets: [] }; - } - const sanitisedString = dynamicString.trim(); - // Get the {{binding}} bound values - const stringSegments = getDynamicStringSegments(sanitisedString); - // Get the "binding" path values - const paths = stringSegments.map((segment) => { - const length = segment.length; - const matches = isDynamicValue(segment); - if (matches) { - return segment.substring(2, length - 2); - } - return ""; }); - return { stringSegments: stringSegments, jsSnippets: paths }; + return dependantsEntities; }; -//{{}}{{}}} -function getDynamicStringSegments(dynamicString: string): string[] { - let stringSegments = []; - const indexOfDoubleParanStart = dynamicString.indexOf("{{"); - if (indexOfDoubleParanStart === -1) { - return [dynamicString]; - } - //{{}}{{}}} - const firstString = dynamicString.substring(0, indexOfDoubleParanStart); - firstString && stringSegments.push(firstString); - let rest = dynamicString.substring( - indexOfDoubleParanStart, - dynamicString.length, - ); - //{{}}{{}}} - let sum = 0; - for (let i = 0; i <= rest.length - 1; i++) { - const char = rest[i]; - const prevChar = rest[i - 1]; - - if (char === "{") { - sum++; - } else if (char === "}") { - sum--; - if (prevChar === "}" && sum === 0) { - stringSegments.push(rest.substring(0, i + 1)); - rest = rest.substring(i + 1, rest.length); - if (rest) { - stringSegments = stringSegments.concat( - getDynamicStringSegments(rest), - ); - break; - } - } - } - } - if (sum !== 0 && dynamicString !== "") { - return [dynamicString]; - } - return stringSegments; -} +const createEntityDependencyMap = (dependencyMap: DependencyMap) => { + const entityDepMap: DependencyMap = {}; + Object.entries(dependencyMap).forEach(([dependant, dependencies]) => { + const entityDependant = dependant.split(".")[0]; + const existing = entityDepMap[entityDependant] || []; + entityDepMap[entityDependant] = existing.concat( + dependencies + .map((dep) => { + const value = dep.split(".")[0]; + if (value !== entityDependant) { + return value; + } + return undefined; + }) + .filter((value) => typeof value === "string") as string[], + ); + }); + return entityDepMap; +}; // referencing DATA_BIND_REGEX fails for the value "{{Table1.tableData[Table1.selectedRowIndex]}}" if you run it multiple times and don't recreate const isDynamicValue = (value: string): boolean => DATA_BIND_REGEX.test(value); -function getCurrentDependencyValues( - propertyDependencies: Array, - currentTree: DataTree, - currentPropertyPath: string, -): Array { - return propertyDependencies - ? propertyDependencies - .map((path: string) => { - //*** Remove current path from data tree because cached value contains evaluated version while this contains unevaluated version */ - const cleanDataTree = _.omit(currentTree, [currentPropertyPath]); - return _.get(cleanDataTree, path); - }) - .filter((data: any) => { - return data !== undefined; - }) - : []; -} - -const dynamicPropValueCache: Map< - string, - { - unEvaluated: any; - evaluated: any; - } -> = new Map(); - -const parsedValueCache: Map< - string, - { - value: any; - version: number; - } -> = new Map(); - -const getDynamicPropValueCache = (propertyPath: string) => - dynamicPropValueCache.get(propertyPath); - -const getParsedValueCache = (propertyPath: string) => - parsedValueCache.get(propertyPath) || { - value: undefined, - version: 0, - }; - -const clearPropertyCache = (propertyPath: string) => - parsedValueCache.delete(propertyPath); - -/** - * delete all values of a particular widget - * - * @param propertyPath - */ -export const clearPropertyCacheOfWidget = (widgetName: string) => { - parsedValueCache.forEach((value, key) => { - const match = key.match(`${widgetName}.`); - - if (match) return parsedValueCache.delete(key); - }); -}; - -const dependencyCache: Map = new Map(); - -function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity { - if (!_.isObject(entity)) { - ERRORS.push({ - type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR, - message: "Data tree entity is not an object", - context: entity, - }); - return false; - } - return "ENTITY_TYPE" in entity; -} - -function isWidget(entity: DataTreeEntity): boolean { - return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; -} - -function validateAndParseWidgetProperty( - propertyPath: string, - widget: DataTreeWidget, - currentTree: DataTree, - evalPropertyValue: any, - unEvalPropertyValue: string, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const entityPropertyName = _.drop(propertyPath.split(".")).join("."); - let valueToValidate = evalPropertyValue; - if (isPathADynamicTrigger(widget, propertyPath)) { - const { triggers } = getDynamicValue( - unEvalPropertyValue, - currentTree, - true, - undefined, - ); - valueToValidate = triggers; - } - const { parsed, isValid, message, transformed } = validateWidgetProperty( - widget.type, - entityPropertyName, - valueToValidate, - widget, - currentTree, - ); - const evaluatedValue = isValid - ? parsed - : _.isUndefined(transformed) - ? evalPropertyValue - : transformed; - const safeEvaluatedValue = removeFunctions(evaluatedValue); - _.set(widget, `evaluatedValues.${entityPropertyName}`, safeEvaluatedValue); - if (!isValid) { - _.set(widget, `invalidProps.${entityPropertyName}`, true); - _.set(widget, `validationMessages.${entityPropertyName}`, message); - } - - if (isPathADynamicTrigger(widget, entityPropertyName)) { - return unEvalPropertyValue; - } else { - const parsedCache = getParsedValueCache(propertyPath); - if ( - !equal(parsedCache.value, parsed) || - (cachedDependencyValues !== undefined && - !equal(currentDependencyValues, cachedDependencyValues)) - ) { - parsedValueCache.set(propertyPath, { - value: parsed, - version: Date.now(), - }); - } - return parsed; - } -} - -function evaluateDynamicProperty( - propertyPath: string, - currentTree: DataTree, - unEvalPropertyValue: any, - currentDependencyValues: Array, - cachedDependencyValues?: Array, -): any { - const cacheObj = getDynamicPropValueCache(propertyPath); - const isCacheHit = - cacheObj && - equal(cacheObj.unEvaluated, unEvalPropertyValue) && - cachedDependencyValues !== undefined && - equal(currentDependencyValues, cachedDependencyValues); - if (isCacheHit && cacheObj) { - return cacheObj.evaluated; - } else { - LOGS.push("eval " + propertyPath); - const dynamicResult = getDynamicValue( - unEvalPropertyValue, - currentTree, - false, - ); - dynamicPropValueCache.set(propertyPath, { - evaluated: dynamicResult, - unEvaluated: unEvalPropertyValue, - }); - dependencyCache.set(propertyPath, currentDependencyValues); - return dynamicResult; - } -} - -type EvalResult = { - result: any; - triggers?: ActionDescription[]; -}; -// Paths are expected to have "{name}.{path}" signature -// Also returns any action triggers found after evaluating value -const evaluateDynamicBoundValue = ( - data: DataTree, - path: string, - callbackData?: Array, -): EvalResult => { - try { - const unescapedJS = unescapeJS(path).replace(/(\r\n|\n|\r)/gm, ""); - return evaluate(unescapedJS, data, callbackData); - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.UNESCAPE_STRING_ERROR, - message: e.message, - context: { - path, - }, - }); - return { result: undefined, triggers: [] }; - } -}; - -const evaluate = ( - js: string, - data: DataTree, - callbackData?: Array, -): EvalResult => { - const scriptToEvaluate = ` - function closedFunction () { - const result = ${js}; - return { result, triggers: self.triggers } - } - closedFunction() - `; - const scriptWithCallback = ` - function callback (script) { - const userFunction = script; - const result = userFunction.apply(self, CALLBACK_DATA); - return { result, triggers: self.triggers }; - } - callback(${js}); - `; - const script = callbackData ? scriptWithCallback : scriptToEvaluate; - try { - const { result, triggers } = (function() { - /**** Setting the eval context ****/ - const GLOBAL_DATA: Record = {}; - ///// Adding callback data - GLOBAL_DATA.CALLBACK_DATA = callbackData; - ///// Adding Data tree - Object.keys(data).forEach((datum) => { - GLOBAL_DATA[datum] = data[datum]; - }); - ///// Fixing action paths and capturing their execution response - if (data.actionPaths) { - GLOBAL_DATA.triggers = []; - const pusher = function( - this: DataTree, - action: any, - ...payload: any[] - ) { - const actionPayload = action(...payload); - GLOBAL_DATA.triggers.push(actionPayload); - }; - GLOBAL_DATA.actionPaths.forEach((path: string) => { - const action = _.get(GLOBAL_DATA, path); - const entity = _.get(GLOBAL_DATA, path.split(".")[0]); - if (action) { - _.set(GLOBAL_DATA, path, pusher.bind(data, action.bind(entity))); - } - }); - } - - // Set it to self - Object.keys(GLOBAL_DATA).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[key] = GLOBAL_DATA[key]; - }); - - ///// Adding extra libraries separately - extraLibraries.forEach((library) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[library.accessor] = library.lib; - }); - - ///// Remove all unsafe functions - unsafeFunctionForEval.forEach((func) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - self[func] = undefined; - }); - - const evalResult = eval(script); - - // Remove it from self - // This is needed so that next eval can have a clean sheet - Object.keys(GLOBAL_DATA).forEach((key) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: No types available - delete self[key]; - }); - - return evalResult; - })(); - return { result, triggers }; - } catch (e) { - ERRORS.push({ - type: EvalErrorTypes.EVAL_ERROR, - message: e.message, - context: { - binding: js, - }, - }); - return { result: undefined, triggers: [] }; - } -}; - // For creating a final value where bindings could be in a template format const createDynamicValueString = ( binding: string, @@ -1064,756 +1307,102 @@ const createDynamicValueString = ( return finalValue; }; -const getDynamicValue = ( - dynamicBinding: string, - data: DataTree, - returnTriggers: boolean, - callBackData?: Array, -) => { - // Get the {{binding}} bound values - const { stringSegments, jsSnippets } = getDynamicBindings(dynamicBinding); - if (returnTriggers) { - const result = evaluateDynamicBoundValue(data, jsSnippets[0], callBackData); - return result.triggers; +function isValidEntity(entity: DataTreeEntity): entity is DataTreeObjectEntity { + if (!_.isObject(entity)) { + // ERRORS.push({ + // type: EvalErrorTypes.BAD_UNEVAL_TREE_ERROR, + // message: "Data tree entity is not an object", + // context: entity, + // }); + return false; } - if (stringSegments.length) { - // Get the Data Tree value of those "binding "paths - const values = jsSnippets.map((jsSnippet, index) => { - if (jsSnippet) { - const result = evaluateDynamicBoundValue(data, jsSnippet, callBackData); - return result.result; - } else { - return stringSegments[index]; - } - }); + return "ENTITY_TYPE" in entity; +} - // if it is just one binding, no need to create template string - if (stringSegments.length === 1) return values[0]; - // else return a string template with bindings - return createDynamicValueString(dynamicBinding, stringSegments, values); - } - return undefined; -}; +function isWidget(entity: DataTreeEntity): entity is DataTreeWidget { + return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET; +} -const validateWidgetProperty = ( - widgetType: WidgetType, - property: string, - value: any, - props: WidgetProps, - dataTree?: DataTree, -) => { - const propertyValidationTypes = - WIDGET_TYPE_CONFIG_MAP[widgetType].validations; - const validationTypeOrValidator = propertyValidationTypes[property]; - let validator; +function isAction(entity: DataTreeEntity): entity is DataTreeAction { + return isValidEntity(entity) && entity.ENTITY_TYPE === ENTITY_TYPE.ACTION; +} - if (typeof validationTypeOrValidator === "function") { - validator = validationTypeOrValidator; - } else { - validator = VALIDATORS[validationTypeOrValidator]; - } - if (validator) { - return validator(value, props, dataTree); - } else { - return { isValid: true, parsed: value }; - } -}; - -const clearCaches = () => { - dynamicPropValueCache.clear(); - dependencyCache.clear(); - parsedValueCache.clear(); -}; - -const VALIDATORS: Record = { - [VALIDATION_TYPES.TEXT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value) || value === null) { - return { - isValid: true, - parsed: value, - message: "", - }; - } - if (isObject(value)) { - return { - isValid: false, - parsed: JSON.stringify(value, null, 2), - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - let isValid = isString(value); - if (!isValid) { - try { - parsed = toString(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to string`); - console.error(e); - return { - isValid: false, - parsed: "", - message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.REGEX]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( - value, - props, - dataTree, - ); - - if (isValid) { - try { - new RegExp(parsed); - } catch (e) { - return { - isValid: false, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, - }; - } - } - - return { isValid, parsed, message }; - }, - [VALIDATION_TYPES.NUMBER]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - let isValid = isNumber(value); - if (!isValid) { - try { - parsed = toNumber(value); - if (isNaN(parsed)) { - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to number`); - console.error(e); - return { - isValid: false, - parsed: 0, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.BOOLEAN]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: false, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - const isABoolean = isBoolean(value); - const isStringTrueFalse = value === "true" || value === "false"; - const isValid = isABoolean || isStringTrueFalse; - if (isStringTrueFalse) parsed = value !== "false"; - if (!isValid) { - return { - isValid: isValid, - parsed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OBJECT]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - if (isUndefined(value)) { - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - let isValid = isObject(value); - if (!isValid) { - try { - parsed = JSON.parse(value); - isValid = true; - } catch (e) { - console.error(`Error when parsing ${value} to object`); - console.error(e); - return { - isValid: false, - parsed: {}, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, - }; - } - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.ARRAY]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - let parsed = value; - try { - if (isUndefined(value)) { - return { - isValid: false, - parsed: [], - transformed: undefined, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - if (isString(value)) { - parsed = JSON.parse(parsed as string); - } - if (!Array.isArray(parsed)) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - return { isValid: true, parsed, transformed: parsed }; - } catch (e) { - console.error(e); - return { - isValid: false, - parsed: [], - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, - }; - } - }, - [VALIDATION_TYPES.TABS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } else if (!every(parsed, (datum) => isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.TABLE_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, transformed, parsed } = VALIDATORS.ARRAY( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - const isValidTableData = every(parsed, (datum) => { - return ( - isPlainObject(datum) && - Object.keys(datum).filter((key) => isString(key) && key.length === 0) - .length === 0 - ); - }); - if (!isValidTableData) { - return { - isValid: false, - parsed: [], - transformed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.CHART_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - transformed: parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, - }; - } - let validationMessage = ""; - let index = 0; - const isValidChartData = every( - parsed, - (datum: { name: string; data: any }) => { - const validatedResponse: { - isValid: boolean; - parsed: Array; - message?: string; - } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); - validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; - let isValidChart = validatedResponse.isValid; - if (validatedResponse.isValid) { - datum.data = validatedResponse.parsed; - isValidChart = every( - datum.data, - (chartPoint: { x: string; y: any }) => { - return ( - isObject(chartPoint) && - isString(chartPoint.x) && - !isUndefined(chartPoint.y) - ); - }, - ); - } - index++; - return isValidChart; - }, - ); - if (!isValidChartData) { - return { - isValid: false, - parsed: [], - transformed: parsed, - message: validationMessage, - }; - } - return { isValid, parsed, transformed: parsed }; - }, - [VALIDATION_TYPES.MARKERS]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } else if (!every(parsed, (datum) => isObject(datum))) { - return { - isValid: false, - parsed: [], - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, - }; - } - return { isValid, parsed }; - }, - [VALIDATION_TYPES.OPTIONS_DATA]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - if (!isValid) { - return { - isValid, - parsed, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - try { - const isValidOption = (option: { label: any; value: any }) => - _.isObject(option) && - _.isString(option.label) && - _.isString(option.value) && - !_.isEmpty(option.label) && - !_.isEmpty(option.value); - - const hasOptions = every(parsed, isValidOption); - const validOptions = parsed.filter(isValidOption); - const uniqValidOptions = _.uniqBy(validOptions, "value"); - - if (!hasOptions || uniqValidOptions.length !== validOptions.length) { - return { - isValid: false, - parsed: uniqValidOptions, - message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, - }; - } - return { isValid, parsed }; - } catch (e) { - console.error(e); - return { - isValid: false, - parsed: [], - transformed: parsed, - }; - } - }, - [VALIDATION_TYPES.DATE]: ( - dateString: string, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const today = moment() - .hour(0) - .minute(0) - .second(0) - .millisecond(0); - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; - - const todayDateString = today.format(dateFormat); - if (dateString === undefined) { - return { - isValid: false, - parsed: "", - message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.dateFormat - : "", - }; - } - const isValid = moment(dateString, dateFormat).isValid(); - const parsed = isValid ? dateString : todayDateString; - return { - isValid, - parsed, - message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, - }; - }, - [VALIDATION_TYPES.DEFAULT_DATE]: ( - dateString: string, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const today = moment() - .hour(0) - .minute(0) - .second(0) - .millisecond(0); - const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; - - const todayDateString = today.format(dateFormat); - if (dateString === undefined) { - return { - isValid: false, - parsed: "", - message: - `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat - ? props.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) +const addFunctions = (dataTree: Readonly): DataTree => { + const withFunction: DataTree = _.cloneDeep(dataTree); + withFunction.actionPaths = []; + Object.keys(withFunction).forEach((entityName) => { + const entity = withFunction[entityName]; + if (isAction(entity)) { + const runFunction = function( + this: DataTreeAction, + onSuccess: string, + onError: string, + params = "", ) { - isValid = false; - } - - if ( - isValid && - parsedMaxDate.isValid() && - parsedCurrentDate.isAfter(parsedMaxDate) - ) { - isValid = false; - } - } - - const parsed = isValid ? dateString : todayDateString; - - return { - isValid, - parsed, - message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date R`, - }; - }, - [VALIDATION_TYPES.ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - if (Array.isArray(value) && value.length) { - return { - isValid: true, - parsed: undefined, - transformed: "Function Call", - }; - } - /* - if (_.isString(value)) { - if (value.indexOf("navigateTo") !== -1) { - const pageNameOrUrl = modalGetter(value); - if (dataTree) { - if (isDynamicValue(pageNameOrUrl)) { - return { - isValid: true, - parsed: value, - }; - } - const isPage = - (dataTree.pageList as PageListPayload).findIndex( - page => page.pageName === pageNameOrUrl, - ) !== -1; - const isValidUrl = URL_REGEX.test(pageNameOrUrl); - if (!(isValidUrl || isPage)) { - return { - isValid: false, - parsed: value, - message: `${NAVIGATE_TO_VALIDATION_ERROR}`, - }; - } - } - } - } - */ - return { - isValid: false, - parsed: undefined, - transformed: "undefined", - message: "Not a function call", - }; - }, - [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( - value, - props, - dataTree, - ); - let isValidFinal = isValid; - let finalParsed = parsed.slice(); - if (isValid) { - finalParsed = parsed.map((value: any) => { - const { isValid, message } = VALIDATORS[ - VALIDATION_TYPES.ACTION_SELECTOR - ](value.dynamicTrigger, props, dataTree); - - isValidFinal = isValidFinal && isValid; return { - ...value, - message: message, - isValid: isValid, + type: "RUN_ACTION", + payload: { + actionId: this.actionId, + onSuccess: onSuccess ? `{{${onSuccess.toString()}}}` : "", + onError: onError ? `{{${onError.toString()}}}` : "", + params, + }, }; - }); + }; + _.set(withFunction, `${entityName}.run`, runFunction); + withFunction.actionPaths && + withFunction.actionPaths.push(`${entityName}.run`); } - - return { - isValid: isValidFinal, - parsed: finalParsed, - message: message, - }; - }, - [VALIDATION_TYPES.SELECTED_TAB]: ( - value: any, - props: WidgetProps, - dataTree?: DataTree, - ): ValidationResponse => { - const tabs = - props.tabs && isString(props.tabs) - ? JSON.parse(props.tabs) - : props.tabs && Array.isArray(props.tabs) - ? props.tabs - : []; - const tabNames = tabs.map((i: { label: string; id: string }) => i.label); - const isValidTabName = tabNames.includes(value); - return { - isValid: isValidTabName, - parsed: value, - message: isValidTabName - ? "" - : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, - }; - }, - [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( - value: string | string[], - props: WidgetProps, - dataTree?: DataTree, - ) => { - let values = value; - - if (props) { - if (props.selectionType === "SINGLE_SELECT") { - return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); - } else if (props.selectionType === "MULTI_SELECT") { - if (typeof value === "string") { - try { - values = JSON.parse(value); - if (!Array.isArray(values)) { - throw new Error(); - } - } catch { - values = value.length ? value.split(",") : []; - if (values.length > 0) { - values = values.map((value) => value.trim()); - } - } - } - } - } - - if (Array.isArray(values)) { - values = _.uniq(values); - } - - return { - isValid: true, - parsed: values, - }; - }, - [VALIDATION_TYPES.DEFAULT_SELECTED_ROW]: ( - value: string | string[], - props: WidgetProps, - dataTree?: DataTree, - ) => { - let values = value; - - if (props) { - if (props.multiRowSelection) { - if (typeof value === "string") { - try { - values = JSON.parse(value); - if (!Array.isArray(values)) { - throw new Error(); - } - } catch { - values = value.length ? value.split(",") : []; - if (values.length > 0) { - let numbericValues = values.map((value) => { - return isNumber(value.trim()) ? -1 : Number(value.trim()); - }); - numbericValues = _.uniq(numbericValues); - return { - isValid: true, - parsed: numbericValues, - }; - } - } - } - } else { - try { - if (value === "") { - return { - isValid: true, - parsed: -1, - }; - } - const parsed = toNumber(value); - return { - isValid: true, - parsed: parsed, - }; - } catch (e) { - return { - isValid: true, - parsed: -1, - }; - } - } - } - return { - isValid: true, - parsed: values, - }; - }, -}; - -export const makeParentsDependOnChildren = ( - depMap: DynamicDependencyMap, -): DynamicDependencyMap => { - //return depMap; - // Make all parents depend on child - Object.keys(depMap).forEach((key) => { - depMap = makeParentsDependOnChild(depMap, key); - depMap[key].forEach((path) => { - depMap = makeParentsDependOnChild(depMap, path); - }); }); - return depMap; -}; -export const makeParentsDependOnChild = ( - depMap: DynamicDependencyMap, - child: string, -): DynamicDependencyMap => { - const result: DynamicDependencyMap = depMap; - let curKey = child; - const rgx = /^(.*)\..*$/; - let matches: Array | null; - // Note: The `=` is intentional - // Stops looping when match is null - while ((matches = curKey.match(rgx)) !== null) { - const parentKey = matches[1]; - // Todo: switch everything to set. - const existing = new Set(result[parentKey] || []); - existing.add(curKey); - result[parentKey] = Array.from(existing); - curKey = parentKey; - } - return result; + withFunction.navigateTo = function( + pageNameOrUrl: string, + params: Record, + ) { + return { + type: "NAVIGATE_TO", + payload: { pageNameOrUrl, params }, + }; + }; + withFunction.actionPaths.push("navigateTo"); + + withFunction.showAlert = function(message: string, style: string) { + return { + type: "SHOW_ALERT", + payload: { message, style }, + }; + }; + withFunction.actionPaths.push("showAlert"); + + withFunction.showModal = function(modalName: string) { + return { + type: "SHOW_MODAL_BY_NAME", + payload: { modalName }, + }; + }; + withFunction.actionPaths.push("showModal"); + + withFunction.closeModal = function(modalName: string) { + return { + type: "CLOSE_MODAL", + payload: { modalName }, + }; + }; + withFunction.actionPaths.push("closeModal"); + + withFunction.storeValue = function(key: string, value: string) { + return { + type: "STORE_VALUE", + payload: { key, value }, + }; + }; + withFunction.actionPaths.push("storeValue"); + + withFunction.download = function(data: string, name: string, type: string) { + return { + type: "DOWNLOAD", + payload: { data, name, type }, + }; + }; + withFunction.actionPaths.push("download"); + return withFunction; }; diff --git a/app/client/src/workers/evaluationUtils.ts b/app/client/src/workers/evaluationUtils.ts new file mode 100644 index 0000000000..2801f3000f --- /dev/null +++ b/app/client/src/workers/evaluationUtils.ts @@ -0,0 +1,299 @@ +import { DependencyMap, isDynamicValue } from "../utils/DynamicBindingUtils"; +import { WidgetType } from "../constants/WidgetConstants"; +import { WidgetProps } from "../widgets/BaseWidget"; +import { WidgetTypeConfigMap } from "../utils/WidgetFactory"; +import { VALIDATORS } from "./validations"; +import { Diff } from "deep-diff"; +import { + DataTree, + DataTreeEntity, + DataTreeWidget, + ENTITY_TYPE, +} from "../entities/DataTree/dataTreeFactory"; +import _ from "lodash"; + +export enum DataTreeDiffEvent { + NEW = "NEW", + DELETE = "DELETE", + EDIT = "EDIT", + NOOP = "NOOP", +} + +type DataTreeDiff = { + payload: { + propertyPath: string; + value?: string; + }; + event: DataTreeDiffEvent; +}; + +export class CrashingError extends Error {} + +export const convertPathToString = (arrPath: Array) => { + let string = ""; + arrPath.forEach((segment) => { + if (typeof segment === "string") { + if (string.length !== 0) { + string = string + "."; + } + string = string + segment; + } else { + string = string + "[" + segment + "]"; + } + }); + return string; +}; + +export const translateDiffEventToDataTreeDiffEvent = ( + difference: Diff, +): DataTreeDiff => { + const result: DataTreeDiff = { + payload: { + propertyPath: "", + value: "", + }, + event: DataTreeDiffEvent.NOOP, + }; + if (!difference.path) { + return result; + } + const propertyPath = convertPathToString(difference.path); + switch (difference.kind) { + case "N": { + result.event = DataTreeDiffEvent.NEW; + result.payload = { + propertyPath, + }; + break; + } + case "D": { + result.event = DataTreeDiffEvent.DELETE; + result.payload = { propertyPath }; + break; + } + case "E": { + const rhsChange = + typeof difference.rhs === "string" && isDynamicValue(difference.rhs); + + const lhsChange = + typeof difference.lhs === "string" && isDynamicValue(difference.lhs); + + if (rhsChange || lhsChange) { + result.event = DataTreeDiffEvent.EDIT; + result.payload = { + propertyPath, + value: difference.rhs, + }; + } else { + // Handle static value changes that change structure that can lead to + // old bindings being eligible + if ( + difference.lhs === undefined && + typeof difference.rhs === "object" + ) { + result.event = DataTreeDiffEvent.NEW; + result.payload = { propertyPath }; + } + if ( + difference.rhs === undefined && + typeof difference.lhs === "object" + ) { + result.event = DataTreeDiffEvent.DELETE; + result.payload = { propertyPath }; + } + } + + break; + } + case "A": { + break; + } + default: { + break; + } + } + return result; +}; + +export const isPropertyPathOrNestedPath = ( + path: string, + comparePath: string, +): boolean => { + return path === comparePath || comparePath.startsWith(`${path}.`); +}; + +/* + Table1.selectedRow + Table1.selectedRow.email: ["Input1.defaultText"] + */ + +export const addDependantsOfNestedPropertyPaths = ( + parentPaths: Array, + inverseMap: DependencyMap, +): Array => { + const withNestedPaths: Set = new Set(); + const dependantNodes = Object.keys(inverseMap); + parentPaths.forEach((propertyPath) => { + withNestedPaths.add(propertyPath); + dependantNodes + .filter((dependantNodePath) => + isPropertyPathOrNestedPath(propertyPath, dependantNodePath), + ) + .forEach((dependantNodePath) => { + inverseMap[dependantNodePath].forEach((path) => { + withNestedPaths.add(path); + }); + }); + }); + return [...withNestedPaths.values()]; +}; + +export function isWidget(entity: DataTreeEntity): boolean { + return ( + typeof entity === "object" && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.WIDGET + ); +} + +export function isAction(entity: DataTreeEntity): boolean { + return ( + typeof entity === "object" && + "ENTITY_TYPE" in entity && + entity.ENTITY_TYPE === ENTITY_TYPE.ACTION + ); +} + +// We need to remove functions from data tree to avoid any unexpected identifier while JSON parsing +// Check issue https://github.com/appsmithorg/appsmith/issues/719 +export const removeFunctions = (value: any) => { + if (_.isFunction(value)) { + return "Function call"; + } else if (_.isObject(value) && _.some(value, _.isFunction)) { + return JSON.parse(JSON.stringify(value)); + } else { + return value; + } +}; + +export const removeFunctionsFromDataTree = (dataTree: DataTree) => { + dataTree.actionPaths?.forEach((functionPath) => { + _.set(dataTree, functionPath, {}); + }); + delete dataTree.actionPaths; + return dataTree; +}; + +export const makeParentsDependOnChildren = ( + depMap: DependencyMap, +): DependencyMap => { + //return depMap; + // Make all parents depend on child + Object.keys(depMap).forEach((key) => { + depMap = makeParentsDependOnChild(depMap, key); + depMap[key].forEach((path) => { + depMap = makeParentsDependOnChild(depMap, path); + }); + }); + return depMap; +}; +export const makeParentsDependOnChild = ( + depMap: DependencyMap, + child: string, +): DependencyMap => { + const result: DependencyMap = depMap; + let curKey = child; + const rgx = /^(.*)\..*$/; + let matches: Array | null; + // Note: The `=` is intentional + // Stops looping when match is null + while ((matches = curKey.match(rgx)) !== null) { + const parentKey = matches[1]; + // Todo: switch everything to set. + const existing = new Set(result[parentKey] || []); + existing.add(curKey); + result[parentKey] = Array.from(existing); + curKey = parentKey; + } + return result; +}; + +export function validateWidgetProperty( + widgetConfigMap: WidgetTypeConfigMap, + widgetType: WidgetType, + property: string, + value: any, + props: WidgetProps, + dataTree?: DataTree, +) { + const propertyValidationTypes = widgetConfigMap[widgetType].validations; + const validationTypeOrValidator = propertyValidationTypes[property]; + let validator; + + if (typeof validationTypeOrValidator === "function") { + validator = validationTypeOrValidator; + } else { + validator = VALIDATORS[validationTypeOrValidator]; + } + if (validator) { + return validator(value, props, dataTree); + } else { + return { isValid: true, parsed: value }; + } +} + +export function getValidatedTree( + widgetConfigMap: WidgetTypeConfigMap, + tree: DataTree, + only?: Set, +) { + return Object.keys(tree).reduce((tree, entityKey: string) => { + if (only && only.size) { + if (!only.has(entityKey)) { + return tree; + } + } + const entity = tree[entityKey] as DataTreeWidget; + if (!isWidget(entity)) { + return tree; + } + const parsedEntity = { ...entity }; + Object.keys(entity).forEach((property: string) => { + const validationProperties = widgetConfigMap[entity.type].validations; + + if (property in validationProperties) { + const value = _.get(entity, property); + // Pass it through parse + const { + parsed, + isValid, + message, + transformed, + } = validateWidgetProperty( + widgetConfigMap, + entity.type, + property, + value, + entity, + tree, + ); + parsedEntity[property] = parsed; + const evaluatedValue = isValid + ? parsed + : _.isUndefined(transformed) + ? value + : transformed; + const safeEvaluatedValue = removeFunctions(evaluatedValue); + _.set(parsedEntity, `evaluatedValues.${property}`, safeEvaluatedValue); + if (!isValid) { + _.set(parsedEntity, `invalidProps.${property}`, true); + _.set(parsedEntity, `validationMessages.${property}`, message); + } else { + _.set(parsedEntity, `invalidProps.${property}`, false); + _.set(parsedEntity, `validationMessages.${property}`, ""); + } + } + }); + return { ...tree, [entityKey]: parsedEntity }; + }, tree); +} diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts new file mode 100644 index 0000000000..10bfbfe418 --- /dev/null +++ b/app/client/src/workers/validations.ts @@ -0,0 +1,646 @@ +import { + ISO_DATE_FORMAT, + VALIDATION_TYPES, + ValidationResponse, + ValidationType, + Validator, +} from "../constants/WidgetValidation"; +import { DataTree } from "../entities/DataTree/dataTreeFactory"; +import _, { + every, + isBoolean, + isNumber, + isObject, + isString, + isUndefined, + toNumber, + toString, +} from "lodash"; +import { WidgetProps } from "../widgets/BaseWidget"; +import { WIDGET_TYPE_VALIDATION_ERROR } from "../constants/messages"; +import moment from "moment"; + +export const VALIDATORS: Record = { + [VALIDATION_TYPES.TEXT]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value) || value === null) { + return { + isValid: true, + parsed: value, + message: "", + }; + } + if (isObject(value)) { + return { + isValid: false, + parsed: JSON.stringify(value, null, 2), + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + let isValid = isString(value); + if (!isValid) { + try { + parsed = toString(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to string`); + console.error(e); + return { + isValid: false, + parsed: "", + message: `${WIDGET_TYPE_VALIDATION_ERROR}: text`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.REGEX]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.TEXT]( + value, + props, + dataTree, + ); + + if (isValid) { + try { + new RegExp(parsed); + } catch (e) { + return { + isValid: false, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: regex`, + }; + } + } + + return { isValid, parsed, message }; + }, + [VALIDATION_TYPES.NUMBER]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + let isValid = isNumber(value); + if (!isValid) { + try { + parsed = toNumber(value); + if (isNaN(parsed)) { + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to number`); + console.error(e); + return { + isValid: false, + parsed: 0, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: number`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.BOOLEAN]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: false, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + const isABoolean = isBoolean(value); + const isStringTrueFalse = value === "true" || value === "false"; + const isValid = isABoolean || isStringTrueFalse; + if (isStringTrueFalse) parsed = value !== "false"; + if (!isValid) { + return { + isValid: isValid, + parsed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: boolean`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OBJECT]: (value: any): ValidationResponse => { + let parsed = value; + if (isUndefined(value)) { + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + let isValid = isObject(value); + if (!isValid) { + try { + parsed = JSON.parse(value); + isValid = true; + } catch (e) { + console.error(`Error when parsing ${value} to object`); + console.error(e); + return { + isValid: false, + parsed: {}, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Object`, + }; + } + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.ARRAY]: (value: any): ValidationResponse => { + let parsed = value; + try { + if (isUndefined(value)) { + return { + isValid: false, + parsed: [], + transformed: undefined, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + if (isString(value)) { + parsed = JSON.parse(parsed as string); + } + if (!Array.isArray(parsed)) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + return { isValid: true, parsed, transformed: parsed }; + } catch (e) { + console.error(e); + return { + isValid: false, + parsed: [], + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Array/List`, + }; + } + }, + [VALIDATION_TYPES.TABS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } else if (!every(parsed, (datum) => isObject(datum))) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Tabs Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.TABLE_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, transformed, parsed } = VALIDATORS.ARRAY( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + const isValidTableData = every(parsed, (datum) => { + return ( + isObject(datum) && + Object.keys(datum).filter((key) => isString(key) && key.length === 0) + .length === 0 + ); + }); + if (!isValidTableData) { + return { + isValid: false, + parsed: [], + transformed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.CHART_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + transformed: parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`, + }; + } + let validationMessage = ""; + let index = 0; + const isValidChartData = every( + parsed, + (datum: { name: string; data: any }) => { + const validatedResponse: { + isValid: boolean; + parsed: Array; + message?: string; + } = VALIDATORS[VALIDATION_TYPES.ARRAY](datum.data, props, dataTree); + validationMessage = `${index}##${WIDGET_TYPE_VALIDATION_ERROR}: [{ "x": "val", "y": "val" }]`; + let isValidChart = validatedResponse.isValid; + if (validatedResponse.isValid) { + datum.data = validatedResponse.parsed; + isValidChart = every( + datum.data, + (chartPoint: { x: string; y: any }) => { + return ( + isObject(chartPoint) && + isString(chartPoint.x) && + !isUndefined(chartPoint.y) + ); + }, + ); + } + index++; + return isValidChart; + }, + ); + if (!isValidChartData) { + return { + isValid: false, + parsed: [], + transformed: parsed, + message: validationMessage, + }; + } + return { isValid, parsed, transformed: parsed }; + }, + [VALIDATION_TYPES.MARKERS]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } else if (!every(parsed, (datum) => isObject(datum))) { + return { + isValid: false, + parsed: [], + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Marker Data`, + }; + } + return { isValid, parsed }; + }, + [VALIDATION_TYPES.OPTIONS_DATA]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + if (!isValid) { + return { + isValid, + parsed, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + try { + const isValidOption = (option: { label: any; value: any }) => + _.isObject(option) && + _.isString(option.label) && + _.isString(option.value) && + !_.isEmpty(option.label) && + !_.isEmpty(option.value); + + const hasOptions = every(parsed, isValidOption); + const validOptions = parsed.filter(isValidOption); + const uniqValidOptions = _.uniqBy(validOptions, "value"); + + if (!hasOptions || uniqValidOptions.length !== validOptions.length) { + return { + isValid: false, + parsed: uniqValidOptions, + message: `${WIDGET_TYPE_VALIDATION_ERROR}: Options Data`, + }; + } + return { isValid, parsed }; + } catch (e) { + console.error(e); + return { + isValid: false, + parsed: [], + transformed: parsed, + }; + } + }, + [VALIDATION_TYPES.DATE]: ( + dateString: string, + props: WidgetProps, + ): ValidationResponse => { + const today = moment() + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + + const todayDateString = today.format(dateFormat); + if (dateString === undefined) { + return { + isValid: false, + parsed: "", + message: + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat + ? props.dateFormat + : "", + }; + } + const isValid = moment(dateString, dateFormat).isValid(); + const parsed = isValid ? dateString : todayDateString; + return { + isValid, + parsed, + message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date`, + }; + }, + [VALIDATION_TYPES.DEFAULT_DATE]: ( + dateString: string, + props: WidgetProps, + ): ValidationResponse => { + const today = moment() + .hour(0) + .minute(0) + .second(0) + .millisecond(0); + const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT; + + const todayDateString = today.format(dateFormat); + if (dateString === undefined) { + return { + isValid: false, + parsed: "", + message: + `${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat + ? props.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 parsed = isValid ? dateString : todayDateString; + + return { + isValid, + parsed, + message: isValid ? "" : `${WIDGET_TYPE_VALIDATION_ERROR}: Date R`, + }; + }, + [VALIDATION_TYPES.ACTION_SELECTOR]: (value: any): ValidationResponse => { + if (Array.isArray(value) && value.length) { + return { + isValid: true, + parsed: undefined, + transformed: "Function Call", + }; + } + /* + if (_.isString(value)) { + if (value.indexOf("navigateTo") !== -1) { + const pageNameOrUrl = modalGetter(value); + if (dataTree) { + if (isDynamicValue(pageNameOrUrl)) { + return { + isValid: true, + parsed: value, + }; + } + const isPage = + (dataTree.pageList as PageListPayload).findIndex( + page => page.pageName === pageNameOrUrl, + ) !== -1; + const isValidUrl = URL_REGEX.test(pageNameOrUrl); + if (!(isValidUrl || isPage)) { + return { + isValid: false, + parsed: value, + message: `${NAVIGATE_TO_VALIDATION_ERROR}`, + }; + } + } + } + } + */ + return { + isValid: false, + parsed: undefined, + transformed: "undefined", + message: "Not a function call", + }; + }, + [VALIDATION_TYPES.ARRAY_ACTION_SELECTOR]: ( + value: any, + props: WidgetProps, + dataTree?: DataTree, + ): ValidationResponse => { + const { isValid, parsed, message } = VALIDATORS[VALIDATION_TYPES.ARRAY]( + value, + props, + dataTree, + ); + let isValidFinal = isValid; + let finalParsed = parsed.slice(); + if (isValid) { + finalParsed = parsed.map((value: any) => { + const { isValid, message } = VALIDATORS[ + VALIDATION_TYPES.ACTION_SELECTOR + ](value.dynamicTrigger, props, dataTree); + + isValidFinal = isValidFinal && isValid; + return { + ...value, + message: message, + isValid: isValid, + }; + }); + } + + return { + isValid: isValidFinal, + parsed: finalParsed, + message: message, + }; + }, + [VALIDATION_TYPES.SELECTED_TAB]: ( + value: any, + props: WidgetProps, + ): ValidationResponse => { + const tabs = + props.tabs && isString(props.tabs) + ? JSON.parse(props.tabs) + : props.tabs && Array.isArray(props.tabs) + ? props.tabs + : []; + const tabNames = tabs.map((i: { label: string; id: string }) => i.label); + const isValidTabName = tabNames.includes(value); + return { + isValid: isValidTabName, + parsed: value, + message: isValidTabName + ? "" + : `${WIDGET_TYPE_VALIDATION_ERROR}: Invalid tab name.`, + }; + }, + [VALIDATION_TYPES.DEFAULT_OPTION_VALUE]: ( + value: string | string[], + props: WidgetProps, + dataTree?: DataTree, + ) => { + let values = value; + + if (props) { + if (props.selectionType === "SINGLE_SELECT") { + return VALIDATORS[VALIDATION_TYPES.TEXT](value, props, dataTree); + } else if (props.selectionType === "MULTI_SELECT") { + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + values = values.map((value) => value.trim()); + } + } + } + } + } + + if (Array.isArray(values)) { + values = _.uniq(values); + } + + return { + isValid: true, + parsed: values, + }; + }, + [VALIDATION_TYPES.DEFAULT_SELECTED_ROW]: ( + value: string | string[], + props: WidgetProps, + ) => { + let values = value; + + if (props) { + if (props.multiRowSelection) { + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + let numericValues = values.map((value) => { + return isNumber(value.trim()) ? -1 : Number(value.trim()); + }); + numericValues = _.uniq(numericValues); + return { + isValid: true, + parsed: numericValues, + }; + } + } + } + } else { + try { + const parsed = toNumber(value); + return { + isValid: true, + parsed: parsed, + }; + } catch (e) { + return { + isValid: true, + parsed: -1, + }; + } + } + } + return { + isValid: true, + parsed: values, + }; + }, +}; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 76d82aeda5..b5a1a09eb8 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -3835,6 +3835,11 @@ dependencies: "@types/tern" "*" +"@types/deep-diff@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/deep-diff/-/deep-diff-1.0.0.tgz#7eba3202a99b3a207f758f351f7f86387269fc40" + integrity sha512-ENsJcujGbCU/oXhDfQ12mSo/mCBWodT2tpARZKmatoSrf8+cGRCPi0KVj3I0FORhYZfLXkewXu7AoIWqiBLkNw== + "@types/dom4@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.1.tgz#506d5781b9bcab81bd9a878b198aec7dee2a6033" @@ -7715,6 +7720,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-equal@^1.0.1, deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"