diff --git a/app/client/cypress.json b/app/client/cypress.json index 5368626ff0..e1a813ea99 100644 --- a/app/client/cypress.json +++ b/app/client/cypress.json @@ -10,5 +10,7 @@ "overwrite": false, "html": true, "json": false - } + }, + "viewportHeight": 900, + "viewportWidth": 1400 } diff --git a/app/client/cypress/fixtures/ChartTextDsl.json b/app/client/cypress/fixtures/ChartTextDsl.json new file mode 100644 index 0000000000..469fe4d7b9 --- /dev/null +++ b/app/client/cypress/fixtures/ChartTextDsl.json @@ -0,0 +1,186 @@ +{ +"dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 1224, + "snapColumns": 16, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1292, + "containerStyle": "none", + "snapRows": 33, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "dynamicBindings": {}, + "version": 4, + "minHeight": 1292, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [ + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container1", + "type": "CONTAINER_WIDGET", + "containerStyle": "card", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 75.25, + "parentRowSpace": 38, + "dynamicBindings": {}, + "leftColumn": 0, + "rightColumn": 8, + "topRow": 0, + "bottomRow": 9, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "kydabisaxj", + "type": "CANVAS_WIDGET", + "containerStyle": "none", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 602, + "topRow": 0, + "bottomRow": 342, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "isVisible": true, + "widgetName": "Chart1", + "chartType": "LINE_CHART", + "chartName": "Sales on working days", + "allowHorizontalScroll": false, + "chartData": "[{\"seriesName\":\"\",\"data\":\"\"}]", + "xAxisName": "Last Week", + "yAxisName": "Total Order Revenue $", + "type": "CHART_WIDGET", + "isLoading": false, + "parentColumnSpace": 34.6875, + "parentRowSpace": 38, + "leftColumn": 5, + "rightColumn": 11, + "topRow": 0, + "bottomRow": 8, + "parentId": "56c5odk5ic", + "widgetId": "64jukpgbzh", + "dynamicBindings": {} + } + ], + "widgetId": "56c5odk5ic", + "detachFromLayout": true, + "canExtend": false + } + ], + "widgetId": "kzlk5ductp" + }, + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container3", + "type": "CONTAINER_WIDGET", + "containerStyle": "card", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 75.25, + "parentRowSpace": 38, + "dynamicBindings": {}, + "leftColumn": 0, + "rightColumn": 16, + "topRow": 9, + "bottomRow": 23, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "za6o0unktq", + "type": "CANVAS_WIDGET", + "containerStyle": "none", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 1204, + "topRow": 0, + "bottomRow": 532, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "isVisible": true, + "text": "Label", + "textStyle": "LABEL", + "textAlign": "LEFT", + "widgetName": "Text1", + "type": "TEXT_WIDGET", + "isLoading": false, + "parentColumnSpace": 71.75, + "parentRowSpace": 38, + "leftColumn": 3, + "rightColumn": 7, + "topRow": 2, + "bottomRow": 3, + "parentId": "d2i9xsy2fk", + "widgetId": "u210slvpsz", + "dynamicBindings": {} + } + ], + "widgetId": "d2i9xsy2fk", + "detachFromLayout": true, + "canExtend": false + } + ], + "widgetId": "t2a7se9pxe" + }, + { + "backgroundColor": "#FFFFFF", + "widgetName": "Container4", + "type": "CONTAINER_WIDGET", + "containerStyle": "card", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 75.25, + "parentRowSpace": 38, + "dynamicBindings": {}, + "leftColumn": 8, + "rightColumn": 16, + "topRow": 0, + "bottomRow": 9, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [ + { + "backgroundColor": "transparent", + "widgetName": "cli9vgw4yj", + "type": "CANVAS_WIDGET", + "containerStyle": "none", + "isVisible": true, + "isLoading": false, + "parentColumnSpace": 1, + "parentRowSpace": 1, + "leftColumn": 0, + "rightColumn": 602, + "topRow": 0, + "bottomRow": 342, + "snapColumns": 16, + "orientation": "VERTICAL", + "children": [], + "widgetId": "qzniae78ab", + "detachFromLayout": true, + "canExtend": false + } + ], + "widgetId": "gtsbf2q08n" + } + ] +}, +"layoutOnLoadActions": [] +} diff --git a/app/client/cypress/fixtures/example.json b/app/client/cypress/fixtures/example.json index 35d6382f86..9613f42363 100644 --- a/app/client/cypress/fixtures/example.json +++ b/app/client/cypress/fixtures/example.json @@ -152,5 +152,20 @@ "productName": "Avocado Panini", "orderAmount": 7.99 } - ] + ], + "chartInputValidate": [ + { + "x": "Test1", + "y": 5400 + }, + { + "x": "Test2", + "y": 10000 + }, + { + "x": "Test3", + "y": 1000 + } + ], + "Chartval":["Test1", "Test2", "Test3"] } \ No newline at end of file diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index fa12a4d4f8..dfc3f9e090 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -17,6 +17,7 @@ "responsetext2": "qui est esse", "baseUrl3": "https://reqres.in", "methods2": "api/users/2", + "invalidPath": "api/users/a", "responsetext3": "Josh M Krantz", "postUrl": "https://reqres.in", "deleteUrl": "", diff --git a/app/client/cypress/fixtures/viewdsl.json b/app/client/cypress/fixtures/viewdsl.json index 19c9d06392..083aa66cbf 100644 --- a/app/client/cypress/fixtures/viewdsl.json +++ b/app/client/cypress/fixtures/viewdsl.json @@ -148,39 +148,34 @@ "chartType": "BAR_CHART", "chartName": "App Sign Up", "allowHorizontalScroll": false, - "chartData": [ + "singleChartData": [ { - "seriesName": "", - "data": [ - { - "x": "Mon", - "y": 10000 - }, - { - "x": "Tue", - "y": 12000 - }, - { - "x": "Wed", - "y": 32000 - }, - { - "x": "Thu", - "y": 28000 - }, - { - "x": "Fri", - "y": 14000 - }, - { - "x": "Sat", - "y": 19000 - }, - { - "x": "Sun", - "y": 36000 - } - ] + "x": "Mon", + "y": 10000 + }, + { + "x": "Tue", + "y": 12000 + }, + { + "x": "Wed", + "y": 32000 + }, + { + "x": "Thu", + "y": 28000 + }, + { + "x": "Fri", + "y": 14000 + }, + { + "x": "Sat", + "y": 19000 + }, + { + "x": "Sun", + "y": 36000 } ], "xAxisName": "Last Week", @@ -265,4 +260,4 @@ ] }, "layoutOnLoadActions": [] - } \ No newline at end of file + } diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js index 79cf5c43a0..505ac2f1d5 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Edit_spec.js @@ -23,7 +23,7 @@ describe("API Panel Test Functionality", function() { cy.ClearSearch(); cy.SearchAPIandClick("SecondAPI"); //invalid api end point check - cy.EditSourceDetail(testdata.baseUrl3, testdata.methods2); + cy.EditSourceDetail(testdata.baseUrl3, testdata.invalidPath); cy.ResponseStatusCheck("404 NOT_FOUND"); cy.DeleteAPI(); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js index 6d1cb11118..fd502020be 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js @@ -3,7 +3,6 @@ const testdata = require("../../../fixtures/testdata.json"); describe("API Panel Test Functionality ", function() { it("Test Search API fetaure", function() { cy.log("Login Successful"); - cy.viewport("macbook-15"); //To avoid screen Resize issues cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("FirstAPI"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js index 36e8eee5ee..32f432adfd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js @@ -3,7 +3,6 @@ const testdata = require("../../../fixtures/testdata.json"); describe("API Panel Test Functionality ", function() { it("Test API copy/Move/delete feature", function() { cy.log("Login Successful"); - cy.viewport("macbook-15"); //To avoid screen Resize issues cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("FirstAPI"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js new file mode 100644 index 0000000000..8a2ed6b205 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/ChartText.js @@ -0,0 +1,71 @@ +const commonlocators = require("../../../locators/commonlocators.json"); +const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); +const publish = require("../../../locators/publishWidgetspage.json"); +const dsl = require("../../../fixtures/ChartTextDsl.json"); + +describe("Text-Chart Binding Functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Text-Chart Binding Functionality View", function() { + cy.openPropertyPane("textwidget"); + cy.testJsontext("text", JSON.stringify(this.data.chartInputValidate)); + cy.get(commonlocators.TextInside).should( + "have.text", + JSON.stringify(this.data.chartInputValidate), + ); + cy.closePropertyPane(); + cy.openPropertyPane("chartwidget"); + cy.get(viewWidgetsPage.chartType) + .find(commonlocators.dropdownbuttonclick) + .click({ force: true }) + .get(commonlocators.dropdownmenu) + .children() + .contains("Column Chart") + .click(); + cy.get(viewWidgetsPage.chartType) + .find(commonlocators.menuSelection) + .should("have.text", "Column Chart"); + cy.testJsontext("chartdata", "{{Text1.text}}"); + cy.closePropertyPane(); + const labels = [ + this.data.Chartval[0], + this.data.Chartval[1], + this.data.Chartval[2], + ]; + [0, 1, 2].forEach(k => { + cy.get(viewWidgetsPage.rectangleChart) + .eq(k) + .trigger("mousemove", { force: true }); + cy.get(viewWidgetsPage.Chartlabel) + .eq(k) + .should("have.text", labels[k]); + }); + cy.PublishtheApp(); + }); + it("Text-Chart Binding Functionality Publish", function() { + cy.get(publish.chartCanvasVal).should("be.visible"); + cy.get(publish.chartWidget).should("have.css", "opacity", "1"); + const labels = [ + this.data.Chartval[0], + this.data.Chartval[1], + this.data.Chartval[2], + ]; + [0, 1, 2].forEach(k => { + cy.get(publish.rectChart) + .eq(k) + .trigger("mousemove", { force: true }); + cy.get(publish.chartLab) + .eq(k) + .should("have.text", labels[k]); + }); + cy.get(commonlocators.TextInside).should( + "have.text", + JSON.stringify(this.data.chartInputValidate), + ); + }); +}); + +afterEach(() => { + // put your clean up code if any +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js index c5cf1acdbe..02742a6d30 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Binding/TextTable.js @@ -6,13 +6,85 @@ describe("Text-Table Binding Functionality", function() { before(() => { cy.addDsl(dsl); }); - it("Text-Table Binding Functionality For Username", function() { + it("Text-Table Binding Functionality For Id", function() { cy.openPropertyPane("tablewidget"); /** * @param(Index) Provide index value to select the row. */ cy.isSelectRow(1); cy.openPropertyPane("textwidget"); + cy.testJsontext("text", "{{Table1.selectedRow.id}}"); + /** + * @param{Row Index} Provide the row index + * @param(Column Index) Provide column index + */ + cy.readTabledata("1", "0").then(tabData => { + const tabValue = tabData; + cy.get(commonlocators.TextInside).should("have.text", tabValue); + cy.PublishtheApp(); + cy.isSelectRow(1); + cy.readTabledataPublish("1", "0").then(tabDataP => { + const tabValueP = tabDataP; + cy.get(commonlocators.TextInside).should("have.text", tabValueP); + }); + }); + }); + it("Text-Table Binding Functionality For Email", function() { + cy.get(publish.backToEditor) + .first() + .click(); + cy.isSelectRow(2); + cy.openPropertyPane("textwidget"); + cy.testJsontext("text", "{{Table1.selectedRow.email}}"); + /** + * @param{Row Index} Provide the row index + * @param(Column Index) Provide column index + */ + cy.readTabledata("2", "1").then(tabData => { + const tabValue = tabData; + cy.get(commonlocators.TextInside).should("have.text", tabValue); + cy.PublishtheApp(); + cy.isSelectRow(2); + cy.readTabledataPublish("2", "1").then(tabDataP => { + const tabValueP = tabDataP; + cy.get(commonlocators.TextInside).should("have.text", tabValueP); + }); + }); + }); + it("Text-Table Binding Functionality For Total Length", function() { + cy.get(publish.backToEditor) + .first() + .click(); + cy.pageNo(); + cy.openPropertyPane("textwidget"); + cy.testJsontext("text", "{{Table1.pageSize}}"); + cy.get(commonlocators.TableRow) + .find(".tr") + .then(listing => { + const listingCount = listing.length.toString(); + cy.get(commonlocators.TextInside).should("have.text", listingCount); + cy.PublishtheApp(); + cy.pageNo(); + cy.get(publish.tableLength) + .find(".tr") + .then(listing => { + const listingCountP = listing.length.toString(); + cy.get(commonlocators.TextInside).should( + "have.text", + listingCountP, + ); + }); + }); + }); + it("Text-Table Binding Functionality For Username", function() { + cy.get(publish.backToEditor) + .first() + .click(); + /** + * @param(Index) Provide index value to select the row. + */ + cy.isSelectRow(1); + cy.openPropertyPane("textwidget"); cy.testJsontext("text", JSON.stringify(this.data.textfun)); /** * @param{Row Index} Provide the row index diff --git a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js index a7bb48470e..3542c37709 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js @@ -25,14 +25,14 @@ describe("Input Widget Functionality", function() { cy.get(widgetsPage.innertext) .click({ force: true }) .type(this.data.para); - cy.get(publish.inputWidget + " " + "input") + cy.get(widgetsPage.inputWidget + " " + "input") .invoke("attr", "value") .should("contain", this.data.para); cy.openPropertyPane("inputwidget"); cy.get(widgetsPage.defaultInput) .type(this.data.command) .type(this.data.defaultdata); - cy.get(publish.inputWidget + " " + "input") + cy.get(widgetsPage.inputWidget + " " + "input") .invoke("attr", "value") .should("contain", this.data.defaultdata); cy.get(widgetsPage.placeholder) diff --git a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js index 354b056203..8432a53f67 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js @@ -19,16 +19,16 @@ describe("Table Widget Functionality", function() { cy.widgetText("Table1", widgetsPage.tableWidget, commonlocators.tableInner); cy.testJsontext("tabledata", JSON.stringify(this.data.TableInput)); cy.wait("@updateLayout"); - cy.ExportVerify(commonlocators.pdfSupport, "PDF Export"); - cy.ExportVerify(commonlocators.ExcelSupport, "Excel Export"); - cy.ExportVerify(commonlocators.csvSupport, "CSV Export"); + // cy.ExportVerify(commonlocators.pdfSupport, "PDF Export"); + // cy.ExportVerify(commonlocators.ExcelSupport, "Excel Export"); + // cy.ExportVerify(commonlocators.csvSupport, "CSV Export"); cy.get(widgetsPage.ColumnAction).click({ force: true }); - cy.readTabledata("1", "5").then(tabData => { - const tabValue = tabData; - expect(tabValue).to.be.equal("Action"); - cy.log("the value is" + tabValue); - }); - cy.pageNo(2).should("be.visible"); + // cy.readTabledata("1", "5").then(tabData => { + // const tabValue = tabData; + // expect(tabValue).to.be.equal("Action"); + // cy.log("the value is" + tabValue); + // }); + cy.pageNo(); cy.openPropertyPane("tablewidget"); cy.get(widgetsPage.tableOnRowSelected) .get(commonlocators.dropdownSelectButton) @@ -56,31 +56,33 @@ describe("Table Widget Functionality", function() { }); }); it("Table Widget Functionality To Verify The PageNo", function() { - cy.pageNo(2).should("be.visible"); - cy.get(publish.backToEditor).click(); - }); - it("Table Widget Functionality To Verify The Extension Support", function() { - cy.openPropertyPane("tablewidget"); - cy.togglebar(commonlocators.pdfSupport); - cy.PublishtheApp(); - cy.get(publish.tableWidget + " " + "button").should( - "contain", - "PDF Export", - ); - cy.get(publish.backToEditor).click(); - cy.openPropertyPane("tablewidget"); - cy.togglebarDisable(commonlocators.pdfSupport); - cy.togglebar(commonlocators.ExcelSupport); - cy.PublishtheApp(); - cy.get(publish.tableWidget + " " + "button").should( - "not.contain", - "PDF Export", - ); - cy.get(publish.tableWidget + " " + "button").should( - "contain", - "Excel Export", - ); + cy.pageNo(); + cy.get(publish.backToEditor) + .first() + .click(); }); + // it("Table Widget Functionality To Verify The Extension Support", function() { + // cy.openPropertyPane("tablewidget"); + // cy.togglebar(commonlocators.pdfSupport); + // cy.PublishtheApp(); + // cy.get(publish.tableWidget + " " + "button").should( + // "contain", + // "PDF Export", + // ); + // cy.get(publish.backToEditor).click(); + // cy.openPropertyPane("tablewidget"); + // cy.togglebarDisable(commonlocators.pdfSupport); + // cy.togglebar(commonlocators.ExcelSupport); + // cy.PublishtheApp(); + // cy.get(publish.tableWidget + " " + "button").should( + // "not.contain", + // "PDF Export", + // ); + // cy.get(publish.tableWidget + " " + "button").should( + // "contain", + // "Excel Export", + // ); + // }); }); Cypress.on("test:after:run", attributes => { /* eslint-disable no-console */ diff --git a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js index 08aa7ec5b6..359af1807a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/DynamicInput/autocomplete_spec.js @@ -1,6 +1,7 @@ const dsl = require("../../../fixtures/commondsl.json"); const pages = require("../../../locators/Pages.json"); const dynamicInputLocators = require("../../../locators/DynamicInput.json"); +const apiwidget = require("../../../locators/apiWidgetslocator.json"); describe("Dynamic input autocomplete", () => { beforeEach(() => { @@ -52,4 +53,22 @@ describe("Dynamic input autocomplete", () => { }); }); }); + it("opens current value popup", () => { + // Test on widgets pane + cy.get(pages.widgetsEditor).click(); + cy.openPropertyPane("buttonwidget"); + cy.get(dynamicInputLocators.input) + .first() + .focus(); + cy.assertEvaluatedValuePopup("string"); + + // Test on api pane + cy.NavigateToAPI_Panel(); + cy.get(apiwidget.createapi).click({ force: true }); + cy.wait("@createNewApi"); + cy.xpath(apiwidget.headerValue) + .first() + .focus(); + cy.assertEvaluatedValuePopup("string"); + }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js index cc267dcd01..493479fa3e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js @@ -2,7 +2,6 @@ describe("Create and Delete App Functionality", function() { it("Delete App Functionality", function() { cy.log("appname: " + localStorage.getItem("AppName")); const appname = localStorage.getItem("AppName"); - cy.openPropertyPane("textwidget"); cy.DeleteApp(appname); cy.wait("@deleteApplication"); cy.get("@deleteApplication").should("have.property", "status", 200); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Map_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Map_spec.js index da5fff0f59..1389a8fb32 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Map_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Map_spec.js @@ -1,13 +1,17 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); const dsl = require("../../../fixtures/Mapdsl.json"); +const publishPage = require("../../../locators/publishWidgetspage.json"); describe("Map Widget Functionality", function() { - beforeEach(() => { + before(() => { cy.addDsl(dsl); }); - it("Map Widget Functionality", function() { + + this.beforeEach(() => { cy.openPropertyPane("mapwidget"); + }); + it("Map Widget Functionality", function() { /** * @param{Text} Random Text * @param{MapWidget}Mouseover @@ -35,18 +39,88 @@ describe("Map Widget Functionality", function() { cy.get(viewWidgetsPage.zoomLevel) .eq(1) .click({ force: true }); - - cy.get(viewWidgetsPage.mapSearch).should("be.visible"); - cy.get(viewWidgetsPage.mapSearch) - .invoke("attr", "placeholder") - .should("contain", "Enter location to search"); cy.get(viewWidgetsPage.mapSearch) .click({ force: true }) .clear() .type(this.data.location2) .type("{enter}"); }); - afterEach(() => { - // put your clean up code if any + it("Map-Enable Location,Map search and Create Marker Property Validation", function() { + /** + * Enable the Search Location checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enableSearchLocCheckbox); + cy.get(viewWidgetsPage.mapSearch).should("be.visible"); + cy.get(viewWidgetsPage.mapSearch) + .invoke("attr", "placeholder") + .should("contain", "Enter location to search"); + /** + * Enable the Pick Location checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enablePickLocCheckbox); + cy.get(viewWidgetsPage.pickMyLocation).should("exist"); + + /** + * Enable the Createnew Marker checkbox and Validate the same in editor mode + */ + cy.CheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); + /** + * Validation will be added when create marker fun is working fine + */ + + cy.PublishtheApp(); + /** + * Publish mode Validation + */ + cy.get(publishPage.mapSearch).should("be.visible"); + cy.get(publishPage.mapSearch) + .invoke("attr", "placeholder") + .should("contain", "Enter location to search"); + cy.get(publishPage.pickMyLocation).should("exist"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Disable Location, Mapsearch and Create Marker Property Validation", function() { + /** + * Disable the Search Location checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enableSearchLocCheckbox); + cy.get(viewWidgetsPage.mapSearch).should("not.be.visible"); + /** + * Disable the Pick Location checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enablePickLocCheckbox); + cy.get(viewWidgetsPage.pickMyLocation).should("not.exist"); + + /** + * Disable the Createnew Marker checkbox and Validate the same in editor mode + */ + cy.UncheckWidgetProperties(commonlocators.enableCreateMarkerCheckbox); + /** + * Validation will be added when create marker fun is working fine + */ + + cy.PublishtheApp(); + /** + * Publish mode Validation + */ + cy.get(publishPage.mapSearch).should("not.be.visible"); + cy.get(publishPage.pickMyLocation).should("not.exist"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Check Visible field Validation", function() { + //Check the disableed checkbox and Validate + cy.CheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.mapWidget).should("be.visible"); + cy.get(publishPage.backToEditor).click(); + }); + + it("Map-Unckeck Visible field Validation", function() { + //Uncheck the disabled checkbox and validate + cy.UncheckWidgetProperties(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get(publishPage.mapWidget).should("not.be.visible"); }); }); diff --git a/app/client/cypress/locators/DynamicInput.json b/app/client/cypress/locators/DynamicInput.json index b4a8cc2a7e..5c3eb61356 100644 --- a/app/client/cypress/locators/DynamicInput.json +++ b/app/client/cypress/locators/DynamicInput.json @@ -1,4 +1,5 @@ { "input": ".CodeMirror textarea", - "hints": "ul.CodeMirror-hints" + "hints": "ul.CodeMirror-hints", + "evaluatedValue": ".t--CodeEditor-evaluatedValue" } diff --git a/app/client/cypress/locators/ViewWidgets.json b/app/client/cypress/locators/ViewWidgets.json index 242824e97c..a84df6bc25 100644 --- a/app/client/cypress/locators/ViewWidgets.json +++ b/app/client/cypress/locators/ViewWidgets.json @@ -27,5 +27,6 @@ "createMarker": ".t--property-control-createnewmarker [type='checkbox']", "zoomLevel": ".t--property-control-zoomlevel svg", "defaultImage": ".t--property-control-defaultimage .CodeMirror-code", - "Chartlabel": ".t--draggable-chartwidget g:nth-child(5) text" + "Chartlabel": ".t--draggable-chartwidget g:nth-child(5) text", + "pickMyLocation": ".t--draggable-mapwidget div[title='Pick My Location']" } \ No newline at end of file diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 4e6ae0f353..021880366e 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -6,7 +6,7 @@ "popover": ".bp3-popover-target >div>svg", "moveTo": ".single-select >div:contains('Move to')", "copyTo": ".single-select >div:contains('Copy to')", - "home": ".single-select >div:contains('Home')", + "home": ".single-select >div:contains('Page1')", "delete": ".single-select >div:contains('Delete')", "path": ".t--path >div textarea", "editResourceUrl": ".t--dataSourceField input", @@ -33,4 +33,4 @@ "panigationPrevUrl": ".t--apiFormPaginationPrev div>textarea", "TestNextUrl": ".t--apiFormPaginationNextTest", "TestPreUrl": ".t--apiFormPaginationPrevTest" -} \ No newline at end of file +} diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 8179a90e20..f8e22f82eb 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -33,7 +33,7 @@ "editWidgetName": ".bp3-editable-text", "dropDownIcon": ".t--property-control-textstyle span.bp3-icon-chevron-down", "onDateSelectedField": ".t--property-control-ondateselected", - "TableRow": ".t--draggable-tablewidget .e-gridcontent.e-lib.e-droppable", + "TableRow": ".t--draggable-tablewidget .tbody", "Disablejs": ".t--property-control-disabled", "requiredjs": ".t--property-control-required", "horizontalScroll": ".t--property-control-allowhorizontalscroll input", @@ -45,5 +45,8 @@ "disabledBtn": " button[disabled='disabled']", "inputField": " .bp3-input", "csvSupport": ".t--property-control-csvexport input", - "backToEditor": ".bp3-icon.bp3-icon-chevron-left + span.bp3-button-text" -} + "backToEditor": ".t--back-to-editor", + "enableSearchLocCheckbox": ".t--property-control-enablesearchlocation input", + "enablePickLocCheckbox": ".t--property-control-enablepicklocation input", + "enableCreateMarkerCheckbox": ".t--property-control-createnewmarker input" +} \ No newline at end of file diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json index a861d50118..a5e6f071c4 100644 --- a/app/client/cypress/locators/publishWidgetspage.json +++ b/app/client/cypress/locators/publishWidgetspage.json @@ -3,7 +3,7 @@ "textWidget": ".t--widget-textwidget", "richTextEditorWidget": ".t--widget-richtexteditorwidget", "datepickerWidget": ".t--widget-datepickerwidget", - "backToEditor": ".bp3-icon.bp3-icon-chevron-left + span.bp3-button-text", + "backToEditor": ".t--back-to-editor", "inputWidget": ".t--widget-inputwidget", "checkboxWidget": ".t--widget-checkboxwidget", "radioWidget": ".t--widget-radiogroupwidget", @@ -12,7 +12,14 @@ "dropdownWidget": ".t--widget-dropdownwidget", "tabWidget": ".t--widget-tabswidget", "chartWidget": ".t--widget-chartwidget", - "horizontalTab": ".t--widget-chartwidget g[class='raphael-group-104-axis-Line-group'] rect", + "horizontalTab": ".t--widget-chartwidget g[class*='-scrollContainer'] rect", "tableWidget": ".t--widget-tablewidget", - "tableLength": ".t--widget-tablewidget .e-gridcontent.e-lib.e-droppable" + "chartCanvasVal": ".t--widget-chartwidget g[class*='-canvas'] rect ", + "mapWidget": ".t--widget-mapwidget", + "tableLength": ".t--widget-tablewidget .tbody", + "mapSearch": ".t--widget-mapwidget input", + "pickMyLocation": ".t--widget-mapwidget div[title='Pick My Location']", + "rectChart":".t--widget-chartwidget g rect", + "chartLab":".t--widget-chartwidget g:nth-child(5) text" + } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index c051f7e779..1ebcbd747a 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -11,6 +11,7 @@ const LayoutPage = require("../locators/Layout.json"); const formWidgetsPage = require("../locators/FormWidgets.json"); const ApiEditor = require("../locators/ApiEditor.json"); const apiwidget = require("../locators/apiWidgetslocator.json"); +const dynamicInputLocators = require("../locators/DynamicInput.json"); let pageidcopy = " "; Cypress.Commands.add("CreateApp", appname => { @@ -184,7 +185,8 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { cy.xpath(apiwidget.autoSuggest) .first() .click({ force: true }); - cy.get(apiwidget.path) + cy.get(apiwidget.editResourceUrl) + .first() .click({ force: true }) .type(path, { parseSpecialCharSequences: false }); }); @@ -210,16 +212,17 @@ Cypress.Commands.add( Cypress.Commands.add("EditSourceDetail", (baseUrl, v1method) => { cy.get(apiwidget.editResourceUrl) .first() - .clear() .click({ force: true }) - .type(baseUrl); + .clear() + .type(`{backspace}${baseUrl}`); cy.xpath(apiwidget.autoSuggest) .first() .click({ force: true }); cy.get(ApiEditor.ApiRunBtn).scrollIntoView(); - cy.get(apiwidget.path) + cy.get(apiwidget.editResourceUrl) + .first() .focus() - .type("{selectall}{backspace}api/users/2") + .type(v1method) .should("have.value", v1method); cy.SaveAPI(); }); @@ -947,6 +950,10 @@ Cypress.Commands.add("openPropertyPane", widgetType => { .click(); }); +Cypress.Commands.add("closePropertyPane", () => { + cy.get(commonlocators.editPropCrossButton).click(); +}); + Cypress.Commands.add("createApi", (url, parameters) => { cy.get("@createNewApi").then(response => { cy.get(ApiEditor.ApiNameField).should("be.visible"); @@ -961,7 +968,7 @@ Cypress.Commands.add("createApi", (url, parameters) => { cy.contains(url).click({ force: true, }); - cy.get(".CodeMirror.CodeMirror-empty textarea") + cy.get(apiwidget.editResourceUrl) .first() .click({ force: true }) .type(parameters, { force: true }); @@ -972,16 +979,13 @@ Cypress.Commands.add("createApi", (url, parameters) => { Cypress.Commands.add("isSelectRow", index => { cy.get( - '.e-gridcontent.e-lib.e-droppable td[index="' + - index + - '"][aria-colindex="' + - index + - '"]', + '.tbody .td[data-rowindex="' + index + '"][data-colindex="' + 0 + '"]', ).click({ force: true }); }); Cypress.Commands.add("readTabledata", (rowNum, colNum) => { - const selector = `.t--draggable-tablewidget .e-gridcontent.e-lib.e-droppable td[index=${rowNum}][aria-colindex=${colNum}]`; + // const selector = `.t--draggable-tablewidget .e-gridcontent.e-lib.e-droppable td[index=${rowNum}][aria-colindex=${colNum}]`; + const selector = `.t--draggable-tablewidget .tbody .td[data-rowindex=${rowNum}][data-colindex=${colNum}] div`; const tabVal = cy.get(selector).invoke("text"); return tabVal; }); @@ -1002,10 +1006,9 @@ Cypress.Commands.add("setDate", (date, dateFormate) => { }); Cypress.Commands.add("pageNo", index => { - cy.get(".e-pagercontainer a") - .eq(index) - .click({ force: true }) - .should("be.visible"); + cy.get(".page-item") + .first() + .click({ force: true }); }); Cypress.Commands.add("pageNoValidate", index => { @@ -1106,7 +1109,18 @@ Cypress.Commands.add("ExportVerify", (togglecss, name) => { }); Cypress.Commands.add("readTabledataPublish", (rowNum, colNum) => { - const selector = `.t--widget-tablewidget .e-gridcontent.e-lib.e-droppable td[index=${rowNum}][aria-colindex=${colNum}]`; + // const selector = `.t--widget-tablewidget .e-gridcontent.e-lib.e-droppable td[index=${rowNum}][aria-colindex=${colNum}]`; + const selector = `.t--widget-tablewidget .tbody .td[data-rowindex=${rowNum}][data-colindex=${colNum}] div`; const tabVal = cy.get(selector).invoke("text"); return tabVal; }); + +Cypress.Commands.add("assertEvaluatedValuePopup", expectedType => { + cy.get(dynamicInputLocators.evaluatedValue) + .should("be.visible") + .children("p") + .should("contain.text", "Expected type:") + .should("contain.text", "Current Value:") + .siblings("pre") + .should("have.text", expectedType); +}); diff --git a/app/client/cypress/support/index.js b/app/client/cypress/support/index.js index de02bf483e..d8659123cb 100644 --- a/app/client/cypress/support/index.js +++ b/app/client/cypress/support/index.js @@ -22,7 +22,6 @@ let appId; import "./commands"; before(function() { console.log("**** Got Cypress base URL as: ", process.env.CYPRESS_BASE_URL); - cy.viewport("macbook-15"); cy.startServerAndRoutes(); cy.LogintoApp(loginData.username, loginData.password); // cy.SearchApp(inputData.appname) diff --git a/app/client/docker/nginx-linux.conf b/app/client/docker/nginx-linux.conf index fb3273c72b..daa618d138 100644 --- a/app/client/docker/nginx-linux.conf +++ b/app/client/docker/nginx-linux.conf @@ -18,6 +18,10 @@ server { proxy_pass http://localhost:3000; } + location /f { + proxy_pass https://cdn.optimizely.com/; + } + location /api { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; @@ -58,6 +62,10 @@ server { location / { proxy_pass http://localhost:3000; } + + location /f { + proxy_pass https://cdn.optimizely.com/; + } location /api { proxy_set_header X-Forwarded-Proto $scheme; diff --git a/app/client/docker/nginx-mac.conf b/app/client/docker/nginx-mac.conf index 0491d2594b..8fa4870a36 100644 --- a/app/client/docker/nginx-mac.conf +++ b/app/client/docker/nginx-mac.conf @@ -16,6 +16,10 @@ server { proxy_pass http://host.docker.internal:3000; } + location /f { + proxy_pass https://cdn.optimizely.com/; + } + location /api { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; @@ -56,6 +60,10 @@ server { location / { proxy_pass http://host.docker.internal:3000; } + + location /f { + proxy_pass https://cdn.optimizely.com/; + } location /api { proxy_set_header X-Forwarded-Proto $scheme; diff --git a/app/client/netlify.toml b/app/client/netlify.toml index e0f566fe0f..33e87ca645 100644 --- a/app/client/netlify.toml +++ b/app/client/netlify.toml @@ -60,6 +60,12 @@ force = true headers = { X-Forwarded-Host = "APP_HOST_PLACEHOLDER", X-Forwarded-Proto = "https" } +[[redirects]] + from = "/f/*" + to = "https://cdn.optimizely.com/:splat" + status = 200 + force = true + # This must be the last redirect in the chain because it's a catch-all [[redirects]] from = "/*" diff --git a/app/client/package.json b/app/client/package.json index d0b2e9ab4f..2e08e248f8 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -32,6 +32,7 @@ "@types/react-instantsearch-dom": "^6.3.0", "@types/react-redux": "^7.0.1", "@types/react-router-dom": "^5.1.2", + "@types/react-table": "^7.0.13", "@types/styled-components": "^4.1.8", "@types/tinycolor2": "^1.4.2", "@uppy/core": "^1.8.2", @@ -90,6 +91,7 @@ "react-scripts": "^3.3.0", "react-select": "^3.0.8", "react-spring": "^8.0.27", + "react-table": "^7.0.0", "react-tabs": "^3.0.0", "react-toastify": "^5.5.0", "react-transition-group": "^4.3.0", diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index c937db1818..83367a4a50 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -1,18 +1,19 @@ -import { RestAction, PaginationField, ActionResponse } from "api/ActionAPI"; +import { PaginationField, ActionResponse } from "api/ActionAPI"; import { ReduxActionTypes, ReduxAction, ReduxActionErrorTypes, } from "constants/ReduxActionConstants"; +import { Action, RestAction } from "entities/Action"; -export const createActionRequest = (payload: Partial) => { +export const createActionRequest = (payload: Partial) => { return { type: ReduxActionTypes.CREATE_ACTION_INIT, payload, }; }; -export const createActionSuccess = (payload: RestAction) => { +export const createActionSuccess = (payload: Action) => { return { type: ReduxActionTypes.CREATE_ACTION_SUCCESS, payload, diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts index f1b870d7ab..839e433647 100644 --- a/app/client/src/actions/apiPaneActions.ts +++ b/app/client/src/actions/apiPaneActions.ts @@ -48,6 +48,16 @@ export const createNewApiAction = ( payload: { pageId }, }); +export const setDatasourceFieldText = ( + apiId: string, + value: string, +): ReduxAction<{ apiId: string; value: string }> => { + return { + type: ReduxActionTypes.SET_DATASOURCE_FIELD_TEXT, + payload: { apiId, value }, + }; +}; + export const setExtraFormData = ( apiId: string, extraformData: {}, diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index f6044a47ea..0df3f50d36 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -77,6 +77,12 @@ export const initDatasourcePane = ( }; }; +export const storeAsDatasource = () => { + return { + type: ReduxActionTypes.STORE_AS_DATASOURCE_INIT, + }; +}; + export default { createDatasource, fetchDatasources, diff --git a/app/client/src/actions/queryPaneActions.ts b/app/client/src/actions/queryPaneActions.ts index 6c3344f719..66647af349 100644 --- a/app/client/src/actions/queryPaneActions.ts +++ b/app/client/src/actions/queryPaneActions.ts @@ -1,5 +1,5 @@ import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; -import { RestAction } from "api/ActionAPI"; +import { RestAction } from "entities/Action"; export const createQueryRequest = (payload: Partial) => { return { diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 2383fa1d37..4968b6fb58 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -1,12 +1,11 @@ -import API, { HttpMethod } from "./Api"; +import API from "./Api"; import { ApiResponse, GenericApiResponse, ResponseMeta } from "./ApiResponses"; import { APIRequest, DEFAULT_EXECUTE_ACTION_TIMEOUT_MS, } from "constants/ApiConstants"; import { AxiosPromise } from "axios"; -import { Datasource } from "./DatasourcesApi"; -import { PaginationType } from "pages/Editor/APIEditor/Pagination"; +import { RestAction } from "entities/Action"; export interface CreateActionRequest extends APIRequest { datasourceId: string; @@ -19,21 +18,9 @@ export interface UpdateActionRequest extends CreateActionRequest { actionId: string; } -export interface APIConfig { - datasourceId: string; - pageId: string; - name: string; - requestHeaders: Record; - httpMethod: HttpMethod; - path: string; - body: JSON; - queryParams: Record; - actionId: string; -} - export interface Property { key: string; - value: string; + value?: string; } export interface BodyFormData { @@ -45,17 +32,6 @@ export interface BodyFormData { type: string; } -export interface APIConfigRequest { - headers: Property[]; - httpMethod: string; - path: string; - body: JSON | string | Record | null; - queryParameters: Property[]; - paginationType: PaginationType; - bodyFormData: BodyFormData[]; - timeoutInMillisecond: number; -} - export interface QueryConfig { queryString: string; } @@ -65,45 +41,6 @@ export interface ActionCreateUpdateResponse extends ApiResponse { jsonPathKeys: Record; } -export interface RestAction { - id: string; - name: string; - datasource: - | Pick - | Omit - | Partial; - pluginType?: string; - pageId: string; - actionConfiguration: Partial; - jsonPathKeys: string[]; - cacheResponse?: string; - pluginId: string; -} - -export interface RapidApiAction { - id: string; - name: string; - datasource: Pick | Omit; - pluginType: string; - pageId: string; - actionConfiguration: Partial; - jsonPathKeys: string[]; - cacheResponse?: string; - templateId: string; - proverId: string; - provider: ProviderInfo; - pluginId: string; - documentation: { text: string }; -} - -export interface ProviderInfo { - name: string; - imageUrl: string; - url: string; - description: string; - credentialSteps: string; -} - export type PaginationField = "PREV" | "NEXT"; export interface ExecuteActionRequest extends APIRequest { diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index 176fcec23e..4420c4867b 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -63,12 +63,15 @@ export interface CreatePageResponse extends ApiResponse { } export interface FetchPageListResponse extends ApiResponse { - data: Array<{ - id: string; - name: string; - isDefault: boolean; - layouts: Array; - }>; + data: { + pages: Array<{ + id: string; + name: string; + isDefault: boolean; + layouts: Array; + }>; + organizationId: string; + }; } export interface DeletePageRequest { diff --git a/app/client/src/assets/icons/menu/storage.svg b/app/client/src/assets/icons/menu/storage.svg new file mode 100644 index 0000000000..a856115925 --- /dev/null +++ b/app/client/src/assets/icons/menu/storage.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx index 9182c6e375..6088c374b5 100644 --- a/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx +++ b/app/client/src/components/designSystems/appsmith/CreatableDropdown.tsx @@ -1,7 +1,9 @@ import React from "react"; -import Creatable from "react-select/creatable"; +import Select, { InputActionMeta } from "react-select"; import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; + import { theme } from "constants/DefaultTheme"; +import { SelectComponents } from "react-select/src/components"; type DropdownProps = { options: Array<{ @@ -12,9 +14,12 @@ type DropdownProps = { isLoading?: boolean; input: WrappedFieldInputProps; meta: WrappedFieldMetaProps; + components: SelectComponents; onCreateOption: (inputValue: string) => void; formatCreateLabel?: (value: string) => React.ReactNode; noOptionsMessage?: (obj: { inputValue: string }) => string; + inputValue?: string; + onInputChange: (value: string, actionMeta: InputActionMeta) => void; }; const selectStyles = { @@ -22,7 +27,7 @@ const selectStyles = { ...provided, color: "#a3b3bf", }), - singleValue: (provided: any) => ({ + multiValue: (provided: any) => ({ ...provided, backgroundColor: "rgba(104,113,239,0.1)", border: "1px solid rgba(104, 113, 239, 0.5)", @@ -34,9 +39,15 @@ const selectStyles = { display: "inline-block", transform: "none", }), + multiValueRemove: () => { + return { + display: "none", + }; + }, container: (styles: any) => ({ ...styles, flex: 1, + zIndex: "5", }), control: (styles: any, state: any) => ({ ...styles, @@ -69,23 +80,34 @@ class CreatableDropdown extends React.Component { placeholder, options, isLoading, - onCreateOption, input, - formatCreateLabel, noOptionsMessage, + components, + inputValue, + onInputChange, } = this.props; const optionalProps: Partial = {}; - if (formatCreateLabel) optionalProps.formatCreateLabel = formatCreateLabel; if (noOptionsMessage) optionalProps.noOptionsMessage = noOptionsMessage; + if (components) optionalProps.components = components; + if (inputValue) optionalProps.inputValue = inputValue; + if (onInputChange) optionalProps.onInputChange = onInputChange; + return ( - input.onChange(value)} + onChange={value => { + const formattedValue = value; + if (formattedValue && formattedValue.length > 1) { + formattedValue.shift(); + } + + input.onChange(formattedValue); + }} onBlur={() => input.value} isClearable {...optionalProps} diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx new file mode 100644 index 0000000000..5d6b7b5571 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -0,0 +1,483 @@ +import React from "react"; +import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; +import Table from "./Table"; +import { RenderMode, RenderModes } from "constants/WidgetConstants"; +import _ from "lodash"; +import { getMenuOptions, renderActions, renderCell } from "./TableUtilities"; + +interface ReactTableComponentState { + trigger: number; + columnIndex: number; + pageSize: number; + action: string; + columnName: string; +} + +export interface ReactTableColumnProps { + Header: string; + accessor: string; + width: number; + minWidth: number; + draggable: boolean; + isHidden?: boolean; + Cell: (props: any) => JSX.Element; +} + +export interface ColumnMenuOptionProps { + content: string | JSX.Element; + closeOnClick?: boolean; + isSelected?: boolean; + columnAccessor?: string; + id?: string; + category?: boolean; + options?: ColumnMenuSubOptionProps[]; + onClick?: (isSelected: boolean) => void; +} + +export interface ColumnMenuSubOptionProps { + content: string; + isSelected: boolean; + closeOnClick: boolean; + onClick: () => void; +} + +interface ReactTableComponentProps { + widgetId: string; + isDisabled?: boolean; + isVisible?: boolean; + renderMode: RenderMode; + width: number; + height: number; + pageSize: number; + tableData: object[]; + columnOrder?: string[]; + disableDrag: (disable: boolean) => void; + onRowClick: (rowData: object, rowIndex: number) => void; + onCommandClick: (dynamicTrigger: string) => void; + updatePageNo: Function; + updateHiddenColumns: Function; + resetSelectedRowIndex: Function; + nextPageClick: Function; + prevPageClick: Function; + pageNo: number; + serverSidePaginationEnabled: boolean; + columnActions?: ColumnAction[]; + selectedRowIndex: number; + hiddenColumns?: string[]; + columnNameMap?: { [key: string]: string }; + columnTypeMap?: { + [key: string]: { + type: string; + format: string; + }; + }; + columnSizeMap?: { [key: string]: number }; + updateColumnType: Function; + updateColumnName: Function; + handleResizeColumn: Function; + handleReorderColumn: Function; +} + +export class ReactTableComponent extends React.Component< + ReactTableComponentProps, + ReactTableComponentState +> { + private dragged = -1; + constructor(props: ReactTableComponentProps) { + super(props); + this.state = { + trigger: 0, + columnIndex: -1, + action: "", + columnName: "", + pageSize: props.pageSize, + }; + } + + componentDidMount() { + this.mountEvents(); + } + + componentDidUpdate(prevProps: ReactTableComponentProps) { + if (this.props.pageSize !== prevProps.pageSize) { + this.setState({ pageSize: this.props.pageSize }); + } + this.mountEvents(); + } + + mountEvents() { + const headers = Array.prototype.slice.call( + document.querySelectorAll( + `#table${this.props.widgetId} .draggable-header`, + ), + ); + const columns = this.getTableColumns(); + headers.forEach((header, i) => { + header.setAttribute("draggable", true); + + header.ondragstart = (e: React.DragEvent) => { + header.style = + "background: #efefef; border-radius: 4px; z-index: 100; width: 100%; text-overflow: none; overflow: none;"; + e.stopPropagation(); + this.dragged = i; + }; + + header.ondrag = (e: React.DragEvent) => { + e.stopPropagation(); + }; + + header.ondragend = (e: React.DragEvent) => { + header.style = ""; + e.stopPropagation(); + setTimeout(() => (this.dragged = -1), 1000); + }; + + // the dropped header + header.ondragover = (e: React.DragEvent) => { + if (i !== this.dragged && this.dragged !== -1) { + if (this.dragged > i) { + header.parentElement.className = "th header-reorder highlight-left"; + } else if (this.dragged < i) { + header.parentElement.className = + "th header-reorder highlight-right"; + } + } + e.preventDefault(); + }; + + header.ondragenter = (e: React.DragEvent) => { + if (i !== this.dragged && this.dragged !== -1) { + if (this.dragged > i) { + header.parentElement.className = "th header-reorder highlight-left"; + } else if (this.dragged < i) { + header.parentElement.className = + "th header-reorder highlight-right"; + } + } + e.preventDefault(); + }; + + header.ondragleave = (e: React.DragEvent) => { + header.parentElement.className = "th header-reorder"; + e.preventDefault(); + }; + + header.ondrop = (e: React.DragEvent) => { + header.style = ""; + header.parentElement.className = "th header-reorder"; + if (i !== this.dragged && this.dragged !== -1) { + e.preventDefault(); + let columnOrder = this.props.columnOrder; + if (columnOrder === undefined) { + columnOrder = this.props.tableData.length + ? Object.keys(this.props.tableData[0]) + : []; + } + const draggedColumn = columns[this.dragged].accessor; + columnOrder.splice(this.dragged, 1); + columnOrder.splice(i, 0, draggedColumn); + this.props.handleReorderColumn(columnOrder); + this.setState({ trigger: Math.random() }); + } else { + this.dragged = -1; + } + }; + }); + } + + getTableColumns = () => { + const tableData: object[] = this.props.tableData; + let columns: ReactTableColumnProps[] = []; + const hiddenColumns: ReactTableColumnProps[] = []; + if (tableData.length) { + const row = tableData[0]; + for (const i in row) { + const columnName: string = + this.props.columnNameMap && this.props.columnNameMap[i] + ? this.props.columnNameMap[i] + : i; + const columnType: { type: string; format?: string } = + this.props.columnTypeMap && this.props.columnTypeMap[i] + ? this.props.columnTypeMap[i] + : { type: "text" }; + const columnSize: number = + this.props.columnSizeMap && this.props.columnSizeMap[i] + ? this.props.columnSizeMap[i] + : 150; + const isHidden = + !!this.props.hiddenColumns && this.props.hiddenColumns.includes(i); + const columnData = { + Header: columnName, + accessor: i, + width: columnSize, + minWidth: 60, + draggable: true, + isHidden: false, + Cell: (props: any) => { + return renderCell( + props.cell.value, + columnType.type, + isHidden, + columnType.format, + ); + }, + }; + if (isHidden) { + columnData.isHidden = true; + hiddenColumns.push(columnData); + } else { + columns.push(columnData); + } + } + columns = this.reorderColumns(columns); + if (this.props.columnActions?.length) { + columns.push({ + Header: "Actions", + accessor: "actions", + width: 150, + minWidth: 60, + draggable: true, + Cell: () => { + return renderActions({ + columnActions: this.props.columnActions, + onCommandClick: this.props.onCommandClick, + }); + }, + }); + } + if ( + hiddenColumns.length && + this.props.renderMode === RenderModes.CANVAS + ) { + columns = columns.concat(hiddenColumns); + } + } + return columns; + }; + + reorderColumns = (columns: ReactTableColumnProps[]) => { + const columnOrder = this.props.columnOrder || []; + const reorderedColumns = []; + const reorderedFlagMap: { [key: string]: boolean } = {}; + for (let index = 0; index < columns.length; index++) { + const accessor = columnOrder[index]; + if (accessor) { + const column = columns.filter((col: ReactTableColumnProps) => { + return col.accessor === accessor; + }); + if (column.length && !reorderedFlagMap[column[0].accessor]) { + reorderedColumns.push(column[0]); + reorderedFlagMap[column[0].accessor] = true; + } else if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } else if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } + if (reorderedColumns.length < columns.length) { + for (let index = 0; index < columns.length; index++) { + if (!reorderedFlagMap[columns[index].accessor]) { + reorderedColumns.push(columns[index]); + reorderedFlagMap[columns[index].accessor] = true; + } + } + } + return reorderedColumns; + }; + + showMenu = (columnIndex: number) => { + this.setState({ columnIndex: columnIndex, action: "" }); + }; + + getColumnMenu = () => { + const { columnIndex } = this.state; + let columnType = ""; + let columnId = ""; + let format = ""; + if (columnIndex !== -1) { + const columns = this.getTableColumns(); + const column = columns[columnIndex]; + columnId = column.accessor; + columnType = + this.props.columnTypeMap && this.props.columnTypeMap[columnId] + ? this.props.columnTypeMap[columnId].type + : ""; + format = + this.props.columnTypeMap && this.props.columnTypeMap[columnId] + ? this.props.columnTypeMap[columnId].format + : ""; + } + const isColumnHidden = !!( + this.props.hiddenColumns && this.props.hiddenColumns.includes(columnId) + ); + const columnMenuOptions: ColumnMenuOptionProps[] = getMenuOptions({ + columnAccessor: columnId, + isColumnHidden, + columnType, + format, + hideColumn: this.hideColumn, + updateColumnType: this.updateColumnType, + handleUpdateCurrencySymbol: this.handleUpdateCurrencySymbol, + handleDateFormatUpdate: this.handleDateFormatUpdate, + updateAction: (action: string) => this.setState({ action: action }), + }); + return columnMenuOptions; + }; + + hideColumn = (isColumnHidden: boolean) => { + const columns = this.getTableColumns(); + const columnIndex = this.state.columnIndex; + const column = columns[columnIndex]; + let hiddenColumns = this.props.hiddenColumns || []; + if (!isColumnHidden) { + hiddenColumns.push(column.accessor); + const columnOrder = this.props.columnOrder || []; + if (columnOrder.includes(column.accessor)) { + columnOrder.splice(columnOrder.indexOf(column.accessor), 1); + this.props.handleReorderColumn(columnOrder); + } + } else { + hiddenColumns = hiddenColumns.filter(item => { + return item !== column.accessor; + }); + } + this.props.updateHiddenColumns(hiddenColumns); + this.setState({ columnIndex: -1 }); + }; + + updateColumnType = (columnType: string) => { + const columns = this.getTableColumns(); + const columnIndex = this.state.columnIndex; + const column = columns[columnIndex]; + const columnTypeMap = this.props.columnTypeMap || {}; + columnTypeMap[column.accessor] = { + type: columnType, + format: "", + }; + this.props.updateColumnType(columnTypeMap); + this.setState({ action: "", columnIndex: -1 }); + }; + + onColumnNameChange = (event: React.ChangeEvent) => { + this.setState({ columnName: event.target.value }); + }; + + onKeyPress = (key: string) => { + if (key === "Enter") { + this.handleColumnNameUpdate(); + } + }; + + handleColumnNameUpdate = () => { + const columns = this.getTableColumns(); + const columnIndex = this.state.columnIndex; + const columnName = this.state.columnName; + const column = columns[columnIndex]; + const columnNameMap = this.props.columnNameMap || {}; + columnNameMap[column.accessor] = columnName; + this.props.updateColumnName(columnNameMap); + this.setState({ + columnIndex: -1, + columnName: "", + action: "", + }); + }; + + handleUpdateCurrencySymbol = (currencySymbol: string) => { + const columns = this.getTableColumns(); + const columnIndex = this.state.columnIndex; + const column = columns[columnIndex]; + const columnTypeMap = this.props.columnTypeMap || {}; + columnTypeMap[column.accessor] = { + type: "currency", + format: currencySymbol, + }; + this.props.updateColumnType(columnTypeMap); + this.setState({ + columnIndex: -1, + action: "", + }); + }; + + handleDateFormatUpdate = (dateFormat: string) => { + const columns = this.getTableColumns(); + const columnIndex = this.state.columnIndex; + const column = columns[columnIndex]; + const columnTypeMap = this.props.columnTypeMap || {}; + columnTypeMap[column.accessor] = { + type: "date", + format: dateFormat, + }; + this.props.updateColumnType(columnTypeMap); + this.setState({ + columnIndex: -1, + action: "", + }); + }; + + handleResizeColumn = (columnIndex: number, columnWidth: string) => { + const columns = this.getTableColumns(); + const column = columns[columnIndex]; + const columnSizeMap = this.props.columnSizeMap || {}; + const width = Number(columnWidth.split("px")[0]); + columnSizeMap[column.accessor] = width; + this.props.handleResizeColumn(columnSizeMap); + }; + + selectTableRow = ( + row: { original: object; index: number }, + isSelected: boolean, + ) => { + if (!isSelected) { + this.props.onRowClick(row.original, row.index); + } else { + this.props.resetSelectedRowIndex(); + } + }; + + render() { + const columns = this.getTableColumns(); + return ( + { + this.props.nextPageClick(); + }} + prevPageClick={() => { + this.props.prevPageClick(); + }} + onKeyPress={this.onKeyPress} + serverSidePaginationEnabled={this.props.serverSidePaginationEnabled} + selectedRowIndex={this.props.selectedRowIndex} + disableDrag={() => { + this.props.disableDrag(true); + }} + enableDrag={() => { + this.props.disableDrag(false); + }} + /> + ); + } +} + +export default ReactTableComponent; diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx new file mode 100644 index 0000000000..5b0f959c26 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -0,0 +1,274 @@ +import React from "react"; +import { + useTable, + usePagination, + useFlexLayout, + useResizeColumns, + useRowSelect, +} from "react-table"; +import { Icon, InputGroup } from "@blueprintjs/core"; +import { + TableWrapper, + PaginationWrapper, + PaginationItemWrapper, +} from "./TableStyledWrappers"; +import { + ReactTableColumnProps, + ColumnMenuOptionProps, +} from "./ReactTableComponent"; +import { TableColumnMenuPopup } from "./TableColumnMenu"; + +interface TableProps { + width: number; + height: number; + pageSize: number; + widgetId: string; + columns: ReactTableColumnProps[]; + data: object[]; + showMenu: (columnIndex: number) => void; + displayColumnActions: boolean; + columnNameMap?: { [key: string]: string }; + columnMenuOptions: ColumnMenuOptionProps[]; + columnIndex: number; + columnAction: string; + onColumnNameChange: (event: React.ChangeEvent) => void; + handleColumnNameUpdate: () => void; + handleResizeColumn: Function; + selectTableRow: ( + row: { original: object; index: number }, + isSelected: boolean, + ) => void; + pageNo: number; + updatePageNo: Function; + nextPageClick: () => void; + prevPageClick: () => void; + onKeyPress: (key: string) => void; + serverSidePaginationEnabled: boolean; + selectedRowIndex: number; + disableDrag: () => void; + enableDrag: () => void; +} + +export const Table = (props: TableProps) => { + const { data, columns } = props; + const defaultColumn = React.useMemo( + () => ({ + minWidth: 30, + width: 150, + maxWidth: 400, + }), + [], + ); + const pageCount = Math.ceil(data.length / props.pageSize); + const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; + const { + getTableProps, + getTableBodyProps, + headerGroups, + prepareRow, + page, + pageOptions, + } = useTable( + { + columns, + data, + defaultColumn, + initialState: { + pageIndex: currentPageIndex, + pageSize: props.pageSize, + }, + manualPagination: true, + pageCount, + }, + useFlexLayout, + useResizeColumns, + usePagination, + useRowSelect, + ); + let startIndex = currentPageIndex * props.pageSize; + let endIndex = startIndex + props.pageSize; + if (props.serverSidePaginationEnabled) { + startIndex = 0; + endIndex = data.length; + } + const subPage = page.slice(startIndex, endIndex); + const selectedRowIndex = props.selectedRowIndex; + return ( + +
+
+
+ {headerGroups.map((headerGroup: any, index: number) => ( +
+ {headerGroup.headers.map((column: any, columnIndex: number) => { + if (column.isResizing) { + props.handleResizeColumn( + columnIndex, + column.getHeaderProps().style.width, + ); + } + return ( +
+ {props.columnIndex === columnIndex && + props.columnAction === "rename_column" && ( + props.onKeyPress(event.key)} + type="text" + defaultValue={ + props.columnNameMap && + props.columnNameMap[column.id] + ? props.columnNameMap[column.id] + : column.id + } + className="input-group" + onBlur={() => props.handleColumnNameUpdate()} + /> + )} + {(props.columnIndex !== columnIndex || + (props.columnIndex === columnIndex && + "rename_column" !== props.columnAction)) && ( +
+ {column.render("Header")} +
+ )} + {props.displayColumnActions && ( +
+ +
+ )} +
+
+ ); + })} +
+ ))} +
+
+ {subPage.map((row, index) => { + prepareRow(row); + return ( +
{ + row.toggleRowSelected(); + props.selectTableRow(row, row.index === selectedRowIndex); + }} + > + {row.cells.map((cell, cellIndex) => { + return ( +
+ {cell.render("Cell")} +
+ ); + })} +
+ ); + })} +
+
+
+ {props.serverSidePaginationEnabled && ( + + { + props.prevPageClick(); + }} + > + + + + {props.pageNo + 1} + + { + props.nextPageClick(); + }} + > + + + + )} + {!props.serverSidePaginationEnabled && ( + + { + const pageNo = currentPageIndex > 0 ? currentPageIndex - 1 : 0; + props.updatePageNo(pageNo + 1); + }} + > + + + {pageOptions.map((pageNumber: number, index: number) => { + return ( + { + props.updatePageNo(pageNumber + 1); + }} + className="page-item" + > + {index + 1} + + ); + })} + { + const pageNo = + currentPageIndex < pageCount - 1 ? currentPageIndex + 1 : 0; + props.updatePageNo(pageNo + 1); + }} + > + + + + )} + + ); +}; + +export default Table; diff --git a/app/client/src/components/designSystems/appsmith/TableColumnMenu.tsx b/app/client/src/components/designSystems/appsmith/TableColumnMenu.tsx new file mode 100644 index 0000000000..08a469d561 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableColumnMenu.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import { + Popover, + Classes, + PopoverInteractionKind, + Icon, + Position, +} from "@blueprintjs/core"; +import { + DropDownWrapper, + OptionWrapper, + IconOptionWrapper, +} from "./TableStyledWrappers"; +import { + ColumnMenuSubOptionProps, + ColumnMenuOptionProps, +} from "./ReactTableComponent"; + +interface TableColumnMenuPopup { + showMenu: (index: number) => void; + columnIndex: number; + columnMenuOptions: ColumnMenuOptionProps[]; +} + +export const TableColumnMenuPopup = (props: TableColumnMenuPopup) => { + return ( + + { + props.showMenu(props.columnIndex); + }} + /> + + {props.columnMenuOptions.map( + (option: ColumnMenuOptionProps, index: number) => ( + { + if (option.onClick) { + option.onClick(!!option.isSelected); + } + }} + className={ + option.closeOnClick + ? Classes.POPOVER_DISMISS + : option.category + ? "non-selectable" + : "" + } + selected={!!option.isSelected} + > + {!option.options &&
{option.content}
} + {option.options && ( + + {option.content} + + {option.options.map( + (item: ColumnMenuSubOptionProps, itemIndex: number) => ( + + {item.content} + + ), + )} + + + )} +
+ ), + )} +
+
+ ); +}; diff --git a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx new file mode 100644 index 0000000000..af3cc27795 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx @@ -0,0 +1,254 @@ +import styled from "styled-components"; +import { Colors } from "constants/Colors"; + +export const TableWrapper = styled.div<{ width: number; height: number }>` + width: ${props => props.width - 5}px; + height: ${props => props.height - 5}px; + background: white; + border: 1px solid ${Colors.GEYSER_LIGHT}; + box-sizing: border-box; + display: flex; + justify-content: space-between; + flex-direction: column; + .tableWrap { + display: block; + overflow-x: auto; + overflow-y: hidden; + border-bottom: 1px solid ${Colors.GEYSER_LIGHT}; + } + .table { + border-spacing: 0; + color: ${Colors.BLUE_BAYOUX}; + position: relative; + .thead { + overflow-y: auto; + overflow-x: hidden; + } + .tbody { + overflow: scroll; + height: ${props => props.height - 5 - 102}px; + } + .tr { + :last-child { + .td { + border-bottom: 0; + } + } + :nth-child(even) { + background: ${Colors.ATHENS_GRAY_DARKER}; + } + &.selected-row { + background: ${Colors.ATHENS_GRAY}; + } + &:hover { + background: ${Colors.ATHENS_GRAY}; + } + } + .th, + .td { + margin: 0; + padding: 9px 10px; + border-bottom: 1px solid ${Colors.GEYSER_LIGHT}; + border-right: 1px solid ${Colors.GEYSER_LIGHT}; + position: relative; + :last-child { + border-right: 0; + } + .resizer { + display: inline-block; + width: 10px; + height: 100%; + position: absolute; + right: 0; + top: 0; + transform: translateX(50%); + z-index: 1; + ${"" /* prevents from scrolling while dragging on touch devices */} + touch-action:none; + &.isResizing { + cursor: isResizing; + } + } + } + .th { + padding: 0 10px 0 0; + height: 52px; + line-height: 52px; + background: ${Colors.ATHENS_GRAY_DARKER}; + } + .td { + height: 52px; + line-height: 52px; + padding: 0 10px; + } + } + .draggable-header, + .hidden-header { + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + color: ${Colors.OXFORD_BLUE}; + font-weight: 500; + padding-left: 10px; + } + .draggable-header { + cursor: pointer; + &.reorder-line { + width: 1px; + height: 100%; + } + } + .hidden-header { + opacity: 0.6; + } + .column-menu { + cursor: pointer; + height: 52px; + line-height: 52px; + } + .th { + display: flex; + justify-content: space-between; + &.highlight-left { + border-left: 2px solid ${Colors.GREEN}; + } + &.highlight-right { + border-right: 2px solid ${Colors.GREEN}; + } + } + .input-group { + height: 52px; + line-height: 52px; + padding: 0 5px; + } +`; + +export const DropDownWrapper = styled.div` + display: flex; + flex-direction: column; + background: white; + z-index: 1; + padding: 10px; + border-radius: 4px; + border: 1px solid ${Colors.ATHENS_GRAY}; + box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14); +`; + +export const OptionWrapper = styled.div<{ selected: boolean }>` + display: flex; + width: 100%; + justify-content: space-between; + height: 32px; + box-sizing: border-box; + padding: 8px; + color: ${props => (props.selected ? Colors.WHITE : Colors.OXFORD_BLUE)}; + font-size: 14px; + min-width: 200px; + cursor: pointer; + border-radius: 4px; + margin: 3px 0; + background: ${props => (props.selected ? Colors.GREEN : Colors.WHITE)}; + &:hover { + background: ${props => (props.selected ? Colors.GREEN : Colors.POLAR)}; + } + .column-type { + width: 100%; + } + &.non-selectable { + color: ${Colors.GRAY}; + pointer-events: none; + } +`; + +export const IconOptionWrapper = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + cursor: pointer; +`; + +export const PaginationWrapper = styled.div` + box-sizing: border-box; + padding: 10px; + display: flex; + width: 100%; + justify-content: flex-end; + align-items: center; +`; + +export const PaginationItemWrapper = styled.div<{ + disabled?: boolean; + selected?: boolean; +}>` + background: ${props => (props.disabled ? Colors.ATHENS_GRAY : Colors.WHITE)}; + border: 1px solid + ${props => (props.selected ? Colors.GREEN : Colors.GEYSER_LIGHT)}; + box-sizing: border-box; + border-radius: 4px; + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + margin: 0 0 0 8px; + pointer-events: ${props => props.disabled && "none"}; + cursor: pointer; + &:hover { + border-color: ${Colors.GREEN}; + } +`; + +export const MenuColumnWrapper = styled.div<{ selected: boolean }>` + display: flex; + justify-content: flex-start; + align-items: center; + width: 100%; + background: ${props => props.selected && Colors.GREEN}; + position: relative; + .title { + color: ${props => (props.selected ? Colors.WHITE : Colors.OXFORD_BLUE)}; + margin-left: 10px; + } + .sub-menu { + position: absolute; + right: 0; + } +`; + +export const ActionWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + margin: 10px 5px 0 0; + cursor: pointer; + padding: 5px; + height: 32px; + color: ${Colors.WHITE}; + background: ${Colors.GREEN}; + border-radius: 4px; + letter-spacing: -0.03em; + font-weight: bold; +`; + +export const CellWrapper = styled.div<{ isHidden: boolean }>` + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: normal; + opacity: ${props => (props.isHidden ? "0.6" : "1")}; + .image-cell { + width: 40px; + height: 32px; + margin: 0 5px 0 0; + border-radius: 4px; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + } + video { + border-radius: 4px; + } +`; diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx new file mode 100644 index 0000000000..1876dc5071 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -0,0 +1,384 @@ +import React from "react"; +import { Icon } from "@blueprintjs/core"; +import moment from "moment-timezone"; +import { + MenuColumnWrapper, + CellWrapper, + ActionWrapper, +} from "./TableStyledWrappers"; +import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; +import { ColumnMenuOptionProps } from "./ReactTableComponent"; + +interface MenuOptionProps { + columnAccessor?: string; + isColumnHidden: boolean; + columnType: string; + format?: string; + hideColumn: (isColumnHidden: boolean) => void; + updateAction: (action: string) => void; + updateColumnType: (columnType: string) => void; + handleUpdateCurrencySymbol: (currencySymbol: string) => void; + handleDateFormatUpdate: (dateFormat: string) => void; +} + +export const getMenuOptions = (props: MenuOptionProps) => { + const basicOptions: ColumnMenuOptionProps[] = [ + { + content: "Rename a Column", + closeOnClick: true, + id: "rename_column", + onClick: () => { + props.updateAction("rename_column"); + }, + }, + { + content: props.isColumnHidden ? "Show Column" : "Hide Column", + closeOnClick: true, + id: "hide_column", + onClick: () => { + props.hideColumn(props.isColumnHidden); + }, + }, + ]; + if (props.columnAccessor && props.columnAccessor === "actions") { + return basicOptions; + } + const columnMenuOptions: ColumnMenuOptionProps[] = [ + ...basicOptions, + { + content: "Select a Data Type", + id: "change_column_type", + category: true, + }, + { + content: ( + + +
Image
+
+ ), + closeOnClick: true, + isSelected: props.columnType === "image", + onClick: (isSelected: boolean) => { + if (isSelected) { + props.updateColumnType(""); + } else { + props.updateColumnType("image"); + } + }, + }, + { + content: ( + + +
Video
+
+ ), + isSelected: props.columnType === "video", + closeOnClick: true, + onClick: (isSelected: boolean) => { + if (isSelected) { + props.updateColumnType(""); + } else { + props.updateColumnType("video"); + } + }, + }, + { + content: ( + + +
Text
+
+ ), + closeOnClick: true, + isSelected: props.columnType === "text", + onClick: (isSelected: boolean) => { + if (isSelected) { + props.updateColumnType(""); + } else { + props.updateColumnType("text"); + } + }, + }, + { + content: ( + + +
Currency
+ +
+ ), + closeOnClick: false, + isSelected: props.columnType === "currency", + options: [ + { + content: "USD - $", + isSelected: props.format === "$", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("$"); + }, + }, + { + content: "INR - ₹", + isSelected: props.format === "₹", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("₹"); + }, + }, + { + content: "GBP - £", + isSelected: props.format === "£", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("£"); + }, + }, + { + content: "AUD - A$", + isSelected: props.format === "A$", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("A$"); + }, + }, + { + content: "EUR - €", + isSelected: props.format === "€", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("€"); + }, + }, + { + content: "SGD - S$", + isSelected: props.format === "S$", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("S$"); + }, + }, + { + content: "CAD - C$", + isSelected: props.format === "C$", + closeOnClick: true, + onClick: () => { + props.handleUpdateCurrencySymbol("C$"); + }, + }, + ], + }, + { + content: ( + + +
Date
+ +
+ ), + closeOnClick: false, + isSelected: props.columnType === "date", + options: [ + { + content: "MM-DD-YY", + isSelected: props.format === "MM-DD-YY", + closeOnClick: true, + onClick: () => { + props.handleDateFormatUpdate("MM-DD-YY"); + }, + }, + { + content: "DD-MM-YY", + isSelected: props.format === "DD-MM-YY", + closeOnClick: true, + onClick: () => { + props.handleDateFormatUpdate("DD-MM-YY"); + }, + }, + { + content: "DD/MM/YY", + isSelected: props.format === "DD/MM/YY", + closeOnClick: true, + onClick: () => { + props.handleDateFormatUpdate("DD/MM/YY"); + }, + }, + { + content: "MM/DD/YY", + isSelected: props.format === "MM/DD/YY", + closeOnClick: true, + onClick: () => { + props.handleDateFormatUpdate("MM/DD/YY"); + }, + }, + ], + }, + { + content: ( + + +
Time
+
+ ), + closeOnClick: true, + isSelected: props.columnType === "time", + onClick: (isSelected: boolean) => { + if (isSelected) { + props.updateColumnType(""); + } else { + props.updateColumnType("time"); + } + }, + }, + ]; + return columnMenuOptions; +}; + +export const renderCell = ( + value: any, + columnType: string, + isHidden: boolean, + format?: string, +) => { + if (!value) { + return
; + } + switch (columnType) { + case "image": + return ( + + {value + .toString() + .split(",") + .map((item: string, index: number) => { + if (item.match(/\.(jpeg|jpg|gif|png)$/)) { + return ( +
+ ); + } else { + return
Invalid Image
; + } + })} + + ); + case "video": + return ( + + + + ); + case "currency": + if (!isNaN(value)) { + return ( + {`${format}${value}`} + ); + } else { + return Invalid Value; + } + case "date": + let isValidDate = true; + if (isNaN(value)) { + const dateTime = Date.parse(value); + if (isNaN(dateTime)) { + isValidDate = false; + } + } + if (isValidDate) { + return ( + + {moment(value).format(format)} + + ); + } else { + return Invalid Date; + } + case "time": + let isValidTime = true; + if (isNaN(value)) { + const time = Date.parse(value); + if (isNaN(time)) { + isValidTime = false; + } + } + if (isValidTime) { + return ( + + {moment(value).format("HH:mm")} + + ); + } else { + return Invalid Time; + } + case "text": + return {value}; + default: + return {value}; + } +}; + +interface RenderActionProps { + columnActions?: ColumnAction[]; + onCommandClick: (dynamicTrigger: string) => void; +} + +export const renderActions = (props: RenderActionProps) => { + if (!props.columnActions) return ; + return ( + + {props.columnActions.map((action: ColumnAction, index: number) => { + return ( + { + props.onCommandClick(action.dynamicTrigger); + }} + > + {action.label} + + ); + })} + + ); +}; diff --git a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx index 4f8ba631e7..c33a0a0284 100644 --- a/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx +++ b/app/client/src/components/designSystems/appsmith/help/HelpModal.tsx @@ -1,6 +1,5 @@ import React, { useContext } from "react"; import DocumentationSearch from "components/designSystems/appsmith/help/DocumentationSearch"; -import Button from "components/editorComponents/Button"; import { useSelector } from "store"; import { useDispatch } from "react-redux"; @@ -13,8 +12,7 @@ import { setHelpModalVisibility, } from "actions/helpActions"; import styled from "styled-components"; -import { IntentColors, theme } from "constants/DefaultTheme"; -import { Position } from "@blueprintjs/core"; +import { theme } from "constants/DefaultTheme"; import ModalComponent from "components/designSystems/blueprint/ModalComponent"; import { LayersContext } from "constants/Layers"; import { HelpIcons } from "icons/HelpIcons"; @@ -87,7 +85,7 @@ export function HelpModal() { dispatch(setHelpModalVisibility(!helpModalOpen)); }} > - + ); diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index c581a0f3cf..3219c161fa 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -9,11 +9,11 @@ import { AppState } from "reducers"; import { ActionResponse } from "api/ActionAPI"; import { formatBytes } from "utils/helpers"; import { APIEditorRouteParams } from "constants/routes"; -import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; import CodeEditor from "components/editorComponents/CodeEditor"; import { getActionResponses } from "selectors/entitiesSelector"; import { Colors } from "constants/Colors"; +import _ from "lodash"; import FormActionButton from "./form/FormActionButton"; const ResponseWrapper = styled.div` @@ -53,7 +53,7 @@ const TableWrapper = styled.div` interface ReduxStateProps { responses: Record; - apiPane: ApiPaneReduxState; + isRunning: Record; } const ResponseHeadersView = (props: { data: Record }) => { @@ -109,14 +109,13 @@ const ApiResponseView = (props: Props) => { params: { apiId }, }, responses, - apiPane, } = props; let response: ActionResponse = EMPTY_RESPONSE; let isRunning = false; let hasFailed = false; if (apiId && apiId in responses) { response = responses[apiId] || EMPTY_RESPONSE; - isRunning = apiPane.isRunning[apiId]; + isRunning = props.isRunning[apiId]; hasFailed = response.statusCode ? response.statusCode[0] !== "2" : false; } @@ -135,7 +134,7 @@ const ApiResponseView = (props: Props) => { onClick={() => { setSelectedIndex(3); }} - > + /> )} { panelComponent: ( @@ -217,7 +216,7 @@ const ApiResponseView = (props: Props) => { const mapStateToProps = (state: AppState): ReduxStateProps => { return { responses: getActionResponses(state), - apiPane: state.ui.apiPane, + isRunning: state.ui.apiPane.isRunning, }; }; diff --git a/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx index dfac98dcef..95d5821729 100644 --- a/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx +++ b/app/client/src/components/editorComponents/DynamicAutocompleteInput.tsx @@ -13,11 +13,11 @@ import "codemirror/addon/mode/multiplex"; import "codemirror/addon/tern/tern.css"; import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors"; import { AUTOCOMPLETE_MATCH_REGEX } from "constants/BindingsConstants"; -import ErrorTooltip from "components/editorComponents/ErrorTooltip"; import HelperTooltip from "components/editorComponents/HelperTooltip"; +import EvaluatedValuePopup from "components/editorComponents/EvaluatedValuePopup"; import { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; import _ from "lodash"; -import { parseDynamicString } from "utils/DynamicBindingUtils"; +import { getDynamicStringSegments } from "utils/DynamicBindingUtils"; import { DataTree } from "entities/DataTree/dataTreeFactory"; import { Theme } from "constants/DefaultTheme"; import AnalyticsUtil from "utils/AnalyticsUtil"; @@ -63,7 +63,7 @@ CodeMirror.defineMode("js-js", function(config) { const getBorderStyle = ( props: { theme: Theme } & { - editorTheme?: THEME; + editorTheme?: EditorTheme; hasError: boolean; singleLine: boolean; isFocused: boolean; @@ -78,7 +78,7 @@ const getBorderStyle = ( return "transparent"; }; -const HintStyles = createGlobalStyle` +const HintStyles = createGlobalStyle<{ editorTheme: EditorTheme }>` .CodeMirror-hints { position: absolute; z-index: 20; @@ -91,8 +91,11 @@ const HintStyles = createGlobalStyle` max-height: 20em; width: 200px; overflow-y: auto; - background: #FFFFFF; - border: 1px solid #EBEFF2; + background: ${props => + props.editorTheme === "DARK" ? "#090A0F" : "#ffffff"}; + border: 1px solid; + border-color: ${props => + props.editorTheme === "DARK" ? "#535B62" : "#EBEFF2"} box-shadow: 0px 2px 4px rgba(67, 70, 74, 0.14); border-radius: 4px; } @@ -102,7 +105,7 @@ const HintStyles = createGlobalStyle` padding: 3px; margin: 0; white-space: pre; - color: #2E3D49; + color: ${props => (props.editorTheme === "DARK" ? "#F4F4F4" : "#1E242B")}; cursor: pointer; display: flex; align-items: center; @@ -110,7 +113,10 @@ const HintStyles = createGlobalStyle` } li.CodeMirror-hint-active { - background: #E9FAF3; + background: ${props => + props.editorTheme === "DARK" + ? "rgba(244,244,244,0.1)" + : "rgba(128,136,141,0.1)"}; border-radius: 4px; } .CodeMirror-Tern-completion { @@ -121,10 +127,30 @@ const HintStyles = createGlobalStyle` bottom: 7px !important; line-height: 15px !important; } + .CodeMirror-Tern-tooltip { + z-index: 20 !important; + } + .CodeMirror-Tern-hint-doc { + background-color: ${props => + props.editorTheme === "DARK" ? "#23292e" : "#fff"} !important; + color: ${props => + props.editorTheme === "DARK" ? "#F4F4F4" : "#1E242B"} !important; + max-height: 150px; + width: 250px; + padding: 12px !important; + border: 1px solid #DEDEDE !important; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.12) !important; + } `; -const Wrapper = styled.div<{ - editorTheme?: THEME; +const Wrapper = styled.div` + position: relative; + flex: 1; + height: 100%; +`; + +const EditorWrapper = styled.div<{ + editorTheme?: EditorTheme; hasError: boolean; singleLine: boolean; isFocused: boolean; @@ -245,12 +271,12 @@ const IconContainer = styled.div` } `; -const THEMES = { +const THEMES: Record = { LIGHT: "LIGHT", DARK: "DARK", }; -type THEME = "LIGHT" | "DARK"; +export type EditorTheme = "LIGHT" | "DARK"; const AUTOCOMPLETE_CLOSE_KEY_CODES = ["Enter", "Tab", "Escape"]; @@ -264,7 +290,7 @@ export type DynamicAutocompleteInputProps = { rightIcon?: Function; description?: string; height?: number; - theme?: THEME; + theme?: EditorTheme; meta?: Partial; showLineNumbers?: boolean; allowTabIndent?: boolean; @@ -276,6 +302,8 @@ export type DynamicAutocompleteInputProps = { link?: string; baseMode?: string | object; setMaxHeight?: boolean; + dataTreePath?: string; + expected?: string; }; type Props = ReduxStateProps & @@ -316,7 +344,7 @@ class DynamicAutocompleteInput extends Component { mode: this.props.mode || { name: "javascript", globalVars: true }, viewportMargin: 10, tabSize: 2, - indentWithTabs: true, + indentWithTabs: !!this.props.allowTabIndent, lineWrapping: !this.props.singleLine, extraKeys, autoCloseBrackets: true, @@ -324,6 +352,7 @@ class DynamicAutocompleteInput extends Component { }); this.editor.on("change", _.debounce(this.handleChange, 300)); + this.editor.on("change", this.handleAutocompleteVisibility); this.editor.on("keyup", this.handleAutocompleteHide); this.editor.on("focus", this.handleEditorFocus); this.editor.on("blur", this.handleEditorBlur); @@ -409,7 +438,6 @@ class DynamicAutocompleteInput extends Component { globalScope: this.props.dynamicData, }); } - this.editor.on("cursorActivity", this.handleAutocompleteVisibility); } handleEditorFocus = () => { @@ -447,20 +475,30 @@ class DynamicAutocompleteInput extends Component { let cursorBetweenBinding = false; const cursor = this.editor.getCursor(); const value = this.editor.getValue(); + let cursorIndex = cursor.ch; + if (cursor.line > 0) { + for (let lineIndex = 0; lineIndex < cursor.line; lineIndex++) { + const line = this.editor.getLine(lineIndex); + // Add line length + 1 for new line character + cursorIndex = cursorIndex + line.length + 1; + } + } + const stringSegments = getDynamicStringSegments(value); + // count of chars processed let cumulativeCharCount = 0; - parseDynamicString(value).forEach(segment => { + stringSegments.forEach((segment: string) => { const start = cumulativeCharCount; const dynamicStart = segment.indexOf("{{"); const dynamicDoesStart = dynamicStart > -1; const dynamicEnd = segment.indexOf("}}"); const dynamicDoesEnd = dynamicEnd > -1; - const dynamicStartIndex = dynamicStart + start + 1; - const dynamicEndIndex = dynamicEnd + start + 1; + const dynamicStartIndex = dynamicStart + start + 2; + const dynamicEndIndex = dynamicEnd + start; if ( dynamicDoesStart && - cursor.ch > dynamicStartIndex && - ((dynamicDoesEnd && cursor.ch < dynamicEndIndex) || - (!dynamicDoesEnd && cursor.ch > dynamicStartIndex)) + cursorIndex >= dynamicStartIndex && + ((dynamicDoesEnd && cursorIndex <= dynamicEndIndex) || + (!dynamicDoesEnd && cursorIndex >= dynamicStartIndex)) ) { cursorBetweenBinding = true; } @@ -523,63 +561,75 @@ class DynamicAutocompleteInput extends Component { disabled, className, setMaxHeight, + dataTreePath, + dynamicData, + expected, } = this.props; const hasError = !!(meta && meta.error); - let showError = false; - if (this.editor) { - showError = - hasError && this.state.isFocused && !this.state.autoCompleteVisible; + let evaluatedValue: any = "undefined"; + if (dataTreePath) { + evaluatedValue = _.get(dynamicData, dataTreePath); } + const showEvaluatedValue = + this.state.isFocused && "dataTreePath" in this.props; return ( - - + - - - {this.props.leftIcon && } - + + + + {this.props.leftIcon && } + - {this.props.leftImage && ( - img - )} + {this.props.leftImage && ( + img + )} -