diff --git a/app/client/.gitlab-ci.yml b/app/client/.gitlab-ci.yml index 89ec8a5b57..b6f48df47d 100644 --- a/app/client/.gitlab-ci.yml +++ b/app/client/.gitlab-ci.yml @@ -75,6 +75,33 @@ cypress-test-release: - release - merge_requests +# cypress-test-fix: +# stage: build +# script: +# - *set_env_variables +# - yarn install +# # show where the Cypress test runner binaries are cached +# - $(npm bin)/cypress cache path +# # show all installed versions of Cypress binary +# - $(npm bin)/cypress cache list +# - $(npm bin)/cypress verify +# - curl https://s3.ap-south-1.amazonaws.com/dev.public.appsmith/react-build/build1.tar.gz --output build.tar.gz +# - tar xvf build.tar.gz +# - yarn global add serve +# - serve -s build -p 3000 & +# # This is required in order to ensure that all the test cases pass +# - echo "127.0.0.1 dev.appsmith.com" >> /etc/hosts +# - yarn test +# artifacts: +# when: always +# expire_in: 1 week +# paths: +# - cypress/screenshots +# - cypress/videos +# only: +# # We don't test on master right now because of changing environment variables for REACT_APP_BASE_URL. Need to figure out a way to configure that. +# - fix/test-revert + docker-package-release: image: docker:dind services: diff --git a/app/client/craco.build.config.js b/app/client/craco.build.config.js index f20adf3bd2..f8526e92ca 100644 --- a/app/client/craco.build.config.js +++ b/app/client/craco.build.config.js @@ -1,22 +1,24 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const SentryWebpackPlugin = require('@sentry/webpack-plugin'); -const merge = require('webpack-merge'); -const common = require('./craco.common.config.js'); +const SentryWebpackPlugin = require("@sentry/webpack-plugin"); +const merge = require("webpack-merge"); +const common = require("./craco.common.config.js"); -const env = process.env.REACT_APP_ENVIRONMENT +const env = process.env.REACT_APP_ENVIRONMENT; -const plugins = [] +const plugins = []; -if(env === "PRODUCTION" || env === "STAGING") { - plugins.push(new SentryWebpackPlugin({ - include: 'build', - ignore: ['node_modules', 'webpack.config.js'], - release: process.env.REACT_APP_SENTRY_RELEASE - })) +if (env === "PRODUCTION" || env === "STAGING") { + plugins.push( + new SentryWebpackPlugin({ + include: "build", + ignore: ["node_modules", "webpack.config.js"], + release: process.env.REACT_APP_SENTRY_RELEASE, + }), + ); } module.exports = merge(common, { webpack: { - plugins: plugins + plugins: plugins, }, }); diff --git a/app/client/cypress.json b/app/client/cypress.json index 430cc545c4..b0c01cbf99 100644 --- a/app/client/cypress.json +++ b/app/client/cypress.json @@ -1,8 +1,9 @@ { "baseUrl":"http://dev.appsmith.com:3000/", - "defaultCommandTimeout": 10000, + "defaultCommandTimeout": 20000, + "requestTimeout": 21000, "pageLoadTimeout": 20000, - + "video": false, "reporter": "mochawesome", "reporterOptions": { "reportDir": "results", diff --git a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Button_spec.js b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Button_spec.js index 91aae816f2..dc50f7b2be 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Button_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Button_spec.js @@ -1,25 +1,14 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); -context("Cypress test", function() { +describe("Button Widget Functionality", function() { it("Button Widget Functionality", function() { cy.NavigateToCommonWidgets(); cy.get(".t--nav-link-widgets-editor").click(); cy.get(widgetsPage.buttonWidget).click({ force: true }); //Changing the text on the Button - cy.get(".CodeMirror textarea") - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("Test Button Text", { force: true }) - .wait(5000); - - // TODO instead of testing the textarea, test the actual widget - // cy.get(".CodeMirror textarea") - // .first() - // .should("have.value", "Test Button Text"); + cy.testCodeMirror("Test Button Text"); //Select and verify the Show Modal from the onClick dropdown cy.get(widgetsPage.buttonOnClick) @@ -34,6 +23,6 @@ context("Cypress test", function() { .find(".bp3-button-text") .should("have.text", "Show Modal"); //Verify Modal Widget - cy.CreateModal(); + // cy.CreateModal(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Container_spec.js b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Container_spec.js index d63e0c60fe..74a96b3306 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Container_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Container_spec.js @@ -1,7 +1,7 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); -context("Cypress test", function() { +describe("Container Widget Functionality", function() { it("Container Widget Functionality", function() { cy.NavigateToCommonWidgets(); cy.get(widgetsPage.containerWidget) @@ -12,16 +12,7 @@ context("Cypress test", function() { .first() .click(); //Checking the edit props for container changing the background color of container - cy.get(".CodeMirror textarea") - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("#C0C0C0", { force: true }) - .wait(5000); - - // TODO instead of testing the textarea, test the actual widget - // cy.get(".CodeMirror textarea").should("have.value", "#C0C0C0"); + cy.testCodeMirror("#C0C0C0"); cy.get(commonlocators.editPropCrossButton).click(); }); }); 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 f8eb3cdc51..0be816d1f3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Input_spec.js @@ -1,7 +1,7 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); -context("Cypress test", function() { +describe("Input Widget Functionality", function() { it("Input Widget Functionality", function() { cy.NavigateToCommonWidgets(); cy.get(widgetsPage.inputWidget) @@ -12,14 +12,7 @@ context("Cypress test", function() { .first() .click(); //Checking the edit props for container and changing the Input label name - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("Test Input Label", { force: true }) - .wait(5000); + cy.testCodeMirror("Test Input Label"); cy.get(commonlocators.editPropCrossButton).click(); }); 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 488d5ca416..72fc7219e7 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Table_spec.js @@ -1,7 +1,7 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); -context("Cypress test", function() { +describe("Table Widget Functionality", function() { it("Table Widget Functionality", function() { cy.NavigateToCommonWidgets(); cy.get(widgetsPage.tableWidget) @@ -12,18 +12,6 @@ context("Cypress test", function() { .first() .click(); //Checking the edit props for Table Widget and also the properties of Table widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .should("be.empty") - .clear({ force: true }) - .should("be.empty") - .type("{{UsersApi.data}}", { - parseSpecialCharSequences: false, - force: true, - }) - .wait(5000); cy.get(widgetsPage.tableOnRowSelected) .get(commonlocators.dropdownSelectButton) @@ -31,13 +19,14 @@ context("Cypress test", function() { .click({ force: true }) .get("ul.bp3-menu") .children() - .contains("Navigate to URL") + .contains("Navigate To") .click(); + cy.wait("@updateLayout"); cy.get(widgetsPage.tableOnRowSelected) .get(commonlocators.dropdownSelectButton) .first() .find("> .bp3-button-text") - .should("have.text", "Navigate to URL"); + .should("have.text", "Navigate To"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Text_spec.js b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Text_spec.js index cdbca668f8..d7e688f3a1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Text_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/CommonWidgets/Text_spec.js @@ -1,7 +1,7 @@ const widgetsPage = require("../../../locators/Widgets.json"); const commonlocators = require("../../../locators/commonlocators.json"); -context("Cypress test", function() { +describe("Text Widget Functionality", function() { it("Text Widget Functionality", function() { cy.NavigateToCommonWidgets(); cy.get(widgetsPage.textWidget) @@ -12,19 +12,7 @@ context("Cypress test", function() { .first() .click(); //Changing the text on the text widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("Test text", { force: true }) - .wait(5000); - - // TODO instead of testing the textarea, test the actual widget - // cy.get(".CodeMirror textarea") - // .first() - // .should("have.value", "Test text"); + cy.testCodeMirror("Test text"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js index 24f7d0e9a7..0bde83a76f 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/CheckBox_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("Checkbox Widget Functionality", function() { it("Checkbox Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.checkboxWidget) @@ -12,16 +12,7 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Checkbox and also the properties of Checkbox widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .should("be.empty") - .clear({ force: true }) - .type("Test Checkbox"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "Test Checkbox"); + cy.testCodeMirror("Test Checkbox"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js index 6dbf32baf9..2ee9f08314 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/DatePicker_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("DatePicker Widget Functionality", function() { it("DatePicker Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.datepickerWidget) @@ -12,16 +12,7 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for DatePicker and also the properties of DatePicker widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .should("be.empty") - .clear({ force: true }) - .type("From Date"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "From Date"); + cy.testCodeMirror("From Date"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js index 8e5b3af13e..076b63b647 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Dropdown_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("Dropdown Widget Functionality", function() { it("Dropdown Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.dropdownWidget) @@ -12,16 +12,7 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Dropdown and also the properties of Dropdown widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("Test Dropdown"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "Test Dropdown"); + cy.testCodeMirror("Test Dropdown"); cy.get(formWidgetsPage.dropdownSelectionType) .find(".bp3-button") diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js index 01c7cfb7a9..368291a6a1 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FilePicker_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("FilePicker Widget Functionality", function() { it("FilePicker Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.filepickerWidget) @@ -12,16 +12,8 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for FilePicker and also the properties of FilePicker widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .should("be.empty") - .clear({ force: true }) - .type("Upload Files"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "Upload Files"); + + cy.testCodeMirror("Upload Files"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js index 3d0db8fe43..8fbf98c304 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/FormWidget_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("Form Widget Functionality", function() { it("Form Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.formWidget) @@ -12,16 +12,8 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Form and also the properties of Form widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .should("be.empty") - .clear({ force: true }) - .type("Gray"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "Gray"); + cy.testCodeMirror("Gray"); + cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js index 5470127547..2c999163c9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/Radio_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("Radio Widget Functionality", function() { it("Radio Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.radioWidget) @@ -12,27 +12,18 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Radio Widget and also the properties of Radio widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("Test Radio"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "Test Radio"); + cy.testCodeMirror("Test Radio"); cy.get(formWidgetsPage.radioOnSelectionChangeDropdown) .get(commonlocators.dropdownSelectButton) .click({ force: true }) .get("ul.bp3-menu") .children() - .contains("Navigate to URL") + .contains("Navigate To") .click(); cy.get(formWidgetsPage.radioOnSelectionChangeDropdown) .get(commonlocators.dropdownSelectButton) .find("> .bp3-button-text") - .should("have.text", "Navigate to URL"); + .should("have.text", "Navigate To"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js index 8e0a32e9cd..54391a9f9e 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/FormWidgets/RichTextEditor_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json"); -context("Cypress test", function() { +describe("RichTextEditor Widget Functionality", function() { it("RichTextEditor Widget Functionality", function() { cy.NavigateToFormWidgets(); cy.get(formWidgetsPage.richTextEditorWidget) @@ -17,13 +17,13 @@ context("Cypress test", function() { .click({ force: true }) .get("ul.bp3-menu") .children() - .contains("Navigate to URL") + .contains("Navigate To") .click(); cy.get(formWidgetsPage.richEditorOnTextChange) .get(commonlocators.dropdownSelectButton) .find("> span") .eq(0) - .should("have.text", "Navigate to URL"); + .should("have.text", "Navigate To"); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Chart_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Chart_spec.js index 82172503c9..9715ee3187 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Chart_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Chart_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); -context("Cypress test", function() { +describe("Chart Widget Functionality", function() { it("Chart Widget Functionality", function() { cy.NavigateToViewWidgets(); cy.get(viewWidgetsPage.chartWidget) @@ -12,16 +12,7 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Chart and also the properties of Chart widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type("App Sign Up"); - cy.get(".CodeMirror textarea") - .first() - .should("have.value", "App Sign Up"); + cy.testCodeMirror("App Sign Up"); cy.get(viewWidgetsPage.chartSelectChartType) .find(".bp3-button") .click({ force: true }) diff --git a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Image_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Image_spec.js index a3c3b46ef9..b1b066a25a 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Image_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ViewWidgets/Image_spec.js @@ -1,7 +1,7 @@ const commonlocators = require("../../../locators/commonlocators.json"); const viewWidgetsPage = require("../../../locators/ViewWidgets.json"); -context("Cypress test", function() { +describe("Image Widget Functionality", function() { it("Image Widget Functionality", function() { cy.NavigateToViewWidgets(); cy.get(viewWidgetsPage.imageWidget) @@ -12,21 +12,9 @@ context("Cypress test", function() { .first() .click({ force: true }); //Checking the edit props for Image and also the properties of Image widget - cy.get(".CodeMirror textarea") - .first() - .focus() - .type("{ctrl}{shift}{downarrow}") - .clear({ force: true }) - .should("be.empty") - .type( - "https://images.pexels.com/photos/60597/dahlia-red-blossom-bloom-60597.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", - ); - cy.get(".CodeMirror textarea") - .first() - .should( - "have.value", - "https://images.pexels.com/photos/60597/dahlia-red-blossom-bloom-60597.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", - ); + cy.testCodeMirror( + "https://images.pexels.com/photos/60597/dahlia-red-blossom-bloom-60597.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940", + ); cy.get(commonlocators.editPropCrossButton).click(); }); }); diff --git a/app/client/cypress/locators/commonlocators.json b/app/client/cypress/locators/commonlocators.json index 766a9416be..204cd880d2 100644 --- a/app/client/cypress/locators/commonlocators.json +++ b/app/client/cypress/locators/commonlocators.json @@ -1,8 +1,8 @@ { "editIcon":".t--widget-propertypane-toggle", - "editPropCrossButton":"span[icon='cross']", + "editPropCrossButton":".t--property-pane-close-btn", "deleteWidgetIcon":".t--widget-delete-control", - "dropdownSelectButton":".t--open-dropdown-Select" + "dropdownSelectButton":".t--open-dropdown-Select-Action" } \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index 147670193c..3e25cc2136 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -10,6 +10,11 @@ Cypress.Commands.add("LogintoApp", (uname, pword) => { cy.get(loginPage.username).type(uname); cy.get(loginPage.password).type(pword); cy.get(loginPage.submitBtn).click(); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); }); Cypress.Commands.add("SearchApp", appname => { @@ -19,6 +24,7 @@ Cypress.Commands.add("SearchApp", appname => { .first() .click({ force: true }); cy.get("#loading").should("not.exist"); + // Wait added because after opening the application editor, sometimes it takes a little time. }); Cypress.Commands.add("NavigateToCommonWidgets", () => { @@ -28,6 +34,7 @@ Cypress.Commands.add("NavigateToCommonWidgets", () => { .click({ force: true }); cy.get("#loading").should("not.exist"); cy.get(pages.widgetsEditor).click(); + cy.wait("@getPage"); cy.get("#loading").should("not.exist"); }); @@ -51,30 +58,65 @@ Cypress.Commands.add("NavigateToViewWidgets", () => { cy.get("#loading").should("not.exist"); }); -Cypress.Commands.add("CreateModal", () => { - cy.get(modalWidgetPage.selectModal).click(); - cy.get(modalWidgetPage.createModalButton).click({ force: true }); - cy.get(modalWidgetPage.controlModalType) - .find(".bp3-button") - .click({ force: true }) - .get("ul.bp3-menu") - .children() - .contains("Alert Modal") - .click(); - cy.get(modalWidgetPage.controlModalType) - .find(".bp3-button > .bp3-button-text") - .should("have.text", "Alert Modal"); - cy.get(commonlocators.editPropCrossButton).click(); - cy.get(modalWidgetPage.modalWidget) - .get(commonlocators.deleteWidgetIcon) - .first() - .click({ force: true }); -}); +// Cypress.Commands.add("CreateModal", () => { +// cy.get(modalWidgetPage.selectModal).click(); +// cy.get(modalWidgetPage.createModalButton).click({ force: true }); +// cy.get(modalWidgetPage.controlModalType) +// .find(".bp3-button") +// .click({ force: true }) +// .get("ul.bp3-menu") +// .children() +// .contains("Alert Modal") +// .click(); +// cy.get(modalWidgetPage.controlModalType) +// .find(".bp3-button > .bp3-button-text") +// .should("have.text", "Alert Modal"); +// cy.get(commonlocators.editPropCrossButton).click(); +// cy.get(modalWidgetPage.modalWidget) +// .get(commonlocators.deleteWidgetIcon) +// .first() +// .click({ force: true }); +// }); Cypress.Commands.add("PublishtheApp", () => { cy.xpath(homePage.homePageID).contains("All changes saved"); cy.get(homePage.publishButton).click(); - // cy.window().then(win => { - // cy.get(homePage.publishCrossButton).click(); - // }); + cy.wait("@publishApp"); + cy.get(homePage.publishCrossButton).click(); +}); + +Cypress.Commands.add("getCodeMirror", () => { + return cy + .get(".CodeMirror textarea") + .first() + .focus() + .type("{ctrl}{shift}{downarrow}"); +}); + +Cypress.Commands.add("testCodeMirror", value => { + cy.get(".CodeMirror textarea") + .first() + .focus() + .type("{ctrl}{shift}{downarrow}") + .then($cm => { + if ($cm.val() !== "") { + cy.get(".CodeMirror textarea") + .first() + .clear({ + force: true, + }); + cy.wait("@updateLayout"); + } + + cy.get(".CodeMirror textarea") + .first() + .type(value, { + force: true, + parseSpecialCharSequences: false, + }); + cy.wait("@updateLayout"); + cy.get(".CodeMirror textarea") + .first() + .should("have.value", value); + }); }); diff --git a/app/client/cypress/support/index.js b/app/client/cypress/support/index.js index 7c57379787..2efb143d99 100644 --- a/app/client/cypress/support/index.js +++ b/app/client/cypress/support/index.js @@ -19,6 +19,22 @@ const inputData = require("../fixtures/inputdata.json"); // Import commands.js using ES2015 syntax: import "./commands"; before(function() { + cy.server(); + cy.route("GET", "/api/v1/applications").as("applications"); + cy.route("GET", "/api/v1/users/profile").as("getUser"); + cy.route("GET", "/api/v1/plugins").as("getPlugins"); + + cy.route("GET", "/api/v1/configs/name/propertyPane").as("getPropertyPane"); + cy.route("GET", "/api/v1/datasources").as("getDataSources"); + cy.route("GET", "/api/v1/pages/application/*").as("getPagesForApp"); + cy.route("GET", "/api/v1/pages/*").as("getPage"); + cy.route("GET", "/api/v1/actions*").as("getActions"); + + cy.route("GET", "/api/v1/organizations").as("organizations"); + cy.route("POST", "/api/v1/actions/execute").as("executeAction"); + cy.route("POST", "/api/v1/applications/publish/*").as("publishApp"); + cy.route("PUT", "/api/v1/layouts/*/pages/*").as("updateLayout"); + cy.LogintoApp(loginData.username, loginData.password); cy.SearchApp(inputData.appname); }); diff --git a/app/client/netlify.toml b/app/client/netlify.toml index 7816bc5ae3..6676f67554 100644 --- a/app/client/netlify.toml +++ b/app/client/netlify.toml @@ -1,3 +1,8 @@ +[[headers]] + for = "/static/*" + [header.values] + cache-control = "max-age=604800" + [context.production] [context.production.environment] REACT_APP_ENVIRONMENT = "PRODUCTION" diff --git a/app/client/package.json b/app/client/package.json index 1f5ebbc78a..1e284b1ccb 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -73,7 +73,11 @@ "react-dnd-html5-backend": "^9.3.4", "react-dnd-touch-backend": "^9.4.0", "react-dom": "^16.7.0", + "react-google-maps": "^9.4.5", "react-helmet": "^5.2.1", + "react-infinite-scroller": "^1.2.4", + "react-json-view": "^1.19.1", + "react-paginating": "^1.4.0", "react-redux": "^7.1.3", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", @@ -89,10 +93,8 @@ "redux-saga": "^1.1.3", "reselect": "^4.0.0", "shallowequal": "^1.1.0", - "source-map-explorer": "^2.1.1", "styled-components": "^4.1.3", "tinycolor2": "^1.4.1", - "tinymce": "^5.2.0", "toposort": "^2.0.2", "ts-loader": "^6.0.4", "typescript": "^3.6.3", @@ -103,6 +105,8 @@ "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "REACT_APP_BASE_URL=https://release-api.appsmith.com REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start", "build": "./build.sh", + "build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js", + "build-staging": "REACT_APP_BASE_URL=https://release-api.appsmith.com REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js", "test": "cypress/test.sh", "eject": "react-scripts eject", "start-prod": "REACT_APP_BASE_URL=https://api.appsmith.com REACT_APP_ENVIRONMENT=PRODUCTION craco start", @@ -160,6 +164,7 @@ "react-test-renderer": "^16.11.0", "redux-devtools": "^3.5.0", "redux-devtools-extension": "^2.13.8", + "source-map-explorer": "^2.4.2", "storybook-addon-designs": "^5.1.1", "webpack-merge": "^4.2.2" }, diff --git a/app/client/public/index.html b/app/client/public/index.html index 8e809a7c22..73698b02c1 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -2,15 +2,12 @@ - - + - - - + Appsmith diff --git a/app/client/src/actions/apiPaneActions.ts b/app/client/src/actions/apiPaneActions.ts index 36cb77d357..d968f86042 100644 --- a/app/client/src/actions/apiPaneActions.ts +++ b/app/client/src/actions/apiPaneActions.ts @@ -13,3 +13,10 @@ export const initApiPane = (urlId?: string): ReduxAction<{ id?: string }> => { payload: { id: urlId }, }; }; + +export const createNewApiAction = ( + pageId: string, +): ReduxAction<{ pageId: string }> => ({ + type: ReduxActionTypes.CREATE_NEW_API_ACTION, + payload: { pageId }, +}); diff --git a/app/client/src/actions/collectionAction.ts b/app/client/src/actions/collectionAction.ts new file mode 100644 index 0000000000..9b2bbcd62e --- /dev/null +++ b/app/client/src/actions/collectionAction.ts @@ -0,0 +1,7 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; + +export const fetchImportedCollections = () => { + return { + type: ReduxActionTypes.FETCH_IMPORTED_COLLECTIONS_INIT, + }; +}; diff --git a/app/client/src/actions/importActions.ts b/app/client/src/actions/importActions.ts new file mode 100644 index 0000000000..63e800e663 --- /dev/null +++ b/app/client/src/actions/importActions.ts @@ -0,0 +1,9 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; +import { curlImportFormValues } from "pages/Editor/APIEditor/helpers"; + +export const submitCurlImportForm = (payload: curlImportFormValues) => { + return { + type: ReduxActionTypes.SUBMIT_CURL_FORM_INIT, + payload, + }; +}; diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index 1310b45c07..ac3fa64f50 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -101,6 +101,7 @@ export type WidgetAddChild = { parentRowSpace: number; parentColumnSpace: number; newWidgetId: string; + tabId: string; props?: Record; }; diff --git a/app/client/src/actions/providerActions.ts b/app/client/src/actions/providerActions.ts new file mode 100644 index 0000000000..2ac702309a --- /dev/null +++ b/app/client/src/actions/providerActions.ts @@ -0,0 +1,46 @@ +import { ReduxActionTypes } from "constants/ReduxActionConstants"; + +import { + AddApiToPageRequest, + FetchProviderWithCategoryRequest, +} from "api/ProvidersApi"; + +export const fetchProviders = () => { + return { + type: ReduxActionTypes.FETCH_PROVIDERS_INIT, + }; +}; + +export const fetchProviderCategories = () => { + return { + type: ReduxActionTypes.FETCH_PROVIDERS_CATEGORIES_INIT, + }; +}; + +export const fetchProviderTemplates = () => { + return { + type: ReduxActionTypes.FETCH_PROVIDER_TEMPLATES_INIT, + }; +}; + +export const addApiToPage = (payload: AddApiToPageRequest) => { + return { + type: ReduxActionTypes.ADD_API_TO_PAGE_INIT, + payload, + }; +}; + +export const fetchProvidersWithCategory = ( + payload: FetchProviderWithCategoryRequest, +) => { + return { + type: ReduxActionTypes.FETCH_PROVIDERS_WITH_CATEGORY_INIT, + payload, + }; +}; + +export const clearProviders = () => { + return { + type: ReduxActionTypes.CLEAR_PROVIDERS, + }; +}; diff --git a/app/client/src/actions/widgetActions.tsx b/app/client/src/actions/widgetActions.tsx index 476c72452b..49643f6b7c 100644 --- a/app/client/src/actions/widgetActions.tsx +++ b/app/client/src/actions/widgetActions.tsx @@ -8,15 +8,15 @@ import { ExecuteErrorPayload, PageAction, } from "constants/ActionConstants"; +import { BatchAction, batchAction } from "actions/batchActions"; export const executeAction = ( payload: ExecuteActionPayload, -): ReduxAction => { - return { +): BatchAction => + batchAction({ type: ReduxActionTypes.EXECUTE_ACTION, payload, - }; -}; + }); export const executeActionError = ( executeErrorPayload: ExecuteErrorPayload, @@ -27,10 +27,11 @@ export const executeActionError = ( export const executePageLoadActions = ( payload: PageAction[][], -): ReduxAction => ({ - type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, - payload, -}); +): BatchAction => + batchAction({ + type: ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, + payload, + }); export const disableDragAction = ( disable: boolean, diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index bbbba58866..a1605af5c5 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -33,6 +33,15 @@ export interface Property { value: string; } +export interface BodyFormData { + editable: boolean; + mandatory: boolean; + description: string; + key: string; + value?: string; + type: string; +} + export interface APIConfigRequest { headers: Property[]; httpMethod: string; @@ -40,6 +49,7 @@ export interface APIConfigRequest { body: JSON | string | Record | null; queryParameters: Property[]; paginationType: PaginationType; + bodyFormData: BodyFormData[]; } export interface QueryConfig { @@ -54,12 +64,40 @@ export interface ActionCreateUpdateResponse extends ApiResponse { export interface RestAction { id: string; name: string; - datasource: Pick | Omit; + 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"; diff --git a/app/client/src/api/Api.tsx b/app/client/src/api/Api.tsx index a737ea47ee..9d02349f9b 100644 --- a/app/client/src/api/Api.tsx +++ b/app/client/src/api/Api.tsx @@ -8,6 +8,7 @@ import { import { ActionApiResponse } from "./ActionAPI"; import { AUTH_LOGIN_URL } from "constants/routes"; import { setRouteBeforeLogin } from "utils/storage"; +import history from "utils/history"; const { apiUrl, baseUrl } = getAppsmithConfigs(); //TODO(abhinav): Refactor this to make more composable. @@ -58,8 +59,12 @@ axiosInstance.interceptors.response.use( // console.log(error.response.headers); if (error.response.status === 401) { setRouteBeforeLogin(window.location.pathname); - window.location.href = AUTH_LOGIN_URL; - return; + history.push(AUTH_LOGIN_URL); + return Promise.reject({ + code: 401, + message: "Unauthorized. Redirecting to login page...", + show: false, + }); } if (error.response.data.responseMeta) { return Promise.resolve(error.response.data); diff --git a/app/client/src/api/CollectionApi.ts b/app/client/src/api/CollectionApi.ts new file mode 100644 index 0000000000..426d867fb7 --- /dev/null +++ b/app/client/src/api/CollectionApi.ts @@ -0,0 +1,12 @@ +import { AxiosPromise } from "axios"; +import Api from "./Api"; +import { ImportedCollections } from "constants/collectionsConstants"; + +class ImportedCollectionsApi extends Api { + static importedCollectionsURL = "v1/import/templateCollections"; + static fetchImportedCollections(): AxiosPromise { + return Api.get(ImportedCollectionsApi.importedCollectionsURL); + } +} + +export default ImportedCollectionsApi; diff --git a/app/client/src/api/ImportApi.ts b/app/client/src/api/ImportApi.ts new file mode 100644 index 0000000000..25f890a4bc --- /dev/null +++ b/app/client/src/api/ImportApi.ts @@ -0,0 +1,25 @@ +import { AxiosPromise } from "axios"; +import Api from "./Api"; +import { ApiResponse } from "./ApiResponses"; + +export interface CurlImportRequest { + type: string; + pageId: string; + name: string; + curl: string; +} + +class CurlImportApi extends Api { + static curlImportURL = `v1/import`; + + static curlImport(request: CurlImportRequest): AxiosPromise { + const { pageId, name, curl } = request; + return Api.post(CurlImportApi.curlImportURL, curl, { + type: "CURL", + pageId, + name, + }); + } +} + +export default CurlImportApi; diff --git a/app/client/src/api/PluginApi.ts b/app/client/src/api/PluginApi.ts index ebb5dbb751..96df6a1061 100644 --- a/app/client/src/api/PluginApi.ts +++ b/app/client/src/api/PluginApi.ts @@ -6,6 +6,8 @@ export interface Plugin { id: string; name: string; type: "API" | "DB"; + packageName: string; + uiComponent: "ApiEditorForm" | "RapidApiEditorForm" | "DbEditorForm"; } class PluginsApi extends Api { diff --git a/app/client/src/api/ProvidersApi.ts b/app/client/src/api/ProvidersApi.ts new file mode 100644 index 0000000000..0006c2da45 --- /dev/null +++ b/app/client/src/api/ProvidersApi.ts @@ -0,0 +1,78 @@ +import { AxiosPromise } from "axios"; +import Api from "./Api"; +import { ApiResponse } from "./ApiResponses"; +import { Providers, ProviderTemplates } from "constants/providerConstants"; + +export interface FetchProvidersResponse extends ApiResponse { + data: Providers; +} + +export interface FetchProviderCategoriesResponse extends ApiResponse { + data: string[]; +} + +export interface FetchProviderTemplateResponse extends ApiResponse { + data: ProviderTemplates[]; +} + +export interface FetchProviderTemplatesRequest { + providerId: string; +} + +export interface FetchProviderWithCategoryRequest { + category: string; + page: number; +} + +export interface AddApiToPageRequest { + name: string; + pageId: string; + marketplaceElement: any; +} + +export class ProvidersApi extends Api { + static providersURL = "v1/providers"; + static providerCategoriesURL = "v1/providers/categories"; + + static providerTemplateURL = (providerId: string) => { + return `v1/marketplace/templates?providerId=${providerId}`; + }; + + static providersWithCategoryURL = (category: string, page: number) => { + return `v1/marketplace/providers?category=${category}&page=${page}&size=50`; + }; + + static addApiToPageURL = `v1/items/addToPage`; + + static fetchProviders(): AxiosPromise { + return Api.get(ProvidersApi.providersURL); + } + + static fetchProviderTemplates( + request: FetchProviderTemplatesRequest, + ): AxiosPromise { + const { providerId } = request; + return Api.get(ProvidersApi.providerTemplateURL(providerId)); + } + + static addApiToPage(request: AddApiToPageRequest): AxiosPromise { + return Api.post(ProvidersApi.addApiToPageURL, request); + } + + static fetchProvidersCategories(): AxiosPromise< + FetchProviderCategoriesResponse + > { + return Api.get(ProvidersApi.providerCategoriesURL); + } + + static fetchProvidersWithCategory( + request: FetchProviderWithCategoryRequest, + ): AxiosPromise { + const { page } = request; + return Api.get( + ProvidersApi.providersWithCategoryURL(request.category, page), + ); + } +} + +export default ProvidersApi; diff --git a/app/client/src/assets/icons/control/decrease.svg b/app/client/src/assets/icons/control/decrease.svg new file mode 100644 index 0000000000..f8ffdccb6e --- /dev/null +++ b/app/client/src/assets/icons/control/decrease.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/control/draggable.svg b/app/client/src/assets/icons/control/draggable.svg new file mode 100644 index 0000000000..4be5a7b6d6 --- /dev/null +++ b/app/client/src/assets/icons/control/draggable.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/client/src/assets/icons/control/increase.svg b/app/client/src/assets/icons/control/increase.svg new file mode 100644 index 0000000000..e20986e3e8 --- /dev/null +++ b/app/client/src/assets/icons/control/increase.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/icons/control/pick-my-location.svg b/app/client/src/assets/icons/control/pick-my-location.svg new file mode 100644 index 0000000000..e519a17dd3 --- /dev/null +++ b/app/client/src/assets/icons/control/pick-my-location.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/client/src/assets/icons/form/info-outline.svg b/app/client/src/assets/icons/form/info-outline.svg new file mode 100644 index 0000000000..0cfe7d23e0 --- /dev/null +++ b/app/client/src/assets/icons/form/info-outline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/app/client/src/assets/icons/widget/map.svg b/app/client/src/assets/icons/widget/map.svg new file mode 100755 index 0000000000..61c7b73472 --- /dev/null +++ b/app/client/src/assets/icons/widget/map.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/client/src/assets/images/Curl-logo.svg b/app/client/src/assets/images/Curl-logo.svg new file mode 100644 index 0000000000..e574f5f631 --- /dev/null +++ b/app/client/src/assets/images/Curl-logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/client/src/assets/images/Curl.png b/app/client/src/assets/images/Curl.png new file mode 100644 index 0000000000..474feeef84 Binary files /dev/null and b/app/client/src/assets/images/Curl.png differ diff --git a/app/client/src/assets/images/Postman-logo.svg b/app/client/src/assets/images/Postman-logo.svg new file mode 100644 index 0000000000..9fb1dc9a81 --- /dev/null +++ b/app/client/src/assets/images/Postman-logo.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/assets/images/Postman.png b/app/client/src/assets/images/Postman.png new file mode 100644 index 0000000000..cfd823454d Binary files /dev/null and b/app/client/src/assets/images/Postman.png differ diff --git a/app/client/src/assets/images/no_image.png b/app/client/src/assets/images/no_image.png new file mode 100644 index 0000000000..cb274965d4 Binary files /dev/null and b/app/client/src/assets/images/no_image.png differ diff --git a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx index db0bb4c45a..9174cb5ca9 100644 --- a/app/client/src/components/designSystems/appsmith/ChartComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ChartComponent.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ChartType, ChartData } from "widgets/ChartWidget"; +import { ChartType, ChartData, ChartDataPoint } from "widgets/ChartWidget"; import styled from "styled-components"; import { invisible } from "constants/DefaultTheme"; import _ from "lodash"; @@ -18,6 +18,7 @@ export interface ChartComponentProps { chartName: string; widgetId: string; isVisible?: boolean; + allowHorizontalScroll: boolean; } const CanvasContainer = styled.div` @@ -29,29 +30,49 @@ const CanvasContainer = styled.div` box-shadow: 0 1px 1px 0 rgba(60,75,100,.14),0 2px 1px -1px rgba(60,75,100,.12),0 1px 3px 0 rgba(60,75,100,.2); position: relative; ${props => (!props.isVisible ? invisible : "")}; + padding: 10px 0 0 0; }`; class ChartComponent extends React.Component { chartInstance = new FusionCharts(); - getChartType = (chartType: ChartType) => { + getChartType = () => { + const { chartType, allowHorizontalScroll, chartData } = this.props; + const isMSChart = chartData.length > 1; switch (chartType) { - case "LINE_CHART": - return "line"; - case "BAR_CHART": - return "bar2d"; case "PIE_CHART": return "pie2d"; + case "LINE_CHART": + return allowHorizontalScroll + ? "scrollline2d" + : isMSChart + ? "msline" + : "line"; + case "BAR_CHART": + return allowHorizontalScroll + ? "scrollBar2D" + : isMSChart + ? "msbar2d" + : "bar2d"; case "COLUMN_CHART": - return "column2d"; + return allowHorizontalScroll + ? "scrollColumn2D" + : isMSChart + ? "mscolumn2d" + : "column2d"; case "AREA_CHART": - return "area2d"; + return allowHorizontalScroll + ? "scrollarea2d" + : isMSChart + ? "msarea" + : "area2d"; default: - return "column2d"; + return allowHorizontalScroll ? "scrollColumn2D" : "mscolumn2d"; } }; getChartData = (chartData: ChartData[]) => { - return chartData.map(item => { + const data: ChartDataPoint[] = chartData[0].data; + return data.map(item => { return { label: item.x, value: item.y, @@ -59,25 +80,121 @@ class ChartComponent extends React.Component { }); }; + getChartCategoriesMutliSeries = (chartData: ChartData[]) => { + const categories: string[] = []; + for (let index = 0; index < chartData.length; index++) { + const data: ChartDataPoint[] = chartData[index].data; + for (let dataIndex = 0; dataIndex < data.length; dataIndex++) { + const category = data[dataIndex].x; + if (!categories.includes(category)) { + categories.push(category); + } + } + } + return categories; + }; + + getChartCategories = (chartData: ChartData[]) => { + const categories: string[] = this.getChartCategoriesMutliSeries(chartData); + return categories.map(item => { + return { + label: item, + }; + }); + }; + + getSeriesChartData = (data: ChartDataPoint[], categories: string[]) => { + const dataMap: { [key: string]: string } = {}; + for (let index = 0; index < data.length; index++) { + const item: ChartDataPoint = data[index]; + dataMap[item.x] = item.y; + } + return categories.map((category: string) => { + return { + value: dataMap[category] ? dataMap[category] : null, + }; + }); + }; + + getChartDataset = (chartData: ChartData[]) => { + const categories: string[] = this.getChartCategoriesMutliSeries(chartData); + return chartData.map((item: ChartData) => { + const seriesChartData: object[] = this.getSeriesChartData( + item.data, + categories, + ); + return { + seriesName: item.seriesName, + data: seriesChartData, + }; + }); + }; + + getChartConfig = () => { + return { + caption: this.props.chartName, + xAxisName: this.props.xAxisName, + yAxisName: this.props.yAxisName, + theme: "fusion", + captionAlignment: "left", + captionHorizontalPadding: 10, + alignCaptionWithCanvas: 0, + }; + }; + + getChartDataSource = () => { + if ( + this.props.chartData.length === 1 || + this.props.chartType === "PIE_CHART" + ) { + return { + chart: this.getChartConfig(), + data: this.getChartData(this.props.chartData), + }; + } else { + return { + chart: this.getChartConfig(), + categories: [ + { + category: this.getChartCategories(this.props.chartData), + }, + ], + dataset: this.getChartDataset(this.props.chartData), + }; + } + }; + + getScrollChartDataSource = () => { + const chartConfig = this.getChartConfig(); + return { + chart: { + ...chartConfig, + scrollheight: "10", + showvalues: "1", + numVisiblePlot: "5", + flatScrollBars: "1", + }, + categories: [ + { + category: this.getChartCategories(this.props.chartData), + }, + ], + dataset: this.getChartDataset(this.props.chartData), + }; + }; + createGraph = () => { + const dataSource = + this.props.allowHorizontalScroll && this.props.chartType !== "PIE_CHART" + ? this.getScrollChartDataSource() + : this.getChartDataSource(); const chartConfig = { - type: this.getChartType(this.props.chartType), + type: this.getChartType(), renderAt: this.props.widgetId + "chart-container", width: "100%", height: "100%", dataFormat: "json", - dataSource: { - chart: { - caption: this.props.chartName, - xAxisName: this.props.xAxisName, - yAxisName: this.props.yAxisName, - theme: "fusion", - captionAlignment: "left", - captionHorizontalPadding: 10, - alignCaptionWithCanvas: 0, - }, - data: this.getChartData(this.props.chartData), - }, + dataSource: dataSource, }; this.chartInstance = new FusionCharts(chartConfig); }; @@ -91,19 +208,15 @@ class ChartComponent extends React.Component { componentDidUpdate(prevProps: ChartComponentProps) { if (!_.isEqual(prevProps, this.props)) { - if (prevProps.chartType !== this.props.chartType) { - const chartType = this.getChartType(this.props.chartType); - this.chartInstance.chartType(chartType); + const chartType = this.getChartType(); + this.chartInstance.chartType(chartType); + if ( + this.props.allowHorizontalScroll && + this.props.chartType !== "PIE_CHART" + ) { + this.chartInstance.setChartData(this.getScrollChartDataSource()); } else { - this.chartInstance.setChartData({ - chart: { - caption: this.props.chartName, - xAxisName: this.props.xAxisName, - yAxisName: this.props.yAxisName, - theme: "fusion", - }, - data: this.getChartData(this.props.chartData), - }); + this.chartInstance.setChartData(this.getChartDataSource()); } } } diff --git a/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx new file mode 100644 index 0000000000..2a3036bcc5 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/DraggableListComponent.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { ControlWrapper } from "../../propertyControls/StyledControls"; +import styled from "constants/DefaultTheme"; +import { Droppable, Draggable } from "react-beautiful-dnd"; + +const StyledListWrapper = styled(ControlWrapper)` + display: flex; + justify-content: flex-start; + padding-right: 16px; + margin: 8px 0 0 0; +`; + +type RenderComponentProps = { + index: number; + item: { + label: string; + }; + deleteOption: (index: number) => void; + updateOption: (index: number, value: string) => void; +}; + +type DraggableComponentProps = { + index: number; + draggableId: string; + item: { + label: string; + }; + deleteOption: (index: number) => void; + updateOption: (index: number, value: string) => void; + renderComponent: (props: RenderComponentProps) => JSX.Element; +}; + +export const DraggableComponent = (props: DraggableComponentProps) => { + const { + deleteOption, + updateOption, + item, + index, + draggableId, + renderComponent, + } = props; + return ( + + {({ innerRef, draggableProps, dragHandleProps }) => ( + } + style={{ + ...draggableProps.style, + userSelect: "none", + position: "static", + }} + > + {renderComponent({ deleteOption, updateOption, item, index })} + + )} + + ); +}; + +type DroppableComponentProps = { + items: object[]; + renderComponent: (props: RenderComponentProps) => JSX.Element; + deleteOption: (index: number) => void; + updateOption: (index: number, value: string) => void; +}; + +export const DroppableComponent = (props: DroppableComponentProps) => { + const { items } = props; + return ( + + {({ innerRef, droppableProps, placeholder }) => ( +
} {...droppableProps}> + {items.map((item: { id: string } & any, index: number) => { + return ( + + ); + })} +
+ )} +
+ ); +}; diff --git a/app/client/src/components/designSystems/appsmith/Dropdown.tsx b/app/client/src/components/designSystems/appsmith/Dropdown.tsx index 5be9a2bb54..cc48be9a33 100644 --- a/app/client/src/components/designSystems/appsmith/Dropdown.tsx +++ b/app/client/src/components/designSystems/appsmith/Dropdown.tsx @@ -12,6 +12,8 @@ type DropdownProps = { input: WrappedFieldInputProps; placeholder: string; width?: number; + isSearchable?: boolean; + isDisabled?: boolean; }; const selectStyles = { @@ -56,6 +58,8 @@ export const BaseDropdown = (props: DropdownProps) => { {...input} width={props.width} onChange={value => input.onChange(value)} + isSearchable={props.isSearchable} + isDisabled={props.isDisabled} /> ); }; diff --git a/app/client/src/components/designSystems/appsmith/FilePickerComponent.tsx b/app/client/src/components/designSystems/appsmith/FilePickerComponent.tsx index ee451922a1..24a40f8134 100644 --- a/app/client/src/components/designSystems/appsmith/FilePickerComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/FilePickerComponent.tsx @@ -4,7 +4,6 @@ import "@uppy/core/dist/style.css"; import "@uppy/dashboard/dist/style.css"; import "@uppy/webcam/dist/style.css"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; -// import { DashboardModal } from "@uppy/react"; class FilePickerComponent extends React.Component< FilePickerComponentProps, @@ -18,7 +17,6 @@ class FilePickerComponent extends React.Component< } openModal = () => { - // this.setState({ isOpen: true }); this.props.uppy.getPlugin("Dashboard").openModal(); }; @@ -36,19 +34,11 @@ class FilePickerComponent extends React.Component< text={label} onClick={this.openModal} /> - {/* **/} ); } public closeModal() { - // this.setState({ isOpen: false }); this.props.uppy.getPlugin("Dashboard").closeModal(); } } diff --git a/app/client/src/components/designSystems/appsmith/IconComponent.tsx b/app/client/src/components/designSystems/appsmith/IconComponent.tsx new file mode 100644 index 0000000000..14058919bf --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/IconComponent.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Icon, Intent } from "@blueprintjs/core"; +import { IconName } from "@blueprintjs/icons"; +import { noop } from "utils/AppsmithUtils"; +export type IconType = IconName | string; + +class IconComponent extends React.Component { + render() { + return ( + + ); + } +} + +export interface IconComponentProps { + iconSize?: number; + iconName?: IconType; + intent?: Intent; + disabled?: boolean; + onClick?: () => void; + color: string; +} + +export default IconComponent; diff --git a/app/client/src/components/designSystems/appsmith/MapComponent.tsx b/app/client/src/components/designSystems/appsmith/MapComponent.tsx new file mode 100644 index 0000000000..34df7e16a5 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/MapComponent.tsx @@ -0,0 +1,159 @@ +import React from "react"; +import { + withScriptjs, + withGoogleMap, + GoogleMap, + Marker, +} from "react-google-maps"; +import SearchBox from "react-google-maps/lib/components/places/SearchBox"; +import { MarkerProps } from "widgets/MapWidget"; +import { ControlIcons } from "icons/ControlIcons"; +import styled, { AnyStyledComponent } from "styled-components"; + +interface MapComponentProps { + widgetId: string; + isDisabled?: boolean; + isVisible?: boolean; + enableSearch: boolean; + zoomLevel: number; + enablePickLocation: boolean; + allowZoom: boolean; + center: { + lat: number; + lng: number; + }; + markers?: Array; + enableCreateMarker: boolean; + updateCenter: (lat: number, lng: number) => void; + saveMarker: (lat: number, lng: number) => void; + selectMarker: (lat: number, lng: number, title: string) => void; +} + +const StyledInput = styled.input` + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + margin-top: 27px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; +`; +type PickMyLocationProps = { + allowZoom: boolean; +}; + +const PickMyLocationWrapper = styled.div` + position: absolute; + bottom: ${props => (props.allowZoom ? 110 : 20)}px; + right: -95px; + width: 140px; +`; + +const StyledPickMyLocationIcon = styled( + ControlIcons.PICK_MY_LOCATION_CONTROL as AnyStyledComponent, +)` + position: relative; + cursor: pointer; +`; + +const MyMapComponent = withScriptjs( + withGoogleMap((props: any) => ( + { + if (props.enableCreateMarker) { + props.saveMarker(e.latLng.lat(), e.latLng.lng()); + } + }} + > + {props.enableSearch && ( + + + + )} + {props.markers.map((marker: any, index: number) => ( + { + props.selectMarker(marker.lat, marker.lng, marker.title); + }} + /> + ))} + {props.enablePickLocation && ( + + + + )} + + )), +); + +class MapComponent extends React.Component { + private searchBox = React.createRef(); + + onSearchBoxMounted = (ref: any) => { + this.searchBox = ref; + }; + onPlacesChanged = () => { + const node: any = this.searchBox; + if (node) { + const places: any = node.getPlaces(); + const location = places[0].geometry.location; + const lat = location.lat(); + const lng = location.lng(); + this.props.updateCenter(lat, lng); + } + }; + + getUserLocation = () => { + if ("geolocation" in navigator) { + return navigator.geolocation.getCurrentPosition(data => { + const { + coords: { latitude: lat, longitude: lng }, + } = data; + this.props.saveMarker(lat, lng); + }); + } + }; + + render() { + const zoom = Math.floor(this.props.zoomLevel / 5); + return ( + } + containerElement={
} + mapElement={
} + {...this.props} + zoom={zoom} + onPlacesChanged={this.onPlacesChanged} + onSearchBoxMounted={this.onSearchBoxMounted} + getUserLocation={this.getUserLocation} + /> + ); + } +} + +export default MapComponent; diff --git a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx index f4aa3ede2a..83a1cbe223 100644 --- a/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/RichTextEditorComponent.tsx @@ -1,9 +1,6 @@ import React from "react"; import { Editor } from "@tinymce/tinymce-react"; import styled from "styled-components"; -require("tinymce/tinymce"); -require("tinymce/themes/silver"); - const StyledRTEditor = styled.div` && { width: 100%; @@ -28,6 +25,7 @@ export const RichtextEditorComponent = ( return ( props.onValueChange(content)} /> diff --git a/app/client/src/components/designSystems/appsmith/StepComponent.tsx b/app/client/src/components/designSystems/appsmith/StepComponent.tsx new file mode 100644 index 0000000000..5c5b1237ba --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/StepComponent.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { ControlIcons } from "icons/ControlIcons"; +import { AnyStyledComponent } from "styled-components"; +import styled from "constants/DefaultTheme"; + +const StyledIncreaseIcon = styled( + ControlIcons.INCREASE_CONTROL as AnyStyledComponent, +)` + display: flex; + justify-content: center; + align-items: center; + position: relative; + cursor: pointer; + width: 40px; + height: 32px; + svg { + path { + fill: ${props => props.theme.colors.paneSectionLabel}; + } + } +`; + +const StyledDecreaseIcon = styled( + ControlIcons.DECREASE_CONTROL as AnyStyledComponent, +)` + display: flex; + justify-content: center; + align-items: center; + position: relative; + cursor: pointer; + width: 40px; + height: 32px; + svg { + path { + fill: ${props => props.theme.colors.paneSectionLabel}; + } + } +`; + +const StepWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: 100%; + background: #121518; + border-radius: 4px; + height: 32px; + line-height: 32px; +`; + +const InputWrapper = styled.div` + width: calc(100% - 80px); + height: 32px; + line-height: 32px; + background: #23292e; + font-size: 14px; + color: ${props => props.theme.colors.textOnDarkBG}; + text-align: center; + letter-spacing: 1.44px; +`; + +interface StepComponentProps { + value: number; + min: number; + max: number; + steps: number; + displayFormat: (value: number) => string; + onChange: (value: number) => void; +} + +export const StepComponent = (props: StepComponentProps) => { + function decrease() { + if (props.value < props.min) { + return; + } + const value = props.value - props.steps; + props.onChange(value); + } + function increase() { + if (props.value > props.max) { + return; + } + const value = props.value + props.steps; + props.onChange(value); + } + return ( + + + {props.displayFormat(props.value)} + + + ); +}; + +export default StepComponent; diff --git a/app/client/src/components/designSystems/appsmith/TabsComponent.tsx b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx new file mode 100644 index 0000000000..e37e235c85 --- /dev/null +++ b/app/client/src/components/designSystems/appsmith/TabsComponent.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import styled from "styled-components"; + +interface TabsComponentProps { + isVisible?: boolean; + tabs?: Array<{ + id: string; + label: string; + }>; + selectedTabId?: string; + onTabChange: (tabId: string) => void; +} + +const TabsContainer = styled.div` + && { + height: 32px; + width: 100%; + display: flex; + justify-content: flex-start; + align-items: flex-start; + } +`; + +type TabProps = { + selected?: boolean; +}; + +const StyledTab = styled.div` + height: 32px; + border-bottom: 1px solid; + border-color: ${props => props.theme.colors.bodyBG}; + width: 100%; +`; + +const StyledText = styled.div` + white-space: nowrap; + background: ${props => props.theme.colors.builderBodyBG}; + color: ${props => props.theme.colors.menuIconColorInactive}; + font-size: ${props => props.theme.fontSizes[3]}px; + line-height: 32px; + height: 32px; + padding: 0 16px; + cursor: pointer; + box-shadow: ${props => (props.selected ? props.theme.shadows[2] : "")}; + border-bottom: ${props => (props.selected ? "none" : "1px solid")}; + border-color: ${props => props.theme.colors.bodyBG}; + &:hover { + background: ${props => + props.selected + ? props.theme.colors.textOnDarkBG + : props.theme.colors.hover}; + box-shadow: ${props => (props.selected ? "" : props.theme.shadows[3])}; + } +`; + +class TabsComponent extends React.Component { + selectTab = (tab: { id: string; label: string }) => { + this.props.onTabChange(tab.id); + }; + + render() { + return ( + + {this.props.tabs && + this.props.tabs.map((tab, index) => ( + + {tab.label} + + ))} + + + ); + } +} + +export default TabsComponent; diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index ec7144c2b5..5d79596bc2 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -45,7 +45,6 @@ const ButtonWrapper = styled((props: ButtonStyleProps & IButtonProps) => ( : props.theme.colors.secondary}; border-radius: 4px; font-weight: ${props => props.theme.fontWeights[2]}; - font-family: "DM Sans"; outline: none; &.bp3-button { padding: 0px 10px; diff --git a/app/client/src/components/designSystems/blueprint/CloseButton.tsx b/app/client/src/components/designSystems/blueprint/CloseButton.tsx index b683e08d57..3fc440c064 100644 --- a/app/client/src/components/designSystems/blueprint/CloseButton.tsx +++ b/app/client/src/components/designSystems/blueprint/CloseButton.tsx @@ -6,6 +6,7 @@ type CloseButtonProps = { color: Color; size: number; onClick: React.MouseEventHandler; + className?: string; }; const StyledButton = styled(Button)` @@ -22,5 +23,12 @@ const StyledButton = styled(Button)` `; export const CloseButton = (props: CloseButtonProps) => { - return ; + return ( + + ); }; diff --git a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx index 40d4f1ceaf..a788138587 100644 --- a/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/DatePickerComponent.tsx @@ -8,6 +8,7 @@ import moment from "moment-timezone"; import "../../../../node_modules/@blueprintjs/datetime/lib/css/blueprint-datetime.css"; import { DatePickerType } from "widgets/DatePickerWidget"; import { WIDGET_PADDING } from "constants/WidgetConstants"; +import { TimePrecision } from "@blueprintjs/datetime"; import { Colors } from "constants/Colors"; const StyledControlGroup = styled(ControlGroup)` @@ -45,19 +46,34 @@ const StyledControlGroup = styled(ControlGroup)` } &&& { input { - border: 1px solid #a1acb3; - border-radius: 4px; + border: 1px solid ${Colors.HIT_GRAY}; + border-radius: ${props => props.theme.radii[1]}px; box-shadow: none; - color: #2e3d49; - font-size: 14px; + color: ${Colors.OXFORD_BLUE}; + font-size: ${props => props.theme.fontSizes[3]}px; } } `; class DatePickerComponent extends React.Component { render() { + const now = moment(); + const year = now.get("year"); + const month = now.get("month"); + const date = now.get("date"); + const minDate = now.clone().set({ month, date: date - 1, year: year - 20 }); + const maxDate = now.clone().set({ month, date: date + 1, year: year + 20 }); + const selectedDate = new Date( + new Date(this.props.selectedDate || "").getTime() + + this.getOffset(new Date(this.props.selectedDate || "")), + ); return ( - + { + e.stopPropagation(); + }} + > {this.props.label && (