From 0ffff9db4f7e077648e81be41ef594df2a3de293 Mon Sep 17 00:00:00 2001 From: Tejaaswini Date: Thu, 18 Jun 2020 17:46:46 +0530 Subject: [PATCH] merge 'release' into 'fix/minor-bugs' --- app/client/cypress/fixtures/testdata.json | 8 +- .../ApiFlow/3PImportFlow_spec.js | 26 --- .../ApiFlow/CurlImportFlow_spec.js | 5 +- .../ApiPaneTests/API_CurlPOSTImport_spec.js | 2 +- .../ApiPaneTests/API_Search_spec.js | 2 + .../ApiPaneTests/API_Unique_name_spec.js | 10 + .../API_all_sidebar_actions_spec.js | 4 +- .../ApiPaneTests/Api_Marketplace_spec.js | 3 - .../Datasources/MongoDatasource_spec.js | 16 +- .../Datasources/PostgresDatasource_spec.js | 15 +- .../Datasources/RestApiDatasource_spec.js | 13 +- .../QueryPane/MongoDatasource_spec.js | 11 +- .../QueryPane/PostgreDatasource_spec.js | 11 +- .../UnitTest/CreateDeleteApp_spec.js | 9 - .../UnitTest/CreateDeletePage_spec.js | 9 - .../UnitTest/LoginFromUIApp_spec.js | 26 +++ app/client/cypress/locators/ApiEditor.json | 4 +- .../cypress/locators/DatasourcesEditor.json | 3 + app/client/cypress/locators/HomePage.json | 5 +- .../cypress/locators/apiWidgetslocator.json | 8 +- app/client/cypress/support/commands.js | 88 +++++++-- app/client/cypress/support/index.js | 17 +- app/client/src/actions/actionActions.ts | 24 +++ app/client/src/api/ActionAPI.tsx | 11 ++ .../appsmith/ReactTableComponent.tsx | 7 +- .../designSystems/appsmith/Table.tsx | 50 +++++ .../appsmith/TableStyledWrappers.tsx | 18 +- .../designSystems/appsmith/TableUtilities.tsx | 40 +++- .../blueprint/ModalComponent.tsx | 5 +- .../editorComponents/ActionNameEditor.tsx | 130 ++++++++++++ .../components/editorComponents/Button.tsx | 1 - .../editorComponents/EditableText.tsx | 110 +++++++++-- .../editorComponents/EntityNameComponent.tsx | 159 +++++++++++++++ .../editorComponents/form/FieldError.tsx | 8 +- .../form/fields/SelectField.tsx | 1 - .../propertyControls/InputTextControl.tsx | 5 +- .../src/constants/ReduxActionConstants.tsx | 5 + app/client/src/constants/WidgetValidation.ts | 1 - app/client/src/constants/messages.ts | 1 + app/client/src/pages/AppViewer/index.tsx | 2 - .../AppViewer/viewer/AppViewerHeader.tsx | 6 +- .../pages/Applications/ApplicationCard.tsx | 2 +- .../Applications/CreateApplicationForm.tsx | 2 +- app/client/src/pages/Applications/index.tsx | 8 +- .../src/pages/Editor/APIEditor/Form.tsx | 31 ++- .../Editor/APIEditor/RapidApiEditorForm.tsx | 29 ++- .../src/pages/Editor/APIEditor/index.tsx | 24 ++- app/client/src/pages/Editor/ApiSidebar.tsx | 2 +- .../pages/Editor/DataSourceEditor/DBForm.tsx | 2 +- .../PageListSidebar/CreatePageButton.tsx | 7 +- .../Editor/PageListSidebar/PageListItem.tsx | 22 ++- .../src/pages/Editor/PropertyPane/index.tsx | 6 +- .../src/pages/Editor/PropertyPaneTitle.tsx | 18 +- app/client/src/pages/Editor/Sidebar.tsx | 2 +- app/client/src/pages/common/AppRoute.tsx | 4 - .../CustomizedDropdown/HeaderDropdownData.tsx | 49 +---- app/client/src/pages/common/PageHeader.tsx | 2 +- .../pages/organization/InviteUsersFromv2.tsx | 12 +- .../src/pages/organization/settings.tsx | 18 +- app/client/src/reducers/index.tsx | 2 + .../src/reducers/uiReducers/apiNameReducer.ts | 70 +++++++ .../src/reducers/uiReducers/apiPaneReducer.ts | 48 ++++- app/client/src/reducers/uiReducers/index.tsx | 2 + app/client/src/sagas/ActionSagas.ts | 111 +++++++++++ app/client/src/sagas/ApiPaneSagas.ts | 6 +- app/client/src/sagas/ApplicationSagas.tsx | 1 - app/client/src/sagas/OrgSagas.ts | 1 - app/client/src/sagas/PageSagas.tsx | 40 ++-- app/client/src/sagas/userSagas.tsx | 7 - app/client/src/selectors/formSelectors.ts | 14 ++ .../src/selectors/organizationSelectors.tsx | 2 +- app/client/src/utils/Validators.ts | 65 ++---- app/client/src/utils/WidgetPropsUtils.tsx | 36 ++++ app/client/src/utils/helpers.tsx | 14 ++ app/client/src/widgets/FormWidget.tsx | 2 +- app/client/src/widgets/TableWidget.tsx | 186 +++++++++--------- 76 files changed, 1251 insertions(+), 475 deletions(-) delete mode 100644 app/client/cypress/integration/Smoke_TestSuite/ApiFlow/3PImportFlow_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Unique_name_spec.js delete mode 100644 app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js delete mode 100644 app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeletePage_spec.js create mode 100644 app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js create mode 100644 app/client/src/components/editorComponents/ActionNameEditor.tsx create mode 100644 app/client/src/components/editorComponents/EntityNameComponent.tsx create mode 100644 app/client/src/reducers/uiReducers/apiNameReducer.ts diff --git a/app/client/cypress/fixtures/testdata.json b/app/client/cypress/fixtures/testdata.json index dfc3f9e090..d7047f2a02 100644 --- a/app/client/cypress/fixtures/testdata.json +++ b/app/client/cypress/fixtures/testdata.json @@ -1,5 +1,5 @@ { - "baseUrl": "https://mock-api.appsmith.com", + "baseUrl": "https://mock-api.appsmith.com/", "methods": "users", "headerKey": "Content-Type", "headerValue": "application/json", @@ -12,14 +12,14 @@ "responsetext": "Roger Brickelberry", "pageResponsetext": "Josh M Krantz", "apiname": "SecondAPI", - "baseUrl2": "https://reqres.in", + "baseUrl2": "https://reqres.in/", "methods1": "api/users/1", "responsetext2": "qui est esse", - "baseUrl3": "https://reqres.in", + "baseUrl3": "https://reqres.in/", "methods2": "api/users/2", "invalidPath": "api/users/a", "responsetext3": "Josh M Krantz", - "postUrl": "https://reqres.in", + "postUrl": "https://reqres.in/", "deleteUrl": "", "Post": "POST", "Delete": "DELETE", diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/3PImportFlow_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/3PImportFlow_spec.js deleted file mode 100644 index 81ecf5934e..0000000000 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/3PImportFlow_spec.js +++ /dev/null @@ -1,26 +0,0 @@ -const ApiEditor = require("../../../locators/ApiEditor.json"); - -describe("Test 3P provider API import flow", function() { - it("Test 3P provider API import flow", function() { - localStorage.setItem("ApiPaneV2", "ApiPaneV2"); - cy.NavigateToApiEditor(); - cy.wait("@get3PProviders").should( - "have.nested.property", - "response.body.responseMeta.status", - 200, - ); - cy.get(ApiEditor.eachProviderCard) - .first() - .click({ force: true }); - cy.wait("@get3PProviderTemplates"); - cy.url().should("include", "/edit/api/provider/"); - cy.contains("Add to page").click(); - cy.wait("@add3PApiToPage").should( - "have.nested.property", - "response.body.responseMeta.status", - 201, - ); - cy.get(ApiEditor.addToPageBtn).should("be.disabled"); - cy.get(ApiEditor.addToPageBtnsId).should("contain", "Added"); - }); -}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js index 280b9fa0c2..7b143c4756 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiFlow/CurlImportFlow_spec.js @@ -18,13 +18,10 @@ describe("Test curl import flow", function() { response.response.body.data.name, ); }); - cy.WaitAutoSave(); + // cy.WaitAutoSave(); cy.RunAPI(); cy.get(ApiEditor.formActionButtons).should("be.visible"); - cy.get("@postExecute").then(httpResponse => { - cy.expect(httpResponse.response.body.responseMeta.success).to.eq(true); - }); cy.get(ApiEditor.ApiDeleteBtn).click(); cy.get(ApiEditor.ApiDeleteBtn).should("be.disabled"); cy.testDeleteApi(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js index fc17f71cff..c0311f27c3 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_CurlPOSTImport_spec.js @@ -6,7 +6,7 @@ describe("Test curl import flow", function() { cy.NavigateToApiEditor(); cy.get(ApiEditor.curlImage).click({ force: true }); cy.get("textarea").type( - "curl -d '{'name': 'morpheus','job': 'leader'}' -H 'Content-Type: application/json' “https://reqres.in/api/users”", + "curl -d { name : 'morpheus',job : 'leader'} -H Content-Type: application/json https://reqres.in/api/users", { force: true, parseSpecialCharSequences: false, diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js index fd502020be..1df61ba5f9 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Search_spec.js @@ -6,8 +6,10 @@ describe("API Panel Test Functionality ", function() { cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); cy.CreateAPI("FirstAPI"); + cy.RunAPI(); cy.log("Creation of FirstAPI Action successful"); cy.CreateAPI("SecondAPI"); + cy.RunAPI(); cy.log("Creation of SecondAPI Action successful"); cy.SearchAPI("SecondAPI", "FirstAPI"); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Unique_name_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Unique_name_spec.js new file mode 100644 index 0000000000..ec9d6c2832 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_Unique_name_spec.js @@ -0,0 +1,10 @@ +describe("Name uniqueness test", function() { + it("Test api name unique error", () => { + cy.log("Login Successful"); + cy.NavigateToAPI_Panel(); + cy.log("Navigation to API Panel screen successful"); + cy.CreateAPI("UniqueName"); + cy.log("Creation of UniqueName Action successful"); + cy.CreationOfUniqueAPIcheck("UniqueName"); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js index fe4e7ae45b..7ad661a736 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/API_all_sidebar_actions_spec.js @@ -3,11 +3,13 @@ describe("API Panel Test Functionality ", function() { cy.log("Login Successful"); cy.NavigateToAPI_Panel(); cy.log("Navigation to API Panel screen successful"); + cy.CreateAPI("FirstAPI"); cy.log("Creation of FirstAPI Action successful"); + cy.CopyAPIToHome("FirstAPI"); - cy.MoveAPIToPage(); cy.DeleteAPI("FirstAPI"); + //cy.MoveAPIToPage(); cy.CreateAPI("FirstAPI"); cy.log("Creation of FirstAPI Action successful"); cy.CreationOfUniqueAPIcheck("FirstAPI"); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Api_Marketplace_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Api_Marketplace_spec.js index 665f4b1fed..1e62568093 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Api_Marketplace_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ApiPaneTests/Api_Marketplace_spec.js @@ -8,13 +8,11 @@ describe("API Panel Test Functionality ", function() { cy.wait("@getCategories"); cy.wait("@getTemplateCollections"); cy.wait("@get3PProviders"); - cy.wait("@getUser"); cy.log("Navigation to API Panel screen successful"); cy.get(apiwidget.marketPlaceapi) .first() .click(); cy.wait("@get3PProviderTemplates"); - cy.wait("@getUser"); cy.get(".apiName") .first() .invoke("text") @@ -24,7 +22,6 @@ describe("API Panel Test Functionality ", function() { .click(); const searchApiName = ApiName.replace(/\s/g, ""); cy.log(searchApiName); - cy.wait("@add3PApiToPage"); cy.wait("@getActions"); cy.SearchAPIandClick(searchApiName); cy.get(apiwidget.apidocumentaionLink) diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/MongoDatasource_spec.js index 90f1d18133..15d78e012b 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Datasources/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/MongoDatasource_spec.js @@ -1,20 +1,12 @@ +const datasource = require("../../../locators/DatasourcesEditor.json"); +let pageid; + describe("Create, test, save then delete a mongo datasource", function() { it("Create, test, save then delete a mongo datasource", function() { cy.NavigateToDatasourceEditor(); - cy.get("@getPlugins").then(httpResponse => { - const pluginName = httpResponse.response.body.data.find( - plugin => plugin.packageName === "mongo-plugin", - ).name; - - cy.get(".t--plugin-name") - .contains(pluginName) - .click(); - }); - + cy.get(datasource.MongoDB).click(); cy.getPluginFormsAndCreateDatasource(); - cy.fillMongoDatasourceForm(); - cy.testSaveDeleteDatasource(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js index a7607aedf6..3d18bb62ce 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/PostgresDatasource_spec.js @@ -1,20 +1,11 @@ +const datasource = require("../../../locators/DatasourcesEditor.json"); + describe("Create, test, save then delete a postgres datasource", function() { it("Create, test, save then delete a postgres datasource", function() { cy.NavigateToDatasourceEditor(); - cy.get("@getPlugins").then(httpResponse => { - const pluginName = httpResponse.response.body.data.find( - plugin => plugin.packageName === "postgres-plugin", - ).name; - - cy.get(".t--plugin-name") - .contains(pluginName) - .click(); - }); - + cy.get(datasource.PostgreSQL).click(); cy.getPluginFormsAndCreateDatasource(); - cy.fillPostgresDatasourceForm(); - cy.testSaveDeleteDatasource(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js index 7f5b1596ff..d7e2fab038 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/Datasources/RestApiDatasource_spec.js @@ -4,20 +4,9 @@ const datasourceFormData = require("../../../fixtures/datasources.json"); describe("Create, test, save then delete a restapi datasource", function() { it("Create, test, save then delete a restapi datasource", function() { cy.NavigateToDatasourceEditor(); - cy.get("@getPlugins").then(httpResponse => { - const pluginName = httpResponse.response.body.data.find( - plugin => plugin.packageName === "restapi-plugin", - ).name; - - cy.get(".t--plugin-name") - .contains(pluginName) - .click(); - }); - + cy.get(datasourceEditor.RESTAPI).click(); cy.getPluginFormsAndCreateDatasource(); - cy.get(datasourceEditor.url).type(datasourceFormData["restapi-url"]); - cy.testSaveDeleteDatasource(); }); }); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js index 8248c5dd07..a066abffce 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/MongoDatasource_spec.js @@ -1,18 +1,11 @@ const queryLocators = require("../../../locators/QueryEditor.json"); const plugins = require("../../../fixtures/plugins.json"); +const datasource = require("../../../locators/DatasourcesEditor.json"); describe("Create a query with a mongo datasource, run, save and then delete the query", function() { it("Create a query with a mongo datasource, run, save and then delete the query", function() { cy.NavigateToDatasourceEditor(); - cy.get("@getPlugins").then(httpResponse => { - const pluginName = httpResponse.response.body.data.find( - plugin => plugin.packageName === plugins.mongoPackageName, - ).name; - - cy.get(".t--plugin-name") - .contains(pluginName) - .click(); - }); + cy.get(datasource.MongoDB).click(); cy.getPluginFormsAndCreateDatasource(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js index c68cd62160..f13b727306 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/QueryPane/PostgreDatasource_spec.js @@ -1,17 +1,10 @@ const queryLocators = require("../../../locators/QueryEditor.json"); +const datasource = require("../../../locators/DatasourcesEditor.json"); describe("Create a query with a postgres datasource, run, save and then delete the query", function() { it("Create a query with a postgres datasource, run, save and then delete the query", function() { cy.NavigateToDatasourceEditor(); - cy.get("@getPlugins").then(httpResponse => { - const pluginName = httpResponse.response.body.data.find( - plugin => plugin.packageName === "postgres-plugin", - ).name; - - cy.get(".t--plugin-name") - .contains(pluginName) - .click(); - }); + cy.get(datasource.PostgreSQL).click(); cy.getPluginFormsAndCreateDatasource(); diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js deleted file mode 100644 index 493479fa3e..0000000000 --- a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeleteApp_spec.js +++ /dev/null @@ -1,9 +0,0 @@ -describe("Create and Delete App Functionality", function() { - it("Delete App Functionality", function() { - cy.log("appname: " + localStorage.getItem("AppName")); - const appname = localStorage.getItem("AppName"); - cy.DeleteApp(appname); - cy.wait("@deleteApplication"); - cy.get("@deleteApplication").should("have.property", "status", 200); - }); -}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeletePage_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeletePage_spec.js deleted file mode 100644 index 72830024c3..0000000000 --- a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/CreateDeletePage_spec.js +++ /dev/null @@ -1,9 +0,0 @@ -describe("Create and Delete Page Functionality", function() { - it("Delete Page Functionality", function() { - cy.log("PageName: " + localStorage.getItem("PageName")); - const PageName = localStorage.getItem("PageName"); - cy.Deletepage(PageName); - cy.wait("@deletePage"); - cy.get("@deletePage").should("have.property", "status", 200); - }); -}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js new file mode 100644 index 0000000000..7e177c60b3 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/UnitTest/LoginFromUIApp_spec.js @@ -0,0 +1,26 @@ +const loginData = require("../../../fixtures/user.json"); +let pageid; +let appId; + +describe("Login from UI and check the functionality", function() { + it("Login/create page/delete page/delete app from UI", function() { + const appname = localStorage.getItem("AppName"); + cy.LogintoApp(loginData.username, loginData.password); + cy.SearchApp(appname); + cy.get("#loading").should("not.exist"); + cy.wait("@getPropertyPane"); + cy.get("@getPropertyPane").should("have.property", "status", 200); + cy.generateUUID().then(uid => { + pageid = uid; + cy.Createpage(pageid); + cy.NavigateToWidgets(pageid); + localStorage.setItem("PageName", pageid); + cy.Deletepage(pageid); + }); + cy.wait("@deletePage"); + cy.get("@deletePage").should("have.property", "status", 200); + cy.DeleteApp(appname); + cy.wait("@deleteApplication"); + cy.get("@deleteApplication").should("have.property", "status", 200); + }); +}); diff --git a/app/client/cypress/locators/ApiEditor.json b/app/client/cypress/locators/ApiEditor.json index 7a882663fa..9b9fc8a132 100644 --- a/app/client/cypress/locators/ApiEditor.json +++ b/app/client/cypress/locators/ApiEditor.json @@ -4,7 +4,7 @@ "createBlankApiCard": ".t--createBlankApiCard", "eachProviderCard": ".t--eachProviderCard", "nameOfApi": ".t--nameOfApi", - "ApiNameField":"input[name='name']", + "ApiNameField": ".t--nameOfApi input", "addToPageBtn": ".t--addToPageBtn", "ApiDeleteBtn": ".t--apiFormDeleteBtn", "ApiRunBtn": ".t--apiFormRunBtn", @@ -20,4 +20,4 @@ "apiPaginationNextTest": ".t--apiFormPaginationNextTest", "apiPaginationTab": ".t--apiFormPaginationType", "apiTab": ".react-tabs__tab-list li" -} +} \ No newline at end of file diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json index 8e89c48638..cbfad3500e 100644 --- a/app/client/cypress/locators/DatasourcesEditor.json +++ b/app/client/cypress/locators/DatasourcesEditor.json @@ -8,6 +8,9 @@ "authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]", "sslAuthtype": "[data-cy=datasourceConfiguration\\.connection\\.ssl\\.authType]", "url": "input[name='datasourceConfiguration.url']", + "MongoDB": ".t--plugin-name:contains('MongoDB')", + "RESTAPI": ".t--plugin-name:contains('REST API')", + "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", "sectionAuthentication": "[data-cy=section-Authentication]", "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]" } diff --git a/app/client/cypress/locators/HomePage.json b/app/client/cypress/locators/HomePage.json index 9a0540cf8a..6b2e2e8822 100644 --- a/app/client/cypress/locators/HomePage.json +++ b/app/client/cypress/locators/HomePage.json @@ -8,5 +8,8 @@ "appMoreIcon":".bp3-popover-wrapper.more .bp3-popover-target", "deleteButton":".bp3-menu-item.bp3-popover-dismiss", "selectAction":"#Base", - "deleteApp":".bp3-menu-item" + "deleteApp":".bp3-menu-item", + "homeIcon": ".bp3-icon-home", + "inputAppName": "input[name=applicationName]", + "createNew": ".createnew" } \ No newline at end of file diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json index 021880366e..9cf0cb2779 100644 --- a/app/client/cypress/locators/apiWidgetslocator.json +++ b/app/client/cypress/locators/apiWidgetslocator.json @@ -2,11 +2,11 @@ "resourceUrl": ".t--dataSourceField", "searchApi": ".t--sidebar input[type=text]", "createapi": ".t--createBlankApiCard", - "apiTxt": "input[name=name]", + "apiTxt": ".t--nameOfApi input", "popover": ".bp3-popover-target >div>svg", "moveTo": ".single-select >div:contains('Move to')", "copyTo": ".single-select >div:contains('Copy to')", - "home": ".single-select >div:contains('Page1')", + "home": ".single-select >div:contains('Home')", "delete": ".single-select >div:contains('Delete')", "path": ".t--path >div textarea", "editResourceUrl": ".t--dataSourceField input", @@ -22,7 +22,7 @@ "addHeader": ".t--addApiHeader svg", "marketPlaceapi": ".t--eachProviderCard p", "addPageButton": ".t--addToPageBtn", - "apidocumentaionLink": ".linkStyles", + "apidocumentaionLink": ".t--apiDocumentationLink", "postbody": "(//div[contains(@class,'CodeMirror-wrap')]//textarea)[1]", "paginationTab": "li:contains('Pagination')", "apiInputTab": "li:contains('API Input')", @@ -33,4 +33,4 @@ "panigationPrevUrl": ".t--apiFormPaginationPrev div>textarea", "TestNextUrl": ".t--apiFormPaginationNextTest", "TestPreUrl": ".t--apiFormPaginationPrevTest" -} +} \ No newline at end of file diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index cc7dacd0f0..6ecead3c66 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -15,20 +15,16 @@ const dynamicInputLocators = require("../locators/DynamicInput.json"); let pageidcopy = " "; Cypress.Commands.add("CreateApp", appname => { - // cy.get(homePage.CreateApp) - cy.contains("Create New").click({ force: true }); - // .click({ force: true }); - cy.get("form input").type(appname); + cy.get(homePage.createNew) + .first() + .click({ force: true }); + cy.get(homePage.inputAppName).type(appname); cy.get(homePage.CreateApp) .contains("Submit") .click({ force: true }); cy.get("#loading").should("not.exist"); cy.wait("@getPropertyPane"); cy.get("@getPropertyPane").should("have.property", "status", 200); - cy.wait("@getDataSources"); - cy.get("@getDataSources").should("have.property", "status", 200); - cy.wait("@getUser"); - cy.get("@getUser").should("have.property", "status", 200); }); Cypress.Commands.add("DeleteApp", appName => { @@ -66,12 +62,64 @@ Cypress.Commands.add("LogintoApp", (uname, pword) => { 200, ); }); + +Cypress.Commands.add("LoginFromAPI", (uname, pword) => { + cy.request({ + method: "POST", + url: "api/v1/login", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + followRedirect: false, + form: true, + body: { + username: uname, + password: pword, + }, + }).then(response => { + expect(response.status).equal(302); + cy.log(response.body); + }); +}); + +Cypress.Commands.add("DeleteApp", appName => { + cy.get(commonlocators.homeIcon).click({ force: true }); + cy.get(homePage.searchInput).type(appName); + cy.wait(2000); + cy.get(homePage.appMoreIcon) + .first() + .click({ force: true }); + cy.get(homePage.deleteButton).click({ force: true }); +}); + +Cypress.Commands.add("Deletepage", Pagename => { + cy.get(pages.pagesIcon).click({ force: true }); + cy.get(".t--page-sidebar-" + Pagename + ""); + cy.get( + ".t--page-sidebar-" + + Pagename + + ">.t--page-sidebar-menu-actions>.bp3-popover-target", + ).click({ force: true }); + cy.get(pages.Menuaction).click({ force: true }); + cy.get(pages.Delete).click({ force: true }); + cy.wait(2000); +}); + Cypress.Commands.add("LogOut", () => { cy.request("POST", "/api/v1/logout").then(response => { expect(response.status).equal(200); }); }); +Cypress.Commands.add("NavigateToHome", () => { + cy.get(commonlocators.homeIcon).click({ force: true }); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); +}); + Cypress.Commands.add("NavigateToWidgets", pageName => { cy.get(pages.pagesIcon).click({ force: true }); cy.get(".t--page-sidebar-" + pageName + "") @@ -131,13 +179,18 @@ Cypress.Commands.add("CreateAPI", apiname => { .first() .click({ force: true }); cy.get(apiwidget.createapi).click({ force: true }); - cy.wait("@getUser"); + cy.wait("@createNewApi"); + //cy.wait("@getUser"); cy.get(apiwidget.resourceUrl).should("be.visible"); cy.get(apiwidget.apiTxt) .clear() .type(apiname) + .blur() .should("have.value", apiname); cy.WaitAutoSave(); + // Added because api name edit takes some time to + // reflect in api sidebar after the call passes. + cy.wait(4000); }); Cypress.Commands.add("CreateSubsequentAPI", apiname => { @@ -145,6 +198,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => { .first() .click({ force: true }); cy.get(apiwidget.resourceUrl).should("be.visible"); + // cy.get(ApiEditor.nameOfApi) cy.get(apiwidget.apiTxt) .clear() .type(apiname) @@ -153,7 +207,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => { }); Cypress.Commands.add("EditApiName", apiname => { - cy.wait("@getUser"); + //cy.wait("@getUser"); cy.get(apiwidget.apiTxt) .clear() .type(apiname) @@ -162,7 +216,8 @@ Cypress.Commands.add("EditApiName", apiname => { }); Cypress.Commands.add("WaitAutoSave", () => { - cy.wait("@saveQuery"); + //cy.wait("@saveQuery"); + // cy.wait("@postExecute"); }); Cypress.Commands.add("RunAPI", () => { @@ -203,9 +258,11 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => { .first() .click({ force: true }) .type(datasource); + /* cy.xpath(apiwidget.autoSuggest) .first() .click({ force: true }); + */ cy.get(apiwidget.editResourceUrl) .first() .click({ force: true }) @@ -325,7 +382,8 @@ Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => { .first() .click({ force: true }); cy.get(apiwidget.createapi).click({ force: true }); - cy.wait("@getUser"); + cy.wait("@createNewApi"); + // cy.wait("@getUser"); cy.get(apiwidget.resourceUrl).should("be.visible"); cy.get(apiwidget.apiTxt) .clear() @@ -477,7 +535,7 @@ Cypress.Commands.add( Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { cy.get(commonlocators.editWidgetName) .dblclick({ force: true }) - .type(text) + .type(text, { force: true }) .type("{enter}"); cy.get(inputcss) .first() @@ -1061,7 +1119,7 @@ Cypress.Commands.add("validateHTMLText", (widgetCss, htmlTag, value) => { Cypress.Commands.add("startServerAndRoutes", () => { cy.server(); - cy.route("GET", "/api/v1/applications").as("applications"); + cy.route("GET", "/api/v1/applications/new").as("applications"); cy.route("GET", "/api/v1/users/profile").as("getUser"); cy.route("GET", "/api/v1/plugins").as("getPlugins"); cy.route("POST", "/api/v1/logout").as("postLogout"); @@ -1229,7 +1287,7 @@ Cypress.Commands.add("callApi", apiname => { .first() .click(); cy.get(commonlocators.singleSelectMenuItem) - .contains("Call API") + .contains("Call An API") .click(); cy.get(commonlocators.selectMenuItem) .contains(apiname) diff --git a/app/client/cypress/support/index.js b/app/client/cypress/support/index.js index d8659123cb..ba3290c81f 100644 --- a/app/client/cypress/support/index.js +++ b/app/client/cypress/support/index.js @@ -20,11 +20,26 @@ let appId; // Import commands.js using ES2015 syntax: import "./commands"; + +Cypress.on("uncaught:exception", (err, runnable) => { + // returning false here prevents Cypress from + // failing the test + return false; +}); + before(function() { console.log("**** Got Cypress base URL as: ", process.env.CYPRESS_BASE_URL); cy.startServerAndRoutes(); cy.LogintoApp(loginData.username, loginData.password); - // cy.SearchApp(inputData.appname) + /* + cy.LoginFromAPI(loginData.username, loginData.password); + cy.visit("/applications"); + cy.wait("@applications").should( + "have.nested.property", + "response.body.responseMeta.status", + 200, + ); + */ cy.generateUUID().then(id => { appId = id; cy.CreateApp(id); diff --git a/app/client/src/actions/actionActions.ts b/app/client/src/actions/actionActions.ts index 83367a4a50..31e41b8fbb 100644 --- a/app/client/src/actions/actionActions.ts +++ b/app/client/src/actions/actionActions.ts @@ -158,6 +158,30 @@ export const executeApiActionSuccess = (payload: { payload: payload, }); +export const editApiName = (payload: { id: string; value: string }) => ({ + type: ReduxActionTypes.EDIT_API_NAME, + payload: payload, +}); + +export const saveApiName = (payload: { id: string; name: string }) => ({ + type: ReduxActionTypes.SAVE_API_NAME, + payload: payload, +}); + +export const updateApiNameDraft = (payload: { + id: string; + draft?: { + value: string; + validation: { + isValid: boolean; + validationMessage: string; + }; + }; +}) => ({ + type: ReduxActionTypes.UPDATE_API_NAME_DRAFT, + payload: payload, +}); + export default { createAction: createActionRequest, fetchActions, diff --git a/app/client/src/api/ActionAPI.tsx b/app/client/src/api/ActionAPI.tsx index 4968b6fb58..9317df0659 100644 --- a/app/client/src/api/ActionAPI.tsx +++ b/app/client/src/api/ActionAPI.tsx @@ -94,6 +94,13 @@ export interface CopyActionRequest { pageId: string; } +export interface UpdateActionNameRequest { + pageId: string; + layoutId: string; + newName: string; + oldName: string; +} + class ActionAPI extends API { static url = "v1/actions"; @@ -125,6 +132,10 @@ class ActionAPI extends API { return API.put(`${ActionAPI.url}/${apiConfig.id}`, apiConfig); } + static updateActionName(updateActionNameRequest: UpdateActionNameRequest) { + return API.put(ActionAPI.url + "/refactor", updateActionNameRequest); + } + static deleteAction(id: string) { return API.delete(`${ActionAPI.url}/${id}`); } diff --git a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx index 5d6b7b5571..49bc5a228b 100644 --- a/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx +++ b/app/client/src/components/designSystems/appsmith/ReactTableComponent.tsx @@ -216,8 +216,10 @@ export class ReactTableComponent extends React.Component< Cell: (props: any) => { return renderCell( props.cell.value, + props.cell.row.index, columnType.type, isHidden, + this.props.widgetId, columnType.format, ); }, @@ -232,7 +234,10 @@ export class ReactTableComponent extends React.Component< columns = this.reorderColumns(columns); if (this.props.columnActions?.length) { columns.push({ - Header: "Actions", + Header: + this.props.columnNameMap && this.props.columnNameMap["actions"] + ? this.props.columnNameMap["actions"] + : "Actions", accessor: "actions", width: 150, minWidth: 60, diff --git a/app/client/src/components/designSystems/appsmith/Table.tsx b/app/client/src/components/designSystems/appsmith/Table.tsx index 5b0f959c26..e7dcd6825c 100644 --- a/app/client/src/components/designSystems/appsmith/Table.tsx +++ b/app/client/src/components/designSystems/appsmith/Table.tsx @@ -59,6 +59,7 @@ export const Table = (props: TableProps) => { }), [], ); + const pageCount = Math.ceil(data.length / props.pageSize); const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; const { @@ -171,6 +172,8 @@ export const Table = (props: TableProps) => { })} ))} + {headerGroups.length === 0 && + renderEmptyRows(1, props.columns, props.width)}
{subPage.map((row, index) => { @@ -204,6 +207,12 @@ export const Table = (props: TableProps) => {
); })} + {props.pageSize > subPage.length && + renderEmptyRows( + props.pageSize - subPage.length, + props.columns, + props.width, + )} @@ -272,3 +281,44 @@ export const Table = (props: TableProps) => { }; export default Table; + +const renderEmptyRows = ( + rowCount: number, + columns: any, + tableWidth: number, +) => { + const rows: string[] = new Array(rowCount).fill(""); + const tableColumns = columns.length + ? columns + : new Array(3).fill({ width: tableWidth / 3 }); + return ( + + {rows.map((row: string, index: number) => { + return ( +
+ {tableColumns.map((column: any, colIndex: number) => { + return ( +
+ ); + })} +
+ ); + })} + + ); +}; diff --git a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx index af3cc27795..55e3baa22b 100644 --- a/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx +++ b/app/client/src/components/designSystems/appsmith/TableStyledWrappers.tsx @@ -11,10 +11,10 @@ export const TableWrapper = styled.div<{ width: number; height: number }>` justify-content: space-between; flex-direction: column; .tableWrap { + height: 100%; display: block; overflow-x: auto; overflow-y: hidden; - border-bottom: 1px solid ${Colors.GEYSER_LIGHT}; } .table { border-spacing: 0; @@ -29,16 +29,11 @@ export const TableWrapper = styled.div<{ width: number; height: number }>` height: ${props => props.height - 5 - 102}px; } .tr { - :last-child { - .td { - border-bottom: 0; - } - } :nth-child(even) { background: ${Colors.ATHENS_GRAY_DARKER}; } &.selected-row { - background: ${Colors.ATHENS_GRAY}; + background: ${Colors.POLAR}; } &:hover { background: ${Colors.ATHENS_GRAY}; @@ -237,7 +232,7 @@ export const CellWrapper = styled.div<{ isHidden: boolean }>` width: 100%; overflow: hidden; text-overflow: ellipsis; - white-space: normal; + white-space: nowrap; opacity: ${props => (props.isHidden ? "0.6" : "1")}; .image-cell { width: 40px; @@ -251,4 +246,11 @@ export const CellWrapper = styled.div<{ isHidden: boolean }>` video { border-radius: 4px; } + &.video-cell { + height: 100%; + iframe { + border: none; + border-radius: 4px; + } + } `; diff --git a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx index 1876dc5071..4adcd20eb2 100644 --- a/app/client/src/components/designSystems/appsmith/TableUtilities.tsx +++ b/app/client/src/components/designSystems/appsmith/TableUtilities.tsx @@ -8,6 +8,7 @@ import { } from "./TableStyledWrappers"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { ColumnMenuOptionProps } from "./ReactTableComponent"; +import { isString } from "lodash"; interface MenuOptionProps { columnAccessor?: string; @@ -272,8 +273,10 @@ export const getMenuOptions = (props: MenuOptionProps) => { export const renderCell = ( value: any, + rowIndex: number, columnType: string, isHidden: boolean, + widgetId: string, format?: string, ) => { if (!value) { @@ -281,6 +284,13 @@ export const renderCell = ( } switch (columnType) { case "image": + if (!isString(value)) { + return ( + +
Invalid Image
+
+ ); + } return ( {value @@ -302,13 +312,25 @@ export const renderCell = ( ); case "video": - return ( - - - + const youtubeRegex = new RegExp( + "^(https?://)?(www.)?(youtube.com|youtu.?be)/embed/.+$", ); + if (isString(value) && youtubeRegex.test(value)) { + return ( + + + + ); + } else { + return ( + Invalid Video Link + ); + } case "currency": if (!isNaN(value)) { return ( @@ -352,9 +374,11 @@ export const renderCell = ( return Invalid Time; } case "text": - return {value}; + const text = isString(value) ? value : JSON.stringify(value); + return {text}; default: - return {value}; + const data = isString(value) ? value : JSON.stringify(value); + return {data}; } }; diff --git a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx index ad69c4f190..03206a6636 100644 --- a/app/client/src/components/designSystems/blueprint/ModalComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ModalComponent.tsx @@ -12,7 +12,7 @@ const Container = styled.div<{ &&& { .${Classes.OVERLAY} { .${Classes.OVERLAY_BACKDROP} { - z-index: ${props => props.zIndex}; + z-index: ${props => props.zIndex || 2 - 1}; } position: fixed; top: ${props => props.theme.headerHeight}; @@ -32,7 +32,6 @@ const Container = styled.div<{ border-radius: ${props => props.theme.radii[1]}px; top: ${props => props.top}px; left: ${props => props.left}px; - // z-index: ${props => props.zIndex}; } } } @@ -80,7 +79,7 @@ export const ModalComponent = (props: ModalComponentProps) => { height={props.height} top={props.top} left={props.left} - zIndex={props.zIndex !== undefined ? props.zIndex : 3} + zIndex={props.zIndex !== undefined ? props.zIndex : 2} > div { + max-width: 100%; + flex: 0 1 auto; + font-size: ${props => props.theme.fontSizes[5]}px; + font-weight: ${props => props.theme.fontWeights[2]}; + } +`; + +export const ActionNameEditor = () => { + const params = useParams<{ apiId?: string; queryId?: string }>(); + const [forceUpdate, setForceUpdate] = useState(false); + const dispatch = useDispatch(); + if (!params.apiId && !params.queryId) { + console.log("No API id or Query id found in the url."); + } + + const actions: RestAction[] = useSelector((state: AppState) => + state.entities.actions.map(action => action.config), + ); + + const existingPageNames: string[] = useSelector((state: AppState) => + state.entities.pageList.pages.map((page: Page) => page.pageName), + ); + + const currentActionConfig: RestAction | undefined = actions.find( + action => action.id === params.apiId || action.id === params.queryId, + ); + + const existingWidgetNames: string[] = useSelector((state: AppState) => + Object.values(state.entities.canvasWidgets).map( + widget => widget.widgetName, + ), + ); + + const saveStatus: { + isSaving: boolean; + error: boolean; + } = useSelector((state: AppState) => { + const id = currentActionConfig ? currentActionConfig.id : ""; + return { + isSaving: state.ui.apiName.isSaving[id], + error: state.ui.apiName.errors[id], + }; + }); + + const hasActionNameConflict = (name: string) => + !( + existingPageNames.indexOf(name) === -1 && + actions.findIndex(action => action.name === name) === -1 && + existingWidgetNames.indexOf(name) === -1 + ); + + const isInvalidActionName = (name: string): string | boolean => { + if (!name || name.trim().length === 0) { + return "Please enter a valid name"; + } else if ( + name !== currentActionConfig?.name && + hasActionNameConflict(name) + ) { + return `${name} is already being used.`; + } + return false; + }; + + const handleAPINameChange = (name: string) => { + if ( + currentActionConfig && + name !== currentActionConfig?.name && + !isInvalidActionName(name) + ) { + dispatch(saveApiName({ id: currentActionConfig.id, name })); + } + }; + + useEffect(() => { + if (saveStatus.isSaving === false && saveStatus.error === true) { + setForceUpdate(true); + } else if (saveStatus.isSaving === true) { + setForceUpdate(false); + } + }, [saveStatus.isSaving, saveStatus.error]); + + return ( + +
+ + {saveStatus.isSaving && } +
+
+ ); +}; + +export default ActionNameEditor; diff --git a/app/client/src/components/editorComponents/Button.tsx b/app/client/src/components/editorComponents/Button.tsx index cb77b84bee..4bd9d3d302 100644 --- a/app/client/src/components/editorComponents/Button.tsx +++ b/app/client/src/components/editorComponents/Button.tsx @@ -50,7 +50,6 @@ const buttonStyles = css<{ } &.bp3-button { display: flex; - width: 100%; justify-content: ${props => props.skin === undefined ? "center" diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index ed35c0bc51..f2e1f7c238 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -4,24 +4,51 @@ import { Classes, } from "@blueprintjs/core"; import styled from "styled-components"; +import _ from "lodash"; +import Edit from "assets/images/EditPen.svg"; +import ErrorTooltip from "./ErrorTooltip"; + +export enum EditInteractionKind { + SINGLE, + DOUBLE, +} + type EditableTextProps = { type: "text" | "password" | "email" | "phone" | "date"; defaultValue: string; onTextChanged: (value: string) => void; - isEditing: boolean; placeholder: string; - onChange?: (value: string) => void; - value?: string; className?: string; + valueTransform?: (value: string) => string; + isEditingDefault?: boolean; + forceDefault?: boolean; + updating?: boolean; + isInvalid?: (value: string) => string | boolean; + editInteractionKind: EditInteractionKind; + hideEditIcon?: boolean; }; +const EditPen = styled.img` + width: 14px; + : hover { + cursor: pointer; + } +`; + const EditableTextWrapper = styled.div<{ isEditing: boolean }>` && { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; & .${Classes.EDITABLE_TEXT} { border: ${props => (props.isEditing ? "1px solid #ccc" : "none")}; cursor: pointer; - padding: 5px 10px; + padding: 5px 5px; text-transform: none; + flex: 1 0 100%; + max-width: 100%; + display: flex; &:before, &:after { display: none; @@ -29,34 +56,83 @@ const EditableTextWrapper = styled.div<{ isEditing: boolean }>` } & div.${Classes.EDITABLE_TEXT_INPUT} { text-transform: none; + width: 100%; } } `; +const TextContainer = styled.div<{ isValid: boolean }>` + display: flex; + &&&& .bp3-editable-text { + border-radius: 3px; + border-color: ${props => (props.isValid ? "hsl(0,0%,80%)" : "red")}; + } +`; + export const EditableText = (props: EditableTextProps) => { - const [isEditing, setIsEditing] = useState(props.isEditing); + const [isEditing, setIsEditing] = useState(!!props.isEditingDefault); + const [value, setValue] = useState(props.defaultValue); + useEffect(() => { - setIsEditing(props.isEditing); - }, [props.isEditing]); + setValue(props.defaultValue); + }, [props.defaultValue]); + + useEffect(() => { + if (props.forceDefault === true) setValue(props.defaultValue); + }, [props.forceDefault]); const edit = (e: any) => { setIsEditing(true); e.preventDefault(); e.stopPropagation(); }; - const onChange = (value: string) => { - props.onTextChanged(value); + const onChange = (_value: string) => { + const isInvalid = props.isInvalid ? props.isInvalid(_value) : false; + if (!isInvalid) { + props.onTextChanged(_value); + } else { + setValue(props.defaultValue); + } setIsEditing(false); }; + + const onInputchange = (_value: string) => { + let finalVal: string = _value; + if (props.valueTransform) { + finalVal = props.valueTransform(_value); + } + setValue(finalVal); + }; + + const errorMessage = props.isInvalid && props.isInvalid(value); + const error = errorMessage ? errorMessage : undefined; return ( - - + + + + + {!props.hideEditIcon && !props.updating && !isEditing && ( + + )} + + ); }; diff --git a/app/client/src/components/editorComponents/EntityNameComponent.tsx b/app/client/src/components/editorComponents/EntityNameComponent.tsx new file mode 100644 index 0000000000..e296f8ed6b --- /dev/null +++ b/app/client/src/components/editorComponents/EntityNameComponent.tsx @@ -0,0 +1,159 @@ +import styled from "styled-components"; +import React from "react"; + +import Edit from "assets/images/EditPen.svg"; +import ErrorTooltip from "components/editorComponents/ErrorTooltip"; +import { + FIELD_REQUIRED_ERROR, + VALID_FUNCTION_NAME_ERROR, + UNIQUE_NAME_ERROR, +} from "constants/messages"; + +const InputContainer = styled.div<{ focused: boolean; isValid: boolean }>` + align-items: center; + display: flex; + position: relative; + width: 250px; + input { + padding: 3px 6px; + margin-left: 10px; + transition: font-size 0.2s; + font-size: ${props => (props.focused ? "17px" : "18px")}; + border 1px solid; + border-radius: 3px; + border-color: ${props => { + let color = props.focused ? "hsl(0,0%,80%)" : "white"; + color = !props.isValid ? "red" : color; + return color; + }}; + display: block; + width: 100%; + font-weight: 200; + line-height: 24px; + text-overflow: ellipsis; + :hover { + border-color: hsl(0, 0 %, 80 %); + cursor: ${props => (props.focused ? "auto" : "pointer")}; + } + } +`; + +const EditPen = styled.img` + height: 14px; + width: 14px; + position: absolute; + right: 7px; + : hover { + cursor: pointer; + } +`; + +export function validateEntityName(name: string, allNames?: string[]) { + const validation = { + isValid: true, + validationMessage: "", + }; + + if (!/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(name)) { + validation.isValid = false; + validation.validationMessage += VALID_FUNCTION_NAME_ERROR; + } + if (!name) { + validation.isValid = false; + validation.validationMessage += FIELD_REQUIRED_ERROR; + } + + if ( + allNames && + allNames.findIndex(entityName => entityName === name) !== -1 + ) { + validation.isValid = false; + validation.validationMessage += UNIQUE_NAME_ERROR; + } + + return validation; +} + +interface EntityNameProps { + onBlur: Function; + onChange: (event: React.ChangeEvent) => void; + value: string; + isValid: boolean; + validationMessage?: string; + focusOnMount?: boolean; + readOnly?: boolean; + disabled?: boolean; + placeholder: string; +} + +interface EntityNameState { + focused: boolean; +} + +class EntityNameComponent extends React.Component< + EntityNameProps, + EntityNameState +> { + nameInput!: HTMLInputElement | null; + + constructor(props: EntityNameProps) { + super(props); + + this.state = { + focused: false, + }; + } + + handleFocus = (event: { target: { select: () => any } }) => { + event.target.select(); + }; + + onFocus = () => { + this.setState({ focused: true }); + }; + + onBlur = () => { + this.setState({ focused: false }); + this.props.onBlur(); + }; + + onPressEnter = (event: any) => { + event.preventDefault(); + event.target.blur(); + }; + + render() { + const { focused } = this.state; + const { + isValid, + validationMessage, + value, + placeholder, + onChange, + } = this.props; + + return ( + + + { + if (e.key === "Enter") { + this.onPressEnter(e); + } + }} + onFocus={this.onFocus} + onBlur={this.onBlur} + /> + {!focused && ( + + )} + + + ); + } +} + +export default EntityNameComponent; diff --git a/app/client/src/components/editorComponents/form/FieldError.tsx b/app/client/src/components/editorComponents/form/FieldError.tsx index 12d100bd41..d604ae46c0 100644 --- a/app/client/src/components/editorComponents/form/FieldError.tsx +++ b/app/client/src/components/editorComponents/form/FieldError.tsx @@ -17,11 +17,17 @@ const StyledError = styled.span<{ show: boolean }>` type FormFieldErrorProps = { error?: string; + className?: string; }; export const FormFieldError = (props: FormFieldErrorProps) => { return ( - {props.error || " "} + + {props.error || " "} + ); }; diff --git a/app/client/src/components/editorComponents/form/fields/SelectField.tsx b/app/client/src/components/editorComponents/form/fields/SelectField.tsx index 6663e5227c..7d0e3dfa50 100644 --- a/app/client/src/components/editorComponents/form/fields/SelectField.tsx +++ b/app/client/src/components/editorComponents/form/fields/SelectField.tsx @@ -4,7 +4,6 @@ import { WrappedFieldMetaProps, WrappedFieldInputProps, } from "redux-form"; -import FormFieldError from "components/editorComponents/form/FieldError"; import SelectComponent from "components/editorComponents/SelectComponent"; const renderComponent = ( diff --git a/app/client/src/components/propertyControls/InputTextControl.tsx b/app/client/src/components/propertyControls/InputTextControl.tsx index 9faf5881b6..31f7bc237b 100644 --- a/app/client/src/components/propertyControls/InputTextControl.tsx +++ b/app/client/src/components/propertyControls/InputTextControl.tsx @@ -50,13 +50,13 @@ export function InputText(props: { class InputTextControl extends BaseControl { render() { const { - errorMessage, expected, propertyValue, isValid, label, placeholderText, dataTreePath, + validationMessage, } = this.props; return ( { value={propertyValue} onChange={this.onTextChange} isValid={isValid} - errorMessage={errorMessage} + errorMessage={validationMessage} expected={expected} placeholder={placeholderText} dataTreePath={dataTreePath} @@ -101,6 +101,7 @@ class InputTextControl extends BaseControl { export interface InputControlProps extends ControlProps { placeholderText: string; inputType: InputType; + validationMessage?: string; isDisabled?: boolean; } diff --git a/app/client/src/constants/ReduxActionConstants.tsx b/app/client/src/constants/ReduxActionConstants.tsx index 7f5a0e8537..b37c846d04 100644 --- a/app/client/src/constants/ReduxActionConstants.tsx +++ b/app/client/src/constants/ReduxActionConstants.tsx @@ -234,6 +234,10 @@ export const ReduxActionTypes: { [key: string]: string } = { CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR", SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT", SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN", + EDIT_API_NAME: "EDIT_API_NAME", + SAVE_API_NAME: "SAVE_API_NAME", + SAVE_API_NAME_SUCCESS: "SAVE_API_NAME_SUCCESS", + UPDATE_API_NAME_DRAFT: "UPDATE_API_NAME_DRAFT", }; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes]; @@ -314,6 +318,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = { CREATE_MODAL_ERROR: "CREATE_MODAL_ERROR", FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR: "FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR", + SAVE_API_NAME_ERROR: "SAVE_API_NAME_ERROR", FETCH_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR", FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR", FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR", diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index 9c185cbda1..8dbc8b9ef0 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -11,7 +11,6 @@ export const VALIDATION_TYPES = { ARRAY: "ARRAY", TABLE_DATA: "TABLE_DATA", OPTIONS_DATA: "OPTIONS_DATA", - SINGLE_CHART_DATA: "SINGLE_CHART_DATA", DATE: "DATE", TABS_DATA: "TABS_DATA", CHART_DATA: "CHART_DATA", diff --git a/app/client/src/constants/messages.ts b/app/client/src/constants/messages.ts index d03030d7fa..fbdb2ce97b 100644 --- a/app/client/src/constants/messages.ts +++ b/app/client/src/constants/messages.ts @@ -7,6 +7,7 @@ export const FIELD_REQUIRED_ERROR = "This field is required"; export const VALID_FUNCTION_NAME_ERROR = "Must be a valid variable name (camelCase)"; export const UNIQUE_NAME_ERROR = "Name must be unique"; +export const NAME_SPACE_ERROR = "Name must not have spaces"; export const FORM_VALIDATION_EMPTY_EMAIL = "Please enter an email"; export const FORM_VALIDATION_INVALID_EMAIL = diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 4b0be58293..f1c7448497 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -36,9 +36,7 @@ import { } from "actions/metaActions"; import AppRoute from "pages/common/AppRoute"; import { editorInitializer } from "utils/EditorUtils"; -import { getCurrentOrg } from "selectors/organizationSelectors"; import { PERMISSION_TYPE } from "pages/Applications/permissionHelpers"; -import { Organization } from "constants/orgConstants"; const AppViewWrapper = styled.div` margin-top: ${props => props.theme.headerHeight}; diff --git a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx index 71fda01e83..1f616b9899 100644 --- a/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx +++ b/app/client/src/pages/AppViewer/viewer/AppViewerHeader.tsx @@ -12,6 +12,10 @@ const HeaderWrapper = styled(StyledHeader)` display: flex; justify-content: flex-end; `; + +const StyledButton = styled(Button)` + max-width: 200px; +`; type AppViewerHeaderProps = { url?: string; permissionRequired: string; @@ -27,7 +31,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => { return ( {props.url && hasPermission && ( -