diff --git a/.github/workflows/TestReuseActions.yml b/.github/workflows/TestReuseActions.yml index 6a895c3213..91552700ea 100644 --- a/.github/workflows/TestReuseActions.yml +++ b/.github/workflows/TestReuseActions.yml @@ -459,7 +459,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/.github/workflows/integration-tests-command.yml b/.github/workflows/integration-tests-command.yml index ce614b761e..3bd45b936f 100644 --- a/.github/workflows/integration-tests-command.yml +++ b/.github/workflows/integration-tests-command.yml @@ -471,7 +471,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -806,7 +806,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -1218,12 +1218,11 @@ jobs: summary: "https://github.com/" + process.env.repository + "/actions/runs/" + process.env.run_id } }); + console.log({ result }); + return result; } catch(e) { console.error({ error: e.message }); } - - console.log({ result }); - return result; } - name: Dump the client payload context diff --git a/.github/workflows/test-build-docker-image-fat.yml b/.github/workflows/test-build-docker-image-fat.yml index e246704e26..737c8d5f60 100644 --- a/.github/workflows/test-build-docker-image-fat.yml +++ b/.github/workflows/test-build-docker-image-fat.yml @@ -446,7 +446,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -785,7 +785,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/.github/workflows/test-build-docker-image.yml b/.github/workflows/test-build-docker-image.yml index d63ac1321d..2df7952157 100644 --- a/.github/workflows/test-build-docker-image.yml +++ b/.github/workflows/test-build-docker-image.yml @@ -455,7 +455,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result @@ -829,7 +829,7 @@ jobs: # In case this is second attempt try restoring status of the prior attempt from cache - name: Restore the previous run result - uses: martijnhols/actions-cache@v3 + uses: martijnhols/actions-cache@v3.0.2 with: path: | ~/run_result diff --git a/Dockerfile b/Dockerfile index 8a7196860d..c06417ecba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,18 +16,15 @@ RUN apt-get update \ supervisor curl cron certbot nginx gnupg wget netcat openssh-client \ software-properties-common gettext openjdk-11-jre \ python3-pip python-setuptools git \ - && add-apt-repository ppa:redislabs/redis \ && pip install --no-cache-dir git+https://github.com/coderanger/supervisor-stdout@973ba19967cdaf46d9c1634d1675fc65b9574f6e \ - && apt-get remove -y git python3-pip \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && apt-get remove -y git python3-pip # Install MongoDB v4.0.5, Redis, NodeJS - Service Layer RUN wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add - RUN echo "deb [ arch=amd64,arm64 ]http://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list \ && apt-get remove wget -y RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - \ - && apt-get -y install --no-install-recommends -y mongodb-org=4.4.6 nodejs redis build-essential \ + && apt-get install --no-install-recommends -y mongodb-org=4.4.6 nodejs redis build-essential \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* @@ -88,6 +85,9 @@ RUN chmod 0644 /etc/cron.d/* RUN chmod +x entrypoint.sh renew-certificate.sh +# Disable setuid/setgid bits for the files inside container. +RUN find / \( -path /proc -prune \) -o \( \( -perm -2000 -o -perm -4000 \) -print -exec chmod -s '{}' + \) || true + # Update path to load appsmith utils tool as default ENV PATH /opt/appsmith/utils/node_modules/.bin:$PATH diff --git a/app/client/cypress/fixtures/githubSource.json b/app/client/cypress/fixtures/githubSource.json deleted file mode 100644 index 2b7975ebcc..0000000000 --- a/app/client/cypress/fixtures/githubSource.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "githubClientId": "", - "githubClientSecret": "" -} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts index 504250e2c0..1b89612643 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/AForceMigration_Spec.ts @@ -25,17 +25,27 @@ describe("AForce - Community Issues page validations", function () { let reconnect = true, selectedRow: number; it("1. Import application json and validate headers", () => { - - homePage.ImportApp("AForceMigrationExport.json", reconnect) - if (reconnect) - dataSources.ReconnectDataSourcePostgres("AForceDB") - //Validate table is not empty! - table.WaitUntilTableLoad() - //Validating order of header columns! - table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at") - //Validating hidden columns: - table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at']) - + cy.visit("/applications"); + homePage.ImportApp("AForceMigrationExport.json", reconnect); + cy.wait("@importNewApplication").then((interception) => { + cy.wait(100); + const { isPartialImport } = interception.response.body.data; + if (isPartialImport) { + // should reconnect modal + dataSources.ReconnectDataSourcePostgres("AForceDB") + } else { + cy.get(homePage.toastMessage).should( + "contain", + "Application imported successfully", + ); + } + //Validate table is not empty! + table.WaitUntilTableLoad() + //Validating order of header columns! + table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at") + //Validating hidden columns: + table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at']) + }); }); it("2. Validate table navigation with Server Side pagination enabled with Default selected row", () => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js index 64af819493..ebbd089f93 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Application/ReconnectDatasource_spec.js @@ -6,7 +6,7 @@ describe("Reconnect Datasource Modal validation while importing application", fu let appid; let newOrganizationName; let appName; - it("Import application from json with one postgres", function() { + it("Import application from json with one postgres and success modal", function() { cy.NavigateToHome(); // import application cy.generateUUID().then((uid) => { @@ -49,16 +49,32 @@ describe("Reconnect Datasource Modal validation while importing application", fu cy.get( "[data-cy='datasourceConfiguration.connection.ssl.authType']", ).should("contain", "Default"); - cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ - force: true, - }); + + cy.ReconnectDatasource("Untitled Datasource"); + cy.wait(1000); + cy.fillPostgresDatasourceForm(); + cy.testSaveDatasource(); cy.wait(2000); + + // cy.get(reconnectDatasourceModal.SkipToAppBtn).click({ + // force: true, + // }); + // cy.wait(2000); } else { cy.get(homePage.toastMessage).should( "contain", "Application imported successfully", ); } + // check datasource configured success modal + cy.get(".t--import-app-success-modal").should("be.visible"); + cy.get(".t--import-app-success-modal").should( + "contain", + "All your datasources are configuered and ready to use.", + ); + cy.get(".t--import-success-modal-got-it").click({ force: true }); + cy.get(".t--import-app-success-modal").should("not.exist"); + const uuid = () => Cypress._.random(0, 1e4); const name = uuid(); appName = `app${name}`; diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js index 0ad1e94dfc..e270e40b29 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/Bind_TableTextPagination_spec.js @@ -73,6 +73,11 @@ describe("Test Create Api and Bind to Table widget", function() { cy.wait(500); cy.wait("@postExecute"); cy.wait(500); + cy.get(".show-page-items").should("contain", "20 Records"); + cy.get(".page-item") + .next() + .should("contain", "of 2"); + cy.get(".t--table-widget-next-page").should("not.have.attr", "disabled"); cy.ValidateTableData("1"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js index 5f11de06e3..666448dfab 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldChange_spec.js @@ -19,7 +19,7 @@ describe("JSON Form Widget Field Change", () => { cy.get(`${fieldPrefix}-name`) .find("button") .should("have.length", 2); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -36,7 +36,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .invoke("attr", "type") .should("contain", "checkbox"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -53,7 +53,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .click({ force: true }); cy.get(".bp3-popover.bp3-dateinput-popover").should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -71,7 +71,7 @@ describe("JSON Form Widget Field Change", () => { .find(".bp3-control.bp3-switch") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -82,12 +82,12 @@ describe("JSON Form Widget Field Change", () => { cy.get(".bp3-select-popover.select-popover-wrapper").should("not.exist"); cy.openFieldConfiguration("name"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/); cy.get(`${fieldPrefix}-name label`).click({ force: true }); cy.get(".bp3-select-popover.select-popover-wrapper").should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -104,7 +104,7 @@ describe("JSON Form Widget Field Change", () => { .find(".rc-select-multiple") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -122,7 +122,7 @@ describe("JSON Form Widget Field Change", () => { .should("exist") .should("have.length", 2); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -139,7 +139,7 @@ describe("JSON Form Widget Field Change", () => { .find(".t--jsonformfield-array-add-btn") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -160,7 +160,7 @@ describe("JSON Form Widget Field Change", () => { .find("input") .should("exist"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); @@ -185,7 +185,7 @@ describe("JSON Form Widget Field Change", () => { .should("have.length", 2); }); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Text Input/); cy.closePropertyPane(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js index baf9a0e4b6..649ae5c3d0 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/JSONFormWidget/JSONForm_FieldProperties_spec.js @@ -166,7 +166,7 @@ describe("Select Field Property Control", () => { cy.openPropertyPane("jsonformwidget"); cy.testJsontext("sourcedata", JSON.stringify(schema)); cy.openFieldConfiguration("state"); - cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select$/); + cy.selectDropdownValue(commonlocators.jsonFormFieldType, /^Select/); }); it("has valid default value", () => { diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js index 027b0f6b73..a25dd4dba4 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GenerateCRUD/Mongo_Spec.js @@ -2,6 +2,7 @@ const pages = require("../../../../locators/Pages.json"); const generatePage = require("../../../../locators/GeneratePage.json"); import homePage from "../../../../locators/HomePage"; const datasource = require("../../../../locators/DatasourcesEditor.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); describe("Generate New CRUD Page Inside from Mongo as Data Source", function() { let datasourceName; @@ -91,6 +92,9 @@ describe("Generate New CRUD Page Inside from Mongo as Data Source", function() { "response.body.responseMeta.status", 200, ); + cy.get(commonlocators.toastAction) + .should("have.length", 1) + .should("have.text", "Successfully generated a page"); cy.get("span:contains('GOT IT')").click(); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js index fbc0a962e5..e9e5b580ad 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/GitImport/GitImport_spec.js @@ -21,7 +21,7 @@ describe("Git import flow", function() { }); }); it("Import an app from JSON with Postgres, MySQL, Mongo db", () => { - cy.get(homePage.homeIcon).click(); + cy.NavigateToHome(); cy.get(homePage.optionsIcon) .first() .click(); @@ -54,6 +54,10 @@ describe("Git import flow", function() { "contain", "Application imported successfully", ); */ + cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible"); + cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({ + force: true, + }); cy.wait(1000); cy.generateUUID().then((uid) => { repoName = uid; @@ -98,6 +102,11 @@ describe("Git import flow", function() { cy.get(datasourceEditor.sectionAuthentication).click(); cy.testSaveDatasource(); cy.wait(2000); + cy.get(reconnectDatasourceModal.ImportSuccessModal).should("be.visible"); + cy.get(reconnectDatasourceModal.ImportSuccessModalCloseBtn).click({ + force: true, + }); + cy.wait(1000); /* cy.get(homePage.toastMessage).should( "contain", "Application imported successfully", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js new file mode 100644 index 0000000000..fbe4890b31 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/PropertyPane/PropertyPaneJsEnabledVisible_spec.js @@ -0,0 +1,42 @@ +const dsl = require("../../../../fixtures/jsonFormDslWithSchema.json"); +const { ObjectsRegistry } = require("../../../../support/Objects/Registry"); +let ee = ObjectsRegistry.EntityExplorer; + +describe("Property pane js enabled field", function() { + before(() => { + cy.addDsl(dsl); + }); + + it("Ensure text is visible for js enabled field when a section is collapsed by default", function() { + cy.openPropertyPane("jsonformwidget"); + + cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click(); + cy.get(".t--property-control-buttonvariant") + .find(".t--js-toggle") + .first() + .click(); + + cy.get(".t--property-control-buttonvariant") + .find(".t--js-toggle") + .first() + .should("have.class", "is-active"); + + cy.get(".t--property-control-buttonvariant .CodeMirror-code").type( + "PRIMARY", + ); + cy.get(".t--property-control-buttonvariant") + .find(".CodeMirror-code") + .invoke("text") + .should("equal", "PRIMARY"); + + cy.closePropertyPane(); + cy.wait(1000); + + cy.openPropertyPane("jsonformwidget"); + cy.get(".t--property-pane-section-collapse-submitbuttonstyles").click(); + cy.get(".t--property-control-buttonvariant") + .find(".CodeMirror-code") + .invoke("text") + .should("equal", "PRIMARY"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js index ac4a1d5d1c..203a3e2781 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js @@ -131,15 +131,15 @@ describe("Undo/Redo functionality", function() { .first() .focus() .type("{downarrow}{downarrow}{downarrow} ") - .type("test:()=>{},"); + .type("testJSFunction:()=>{},"); cy.get("body").type(`{${modifierKey}}z{${modifierKey}}z{${modifierKey}}z`); - // verifying test function is not visible in response tab after undo - cy.get(".function-name").should("not.contain.text", "test"); + // verifying testJSFunction is not visible on page after undo + cy.contains("testJSFunction").should("not.exist"); cy.get("body").type( `{${modifierKey}}{shift}z{${modifierKey}}{shift}z{${modifierKey}}{shift}z`, ); - // verifying test function is visible in response tab after redo - cy.get(".function-name").should("contain.text", "test"); + // verifying testJSFunction is visible on page after redo + cy.contains("testJSFunction").should("exist"); // performing undo from app menu cy.get(".t--application-name").click({ force: true }); cy.get("li:contains(Edit)") diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts index 8266023142..49f0f70efd 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts +++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/LayoutOnLoadActions/JSOnLoadActions_Spec.ts @@ -1,6 +1,6 @@ import { ObjectsRegistry } from "../../../../support/Objects/Registry"; -let guid: any, jsName : any; +let guid: any, jsName: any; const agHelper = ObjectsRegistry.AggregateHelper, ee = ObjectsRegistry.EntityExplorer, dataSources = ObjectsRegistry.DataSources, @@ -24,7 +24,7 @@ describe("JSObjects OnLoad Actions tests", function() { true, false, ); - jsEditor.EnableOnPageLoad("getId", false, true); + jsEditor.AddJSFunctionSettings("getId", false, true); agHelper.GenerateUUID(); cy.get("@guid").then((uid) => { dataSources.NavigateToDSCreateNew(); @@ -39,17 +39,19 @@ describe("JSObjects OnLoad Actions tests", function() { cy.get("@jsObjName").then((jsObjName) => { jsName = jsObjName; agHelper.EnterValue( - "SELECT * FROM public.users where id = {{" + jsObjName + ".getId.data}}", - );; - }) + "SELECT * FROM public.users where id = {{" + + jsObjName + + ".getId.data}}", + ); + }); }); - ee.SelectEntityByName("Table1", 'WIDGETS'); + ee.SelectEntityByName("Table1", "WIDGETS"); jsEditor.EnterJSContext("Table Data", "{{GetUser.data}}"); agHelper.DeployApp(); agHelper.AssertElementPresence(jsEditor._dialog("Confirmation Dialog")); agHelper.ClickButton("Yes"); - agHelper.Sleep(1000) + agHelper.Sleep(1000); agHelper.ValidateNetworkExecutionSuccess("@postExecute"); table.ReadTableRowColumnData(0, 0).then((cellData) => { diff --git a/app/client/cypress/locators/JSEditor.json b/app/client/cypress/locators/JSEditor.json index c89e9aa26a..2b7d95cf69 100644 --- a/app/client/cypress/locators/JSEditor.json +++ b/app/client/cypress/locators/JSEditor.json @@ -1,5 +1,5 @@ { - "runButton": ".run-button", + "runButton": ".run-js-action", "editNameField": ".bp3-editable-text-input", "outputConsole": ".CodeEditorTarget", "jsObjectName": ".t--action-name-edit-field", diff --git a/app/client/cypress/locators/ReconnectLocators.js b/app/client/cypress/locators/ReconnectLocators.js index 71e9d06619..0f36420058 100644 --- a/app/client/cypress/locators/ReconnectLocators.js +++ b/app/client/cypress/locators/ReconnectLocators.js @@ -2,4 +2,6 @@ export default { Modal: ".reconnect-datasource-modal", ClostBtn: ".t--reconnect-close-btn", SkipToAppBtn: ".t--skip-to-application-btn", + ImportSuccessModal: ".t--import-app-success-modal", + ImportSuccessModalCloseBtn: ".t--import-success-modal-got-it", }; diff --git a/app/client/cypress/support/AdminSettingsCommands.js b/app/client/cypress/support/AdminSettingsCommands.js index 908dd5dff9..a32f6d094d 100644 --- a/app/client/cypress/support/AdminSettingsCommands.js +++ b/app/client/cypress/support/AdminSettingsCommands.js @@ -7,7 +7,6 @@ require("cypress-file-upload"); const googleForm = require("../locators/GoogleForm.json"); const googleData = require("../fixtures/googleSource.json"); const githubForm = require("../locators/GithubForm.json"); -const githubData = require("../fixtures/githubSource.json"); Cypress.Commands.add("fillGoogleFormPartly", () => { cy.get(googleForm.googleClientId).type( diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 978456f258..84e31df9a7 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -20,6 +20,8 @@ export class DataSources { private _datasourceCard = ".t--datasource" _templateMenu = ".t--template-menu" private _createQuery = ".t--create-query" + private _importSuccessModal = ".t--import-app-success-modal" + private _importSuccessModalClose = ".t--import-success-modal-got-it" _visibleTextSpan = (spanText: string) => "//span[contains(text(),'" + spanText + "')]" _dropdownTitle = (ddTitle: string) => "//p[contains(text(),'" + ddTitle + "')]/parent::label/following-sibling::div/div/div" _reconnectModal = "div.reconnect-datasource-modal" @@ -112,6 +114,8 @@ export class DataSources { this.ValidateNSelectDropdown("Connection Mode", "", "Read / Write") this.FillPostgresDSForm() cy.get(this._saveDs).click(); + cy.get(this._importSuccessModal).should("be.visible"); + cy.get(this._importSuccessModalClose).click({ force: true }); } } \ No newline at end of file diff --git a/app/client/cypress/support/Pages/JSEditor.ts b/app/client/cypress/support/Pages/JSEditor.ts index 2750f6f228..750bccbaad 100644 --- a/app/client/cypress/support/Pages/JSEditor.ts +++ b/app/client/cypress/support/Pages/JSEditor.ts @@ -5,18 +5,39 @@ export class JSEditor { public locator = ObjectsRegistry.CommonLocators; public ee = ObjectsRegistry.EntityExplorer; - private _runButton = "//li//*[local-name() = 'svg' and @class='run-button']"; + private _runButton = "button.run-js-action"; + private _settingsTab = ".tab-title:contains('Settings')"; + private _codeTab = ".tab-title:contains('Code')"; + private _onPageLoadRadioButton = (functionName: string, onLoad: boolean) => + `.${functionName}-on-page-load-setting label:contains(${ + onLoad ? "Yes" : "No" + }) span.checkbox`; + private _confirmBeforeExecuteRadioButton = ( + functionName: string, + shouldConfirm: boolean, + ) => + `.${functionName}-confirm-before-execute label:contains(${ + shouldConfirm ? "Yes" : "No" + }) span.checkbox`; private _outputConsole = ".CodeEditorTarget"; private _jsObjName = ".t--js-action-name-edit-field span"; private _jsObjTxt = ".t--js-action-name-edit-field input"; - private _newJSobj = "span:contains('New JS Object')" - private _bindingsClose = ".t--entity-property-close" - private _propertyList = ".t--entity-property" - private _responseTabAction = (funName: string) => "//div[@class='function-name'][text()='" + funName + "']/following-sibling::div//*[local-name()='svg']" - private _functionSetting = (settingTxt: string) => "//span[text()='" + settingTxt + "']/parent::div/following-sibling::input[@type='checkbox']" - _dialog = (dialogHeader: string) => "//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" + dialogHeader + "')]" - private _closeSettings = "span[icon='small-cross']" - + private _newJSobj = "span:contains('New JS Object')"; + private _bindingsClose = ".t--entity-property-close"; + private _propertyList = ".t--entity-property"; + private _responseTabAction = (funName: string) => + "//div[@class='function-name'][text()='" + + funName + + "']/following-sibling::div//*[local-name()='svg']"; + private _functionSetting = (settingTxt: string) => + "//span[text()='" + + settingTxt + + "']/parent::div/following-sibling::input[@type='checkbox']"; + _dialog = (dialogHeader: string) => + "//div[contains(@class, 'bp3-dialog')]//h4[contains(text(), '" + + dialogHeader + + "')]"; + private _closeSettings = "span[icon='small-cross']"; public NavigateToJSEditor() { cy.get(this.locator._createNew) @@ -87,7 +108,7 @@ export class JSEditor { if (toRun) { //clicking 1 times & waits for 3 second for result to be populated! Cypress._.times(1, () => { - cy.xpath(this._runButton) + cy.get(this._runButton) .first() .click() .wait(3000); @@ -109,13 +130,21 @@ export class JSEditor { this.agHelper.AssertAutoSave(); //Ample wait due to open bug # 10284 } - public EnterJSContext(endp: string, value: string, paste = true, toToggleOnJS = false, notField = false) { + public EnterJSContext( + endp: string, + value: string, + paste = true, + toToggleOnJS = false, + notField = false, + ) { if (toToggleOnJS) { cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase())) .invoke("attr", "class") .then((classes: any) => { if (!classes.includes("is-active")) { - cy.get(this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase())) + cy.get( + this.locator._jsToggle(endp.replace(/ +/g, "").toLowerCase()), + ) .first() .click({ force: true }); } @@ -131,20 +160,23 @@ export class JSEditor { // .type("{del}", { force: true }); if (paste) { - this.agHelper.EnterValue(value, endp, notField) - } - else { - cy.get(this.locator._propertyControl + endp.replace(/ +/g, "").toLowerCase() + " " + this.locator._codeMirrorTextArea) + this.agHelper.EnterValue(value, endp, notField); + } else { + cy.get( + this.locator._propertyControl + + endp.replace(/ +/g, "").toLowerCase() + + " " + + this.locator._codeMirrorTextArea, + ) .first() .then((el: any) => { const input = cy.get(el); input.type(value, { parseSpecialCharSequences: false, }); - }) + }); } - // cy.focused().then(($cm: any) => { // if ($cm.contents != "") { // cy.log("The field is not empty"); @@ -175,18 +207,22 @@ export class JSEditor { // }); // }); - this.agHelper.AssertAutoSave()//Allowing time for Evaluate value to capture value - + this.agHelper.AssertAutoSave(); //Allowing time for Evaluate value to capture value } public RemoveText(endp: string) { - cy.get(this.locator._propertyControl + endp + " " + this.locator._codeMirrorTextArea) + cy.get( + this.locator._propertyControl + + endp + + " " + + this.locator._codeMirrorTextArea, + ) .first() .focus() .type("{uparrow}", { force: true }) .type("{ctrl}{shift}{downarrow}", { force: true }) .type("{del}", { force: true }); - this.agHelper.AssertAutoSave() + this.agHelper.AssertAutoSave(); } public RenameJSObjFromForm(renameVal: string) { @@ -216,7 +252,7 @@ export class JSEditor { public validateDefaultJSObjProperties(jsObjName: string) { this.ee.ActionContextMenuByEntityName(jsObjName, "Show Bindings"); - cy.get(this._propertyList).then(function ($lis) { + cy.get(this._propertyList).then(function($lis) { const bindingsLength = $lis.length; expect(bindingsLength).to.be.at.least(4); expect($lis.eq(0).text()).to.be.oneOf([ @@ -239,17 +275,22 @@ export class JSEditor { cy.get(this._bindingsClose).click({ force: true }); } - - public EnableOnPageLoad(funName: string, onLoad = true, bfrCalling = true) { - - this.agHelper.GetNClick(this._responseTabAction(funName)) - this.agHelper.AssertElementPresence(this._dialog('Function settings')) - if (onLoad) - this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_ONPAGELOAD()), true) - if (bfrCalling) - this.agHelper.CheckUncheck(this._functionSetting(Cypress.env("MESSAGES").JS_SETTINGS_CONFIRM_EXECUTION()), true) - - this.agHelper.GetNClick(this._closeSettings) + public AddJSFunctionSettings( + funName: string, + onLoad = true, + bfrCalling = true, + ) { + // Navigate to Settings tab + this.agHelper.GetNClick(this._settingsTab); + // Set onPageLoad + cy.get(this._onPageLoadRadioButton(funName, onLoad)) + .first() + .click(); + // Set confirmBeforeExecute + cy.get(this._confirmBeforeExecuteRadioButton(funName, bfrCalling)) + .first() + .click(); + // Return to code tab + this.agHelper.GetNClick(this._codeTab); } - } diff --git a/app/client/src/actions/jsPaneActions.ts b/app/client/src/actions/jsPaneActions.ts index 9eee273646..0eb39c97c4 100644 --- a/app/client/src/actions/jsPaneActions.ts +++ b/app/client/src/actions/jsPaneActions.ts @@ -4,6 +4,7 @@ import { } from "@appsmith/constants/ReduxActionConstants"; import { JSCollection, JSAction } from "entities/JSCollection"; import { RefactorAction, SetFunctionPropertyPayload } from "api/JSActionAPI"; + export const createNewJSCollection = ( pageId: string, ): ReduxAction<{ pageId: string }> => ({ @@ -89,3 +90,13 @@ export const updateJSFunction = (payload: SetFunctionPropertyPayload) => { payload, }; }; + +export const setActiveJSAction = (payload: { + jsCollectionId: string; + jsActionId: string; +}) => { + return { + type: ReduxActionTypes.SET_ACTIVE_JS_ACTION, + payload, + }; +}; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index e5dd26ab38..0254677d1a 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -699,6 +699,7 @@ export const ReduxActionTypes = { GET_TEMPLATE_SUCCESS: "GET_TEMPLATES_SUCCESS", START_EXECUTE_JS_FUNCTION: "START_EXECUTE_JS_FUNCTION", RESET_PAGE_LIST: "RESET_PAGE_LIST", + SET_ACTIVE_JS_ACTION: "SET_ACTIVE_JS_ACTION", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 3c22ada680..54e4f85be7 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -392,6 +392,8 @@ export const ACTION_CONFIGURATION_UPDATED = () => "Configuration updated"; export const WIDGET_PROPERTIES_UPDATED = () => "Widget properties were updated"; export const EMPTY_RESPONSE_FIRST_HALF = () => "🙌 Click on"; export const EMPTY_RESPONSE_LAST_HALF = () => "to get a response"; +export const EMPTY_JS_RESPONSE_LAST_HALF = () => + "to view response of selected function"; export const INVALID_EMAIL = () => "Please enter a valid email"; export const DEBUGGER_INTERCOM_TEXT = (text: string) => `Hi, \nI'm facing the following error on Appsmith, can you please help? \n\n${text}`; @@ -451,6 +453,8 @@ export const JS_EXECUTION_FAILURE = () => "JS Function execution failed"; export const JS_EXECUTION_FAILURE_TOASTER = () => "There was an error while executing function"; export const JS_SETTINGS_ONPAGELOAD = () => "Run function on page load (Beta)"; +export const JS_EXECUTION_SUCCESS_TOASTER = (actionName: string) => + `${actionName} ran successfully`; export const JS_SETTINGS_ONPAGELOAD_SUBTEXT = () => "Will refresh data every time page is reloaded"; export const JS_SETTINGS_CONFIRM_EXECUTION = () => @@ -459,6 +463,13 @@ export const JS_SETTINGS_CONFIRM_EXECUTION_SUBTEXT = () => "Ask confirmation from the user every time before refreshing data"; export const JS_SETTINGS_EXECUTE_TIMEOUT = () => "Function Timeout (in milliseconds)"; +export const ASYNC_FUNCTION_SETTINGS_HEADING = () => "Async Function Settings"; +export const NO_ASYNC_FUNCTIONS = () => + "There is no asynchronous function in this JSObject"; +export const NO_JS_FUNCTION_TO_RUN = (JSObjectName: string) => + `${JSObjectName} has no function`; +export const NO_JS_FUNCTION_RETURN_VALUE = (JSFunctionName: string) => + `${JSFunctionName} did not return any data. Did you add a return statement?`; // Import/Export Application features export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import application"; @@ -688,6 +699,10 @@ export const CONTACT_SALES_MESSAGE_ON_INTERCOM = (orgName: string) => export const REPOSITORY_LIMIT_REACHED = () => "Repository Limit Reached"; export const REPOSITORY_LIMIT_REACHED_INFO = () => "Adding and using upto 3 repositories is free. To add more repositories kindly upgrade."; +export const APPLICATION_IMPORT_SUCCESS = (username: string) => + `${username}! Your application is ready to use.`; +export const APPLICATION_IMPORT_SUCCESS_DESCRIPTION = () => + "All your datasources are configuered and ready to use."; export const NONE_REVERSIBLE_MESSAGE = () => "This action is non reversible. Proceed with caution."; export const CONTACT_SUPPORT_TO_UPGRADE = () => @@ -999,6 +1014,9 @@ export const TEST_EMAIL_SUCCESS = (email: string) => () => `Test email sent, please check the inbox of ${email}`; export const TEST_EMAIL_SUCCESS_TROUBLESHOOT = () => "Troubleshoot"; export const TEST_EMAIL_FAILURE = () => "Sending Test Email Failed"; +export const DISCONNECT_AUTH_ERROR = () => + "Cannot disconnect the only connected authentication method."; +export const MANDATORY_FIELDS_ERROR = () => "Mandatory fields cannot be empty"; //Reflow Beta Screen export const REFLOW_BETA_CHECKBOX_LABEL = () => "Turn on new drag & drop experience"; diff --git a/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx b/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx index 13667db915..1f37dd8916 100644 --- a/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx +++ b/app/client/src/ce/pages/AdminSettings/config/authentication/AuthPage.tsx @@ -23,6 +23,7 @@ import Icon from "components/ads/Icon"; import TooltipComponent from "components/ads/Tooltip"; import { Position } from "@blueprintjs/core"; import { adminSettingsCategoryUrl } from "RouteBuilder"; +import AnalyticsUtil from "utils/AnalyticsUtil"; const { intercomAppID } = getAppsmithConfigs(); @@ -149,6 +150,30 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) { } }; + const onClickHandler = (method: AuthMethodType) => { + if (!method.needsUpgrade || method.isConnected) { + AnalyticsUtil.logEvent( + method.isConnected + ? "ADMIN_SETTINGS_EDIT_AUTH_METHOD" + : "ADMIN_SETTINGS_ENABLE_AUTH_METHOD", + { + method: method.label, + }, + ); + history.push( + adminSettingsCategoryUrl({ + category: SettingCategories.AUTHENTICATION, + subCategory: method.category, + }), + ); + } else { + AnalyticsUtil.logEvent("ADMIN_SETTINGS_UPGRADE_AUTH_METHOD", { + method: method.label, + }); + triggerIntercom(method.label); + } + }; + return ( @@ -211,16 +236,7 @@ export function AuthPage({ authMethods }: { authMethods: AuthMethodType[] }) { : method.category }`} data-cy="btn-auth-account" - onClick={() => - !method.needsUpgrade || method.isConnected - ? history.push( - adminSettingsCategoryUrl({ - category: SettingCategories.AUTHENTICATION, - subCategory: method.category, - }), - ) - : triggerIntercom(method.label) - } + onClick={() => onClickHandler(method)} text={createMessage( method.isConnected ? EDIT diff --git a/app/client/src/ce/pages/AdminSettings/config/types.ts b/app/client/src/ce/pages/AdminSettings/config/types.ts index b3464eae48..dc5c2ded7b 100644 --- a/app/client/src/ce/pages/AdminSettings/config/types.ts +++ b/app/client/src/ce/pages/AdminSettings/config/types.ts @@ -13,6 +13,7 @@ export enum SettingTypes { UNEDITABLEFIELD = "UNEDITABLEFIELD", ACCORDION = "ACCORDION", TAGINPUT = "TAGINPUT", + DROPDOWN = "DROPDOWN", } export enum SettingSubtype { @@ -52,6 +53,7 @@ export interface Setting { isRequired?: boolean; formName?: string; fieldName?: string; + dropdownOptions?: Array<{ id: string; value: string; label?: string }>; } export interface Category { diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index 60c248b5ab..642831e9e3 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -424,7 +424,7 @@ const ButtonStyles = css` } `; -const StyledButton = styled("button")` +export const StyledButton = styled("button")` ${ButtonStyles} `; diff --git a/app/client/src/components/ads/CopyToClipBoard.tsx b/app/client/src/components/ads/CopyToClipBoard.tsx index 55d4812a67..447d0862e3 100644 --- a/app/client/src/components/ads/CopyToClipBoard.tsx +++ b/app/client/src/components/ads/CopyToClipBoard.tsx @@ -19,7 +19,11 @@ const Wrapper = styled.div<{ offset?: string }>` } `; -function CopyToClipboard(props: { copyText: string; btnWidth?: string }) { +function CopyToClipboard(props: { + className?: string; + copyText: string; + btnWidth?: string; +}) { const { copyText } = props; const copyURLInput = createRef(); const [isCopied, setIsCopied] = useState(false); @@ -38,7 +42,7 @@ function CopyToClipboard(props: { copyText: string; btnWidth?: string }) { } }; return ( - + { diff --git a/app/client/src/components/ads/DialogComponent.tsx b/app/client/src/components/ads/DialogComponent.tsx index b095ade2f7..2f31e183e4 100644 --- a/app/client/src/components/ads/DialogComponent.tsx +++ b/app/client/src/components/ads/DialogComponent.tsx @@ -26,8 +26,9 @@ const StyledDialog = styled(Dialog)<{ padding: 0; background: ${(props) => props.theme.colors.modal.bg}; box-shadow: none; - .${Classes.ICON} { - color: ${(props) => props.theme.colors.modal.iconColor}; + min-height: unset; + svg { + color: ${Colors.GREY_800}; } .${Classes.BUTTON}.${Classes.MINIMAL}:hover { @@ -39,19 +40,23 @@ const StyledDialog = styled(Dialog)<{ color: ${(props) => props.theme.colors.modal.headerText}; font-weight: ${(props) => props.theme.typography.h1.fontWeight}; font-size: ${(props) => props.theme.typography.h1.fontSize}px; - line-height: ${(props) => props.theme.typography.h1.lineHeight}px; + line-height: unset; letter-spacing: ${(props) => props.theme.typography.h1.letterSpacing}; } .${Classes.DIALOG_CLOSE_BUTTON} { - color: ${Colors.CHARCOAL}; + color: ${Colors.SCORPION}; min-width: 0; padding: 0; svg { - fill: ${Colors.CHARCOAL}; + fill: ${Colors.SCORPION}; width: 24px; height: 24px; + + &:hover { + fill: ${Colors.COD_GRAY}; + } } } @@ -78,7 +83,7 @@ const StyledDialog = styled(Dialog)<{ & .${Classes.DIALOG_BODY} { margin: 0; - margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "24px")}; + margin-top: ${(props) => (props.noModalBodyMarginTop ? "0px" : "16px")}; overflow: auto; } diff --git a/app/client/src/components/ads/DraggableListCard.tsx b/app/client/src/components/ads/DraggableListCard.tsx index 0645c8c95d..01fc9fe4da 100644 --- a/app/client/src/components/ads/DraggableListCard.tsx +++ b/app/client/src/components/ads/DraggableListCard.tsx @@ -61,6 +61,10 @@ export function DraggableListCard(props: RenderComponentProps) { const ref = useRef(null); const debouncedUpdate = _.debounce(updateOption, 1000); + useEffect(() => { + setVisibility(item.isVisible); + }, [item.isVisible]); + useEffect(() => { if (!isEditing && item && item.label) setValue(item.label); }, [item?.label, isEditing]); diff --git a/app/client/src/components/ads/Dropdown.test.tsx b/app/client/src/components/ads/Dropdown.test.tsx index 5c80ca8e4c..b51abdc081 100644 --- a/app/client/src/components/ads/Dropdown.test.tsx +++ b/app/client/src/components/ads/Dropdown.test.tsx @@ -11,7 +11,7 @@ import Dropdown from "./Dropdown"; import { lightTheme } from "selectors/themeSelectors"; import userEvent from "@testing-library/user-event"; -const props = { +const optionsProps: any = { options: [ { label: "Primary", value: "PRIMARY" }, { label: "Secondary", value: "SECONDARY" }, @@ -24,8 +24,13 @@ const props = { showLabelOnly: true, }; +const noOptionsProps = { + options: [], +}; + const getTestComponent = ( handleOnSelect: any = undefined, + props = optionsProps, allowDeselection?: boolean, ) => ( @@ -135,15 +140,15 @@ describe(" - Keyboard Navigation", () => { userEvent.keyboard("{ArrowDown}"); userEvent.keyboard("{Enter}"); expect(handleOnSelect).toHaveBeenLastCalledWith( - props.options[1].value, - props.options[1], + optionsProps.options[1].value, + optionsProps.options[1], ); userEvent.keyboard("{Enter}"); userEvent.keyboard("{ArrowDown}"); userEvent.keyboard(" "); expect(handleOnSelect).toHaveBeenLastCalledWith( - props.options[2].value, - props.options[2], + optionsProps.options[2].value, + optionsProps.options[2], ); }); @@ -179,26 +184,26 @@ describe(" - allowDeselection behaviour", () => { // click on Second Item fireEvent.click(screen.queryAllByRole("option")[1]); expect(screen.getByRole("option", { selected: true })).toHaveTextContent( - props.options[1].label, + optionsProps.options[1].label, ); expect(screen.getByRole("listbox")).toHaveTextContent( - props.options[1].label, + optionsProps.options[1].label, ); // click on Second Item Again fireEvent.click(screen.queryAllByRole("option")[1]); expect(screen.getByRole("option", { selected: true })).toHaveTextContent( - props.options[1].label, + optionsProps.options[1].label, ); expect(screen.getByRole("listbox")).toHaveTextContent( - props.options[1].label, + optionsProps.options[1].label, ); expect(screen.getByRole("option", { selected: true })).not.toBeNull(); }); it("Test allowDeselection = true behaviour", async () => { const handleOnSelect = jest.fn(); - render(getTestComponent(handleOnSelect, true)); + render(getTestComponent(handleOnSelect, optionsProps, true)); expect(screen.getByRole("listbox")).toHaveTextContent("Primary"); const dropdown = screen @@ -213,10 +218,10 @@ describe(" - allowDeselection behaviour", () => { // click on Third Item fireEvent.click(screen.queryAllByRole("option")[2]); expect(screen.getByRole("option", { selected: true })).toHaveTextContent( - props.options[2].label, + optionsProps.options[2].label, ); expect(screen.getByRole("listbox")).toHaveTextContent( - props.options[2].label, + optionsProps.options[2].label, ); // click on Third Item Again, that should unselect everything @@ -224,3 +229,19 @@ describe(" - allowDeselection behaviour", () => { expect(screen.queryByRole("option", { selected: true })).toBeNull(); }); }); + +describe(" - when the options is an empty array", () => { + it("Hide options renderer when option list is empty", () => { + const handleOnSelect = jest.fn(); + render(getTestComponent(handleOnSelect, noOptionsProps)); + + const dropdown = screen + .getByRole("listbox") + .querySelector(".bp3-popover-target"); + expect(dropdown).not.toBeNull(); + + // open dropdown + fireEvent.click(dropdown as Element); + expect(screen.queryByTestId("dropdown-options-wrapper")).toBeNull(); + }); +}); diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx index 8dce917833..411417f5ab 100644 --- a/app/client/src/components/ads/Dropdown.tsx +++ b/app/client/src/components/ads/Dropdown.tsx @@ -38,6 +38,7 @@ export type DropdownOption = { onSelect?: DropdownOnSelect; data?: any; isSectionHeader?: boolean; + hasCustomBadge?: boolean; }; export interface DropdownSearchProps { enableSearch?: boolean; @@ -101,6 +102,8 @@ export type DropdownProps = CommonComponentProps & defaultIcon?: IconName; allowDeselection?: boolean; //prevents de-selection of the selected option truncateOption?: boolean; // enabled wrapping and adding tooltip on option item of dropdown menu + customBadge?: JSX.Element; + selectedHighlightBg?: string; }; export interface DefaultDropDownValueNodeProps { selected: DropdownOption | DropdownOption[]; @@ -259,6 +262,10 @@ export const DropdownContainer = styled.div<{ width: string; height?: string }>` span.bp3-popover-target { display: inline-block; width: 100%; + height: 100%; + } + span.bp3-popover-target div { + height: 100%; } span.bp3-popover-wrapper { @@ -333,6 +340,7 @@ const DropdownOptionsWrapper = styled.div<{ const OptionWrapper = styled.div<{ selected: boolean; + selectedHighlightBg?: string; }>` padding: ${(props) => props.theme.spaces[2] + 1}px ${(props) => props.theme.spaces[5]}px; @@ -340,7 +348,9 @@ const OptionWrapper = styled.div<{ display: flex; align-items: center; min-height: 36px; - background-color: ${(props) => (props.selected ? Colors.GREEN_3 : null)}; + background-color: ${(props) => + props.selected ? props.selectedHighlightBg || Colors.GREEN_3 : null}; + &&& svg { rect { fill: ${(props) => props.theme.colors.dropdownIconBg}; @@ -371,7 +381,7 @@ const OptionWrapper = styled.div<{ } &:hover { - background-color: ${Colors.GREEN_3}; + background-color: ${(props) => props.selectedHighlightBg || Colors.GREEN_3}; &&& svg { rect { @@ -668,9 +678,12 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { }; const theme = useTheme() as Theme; + if (!options.length) return null; + return ( @@ -724,6 +737,7 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { } role="option" selected={isSelected} + selectedHighlightBg={props.selectedHighlightBg} > {option.leftElement && ( {option.leftElement} @@ -747,12 +761,18 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) { ) : null} {props.showLabelOnly ? ( props.truncateOption ? ( - + <> + + {option.hasCustomBadge && props.customBadge} + ) : ( - {option.label} + <> + {option.label} + {option.hasCustomBadge && props.customBadge} + ) ) : option.label && option.value ? ( diff --git a/app/client/src/components/ads/Radio.tsx b/app/client/src/components/ads/Radio.tsx index f07fcd97dc..816c6b46f2 100644 --- a/app/client/src/components/ads/Radio.tsx +++ b/app/client/src/components/ads/Radio.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect } from "react"; import styled from "styled-components"; import * as log from "loglevel"; -type OptionProps = { +export type OptionProps = { label: string; value: string; disabled?: boolean; @@ -17,6 +17,9 @@ export type RadioProps = CommonComponentProps & { onSelect?: (value: string) => void; options: OptionProps[]; backgroundColor?: string; + // To prevent interference when there are multiple radio groups, + // options corresponding to the same radio should have same name, which is different from others. + name?: string; }; const RadioGroup = styled.div<{ @@ -149,7 +152,7 @@ export default function RadioComponent(props: RadioProps) { option.onSelect && option.onSelect(e.target.value)} type="radio" value={option.value} diff --git a/app/client/src/components/ads/Tabs.tsx b/app/client/src/components/ads/Tabs.tsx index baf1d67c7a..f6ab87175c 100644 --- a/app/client/src/components/ads/Tabs.tsx +++ b/app/client/src/components/ads/Tabs.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { RefObject, useCallback, useState } from "react"; import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; import "react-tabs/style/react-tabs.css"; import styled from "styled-components"; @@ -6,6 +6,10 @@ import Icon, { IconName, IconSize } from "./Icon"; import { Classes, CommonComponentProps } from "./common"; import { useEffect } from "react"; import { Indices } from "constants/Layers"; +import { theme } from "constants/DefaultTheme"; +import useResizeObserver from "utils/hooks/useResizeObserver"; + +export const TAB_MIN_HEIGHT = `36px`; export type TabProp = { key: string; @@ -28,7 +32,7 @@ const TabsWrapper = styled.div<{ height: 100%; } .react-tabs__tab-panel { - height: calc(100% - 36px); + height: ${() => `calc(100% - ${TAB_MIN_HEIGHT})`}; overflow: auto; } .react-tabs__tab-list { @@ -251,6 +255,13 @@ const TabTitleWrapper = styled.div<{ : ""} `; +const CollapseIconWrapper = styled.div` + position: absolute; + right: 14px; + top: ${() => theme.spaces[3] - 1}px; + cursor: pointer; +`; + export type TabItemProps = { tab: TabProp; selected: boolean; @@ -288,18 +299,88 @@ export type TabbedViewComponentType = CommonComponentProps & { vertical?: boolean; tabItemComponent?: (props: TabItemProps) => JSX.Element; responseViewer?: boolean; + canCollapse?: boolean; + // Reference to container for collapsing or expanding content + containerRef?: RefObject; + // height of container when expanded + expandedHeight?: string; }; -export function TabComponent(props: TabbedViewComponentType) { +// Props required to support a collapsible (foldable) tab component +export type CollapsibleTabProps = { + // Reference to container for collapsing or expanding content + containerRef: RefObject; + // height of container when expanded( usually the default height of the tab component) + expandedHeight: string; +}; + +export type CollapsibleTabbedViewComponentType = TabbedViewComponentType & + CollapsibleTabProps; + +export const collapsibleTabRequiredPropKeys: Array = [ + "containerRef", + "expandedHeight", +]; + +// Tab is considered collapsible only when all required collapsible props are present +export const isCollapsibleTabComponent = ( + props: TabbedViewComponentType | CollapsibleTabbedViewComponentType, +): props is CollapsibleTabbedViewComponentType => + collapsibleTabRequiredPropKeys.every((key) => key in props); + +export function TabComponent( + props: TabbedViewComponentType | CollapsibleTabbedViewComponentType, +) { const TabItem = props.tabItemComponent || DefaultTabItem; // for setting selected state of an uncontrolled component const [selectedIndex, setSelectedIndex] = useState(props.selectedIndex || 0); + const [isExpanded, setIsExpanded] = useState(true); useEffect(() => { if (typeof props.selectedIndex === "number") setSelectedIndex(props.selectedIndex); }, [props.selectedIndex]); + const handleContainerResize = () => { + if (!isCollapsibleTabComponent(props)) return; + const { containerRef, expandedHeight } = props; + if (containerRef?.current && expandedHeight) { + containerRef.current.style.height = isExpanded + ? TAB_MIN_HEIGHT + : expandedHeight; + } + setIsExpanded((prev) => !prev); + }; + + const resizeCallback = useCallback( + (entries: ResizeObserverEntry[]) => { + if (entries && entries.length) { + const { + contentRect: { height }, + } = entries[0]; + if (height > Number(TAB_MIN_HEIGHT.replace("px", "")) + 6) { + !isExpanded && setIsExpanded(true); + } else { + isExpanded && setIsExpanded(false); + } + } + }, + [isExpanded], + ); + + useResizeObserver( + isCollapsibleTabComponent(props) ? props.containerRef?.current : null, + resizeCallback, + ); + + useEffect(() => { + if (!isCollapsibleTabComponent(props)) return; + const { containerRef } = props; + if (!isExpanded && containerRef.current) { + containerRef.current.style.height = TAB_MIN_HEIGHT; + } + }, [isExpanded]); + return ( + {isCollapsibleTabComponent(props) && ( + + + + )} + { props.onSelect && props.onSelect(index); diff --git a/app/client/src/components/ads/formFields/SelectField.tsx b/app/client/src/components/ads/formFields/SelectField.tsx new file mode 100644 index 0000000000..4c6694cb5b --- /dev/null +++ b/app/client/src/components/ads/formFields/SelectField.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useState } from "react"; +import { + Field, + WrappedFieldMetaProps, + WrappedFieldInputProps, +} from "redux-form"; +import Dropdown from "components/ads/Dropdown"; + +type DropdownWrapperProps = { + placeholder: string; + input?: { + value?: string; + onChange?: (value?: string) => void; + }; + options: Array<{ id: string; value: string; label?: string }>; + fillOptions?: boolean; +}; + +function DropdownWrapper(props: DropdownWrapperProps) { + const [selectedOption, setSelectedOption] = useState({ + value: props.placeholder, + }); + const onSelectHandler = (value?: string) => { + props.input && props.input.onChange && props.input.onChange(value); + }; + + useEffect(() => { + if (props.input && props.input.value) { + setSelectedOption({ value: props.input.value }); + } else if (props.placeholder) { + setSelectedOption({ value: props.placeholder }); + } + }, [props.input, props.placeholder]); + + return ( + + ); +} + +const renderComponent = ( + componentProps: SelectFieldProps & { + meta: Partial; + input: Partial; + }, +) => { + return ; +}; + +type SelectFieldProps = { + name: string; + placeholder: string; + options: Array<{ id: string; value: string; label?: string }>; + size?: "large" | "small"; + outline?: boolean; + fillOptions?: boolean; +}; + +export function SelectField(props: SelectFieldProps) { + return ( + + ); +} + +export default SelectField; diff --git a/app/client/src/components/editorComponents/ActionCreator/index.tsx b/app/client/src/components/editorComponents/ActionCreator/index.tsx index b402345d37..aba21c3e3b 100644 --- a/app/client/src/components/editorComponents/ActionCreator/index.tsx +++ b/app/client/src/components/editorComponents/ActionCreator/index.tsx @@ -191,7 +191,7 @@ function getFieldFromValue( const errorArg = args[1] ? args[1][0] : "() => {}"; const successArg = changeValue.endsWith(")") ? `() => ${changeValue}` - : `() => ${changeValue}()`; + : `() => {}`; return value.replace( ACTION_TRIGGER_REGEX, @@ -217,7 +217,8 @@ function getFieldFromValue( const successArg = args[0] ? args[0][0] : "() => {}"; const errorArg = changeValue.endsWith(")") ? `() => ${changeValue}` - : `() => ${changeValue}()`; + : `() => {}`; + return value.replace( ACTION_TRIGGER_REGEX, `{{$1(${successArg}, ${errorArg})}}`, diff --git a/app/client/src/components/editorComponents/ApiResponseView.tsx b/app/client/src/components/editorComponents/ApiResponseView.tsx index d8cf7acec0..416b091d52 100644 --- a/app/client/src/components/editorComponents/ApiResponseView.tsx +++ b/app/client/src/components/editorComponents/ApiResponseView.tsx @@ -153,9 +153,9 @@ const StyledCallout = styled(Callout)` } `; -const InlineButton = styled(Button)` +export const InlineButton = styled(Button)` display: inline-flex; - margin: 0 4px; + margin: 0 8px; `; const HelpSection = styled.div` diff --git a/app/client/src/components/editorComponents/CodeEditor/constants.ts b/app/client/src/components/editorComponents/CodeEditor/constants.ts index 3555d4ba57..c63d301ab2 100644 --- a/app/client/src/components/editorComponents/CodeEditor/constants.ts +++ b/app/client/src/components/editorComponents/CodeEditor/constants.ts @@ -7,8 +7,7 @@ export const WARNING_LINT_ERRORS = { export const LINT_TOOLTIP_CLASS = "CodeMirror-lint-tooltip"; -export const LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS = - "CodeMirror-lint-tooltip-left"; +export const LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS = "CodeMirror-lint-tooltip-left"; export enum LintTooltipDirection { left = "left", diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index d3e40792aa..cebc55a458 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -91,7 +91,7 @@ import { replayHighlightClass } from "globalStyles/portals"; import { LintTooltipDirection, LINT_TOOLTIP_CLASS, - LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS, + LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS, } from "./constants"; interface ReduxStateProps { @@ -135,6 +135,31 @@ export type EditorStyleProps = { popperPlacement?: Placement; popperZIndex?: Indices; }; +/** + * line => Line to which the gutter is added + * + * element => HTML Element that gets added to line + * + * isFocusedAction => function called when focused + */ +export type GutterConfig = { + line: number; + element: HTMLElement; + isFocusedAction: () => void; +}; + +export type CodeEditorGutter = { + getGutterConfig: + | ((editorValue: string, cursorLineNumber: number) => GutterConfig | null) + | null; + gutterId: string; +}; + +export type CustomKeyMap = { + // combination of keys + combination: string; + onKeyDown: (cm: CodeMirror.Editor) => void; +}; export type EditorProps = EditorStyleProps & EditorConfig & { @@ -153,6 +178,8 @@ export type EditorProps = EditorStyleProps & handleMouseLeave?: () => void; isReadOnly?: boolean; isRawView?: boolean; + // Custom gutter + customGutter?: CodeEditorGutter; }; type Props = ReduxStateProps & @@ -198,6 +225,7 @@ class CodeEditor extends Component { componentDidMount(): void { if (this.codeEditorTarget.current) { const options: EditorConfiguration = { + autoRefresh: true, mode: this.props.mode, theme: EditorThemes[this.props.theme], viewportMargin: 10, @@ -221,6 +249,8 @@ class CodeEditor extends Component { tabindex: -1, }; + const gutters = new Set(); + if (!this.props.input.onChange || this.props.disabled) { options.readOnly = true; options.scrollbarStyle = "null"; @@ -230,9 +260,13 @@ class CodeEditor extends Component { if (this.props.tabBehaviour === TabBehaviour.INPUT) { options.extraKeys["Tab"] = false; } + if (this.props.customGutter) { + gutters.add(this.props.customGutter.gutterId); + } if (this.props.folding) { options.foldGutter = true; - options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]; + gutters.add("CodeMirror-linenumbers"); + gutters.add("CodeMirror-foldgutter"); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore options.foldOptions = { @@ -241,6 +275,7 @@ class CodeEditor extends Component { }, }; } + options.gutters = Array.from(gutters); // Set value of the editor const inputValue = getInputValue(this.props.input.value) || ""; @@ -262,7 +297,6 @@ class CodeEditor extends Component { // which means CodeMirror recalculates itself only one time, once all CodeMirror // changes here are completed // - editor.on("beforeChange", this.handleBeforeChange); editor.on("change", this.startChange); editor.on("keyup", this.handleAutocompleteKeyup); @@ -270,6 +304,7 @@ class CodeEditor extends Component { editor.on("cursorActivity", this.handleCursorMovement); editor.on("blur", this.handleEditorBlur); editor.on("postPick", () => this.handleAutocompleteVisibility(editor)); + if (this.props.height) { editor.setSize("100%", this.props.height); } else { @@ -330,7 +365,8 @@ class CodeEditor extends Component { }); } - handleMouseMove = () => { + handleMouseMove = (e: React.MouseEvent) => { + this.handleCustomGutter(this.editor.lineAtHeight(e.clientY, "window")); // this code only runs when we want custom tool tip for any highlighted text inside codemirror instance if ( this.props.showCustomToolTipForHighlightedText && @@ -413,7 +449,29 @@ class CodeEditor extends Component { }); } + handleCustomGutter = (lineNumber: number | null, isFocused = false) => { + const { customGutter } = this.props; + const editor = this.editor; + if (!customGutter || !editor) return; + editor.clearGutter(customGutter.gutterId); + + if (lineNumber && customGutter.getGutterConfig) { + const gutterConfig = customGutter.getGutterConfig( + editor.getValue(), + lineNumber, + ); + if (!gutterConfig) return; + editor.setGutterMarker( + gutterConfig.line, + customGutter.gutterId, + gutterConfig.element, + ); + isFocused && gutterConfig.isFocusedAction(); + } + }; + handleCursorMovement = (cm: CodeMirror.Editor) => { + this.handleCustomGutter(cm.getCursor().line, true); // ignore if disabled if (!this.props.input.onChange || this.props.disabled) { return; @@ -445,6 +503,7 @@ class CodeEditor extends Component { this.handleChange(); this.setState({ isFocused: false }); this.editor.setOption("matchBrackets", false); + this.handleCustomGutter(null); }; handleBeforeChange = ( @@ -474,7 +533,7 @@ class CodeEditor extends Component { tooltip && getLintTooltipDirection(tooltip) === LintTooltipDirection.left ) { - tooltip.classList.add(LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS); + tooltip.classList.add(LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS); } } }; diff --git a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx index 9a7ed9316a..0303e32702 100644 --- a/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx +++ b/app/client/src/components/editorComponents/Debugger/DebuggerLogs.tsx @@ -19,7 +19,7 @@ const ContainerWrapper = styled.div` height: 100%; `; -const ListWrapper = styled.div` +export const ListWrapper = styled.div` overflow: auto; height: calc(100% - ${LIST_HEADER_HEIGHT}); ${thinScrollbar}; diff --git a/app/client/src/components/editorComponents/EntityBottomTabs.tsx b/app/client/src/components/editorComponents/EntityBottomTabs.tsx index 26a62226a9..13834bf1d6 100644 --- a/app/client/src/components/editorComponents/EntityBottomTabs.tsx +++ b/app/client/src/components/editorComponents/EntityBottomTabs.tsx @@ -1,7 +1,12 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, RefObject } from "react"; import { useDispatch, useSelector } from "react-redux"; import { setCurrentTab } from "actions/debuggerActions"; -import { TabComponent, TabProp } from "components/ads/Tabs"; +import { + CollapsibleTabProps, + collapsibleTabRequiredPropKeys, + TabComponent, + TabProp, +} from "components/ads/Tabs"; import { getCurrentDebuggerTab } from "selectors/debuggerSelectors"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; @@ -12,9 +17,26 @@ type EntityBottomTabsProps = { responseViewer?: boolean; onSelect?: (tab: any) => void; selectedTabIndex?: number; // this is used in the event you want to directly control the index changes. + canCollapse?: boolean; + // Reference to container for collapsing or expanding content + containerRef?: RefObject; + // height of container when expanded + expandedHeight?: string; }; + +type CollapsibleEntityBottomTabsProps = EntityBottomTabsProps & + CollapsibleTabProps; + +// Tab is considered collapsible only when all required collapsible props are present +export const isCollapsibleEntityBottomTab = ( + props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps, +): props is CollapsibleEntityBottomTabsProps => + collapsibleTabRequiredPropKeys.every((key) => key in props); + // Using this if there are debugger related tabs -function EntityBottomTabs(props: EntityBottomTabsProps) { +function EntityBottomTabs( + props: EntityBottomTabsProps | CollapsibleEntityBottomTabsProps, +) { const [selectedIndex, setSelectedIndex] = useState(props.defaultIndex); const currentTab = useSelector(getCurrentDebuggerTab); const dispatch = useDispatch(); @@ -51,6 +73,12 @@ function EntityBottomTabs(props: EntityBottomTabsProps) { props.selectedTabIndex ? props.selectedTabIndex : selectedIndex } tabs={props.tabs} + {...(isCollapsibleEntityBottomTab(props) + ? { + containerRef: props.containerRef, + expandedHeight: props.expandedHeight, + } + : {})} /> ); } diff --git a/app/client/src/components/editorComponents/JSResponseView.tsx b/app/client/src/components/editorComponents/JSResponseView.tsx index 3460a250ac..1c7b7670e3 100644 --- a/app/client/src/components/editorComponents/JSResponseView.tsx +++ b/app/client/src/components/editorComponents/JSResponseView.tsx @@ -1,4 +1,10 @@ -import React, { useState, useRef, RefObject, useCallback } from "react"; +import React, { + useEffect, + useRef, + RefObject, + useCallback, + useState, +} from "react"; import { connect, useDispatch } from "react-redux"; import { withRouter, RouteComponentProps } from "react-router"; import styled from "styled-components"; @@ -9,10 +15,10 @@ import { DEBUGGER_ERRORS, DEBUGGER_LOGS, EXECUTING_FUNCTION, - EMPTY_JS_OBJECT, PARSING_ERROR, EMPTY_RESPONSE_FIRST_HALF, - EMPTY_RESPONSE_LAST_HALF, + EMPTY_JS_RESPONSE_LAST_HALF, + NO_JS_FUNCTION_RETURN_VALUE, } from "@appsmith/constants/messages"; import { EditorTheme } from "./CodeEditor/EditorConfig"; import DebuggerLogs from "./Debugger/DebuggerLogs"; @@ -21,40 +27,35 @@ import Resizer, { ResizerCSS } from "./Debugger/Resizer"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { JSCollection, JSAction } from "entities/JSCollection"; import ReadOnlyEditor from "components/editorComponents/ReadOnlyEditor"; -import { startExecutingJSFunction } from "actions/jsPaneActions"; import Text, { TextType } from "components/ads/Text"; import { Classes } from "components/ads/common"; import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen"; -import { sortBy } from "lodash"; -import { ReactComponent as JSFunction } from "assets/icons/menu/js-function.svg"; -import { ReactComponent as RunFunction } from "assets/icons/menu/run.svg"; import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; import Callout from "components/ads/Callout"; import { Variant } from "components/ads/common"; import { EvaluationError } from "utils/DynamicBindingUtils"; -import { Severity } from "entities/AppsmithConsole"; -import { getJSCollectionIdFromURL } from "pages/Editor/Explorer/helpers"; import { DebugButton } from "./Debugger/DebugCTA"; -import { thinScrollbar } from "constants/DefaultTheme"; import { setCurrentTab } from "actions/debuggerActions"; import { DEBUGGER_TAB_KEYS } from "./Debugger/helpers"; import EntityBottomTabs from "./EntityBottomTabs"; import Icon from "components/ads/Icon"; -import { ReactComponent as FunctionSettings } from "assets/icons/menu/settings.svg"; -import JSFunctionSettings from "pages/Editor/JSEditor/JSFunctionSettings"; -import FlagBadge from "components/utils/FlagBadge"; +import { TAB_MIN_HEIGHT } from "components/ads/Tabs"; +import { theme } from "constants/DefaultTheme"; +import { Button, Size } from "components/ads"; +import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/constants"; const ResponseContainer = styled.div` ${ResizerCSS} - // Initial height of bottom tabs - height: ${(props) => props.theme.actionsBottomTabInitialHeight}; width: 100%; // Minimum height of bottom tabs as it can be resized - min-height: 36px; + min-height: ${TAB_MIN_HEIGHT}; background-color: ${(props) => props.theme.colors.apiPane.responseBody.bg}; + height: ${({ theme }) => theme.actionsBottomTabInitialHeight}; .react-tabs__tab-panel { - overflow: hidden; + ${CodeEditorWithGutterStyles} + overflow-y: auto; + height: calc(100% - ${TAB_MIN_HEIGHT}); } `; @@ -71,79 +72,39 @@ const ResponseTabWrapper = styled.div` } `; -const ResponseTabActionsList = styled.ul` - height: 100%; - width: 20%; - list-style: none; - padding-left: 0; - ${thinScrollbar}; - scrollbar-width: thin; - overflow: auto; - padding-bottom: 40px; - margin-top: 0; -`; - -const ResponseTabAction = styled.li` - padding: 10px 0px 10px 20px; - display: flex; - align-items: center; - &:hover { - cursor: pointer; - background-color: #f0f0f0; - } - .function-name { - margin-left: 5px; - display: inline-block; - flex: 1; - } - .function-actions { - margin-left: auto; - order: 2; - svg { - display: inline-block; - } - } - .run-button { - margin: 0 15px; - margin-left: 10px; - } - &.active { - background-color: #f0f0f0; - } -`; - const TabbedViewWrapper = styled.div` height: 100%; &&& { ul.react-tabs__tab-list { padding: 0px ${(props) => props.theme.spaces[12]}px; + height: ${TAB_MIN_HEIGHT}; } } `; const ResponseViewer = styled.div` - width: 80%; + width: 100%; `; const NoResponseContainer = styled.div` height: 100%; - width: 100%; + width: max-content; display: flex; align-items: center; justify-content: center; flex-direction: column; + margin: 0 auto; &.empty { background-color: #fafafa; } .${Classes.ICON} { margin-right: 0px; svg { - width: 150px; + width: auto; height: 150px; } } - .${Classes.TEXT} { margin-top: ${(props) => props.theme.spaces[9]}px; color: #090707; @@ -166,6 +127,23 @@ const StyledCallout = styled(Callout)` } `; +const NoReturnValueWrapper = styled.div` + padding-left: ${(props) => props.theme.spaces[12]}px; + padding-top: ${(props) => props.theme.spaces[6]}px; +`; +const InlineButton = styled(Button)` + display: inline-flex; + margin: 0 4px; +`; + +enum JSResponseState { + IsExecuting = "IsExecuting", + IsDirty = "IsDirty", + NoResponse = "NoResponse", + ShowResponse = "ShowResponse", + NoReturnValue = "NoReturnValue", +} + interface ReduxStateProps { responses: Record; isExecuting: Record; @@ -173,26 +151,35 @@ interface ReduxStateProps { type Props = ReduxStateProps & RouteComponentProps & { + currentFunction: JSAction | null; theme?: EditorTheme; jsObject: JSCollection; errors: Array; + disabled: boolean; + isLoading: boolean; + onButtonClick: (e: React.MouseEvent) => void; }; function JSResponseView(props: Props) { - const { errors, isExecuting, jsObject, responses } = props; + const { + currentFunction, + disabled, + errors, + isExecuting, + isLoading, + jsObject, + onButtonClick, + responses, + } = props; + const [responseStatus, setResponseStatus] = useState( + JSResponseState.NoResponse, + ); const panelRef: RefObject = useRef(null); const dispatch = useDispatch(); - const [selectActionId, setSelectActionId] = useState(""); - const actionList = jsObject?.actions; - const sortedActionList = actionList && sortBy(actionList, "name"); const response = - selectActionId && selectActionId in responses - ? responses[selectActionId] + currentFunction && currentFunction.id && currentFunction.id in responses + ? responses[currentFunction.id] : ""; - const isRunning = selectActionId && !!isExecuting[selectActionId]; - const errorsList = errors.filter((er) => { - return er.severity === Severity.ERROR; - }); const onDebugClick = useCallback(() => { AnalyticsUtil.logEvent("OPEN_DEBUGGER", { @@ -200,18 +187,25 @@ function JSResponseView(props: Props) { }); dispatch(setCurrentTab(DEBUGGER_TAB_KEYS.ERROR_TAB)); }, []); - - const [openSettings, setOpenSettings] = useState(false); - const [selectedFunction, setSelectedFunction] = useState< - undefined | JSAction - >(undefined); - const isSelectedFunctionAsync = (id: string) => { - const jsAction = jsObject.actions.find((action) => action.id === id); - if (!!jsAction) { - return jsAction?.actionConfiguration.isAsync; + useEffect(() => { + if (!currentFunction) { + setResponseStatus(JSResponseState.NoResponse); + } else if (isExecuting[currentFunction.id]) { + setResponseStatus(JSResponseState.IsExecuting); + } else if ( + !responses.hasOwnProperty(currentFunction.id) && + !isExecuting.hasOwnProperty(currentFunction.id) + ) { + setResponseStatus(JSResponseState.NoResponse); + } else if ( + responses.hasOwnProperty(currentFunction.id) && + responses[currentFunction.id] === undefined + ) { + setResponseStatus(JSResponseState.NoReturnValue); + } else if (responses.hasOwnProperty(currentFunction.id)) { + setResponseStatus(JSResponseState.ShowResponse); } - return false; - }; + }, [responses, isExecuting, currentFunction]); const tabs = [ { @@ -219,8 +213,8 @@ function JSResponseView(props: Props) { title: "Response", panelComponent: ( <> - - {errorsList.length > 0 ? ( + {errors.length > 0 && ( + - ) : ( - "" - )} - - - {sortedActionList && !sortedActionList?.length ? ( - - {createMessage(EMPTY_JS_OBJECT)} - - ) : ( + + )} + + <> - - {sortedActionList && - sortedActionList?.length > 0 && - sortedActionList.map((action) => { - return ( - { - setSelectActionId(action.id); - }} - > - {" "} -
{action.name}
-
- {action.actionConfiguration.isAsync ? ( - - ) : ( - "" - )} - {isSelectedFunctionAsync(action.id) ? ( - { - setSelectedFunction(action); - setOpenSettings(true); - }} - /> - ) : ( - "" - )} - - { - runAction(action); - }} - /> -
-
- ); - })} -
- - {isRunning ? ( - - {createMessage(EXECUTING_FUNCTION)} - - ) : !responses.hasOwnProperty(selectActionId) ? ( - - - - {EMPTY_RESPONSE_FIRST_HALF()} - - {EMPTY_RESPONSE_LAST_HALF()} - - - ) : ( - - )} - - {openSettings && - !!selectedFunction && - isSelectedFunctionAsync(selectedFunction.id) && ( - { - setOpenSettings(!openSettings); - }} - /> - )} + {responseStatus === JSResponseState.NoResponse && ( + + + + {createMessage(EMPTY_RESPONSE_FIRST_HALF)} + + {createMessage(EMPTY_JS_RESPONSE_LAST_HALF)} + + + )} + {responseStatus === JSResponseState.IsExecuting && ( + + {createMessage(EXECUTING_FUNCTION)} + + )} + {responseStatus === JSResponseState.NoReturnValue && ( + + + {createMessage( + NO_JS_FUNCTION_RETURN_VALUE, + currentFunction?.name, + )} + + + )} + {responseStatus === JSResponseState.ShowResponse && ( + + )} - )} +
), @@ -339,23 +290,16 @@ function JSResponseView(props: Props) { }, ]; - const runAction = (action: JSAction) => { - setSelectActionId(action.id); - const collectionId = getJSCollectionIdFromURL(); - dispatch( - startExecutingJSFunction({ - collectionName: jsObject?.name || "", - action: action, - collectionId: collectionId || "", - }), - ); - }; - return ( - + ); diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 154b2b1fce..7ea13ce437 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -652,6 +652,7 @@ const lightShades = [ "#F86A2B", "#FFDEDE", "#575757", + "#191919", ] as const; type ShadeColor = typeof darkShades[number] | typeof lightShades[number]; @@ -1373,13 +1374,14 @@ const editorBottomBar = { const gitSyncModal = { menuBackgroundColor: Colors.ALABASTER_ALT, separator: Colors.ALTO2, - closeIcon: "rgba(29, 28, 29, 0.7);", + closeIcon: Colors.SCORPION, + closeIconHover: Colors.COD_GRAY, }; type GitSyncModalColors = typeof gitSyncModal; const tabItemBackgroundFill = { highlightBackground: Colors.Gallery, - highlightTextColor: Colors.CODE_GRAY, + highlightTextColor: Colors.COD_GRAY, textColor: Colors.CHARCOAL, }; @@ -2612,7 +2614,7 @@ export const light: ColorType = { }, modal: { bg: lightShades[11], - headerText: lightShades[10], + headerText: lightShades[20], iconColor: lightShades[5], iconBg: lightShades[18], user: { diff --git a/app/client/src/constants/ast.ts b/app/client/src/constants/ast.ts new file mode 100644 index 0000000000..4582a7261e --- /dev/null +++ b/app/client/src/constants/ast.ts @@ -0,0 +1,25 @@ +export const ECMA_VERSION = 11; + +/* Indicates the mode the code should be parsed in. +This influences global strict mode and parsing of import and export declarations. +*/ +export enum SourceType { + script = "script", + module = "module", +} + +// Each node has an attached type property which further defines +// what all properties can the node have. +// We will just define the ones we are working with +export enum NodeTypes { + MemberExpression = "MemberExpression", + Identifier = "Identifier", + VariableDeclarator = "VariableDeclarator", + FunctionDeclaration = "FunctionDeclaration", + FunctionExpression = "FunctionExpression", + AssignmentPattern = "AssignmentPattern", + Literal = "Literal", + ExportDefaultDeclaration = "ExportDefaultDeclaration", + Property = "Property", + ArrowFunctionExpression = "ArrowFunctionExpression", +} diff --git a/app/client/src/globalStyles/CodemirrorHintStyles.ts b/app/client/src/globalStyles/CodemirrorHintStyles.ts index 8b975c4812..eece02db01 100644 --- a/app/client/src/globalStyles/CodemirrorHintStyles.ts +++ b/app/client/src/globalStyles/CodemirrorHintStyles.ts @@ -1,7 +1,7 @@ import { createGlobalStyle } from "styled-components"; import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig"; import { getTypographyByKey, Theme } from "constants/DefaultTheme"; -import { LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants"; +import { LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS } from "components/editorComponents/CodeEditor/constants"; export const CodemirrorHintStyles = createGlobalStyle<{ editorTheme: EditorTheme; @@ -259,7 +259,7 @@ export const CodemirrorHintStyles = createGlobalStyle<{ padding: 7px 12px; border-radius: 0; - &.${LINT_TOOLTIP_JUSTIFIFIED_LEFT_CLASS}{ + &.${LINT_TOOLTIP_JUSTIFIED_LEFT_CLASS}{ transform: translate(-100%); } diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index b17527820f..288b146f92 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -1,10 +1,14 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { JSCollection } from "entities/JSCollection"; +import React, { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { JSAction, JSCollection } from "entities/JSCollection"; import CloseEditor from "components/editorComponents/CloseEditor"; import MoreJSCollectionsMenu from "../Explorer/JSActions/MoreJSActionsMenu"; import { TabComponent } from "components/ads/Tabs"; -import FormLabel from "components/editorComponents/FormLabel"; import CodeEditor from "components/editorComponents/CodeEditor"; import { EditorModes, @@ -14,184 +18,259 @@ import { } from "components/editorComponents/CodeEditor/EditorConfig"; import FormRow from "components/editorComponents/FormRow"; import JSObjectNameEditor from "./JSObjectNameEditor"; -import { updateJSCollectionBody } from "actions/jsPaneActions"; +import { + setActiveJSAction, + startExecutingJSFunction, + updateJSCollectionBody, +} from "actions/jsPaneActions"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router"; import { ExplorerURLParams } from "../Explorer/helpers"; import JSResponseView from "components/editorComponents/JSResponseView"; -import { EVAL_ERROR_PATH } from "utils/DynamicBindingUtils"; -import { get } from "lodash"; -import { getDataTree } from "selectors/dataTreeSelectors"; -import { EvaluationError } from "utils/DynamicBindingUtils"; +import { isEmpty, isEqual } from "lodash"; import SearchSnippets from "components/ads/SnippetButton"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; +import { JSFunctionRun } from "./JSFunctionRun"; +import { AppState } from "reducers"; +import { + getActiveJSActionId, + getIsExecutingJSAction, + getJSActions, + getJSCollectionParseErrors, +} from "selectors/entitiesSelector"; +import { + convertJSActionsToDropdownOptions, + convertJSActionToDropdownOption, + getActionFromJsCollection, + getJSActionOption, + getJSFunctionLineGutter, + JSActionDropdownOption, +} from "./utils"; +import { DropdownOnSelect } from "components/ads"; +import JSFunctionSettingsView from "./JSFunctionSettings"; +import JSObjectHotKeys from "./JSObjectHotKeys"; +import { + ActionButtons, + Form, + FormWrapper, + MainConfiguration, + NameWrapper, + SecondaryWrapper, + TabbedViewContainer, +} from "./styledComponents"; -const Form = styled.form` - display: flex; - flex-direction: column; - height: calc( - 100vh - ${(props) => props.theme.smallHeaderHeight} - - ${(props) => props.theme.backBanner} - ); - overflow: hidden; - width: 100%; - ${FormLabel} { - padding: ${(props) => props.theme.spaces[3]}px; - } - ${FormRow} { - ${FormLabel} { - padding: 0; - width: 100%; - } - } -`; - -const NameWrapper = styled.div` - width: 49%; - display: flex; - align-items: center; - input { - margin: 0; - box-sizing: border-box; - } -`; - -const ActionButtons = styled.div` - justify-self: flex-end; - display: flex; - align-items: center; - - button:last-child { - margin-left: ${(props) => props.theme.spaces[7]}px; - } -`; - -const SecondaryWrapper = styled.div` - display: flex; - flex-direction: column; - height: calc(100% - 50px); -`; -const MainConfiguration = styled.div` - padding: ${(props) => props.theme.spaces[4]}px - ${(props) => props.theme.spaces[10]}px 0px - ${(props) => props.theme.spaces[10]}px; -`; - -export const TabbedViewContainer = styled.div` - flex: 1; - overflow: auto; - position: relative; - height: 100%; - border-top: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg}; - ${FormRow} { - min-height: auto; - padding: ${(props) => props.theme.spaces[0]}px; - & > * { - margin-right: 0px; - } - } - - &&& { - ul.react-tabs__tab-list { - padding: 0px ${(props) => props.theme.spaces[12]}px; - background-color: ${(props) => - props.theme.colors.apiPane.responseBody.bg}; - } - .react-tabs__tab-panel { - height: calc(100% - 36px); - margin-top: 2px; - background-color: ${(props) => props.theme.colors.apiPane.bg}; - } - } -`; interface JSFormProps { - jsAction: JSCollection; - settingsConfig: any; + jsCollection: JSCollection; } type Props = JSFormProps; -function JSEditorForm(props: Props) { +function JSEditorForm({ jsCollection: currentJSCollection }: Props) { const theme = EditorTheme.LIGHT; const [mainTabIndex, setMainTabIndex] = useState(0); const dispatch = useDispatch(); - const currentJSAction = props.jsAction; - const dataTree = useSelector(getDataTree); - const handleOnChange = (event: string) => { - if (currentJSAction) { - dispatch(updateJSCollectionBody(event, currentJSAction.id)); - } - }; const { pageId } = useParams(); - const getErrors = get( - dataTree, - `${currentJSAction.name}.${EVAL_ERROR_PATH}.body`, - [], - ) as EvaluationError[]; + const [disableRunFunctionality, setDisableRunFunctionality] = useState(false); + + // Currently active response (only changes upon execution) + const [activeResponse, setActiveResponse] = useState(null); + const parseErrors = useSelector( + (state: AppState) => + getJSCollectionParseErrors(state, currentJSCollection.name), + isEqual, + ); + const jsActions = useSelector( + (state: AppState) => getJSActions(state, currentJSCollection.id), + isEqual, + ); + const activeJSActionId = useSelector((state: AppState) => + getActiveJSActionId(state, currentJSCollection.id), + ); + + const activeJSAction = getActionFromJsCollection( + activeJSActionId, + currentJSCollection, + ); + + const [selectedJSActionOption, setSelectedJSActionOption] = useState< + JSActionDropdownOption + >(getJSActionOption(activeJSAction, jsActions)); + + const isExecutingCurrentJSAction = useSelector((state: AppState) => + getIsExecutingJSAction( + state, + currentJSCollection.id, + selectedJSActionOption.data?.id || "", + ), + ); + + // Triggered when there is a change in the code editor + const handleEditorChange = (valueOrEvent: ChangeEvent | string) => { + const value: string = + typeof valueOrEvent === "string" + ? valueOrEvent + : valueOrEvent.target.value; + + dispatch(updateJSCollectionBody(value, currentJSCollection.id)); + }; + + // Executes JS action + const executeJSAction = (jsAction: JSAction) => { + setActiveResponse(jsAction); + if (jsAction.id !== selectedJSActionOption.data?.id) + setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction)); + dispatch( + setActiveJSAction({ + jsCollectionId: currentJSCollection.id || "", + jsActionId: jsAction.id || "", + }), + ); + dispatch( + startExecutingJSFunction({ + collectionName: currentJSCollection.name || "", + action: jsAction, + collectionId: currentJSCollection.id || "", + }), + ); + }; + + const handleActiveActionChange = useCallback( + (jsAction: JSAction) => { + if (!jsAction) return; + + // only update when there is a new active action + if (jsAction.id !== selectedJSActionOption.data?.id) { + setSelectedJSActionOption(convertJSActionToDropdownOption(jsAction)); + } + }, + [selectedJSActionOption], + ); + + const JSGutters = useMemo( + () => + getJSFunctionLineGutter( + jsActions, + executeJSAction, + !parseErrors.length, + handleActiveActionChange, + ), + [jsActions, parseErrors, handleActiveActionChange], + ); + + const handleJSActionOptionSelection: DropdownOnSelect = ( + value, + dropDownOption: JSActionDropdownOption, + ) => { + dropDownOption.data && + setSelectedJSActionOption( + convertJSActionToDropdownOption(dropDownOption.data), + ); + }; + + const handleRunAction = ( + event: React.MouseEvent | KeyboardEvent, + ) => { + event.preventDefault(); + selectedJSActionOption.data && executeJSAction(selectedJSActionOption.data); + }; + + useEffect(() => { + if (parseErrors.length || isEmpty(jsActions)) { + setDisableRunFunctionality(true); + } else { + setDisableRunFunctionality(false); + } + setSelectedJSActionOption(getJSActionOption(activeJSAction, jsActions)); + }, [parseErrors, jsActions, activeJSActionId]); + return ( - <> - -
- - - - - - - + + + + + + + + + + + + + + + + + + + ), + }, + { + key: "settings", + title: "Settings", + panelComponent: ( + + ), + }, + ]} /> - - - - - - - handleOnChange(event), - }} - mode={EditorModes.JAVASCRIPT} - placeholder="Let's write some code!" - showLightningMenu={false} - showLineNumbers - size={EditorSize.EXTENDED} - tabBehaviour={TabBehaviour.INDENT} - theme={theme} - /> - ), - }, - ]} + + - - - -
- + + + + ); } diff --git a/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx b/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx new file mode 100644 index 0000000000..28804d9f9e --- /dev/null +++ b/app/client/src/pages/Editor/JSEditor/JSFunctionRun.tsx @@ -0,0 +1,96 @@ +import React from "react"; +import styled from "styled-components"; +import Dropdown, { + DropdownOnSelect, + DropdownContainer, +} from "components/ads/Dropdown"; +import Button from "components/ads/Button"; +import FlagBadge from "components/utils/FlagBadge"; +import { JSCollection } from "entities/JSCollection"; +import Tooltip from "components/ads/Tooltip"; +import { createMessage, NO_JS_FUNCTION_TO_RUN } from "ce/constants/messages"; +import { StyledButton } from "components/ads/Button"; +import { JSActionDropdownOption } from "./utils"; +import { RUN_BUTTON_DEFAULTS, testLocators } from "./constants"; + +type Props = { + disabled: boolean; + isLoading: boolean; + jsCollection: JSCollection; + onButtonClick: (event: React.MouseEvent) => void; + onSelect: DropdownOnSelect; + options: JSActionDropdownOption[]; + selected: JSActionDropdownOption; + showTooltip: boolean; +}; + +export type DropdownWithCTAWrapperProps = { + isDisabled: boolean; +}; +const disabledStyles = ` +opacity: 0.5; +pointer-events:none; +`; + +const DropdownWithCTAWrapper = styled.div` + display: flex; + + ${StyledButton} { + margin-left: ${RUN_BUTTON_DEFAULTS.GAP_SIZE}; + padding: 0px 20px; + + ${(props) => + props.isDisabled && + ` + ${disabledStyles} + `} + } + ${DropdownContainer} { + ${(props) => + props.isDisabled && + ` + ${disabledStyles} + `} + } +`; + +export function JSFunctionRun({ + disabled, + isLoading, + jsCollection, + onButtonClick, + onSelect, + options, + selected, + showTooltip, +}: Props) { + return ( + + } + height={RUN_BUTTON_DEFAULTS.HEIGHT} + onSelect={onSelect} + options={options} + selected={selected} + selectedHighlightBg={RUN_BUTTON_DEFAULTS.DROPDOWN_HIGHLIGHT_BG} + showLabelOnly + truncateOption + /> + + +