merge 'release' into 'fix/minor-bugs'

This commit is contained in:
Tejaaswini 2020-06-18 17:46:46 +05:30
parent 16606a45a2
commit 0ffff9db4f
76 changed files with 1251 additions and 475 deletions

View File

@ -1,5 +1,5 @@
{ {
"baseUrl": "https://mock-api.appsmith.com", "baseUrl": "https://mock-api.appsmith.com/",
"methods": "users", "methods": "users",
"headerKey": "Content-Type", "headerKey": "Content-Type",
"headerValue": "application/json", "headerValue": "application/json",
@ -12,14 +12,14 @@
"responsetext": "Roger Brickelberry", "responsetext": "Roger Brickelberry",
"pageResponsetext": "Josh M Krantz", "pageResponsetext": "Josh M Krantz",
"apiname": "SecondAPI", "apiname": "SecondAPI",
"baseUrl2": "https://reqres.in", "baseUrl2": "https://reqres.in/",
"methods1": "api/users/1", "methods1": "api/users/1",
"responsetext2": "qui est esse", "responsetext2": "qui est esse",
"baseUrl3": "https://reqres.in", "baseUrl3": "https://reqres.in/",
"methods2": "api/users/2", "methods2": "api/users/2",
"invalidPath": "api/users/a", "invalidPath": "api/users/a",
"responsetext3": "Josh M Krantz", "responsetext3": "Josh M Krantz",
"postUrl": "https://reqres.in", "postUrl": "https://reqres.in/",
"deleteUrl": "", "deleteUrl": "",
"Post": "POST", "Post": "POST",
"Delete": "DELETE", "Delete": "DELETE",

View File

@ -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");
});
});

View File

@ -18,13 +18,10 @@ describe("Test curl import flow", function() {
response.response.body.data.name, response.response.body.data.name,
); );
}); });
cy.WaitAutoSave(); // cy.WaitAutoSave();
cy.RunAPI(); cy.RunAPI();
cy.get(ApiEditor.formActionButtons).should("be.visible"); 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).click();
cy.get(ApiEditor.ApiDeleteBtn).should("be.disabled"); cy.get(ApiEditor.ApiDeleteBtn).should("be.disabled");
cy.testDeleteApi(); cy.testDeleteApi();

View File

@ -6,7 +6,7 @@ describe("Test curl import flow", function() {
cy.NavigateToApiEditor(); cy.NavigateToApiEditor();
cy.get(ApiEditor.curlImage).click({ force: true }); cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type( 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, force: true,
parseSpecialCharSequences: false, parseSpecialCharSequences: false,

View File

@ -6,8 +6,10 @@ describe("API Panel Test Functionality ", function() {
cy.NavigateToAPI_Panel(); cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful"); cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI"); cy.CreateAPI("FirstAPI");
cy.RunAPI();
cy.log("Creation of FirstAPI Action successful"); cy.log("Creation of FirstAPI Action successful");
cy.CreateAPI("SecondAPI"); cy.CreateAPI("SecondAPI");
cy.RunAPI();
cy.log("Creation of SecondAPI Action successful"); cy.log("Creation of SecondAPI Action successful");
cy.SearchAPI("SecondAPI", "FirstAPI"); cy.SearchAPI("SecondAPI", "FirstAPI");
}); });

View File

@ -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");
});
});

View File

@ -3,11 +3,13 @@ describe("API Panel Test Functionality ", function() {
cy.log("Login Successful"); cy.log("Login Successful");
cy.NavigateToAPI_Panel(); cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful"); cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI"); cy.CreateAPI("FirstAPI");
cy.log("Creation of FirstAPI Action successful"); cy.log("Creation of FirstAPI Action successful");
cy.CopyAPIToHome("FirstAPI"); cy.CopyAPIToHome("FirstAPI");
cy.MoveAPIToPage();
cy.DeleteAPI("FirstAPI"); cy.DeleteAPI("FirstAPI");
//cy.MoveAPIToPage();
cy.CreateAPI("FirstAPI"); cy.CreateAPI("FirstAPI");
cy.log("Creation of FirstAPI Action successful"); cy.log("Creation of FirstAPI Action successful");
cy.CreationOfUniqueAPIcheck("FirstAPI"); cy.CreationOfUniqueAPIcheck("FirstAPI");

View File

@ -8,13 +8,11 @@ describe("API Panel Test Functionality ", function() {
cy.wait("@getCategories"); cy.wait("@getCategories");
cy.wait("@getTemplateCollections"); cy.wait("@getTemplateCollections");
cy.wait("@get3PProviders"); cy.wait("@get3PProviders");
cy.wait("@getUser");
cy.log("Navigation to API Panel screen successful"); cy.log("Navigation to API Panel screen successful");
cy.get(apiwidget.marketPlaceapi) cy.get(apiwidget.marketPlaceapi)
.first() .first()
.click(); .click();
cy.wait("@get3PProviderTemplates"); cy.wait("@get3PProviderTemplates");
cy.wait("@getUser");
cy.get(".apiName") cy.get(".apiName")
.first() .first()
.invoke("text") .invoke("text")
@ -24,7 +22,6 @@ describe("API Panel Test Functionality ", function() {
.click(); .click();
const searchApiName = ApiName.replace(/\s/g, ""); const searchApiName = ApiName.replace(/\s/g, "");
cy.log(searchApiName); cy.log(searchApiName);
cy.wait("@add3PApiToPage");
cy.wait("@getActions"); cy.wait("@getActions");
cy.SearchAPIandClick(searchApiName); cy.SearchAPIandClick(searchApiName);
cy.get(apiwidget.apidocumentaionLink) cy.get(apiwidget.apidocumentaionLink)

View File

@ -1,20 +1,12 @@
const datasource = require("../../../locators/DatasourcesEditor.json");
let pageid;
describe("Create, test, save then delete a mongo datasource", function() { describe("Create, test, save then delete a mongo datasource", function() {
it("Create, test, save then delete a mongo datasource", function() { it("Create, test, save then delete a mongo datasource", function() {
cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
cy.get("@getPlugins").then(httpResponse => { cy.get(datasource.MongoDB).click();
const pluginName = httpResponse.response.body.data.find(
plugin => plugin.packageName === "mongo-plugin",
).name;
cy.get(".t--plugin-name")
.contains(pluginName)
.click();
});
cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();
cy.fillMongoDatasourceForm(); cy.fillMongoDatasourceForm();
cy.testSaveDeleteDatasource(); cy.testSaveDeleteDatasource();
}); });
}); });

View File

@ -1,20 +1,11 @@
const datasource = require("../../../locators/DatasourcesEditor.json");
describe("Create, test, save then delete a postgres datasource", function() { describe("Create, test, save then delete a postgres datasource", function() {
it("Create, test, save then delete a postgres datasource", function() { it("Create, test, save then delete a postgres datasource", function() {
cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
cy.get("@getPlugins").then(httpResponse => { cy.get(datasource.PostgreSQL).click();
const pluginName = httpResponse.response.body.data.find(
plugin => plugin.packageName === "postgres-plugin",
).name;
cy.get(".t--plugin-name")
.contains(pluginName)
.click();
});
cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();
cy.fillPostgresDatasourceForm(); cy.fillPostgresDatasourceForm();
cy.testSaveDeleteDatasource(); cy.testSaveDeleteDatasource();
}); });
}); });

View File

@ -4,20 +4,9 @@ const datasourceFormData = require("../../../fixtures/datasources.json");
describe("Create, test, save then delete a restapi datasource", function() { describe("Create, test, save then delete a restapi datasource", function() {
it("Create, test, save then delete a restapi datasource", function() { it("Create, test, save then delete a restapi datasource", function() {
cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
cy.get("@getPlugins").then(httpResponse => { cy.get(datasourceEditor.RESTAPI).click();
const pluginName = httpResponse.response.body.data.find(
plugin => plugin.packageName === "restapi-plugin",
).name;
cy.get(".t--plugin-name")
.contains(pluginName)
.click();
});
cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();
cy.get(datasourceEditor.url).type(datasourceFormData["restapi-url"]); cy.get(datasourceEditor.url).type(datasourceFormData["restapi-url"]);
cy.testSaveDeleteDatasource(); cy.testSaveDeleteDatasource();
}); });
}); });

View File

@ -1,18 +1,11 @@
const queryLocators = require("../../../locators/QueryEditor.json"); const queryLocators = require("../../../locators/QueryEditor.json");
const plugins = require("../../../fixtures/plugins.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() { 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() { it("Create a query with a mongo datasource, run, save and then delete the query", function() {
cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
cy.get("@getPlugins").then(httpResponse => { cy.get(datasource.MongoDB).click();
const pluginName = httpResponse.response.body.data.find(
plugin => plugin.packageName === plugins.mongoPackageName,
).name;
cy.get(".t--plugin-name")
.contains(pluginName)
.click();
});
cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();

View File

@ -1,17 +1,10 @@
const queryLocators = require("../../../locators/QueryEditor.json"); 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() { 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() { it("Create a query with a postgres datasource, run, save and then delete the query", function() {
cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
cy.get("@getPlugins").then(httpResponse => { cy.get(datasource.PostgreSQL).click();
const pluginName = httpResponse.response.body.data.find(
plugin => plugin.packageName === "postgres-plugin",
).name;
cy.get(".t--plugin-name")
.contains(pluginName)
.click();
});
cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -4,7 +4,7 @@
"createBlankApiCard": ".t--createBlankApiCard", "createBlankApiCard": ".t--createBlankApiCard",
"eachProviderCard": ".t--eachProviderCard", "eachProviderCard": ".t--eachProviderCard",
"nameOfApi": ".t--nameOfApi", "nameOfApi": ".t--nameOfApi",
"ApiNameField":"input[name='name']", "ApiNameField": ".t--nameOfApi input",
"addToPageBtn": ".t--addToPageBtn", "addToPageBtn": ".t--addToPageBtn",
"ApiDeleteBtn": ".t--apiFormDeleteBtn", "ApiDeleteBtn": ".t--apiFormDeleteBtn",
"ApiRunBtn": ".t--apiFormRunBtn", "ApiRunBtn": ".t--apiFormRunBtn",

View File

@ -8,6 +8,9 @@
"authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]", "authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]",
"sslAuthtype": "[data-cy=datasourceConfiguration\\.connection\\.ssl\\.authType]", "sslAuthtype": "[data-cy=datasourceConfiguration\\.connection\\.ssl\\.authType]",
"url": "input[name='datasourceConfiguration.url']", "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]", "sectionAuthentication": "[data-cy=section-Authentication]",
"sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]" "sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]"
} }

View File

@ -8,5 +8,8 @@
"appMoreIcon":".bp3-popover-wrapper.more .bp3-popover-target", "appMoreIcon":".bp3-popover-wrapper.more .bp3-popover-target",
"deleteButton":".bp3-menu-item.bp3-popover-dismiss", "deleteButton":".bp3-menu-item.bp3-popover-dismiss",
"selectAction":"#Base", "selectAction":"#Base",
"deleteApp":".bp3-menu-item" "deleteApp":".bp3-menu-item",
"homeIcon": ".bp3-icon-home",
"inputAppName": "input[name=applicationName]",
"createNew": ".createnew"
} }

View File

@ -2,11 +2,11 @@
"resourceUrl": ".t--dataSourceField", "resourceUrl": ".t--dataSourceField",
"searchApi": ".t--sidebar input[type=text]", "searchApi": ".t--sidebar input[type=text]",
"createapi": ".t--createBlankApiCard", "createapi": ".t--createBlankApiCard",
"apiTxt": "input[name=name]", "apiTxt": ".t--nameOfApi input",
"popover": ".bp3-popover-target >div>svg", "popover": ".bp3-popover-target >div>svg",
"moveTo": ".single-select >div:contains('Move to')", "moveTo": ".single-select >div:contains('Move to')",
"copyTo": ".single-select >div:contains('Copy 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')", "delete": ".single-select >div:contains('Delete')",
"path": ".t--path >div textarea", "path": ".t--path >div textarea",
"editResourceUrl": ".t--dataSourceField input", "editResourceUrl": ".t--dataSourceField input",
@ -22,7 +22,7 @@
"addHeader": ".t--addApiHeader svg", "addHeader": ".t--addApiHeader svg",
"marketPlaceapi": ".t--eachProviderCard p", "marketPlaceapi": ".t--eachProviderCard p",
"addPageButton": ".t--addToPageBtn", "addPageButton": ".t--addToPageBtn",
"apidocumentaionLink": ".linkStyles", "apidocumentaionLink": ".t--apiDocumentationLink",
"postbody": "(//div[contains(@class,'CodeMirror-wrap')]//textarea)[1]", "postbody": "(//div[contains(@class,'CodeMirror-wrap')]//textarea)[1]",
"paginationTab": "li:contains('Pagination')", "paginationTab": "li:contains('Pagination')",
"apiInputTab": "li:contains('API Input')", "apiInputTab": "li:contains('API Input')",

View File

@ -15,20 +15,16 @@ const dynamicInputLocators = require("../locators/DynamicInput.json");
let pageidcopy = " "; let pageidcopy = " ";
Cypress.Commands.add("CreateApp", appname => { Cypress.Commands.add("CreateApp", appname => {
// cy.get(homePage.CreateApp) cy.get(homePage.createNew)
cy.contains("Create New").click({ force: true }); .first()
// .click({ force: true }); .click({ force: true });
cy.get("form input").type(appname); cy.get(homePage.inputAppName).type(appname);
cy.get(homePage.CreateApp) cy.get(homePage.CreateApp)
.contains("Submit") .contains("Submit")
.click({ force: true }); .click({ force: true });
cy.get("#loading").should("not.exist"); cy.get("#loading").should("not.exist");
cy.wait("@getPropertyPane"); cy.wait("@getPropertyPane");
cy.get("@getPropertyPane").should("have.property", "status", 200); 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 => { Cypress.Commands.add("DeleteApp", appName => {
@ -66,12 +62,64 @@ Cypress.Commands.add("LogintoApp", (uname, pword) => {
200, 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", () => { Cypress.Commands.add("LogOut", () => {
cy.request("POST", "/api/v1/logout").then(response => { cy.request("POST", "/api/v1/logout").then(response => {
expect(response.status).equal(200); 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 => { Cypress.Commands.add("NavigateToWidgets", pageName => {
cy.get(pages.pagesIcon).click({ force: true }); cy.get(pages.pagesIcon).click({ force: true });
cy.get(".t--page-sidebar-" + pageName + "") cy.get(".t--page-sidebar-" + pageName + "")
@ -131,13 +179,18 @@ Cypress.Commands.add("CreateAPI", apiname => {
.first() .first()
.click({ force: true }); .click({ force: true });
cy.get(apiwidget.createapi).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.resourceUrl).should("be.visible");
cy.get(apiwidget.apiTxt) cy.get(apiwidget.apiTxt)
.clear() .clear()
.type(apiname) .type(apiname)
.blur()
.should("have.value", apiname); .should("have.value", apiname);
cy.WaitAutoSave(); 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 => { Cypress.Commands.add("CreateSubsequentAPI", apiname => {
@ -145,6 +198,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => {
.first() .first()
.click({ force: true }); .click({ force: true });
cy.get(apiwidget.resourceUrl).should("be.visible"); cy.get(apiwidget.resourceUrl).should("be.visible");
// cy.get(ApiEditor.nameOfApi)
cy.get(apiwidget.apiTxt) cy.get(apiwidget.apiTxt)
.clear() .clear()
.type(apiname) .type(apiname)
@ -153,7 +207,7 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => {
}); });
Cypress.Commands.add("EditApiName", apiname => { Cypress.Commands.add("EditApiName", apiname => {
cy.wait("@getUser"); //cy.wait("@getUser");
cy.get(apiwidget.apiTxt) cy.get(apiwidget.apiTxt)
.clear() .clear()
.type(apiname) .type(apiname)
@ -162,7 +216,8 @@ Cypress.Commands.add("EditApiName", apiname => {
}); });
Cypress.Commands.add("WaitAutoSave", () => { Cypress.Commands.add("WaitAutoSave", () => {
cy.wait("@saveQuery"); //cy.wait("@saveQuery");
// cy.wait("@postExecute");
}); });
Cypress.Commands.add("RunAPI", () => { Cypress.Commands.add("RunAPI", () => {
@ -203,9 +258,11 @@ Cypress.Commands.add("enterDatasourceAndPath", (datasource, path) => {
.first() .first()
.click({ force: true }) .click({ force: true })
.type(datasource); .type(datasource);
/*
cy.xpath(apiwidget.autoSuggest) cy.xpath(apiwidget.autoSuggest)
.first() .first()
.click({ force: true }); .click({ force: true });
*/
cy.get(apiwidget.editResourceUrl) cy.get(apiwidget.editResourceUrl)
.first() .first()
.click({ force: true }) .click({ force: true })
@ -325,7 +382,8 @@ Cypress.Commands.add("CreationOfUniqueAPIcheck", apiname => {
.first() .first()
.click({ force: true }); .click({ force: true });
cy.get(apiwidget.createapi).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.resourceUrl).should("be.visible");
cy.get(apiwidget.apiTxt) cy.get(apiwidget.apiTxt)
.clear() .clear()
@ -477,7 +535,7 @@ Cypress.Commands.add(
Cypress.Commands.add("widgetText", (text, inputcss, innercss) => { Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
cy.get(commonlocators.editWidgetName) cy.get(commonlocators.editWidgetName)
.dblclick({ force: true }) .dblclick({ force: true })
.type(text) .type(text, { force: true })
.type("{enter}"); .type("{enter}");
cy.get(inputcss) cy.get(inputcss)
.first() .first()
@ -1061,7 +1119,7 @@ Cypress.Commands.add("validateHTMLText", (widgetCss, htmlTag, value) => {
Cypress.Commands.add("startServerAndRoutes", () => { Cypress.Commands.add("startServerAndRoutes", () => {
cy.server(); 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/users/profile").as("getUser");
cy.route("GET", "/api/v1/plugins").as("getPlugins"); cy.route("GET", "/api/v1/plugins").as("getPlugins");
cy.route("POST", "/api/v1/logout").as("postLogout"); cy.route("POST", "/api/v1/logout").as("postLogout");
@ -1229,7 +1287,7 @@ Cypress.Commands.add("callApi", apiname => {
.first() .first()
.click(); .click();
cy.get(commonlocators.singleSelectMenuItem) cy.get(commonlocators.singleSelectMenuItem)
.contains("Call API") .contains("Call An API")
.click(); .click();
cy.get(commonlocators.selectMenuItem) cy.get(commonlocators.selectMenuItem)
.contains(apiname) .contains(apiname)

View File

@ -20,11 +20,26 @@ let appId;
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import "./commands"; import "./commands";
Cypress.on("uncaught:exception", (err, runnable) => {
// returning false here prevents Cypress from
// failing the test
return false;
});
before(function() { before(function() {
console.log("**** Got Cypress base URL as: ", process.env.CYPRESS_BASE_URL); console.log("**** Got Cypress base URL as: ", process.env.CYPRESS_BASE_URL);
cy.startServerAndRoutes(); cy.startServerAndRoutes();
cy.LogintoApp(loginData.username, loginData.password); 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 => { cy.generateUUID().then(id => {
appId = id; appId = id;
cy.CreateApp(id); cy.CreateApp(id);

View File

@ -158,6 +158,30 @@ export const executeApiActionSuccess = (payload: {
payload: 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 { export default {
createAction: createActionRequest, createAction: createActionRequest,
fetchActions, fetchActions,

View File

@ -94,6 +94,13 @@ export interface CopyActionRequest {
pageId: string; pageId: string;
} }
export interface UpdateActionNameRequest {
pageId: string;
layoutId: string;
newName: string;
oldName: string;
}
class ActionAPI extends API { class ActionAPI extends API {
static url = "v1/actions"; static url = "v1/actions";
@ -125,6 +132,10 @@ class ActionAPI extends API {
return API.put(`${ActionAPI.url}/${apiConfig.id}`, apiConfig); return API.put(`${ActionAPI.url}/${apiConfig.id}`, apiConfig);
} }
static updateActionName(updateActionNameRequest: UpdateActionNameRequest) {
return API.put(ActionAPI.url + "/refactor", updateActionNameRequest);
}
static deleteAction(id: string) { static deleteAction(id: string) {
return API.delete(`${ActionAPI.url}/${id}`); return API.delete(`${ActionAPI.url}/${id}`);
} }

View File

@ -216,8 +216,10 @@ export class ReactTableComponent extends React.Component<
Cell: (props: any) => { Cell: (props: any) => {
return renderCell( return renderCell(
props.cell.value, props.cell.value,
props.cell.row.index,
columnType.type, columnType.type,
isHidden, isHidden,
this.props.widgetId,
columnType.format, columnType.format,
); );
}, },
@ -232,7 +234,10 @@ export class ReactTableComponent extends React.Component<
columns = this.reorderColumns(columns); columns = this.reorderColumns(columns);
if (this.props.columnActions?.length) { if (this.props.columnActions?.length) {
columns.push({ columns.push({
Header: "Actions", Header:
this.props.columnNameMap && this.props.columnNameMap["actions"]
? this.props.columnNameMap["actions"]
: "Actions",
accessor: "actions", accessor: "actions",
width: 150, width: 150,
minWidth: 60, minWidth: 60,

View File

@ -59,6 +59,7 @@ export const Table = (props: TableProps) => {
}), }),
[], [],
); );
const pageCount = Math.ceil(data.length / props.pageSize); const pageCount = Math.ceil(data.length / props.pageSize);
const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0; const currentPageIndex = props.pageNo < pageCount ? props.pageNo : 0;
const { const {
@ -171,6 +172,8 @@ export const Table = (props: TableProps) => {
})} })}
</div> </div>
))} ))}
{headerGroups.length === 0 &&
renderEmptyRows(1, props.columns, props.width)}
</div> </div>
<div {...getTableBodyProps()} className="tbody"> <div {...getTableBodyProps()} className="tbody">
{subPage.map((row, index) => { {subPage.map((row, index) => {
@ -204,6 +207,12 @@ export const Table = (props: TableProps) => {
</div> </div>
); );
})} })}
{props.pageSize > subPage.length &&
renderEmptyRows(
props.pageSize - subPage.length,
props.columns,
props.width,
)}
</div> </div>
</div> </div>
</div> </div>
@ -272,3 +281,44 @@ export const Table = (props: TableProps) => {
}; };
export default Table; 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 (
<React.Fragment>
{rows.map((row: string, index: number) => {
return (
<div
className="tr"
key={index}
style={{
display: "flex",
flex: "1 0 auto",
}}
>
{tableColumns.map((column: any, colIndex: number) => {
return (
<div
key={colIndex}
className="td"
style={{
width: column.width + "px",
boxSizing: "border-box",
flex: `${column.width} 0 auto`,
}}
/>
);
})}
</div>
);
})}
</React.Fragment>
);
};

View File

@ -11,10 +11,10 @@ export const TableWrapper = styled.div<{ width: number; height: number }>`
justify-content: space-between; justify-content: space-between;
flex-direction: column; flex-direction: column;
.tableWrap { .tableWrap {
height: 100%;
display: block; display: block;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
border-bottom: 1px solid ${Colors.GEYSER_LIGHT};
} }
.table { .table {
border-spacing: 0; border-spacing: 0;
@ -29,16 +29,11 @@ export const TableWrapper = styled.div<{ width: number; height: number }>`
height: ${props => props.height - 5 - 102}px; height: ${props => props.height - 5 - 102}px;
} }
.tr { .tr {
:last-child {
.td {
border-bottom: 0;
}
}
:nth-child(even) { :nth-child(even) {
background: ${Colors.ATHENS_GRAY_DARKER}; background: ${Colors.ATHENS_GRAY_DARKER};
} }
&.selected-row { &.selected-row {
background: ${Colors.ATHENS_GRAY}; background: ${Colors.POLAR};
} }
&:hover { &:hover {
background: ${Colors.ATHENS_GRAY}; background: ${Colors.ATHENS_GRAY};
@ -237,7 +232,7 @@ export const CellWrapper = styled.div<{ isHidden: boolean }>`
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: normal; white-space: nowrap;
opacity: ${props => (props.isHidden ? "0.6" : "1")}; opacity: ${props => (props.isHidden ? "0.6" : "1")};
.image-cell { .image-cell {
width: 40px; width: 40px;
@ -251,4 +246,11 @@ export const CellWrapper = styled.div<{ isHidden: boolean }>`
video { video {
border-radius: 4px; border-radius: 4px;
} }
&.video-cell {
height: 100%;
iframe {
border: none;
border-radius: 4px;
}
}
`; `;

View File

@ -8,6 +8,7 @@ import {
} from "./TableStyledWrappers"; } from "./TableStyledWrappers";
import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl";
import { ColumnMenuOptionProps } from "./ReactTableComponent"; import { ColumnMenuOptionProps } from "./ReactTableComponent";
import { isString } from "lodash";
interface MenuOptionProps { interface MenuOptionProps {
columnAccessor?: string; columnAccessor?: string;
@ -272,8 +273,10 @@ export const getMenuOptions = (props: MenuOptionProps) => {
export const renderCell = ( export const renderCell = (
value: any, value: any,
rowIndex: number,
columnType: string, columnType: string,
isHidden: boolean, isHidden: boolean,
widgetId: string,
format?: string, format?: string,
) => { ) => {
if (!value) { if (!value) {
@ -281,6 +284,13 @@ export const renderCell = (
} }
switch (columnType) { switch (columnType) {
case "image": case "image":
if (!isString(value)) {
return (
<CellWrapper isHidden={isHidden}>
<div>Invalid Image </div>
</CellWrapper>
);
}
return ( return (
<CellWrapper isHidden={isHidden}> <CellWrapper isHidden={isHidden}>
{value {value
@ -302,13 +312,25 @@ export const renderCell = (
</CellWrapper> </CellWrapper>
); );
case "video": case "video":
const youtubeRegex = new RegExp(
"^(https?://)?(www.)?(youtube.com|youtu.?be)/embed/.+$",
);
if (isString(value) && youtubeRegex.test(value)) {
return ( return (
<CellWrapper isHidden={isHidden}> <CellWrapper isHidden={isHidden} className="video-cell">
<video width="56" height="32" autoPlay={false}> <iframe
<source src={`${value}`} type="video/mp4" /> title={`video-${widgetId}-${rowIndex}`}
</video> width="56"
height="32"
src={`${value}`}
></iframe>
</CellWrapper> </CellWrapper>
); );
} else {
return (
<CellWrapper isHidden={isHidden}>Invalid Video Link</CellWrapper>
);
}
case "currency": case "currency":
if (!isNaN(value)) { if (!isNaN(value)) {
return ( return (
@ -352,9 +374,11 @@ export const renderCell = (
return <CellWrapper isHidden={isHidden}>Invalid Time</CellWrapper>; return <CellWrapper isHidden={isHidden}>Invalid Time</CellWrapper>;
} }
case "text": case "text":
return <CellWrapper isHidden={isHidden}>{value}</CellWrapper>; const text = isString(value) ? value : JSON.stringify(value);
return <CellWrapper isHidden={isHidden}>{text}</CellWrapper>;
default: default:
return <CellWrapper isHidden={isHidden}>{value}</CellWrapper>; const data = isString(value) ? value : JSON.stringify(value);
return <CellWrapper isHidden={isHidden}>{data}</CellWrapper>;
} }
}; };

View File

@ -12,7 +12,7 @@ const Container = styled.div<{
&&& { &&& {
.${Classes.OVERLAY} { .${Classes.OVERLAY} {
.${Classes.OVERLAY_BACKDROP} { .${Classes.OVERLAY_BACKDROP} {
z-index: ${props => props.zIndex}; z-index: ${props => props.zIndex || 2 - 1};
} }
position: fixed; position: fixed;
top: ${props => props.theme.headerHeight}; top: ${props => props.theme.headerHeight};
@ -32,7 +32,6 @@ const Container = styled.div<{
border-radius: ${props => props.theme.radii[1]}px; border-radius: ${props => props.theme.radii[1]}px;
top: ${props => props.top}px; top: ${props => props.top}px;
left: ${props => props.left}px; left: ${props => props.left}px;
// z-index: ${props => props.zIndex};
} }
} }
} }
@ -80,7 +79,7 @@ export const ModalComponent = (props: ModalComponentProps) => {
height={props.height} height={props.height}
top={props.top} top={props.top}
left={props.left} left={props.left}
zIndex={props.zIndex !== undefined ? props.zIndex : 3} zIndex={props.zIndex !== undefined ? props.zIndex : 2}
> >
<Overlay <Overlay
isOpen={props.isOpen} isOpen={props.isOpen}

View File

@ -0,0 +1,130 @@
import React, { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
import styled from "styled-components";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { convertToCamelCase } from "utils/helpers";
import { AppState } from "reducers";
import { RestAction } from "entities/Action";
import { Page } from "constants/ReduxActionConstants";
import { saveApiName } from "actions/actionActions";
import { Spinner } from "@blueprintjs/core";
const ApiNameWrapper = styled.div`
min-width: 50%;
margin-right: 10px;
display: flex;
justify-content: flex-start;
align-content: center;
& > 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 (
<ApiNameWrapper>
<div
style={{
display: "flex",
}}
>
<EditableText
className="t--action-name-edit-field"
type="text"
defaultValue={currentActionConfig ? currentActionConfig.name : ""}
placeholder="Name of the API in camelCase"
forceDefault={forceUpdate}
onTextChanged={handleAPINameChange}
isInvalid={isInvalidActionName}
valueTransform={convertToCamelCase}
updating={saveStatus.isSaving}
editInteractionKind={EditInteractionKind.SINGLE}
/>
{saveStatus.isSaving && <Spinner size={16} />}
</div>
</ApiNameWrapper>
);
};
export default ActionNameEditor;

View File

@ -50,7 +50,6 @@ const buttonStyles = css<{
} }
&.bp3-button { &.bp3-button {
display: flex; display: flex;
width: 100%;
justify-content: ${props => justify-content: ${props =>
props.skin === undefined props.skin === undefined
? "center" ? "center"

View File

@ -4,24 +4,51 @@ import {
Classes, Classes,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import styled from "styled-components"; 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 EditableTextProps = {
type: "text" | "password" | "email" | "phone" | "date"; type: "text" | "password" | "email" | "phone" | "date";
defaultValue: string; defaultValue: string;
onTextChanged: (value: string) => void; onTextChanged: (value: string) => void;
isEditing: boolean;
placeholder: string; placeholder: string;
onChange?: (value: string) => void;
value?: string;
className?: 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 }>` const EditableTextWrapper = styled.div<{ isEditing: boolean }>`
&& { && {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
& .${Classes.EDITABLE_TEXT} { & .${Classes.EDITABLE_TEXT} {
border: ${props => (props.isEditing ? "1px solid #ccc" : "none")}; border: ${props => (props.isEditing ? "1px solid #ccc" : "none")};
cursor: pointer; cursor: pointer;
padding: 5px 10px; padding: 5px 5px;
text-transform: none; text-transform: none;
flex: 1 0 100%;
max-width: 100%;
display: flex;
&:before, &:before,
&:after { &:after {
display: none; display: none;
@ -29,34 +56,83 @@ const EditableTextWrapper = styled.div<{ isEditing: boolean }>`
} }
& div.${Classes.EDITABLE_TEXT_INPUT} { & div.${Classes.EDITABLE_TEXT_INPUT} {
text-transform: none; 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) => { export const EditableText = (props: EditableTextProps) => {
const [isEditing, setIsEditing] = useState(props.isEditing); const [isEditing, setIsEditing] = useState(!!props.isEditingDefault);
const [value, setValue] = useState(props.defaultValue);
useEffect(() => { useEffect(() => {
setIsEditing(props.isEditing); setValue(props.defaultValue);
}, [props.isEditing]); }, [props.defaultValue]);
useEffect(() => {
if (props.forceDefault === true) setValue(props.defaultValue);
}, [props.forceDefault]);
const edit = (e: any) => { const edit = (e: any) => {
setIsEditing(true); setIsEditing(true);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
}; };
const onChange = (value: string) => { const onChange = (_value: string) => {
props.onTextChanged(value); const isInvalid = props.isInvalid ? props.isInvalid(_value) : false;
if (!isInvalid) {
props.onTextChanged(_value);
} else {
setValue(props.defaultValue);
}
setIsEditing(false); 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 ( return (
<EditableTextWrapper onDoubleClick={edit} isEditing={isEditing}> <EditableTextWrapper
isEditing={isEditing}
onDoubleClick={
props.editInteractionKind === EditInteractionKind.DOUBLE ? edit : _.noop
}
onClick={
props.editInteractionKind === EditInteractionKind.SINGLE ? edit : _.noop
}
>
<ErrorTooltip isOpen={!!error} message={errorMessage as string}>
<TextContainer isValid={!error}>
<BlueprintEditableText <BlueprintEditableText
{...props}
disabled={!isEditing} disabled={!isEditing}
isEditing={isEditing} isEditing={isEditing}
onChange={onInputchange}
onConfirm={onChange} onConfirm={onChange}
selectAllOnFocus selectAllOnFocus
value={value}
placeholder={props.placeholder}
className={props.className}
/> />
{!props.hideEditIcon && !props.updating && !isEditing && (
<EditPen src={Edit} alt="Edit pen" />
)}
</TextContainer>
</ErrorTooltip>
</EditableTextWrapper> </EditableTextWrapper>
); );
}; };

View File

@ -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<HTMLInputElement>) => 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 (
<ErrorTooltip isOpen={!isValid} message={validationMessage || ""}>
<InputContainer focused={focused} isValid={isValid}>
<input
value={value}
placeholder={placeholder}
onChange={onChange}
onKeyPress={e => {
if (e.key === "Enter") {
this.onPressEnter(e);
}
}}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
{!focused && (
<EditPen onClick={this.onFocus} src={Edit} alt="Edit pen" />
)}
</InputContainer>
</ErrorTooltip>
);
}
}
export default EntityNameComponent;

View File

@ -17,11 +17,17 @@ const StyledError = styled.span<{ show: boolean }>`
type FormFieldErrorProps = { type FormFieldErrorProps = {
error?: string; error?: string;
className?: string;
}; };
export const FormFieldError = (props: FormFieldErrorProps) => { export const FormFieldError = (props: FormFieldErrorProps) => {
return ( return (
<StyledError show={!!props.error}>{props.error || "&nbsp;"}</StyledError> <StyledError
className={props.className ? props.className : undefined}
show={!!props.error}
>
{props.error || "&nbsp;"}
</StyledError>
); );
}; };

View File

@ -4,7 +4,6 @@ import {
WrappedFieldMetaProps, WrappedFieldMetaProps,
WrappedFieldInputProps, WrappedFieldInputProps,
} from "redux-form"; } from "redux-form";
import FormFieldError from "components/editorComponents/form/FieldError";
import SelectComponent from "components/editorComponents/SelectComponent"; import SelectComponent from "components/editorComponents/SelectComponent";
const renderComponent = ( const renderComponent = (

View File

@ -50,13 +50,13 @@ export function InputText(props: {
class InputTextControl extends BaseControl<InputControlProps> { class InputTextControl extends BaseControl<InputControlProps> {
render() { render() {
const { const {
errorMessage,
expected, expected,
propertyValue, propertyValue,
isValid, isValid,
label, label,
placeholderText, placeholderText,
dataTreePath, dataTreePath,
validationMessage,
} = this.props; } = this.props;
return ( return (
<InputText <InputText
@ -64,7 +64,7 @@ class InputTextControl extends BaseControl<InputControlProps> {
value={propertyValue} value={propertyValue}
onChange={this.onTextChange} onChange={this.onTextChange}
isValid={isValid} isValid={isValid}
errorMessage={errorMessage} errorMessage={validationMessage}
expected={expected} expected={expected}
placeholder={placeholderText} placeholder={placeholderText}
dataTreePath={dataTreePath} dataTreePath={dataTreePath}
@ -101,6 +101,7 @@ class InputTextControl extends BaseControl<InputControlProps> {
export interface InputControlProps extends ControlProps { export interface InputControlProps extends ControlProps {
placeholderText: string; placeholderText: string;
inputType: InputType; inputType: InputType;
validationMessage?: string;
isDisabled?: boolean; isDisabled?: boolean;
} }

View File

@ -234,6 +234,10 @@ export const ReduxActionTypes: { [key: string]: string } = {
CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR", CHANGE_ORG_USER_ROLE_ERROR: "CHANGE_ORG_USER_ROLE_ERROR",
SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT", SET_DEFAULT_REFINEMENT: "SET_DEFAULT_REFINEMENT",
SET_HELP_MODAL_OPEN: "SET_HELP_MODAL_OPEN", 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]; export type ReduxActionType = typeof ReduxActionTypes[keyof typeof ReduxActionTypes];
@ -314,6 +318,7 @@ export const ReduxActionErrorTypes: { [key: string]: string } = {
CREATE_MODAL_ERROR: "CREATE_MODAL_ERROR", CREATE_MODAL_ERROR: "CREATE_MODAL_ERROR",
FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_ERROR: FETCH_PROVIDER_DETAILS_BY_PROVIDER_ID_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_USER_APPLICATIONS_ORGS_ERROR: "FETCH_USER_APPLICATIONS_ORGS_ERROR",
FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR", FETCH_ALL_USERS_ERROR: "FETCH_ALL_USERS_ERROR",
FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR", FETCH_ALL_ROLES_ERROR: "FETCH_ALL_ROLES_ERROR",

View File

@ -11,7 +11,6 @@ export const VALIDATION_TYPES = {
ARRAY: "ARRAY", ARRAY: "ARRAY",
TABLE_DATA: "TABLE_DATA", TABLE_DATA: "TABLE_DATA",
OPTIONS_DATA: "OPTIONS_DATA", OPTIONS_DATA: "OPTIONS_DATA",
SINGLE_CHART_DATA: "SINGLE_CHART_DATA",
DATE: "DATE", DATE: "DATE",
TABS_DATA: "TABS_DATA", TABS_DATA: "TABS_DATA",
CHART_DATA: "CHART_DATA", CHART_DATA: "CHART_DATA",

View File

@ -7,6 +7,7 @@ export const FIELD_REQUIRED_ERROR = "This field is required";
export const VALID_FUNCTION_NAME_ERROR = export const VALID_FUNCTION_NAME_ERROR =
"Must be a valid variable name (camelCase)"; "Must be a valid variable name (camelCase)";
export const UNIQUE_NAME_ERROR = "Name must be unique"; 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_EMPTY_EMAIL = "Please enter an email";
export const FORM_VALIDATION_INVALID_EMAIL = export const FORM_VALIDATION_INVALID_EMAIL =

View File

@ -36,9 +36,7 @@ import {
} from "actions/metaActions"; } from "actions/metaActions";
import AppRoute from "pages/common/AppRoute"; import AppRoute from "pages/common/AppRoute";
import { editorInitializer } from "utils/EditorUtils"; import { editorInitializer } from "utils/EditorUtils";
import { getCurrentOrg } from "selectors/organizationSelectors";
import { PERMISSION_TYPE } from "pages/Applications/permissionHelpers"; import { PERMISSION_TYPE } from "pages/Applications/permissionHelpers";
import { Organization } from "constants/orgConstants";
const AppViewWrapper = styled.div` const AppViewWrapper = styled.div`
margin-top: ${props => props.theme.headerHeight}; margin-top: ${props => props.theme.headerHeight};

View File

@ -12,6 +12,10 @@ const HeaderWrapper = styled(StyledHeader)`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
`; `;
const StyledButton = styled(Button)`
max-width: 200px;
`;
type AppViewerHeaderProps = { type AppViewerHeaderProps = {
url?: string; url?: string;
permissionRequired: string; permissionRequired: string;
@ -27,7 +31,7 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
return ( return (
<HeaderWrapper> <HeaderWrapper>
{props.url && hasPermission && ( {props.url && hasPermission && (
<Button <StyledButton
className="t--back-to-editor" className="t--back-to-editor"
href={props.url} href={props.url}
intent="primary" intent="primary"

View File

@ -5,7 +5,7 @@ import {
getApplicationViewerPageURL, getApplicationViewerPageURL,
BUILDER_PAGE_URL, BUILDER_PAGE_URL,
} from "constants/routes"; } from "constants/routes";
import { Card, Tooltip, Classes, Icon } from "@blueprintjs/core"; import { Card, Tooltip, Icon } from "@blueprintjs/core";
import { ApplicationPayload } from "constants/ReduxActionConstants"; import { ApplicationPayload } from "constants/ReduxActionConstants";
import Button from "components/editorComponents/Button"; import Button from "components/editorComponents/Button";
import { import {

View File

@ -24,7 +24,7 @@ type Props = InjectedFormProps<
// TODO(abhinav): abstract onCancel out. // TODO(abhinav): abstract onCancel out.
export const CreateApplicationForm = (props: Props) => { export const CreateApplicationForm = (props: Props) => {
const { error, handleSubmit, pristine, submitting, orgId } = props; const { error, handleSubmit, pristine, submitting } = props;
return ( return (
<Form onSubmit={handleSubmit(createApplicationFormSubmitHandler)}> <Form onSubmit={handleSubmit(createApplicationFormSubmitHandler)}>
{error && !pristine && <FormMessage intent="danger" message={error} />} {error && !pristine && <FormMessage intent="danger" message={error} />}

View File

@ -10,7 +10,6 @@ import {
getIsCreatingApplication, getIsCreatingApplication,
getCreateApplicationError, getCreateApplicationError,
getIsDeletingApplication, getIsDeletingApplication,
getUserApplicationsOrgs,
getUserApplicationsOrgsList, getUserApplicationsOrgsList,
} from "selectors/applicationSelectors"; } from "selectors/applicationSelectors";
import { import {
@ -20,11 +19,9 @@ import {
import PageWrapper from "pages/common/PageWrapper"; import PageWrapper from "pages/common/PageWrapper";
import SubHeader from "pages/common/SubHeader"; import SubHeader from "pages/common/SubHeader";
import PageSectionDivider from "pages/common/PageSectionDivider"; import PageSectionDivider from "pages/common/PageSectionDivider";
import { getApplicationPayloads } from "mockComponentProps/ApplicationPayloads";
import ApplicationCard from "./ApplicationCard"; import ApplicationCard from "./ApplicationCard";
import CreateApplicationForm from "./CreateApplicationForm"; import CreateApplicationForm from "./CreateApplicationForm";
import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2"; import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2";
import { CREATE_APPLICATION_FORM_NAME } from "constants/forms";
import { PERMISSION_TYPE, isPermitted } from "./permissionHelpers"; import { PERMISSION_TYPE, isPermitted } from "./permissionHelpers";
import { MenuIcons } from "icons/MenuIcons"; import { MenuIcons } from "icons/MenuIcons";
import { DELETING_APPLICATION } from "constants/messages"; import { DELETING_APPLICATION } from "constants/messages";
@ -141,9 +138,6 @@ class Applications extends Component<
} }
public render() { public render() {
const applicationList = this.props.isFetchingApplications
? getApplicationPayloads(8)
: this.props.applicationList;
const Form: any = InviteUsersFormv2; const Form: any = InviteUsersFormv2;
const DropdownProps = ( const DropdownProps = (
user: User, user: User,
@ -221,7 +215,7 @@ class Applications extends Component<
/> />
<PageSectionDivider /> <PageSectionDivider />
{this.props.userOrgs && {this.props.userOrgs &&
this.props.userOrgs.length != 0 && this.props.userOrgs.length !== 0 &&
this.props.userOrgs.map((organizationObject: any) => { this.props.userOrgs.map((organizationObject: any) => {
const { organization, applications } = organizationObject; const { organization, applications } = organizationObject;

View File

@ -16,7 +16,6 @@ import FormRow from "components/editorComponents/FormRow";
import { BaseButton } from "components/designSystems/blueprint/ButtonComponent"; import { BaseButton } from "components/designSystems/blueprint/ButtonComponent";
import { PaginationField } from "api/ActionAPI"; import { PaginationField } from "api/ActionAPI";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import TextField from "components/editorComponents/form/fields/TextField";
import DropdownField from "components/editorComponents/form/fields/DropdownField"; import DropdownField from "components/editorComponents/form/fields/DropdownField";
import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField"; import DatasourcesField from "components/editorComponents/form/fields/DatasourcesField";
import { API_EDITOR_FORM_NAME } from "constants/forms"; import { API_EDITOR_FORM_NAME } from "constants/forms";
@ -29,6 +28,10 @@ import CollapsibleHelp from "components/designSystems/appsmith/help/CollapsibleH
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray"; import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import PostBodyData from "./PostBodyData"; import PostBodyData from "./PostBodyData";
import ApiResponseView from "components/editorComponents/ApiResponseView"; import ApiResponseView from "components/editorComponents/ApiResponseView";
import { ApiNameValidation } from "reducers/uiReducers/apiPaneReducer";
import { AppState } from "reducers";
import { getApiName } from "selectors/formSelectors";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
const Form = styled.form` const Form = styled.form`
display: flex; display: flex;
@ -128,10 +131,23 @@ interface APIFormProps {
}; };
dispatch: any; dispatch: any;
datasourceFieldText: string; datasourceFieldText: string;
apiName: string;
apiNameValidation: ApiNameValidation;
} }
type Props = APIFormProps & InjectedFormProps<RestAction, APIFormProps>; type Props = APIFormProps & InjectedFormProps<RestAction, APIFormProps>;
export const NameWrapper = styled.div`
width: 49%;
display: flex;
justify-content: space-between;
input {
margin: 0;
box-sizing: border-box;
// border: 0;
}
`;
const ApiEditorForm: React.FC<Props> = (props: Props) => { const ApiEditorForm: React.FC<Props> = (props: Props) => {
const { const {
pluginId, pluginId,
@ -163,12 +179,9 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<MainConfiguration> <MainConfiguration>
<FormRow> <FormRow>
<TextField <NameWrapper className="t--nameOfApi">
name="name" <ActionNameEditor />
placeholder="nameOfApi (camel case)" </NameWrapper>
showError
className="t--nameOfApi"
/>
<ActionButtons className="t--formActionButtons"> <ActionButtons className="t--formActionButtons">
<ActionButton <ActionButton
text="Delete" text="Delete"
@ -276,15 +289,15 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
const selector = formValueSelector(API_EDITOR_FORM_NAME); const selector = formValueSelector(API_EDITOR_FORM_NAME);
export default connect(state => { export default connect((state: AppState) => {
const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod"); const httpMethodFromForm = selector(state, "actionConfiguration.httpMethod");
const actionConfigurationBody = selector(state, "actionConfiguration.body"); const actionConfigurationBody = selector(state, "actionConfiguration.body");
const actionName = selector(state, "name");
const actionConfigurationHeaders = selector( const actionConfigurationHeaders = selector(
state, state,
"actionConfiguration.headers", "actionConfiguration.headers",
); );
const apiId = selector(state, "id"); const apiId = selector(state, "id");
const actionName = getApiName(state, apiId) || "";
return { return {
actionName, actionName,

View File

@ -22,7 +22,9 @@ import { FormIcons } from "icons/FormIcons";
import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView"; import { BaseTabbedView } from "components/designSystems/appsmith/TabbedView";
import Pagination from "./Pagination"; import Pagination from "./Pagination";
import { PaginationType, RestAction } from "entities/Action"; import { PaginationType, RestAction } from "entities/Action";
import { ApiNameValidation } from "reducers/uiReducers/apiPaneReducer";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import { NameWrapper } from "./Form";
const Form = styled.form` const Form = styled.form`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -114,6 +116,9 @@ interface APIFormProps {
location: { location: {
pathname: string; pathname: string;
}; };
apiName: string;
apiId: string;
apiNameValidation: ApiNameValidation;
dispatch: any; dispatch: any;
} }
@ -171,13 +176,21 @@ const RapidApiEditorForm: React.FC<Props> = (props: Props) => {
> >
<MainConfiguration> <MainConfiguration>
<FormRow> <FormRow>
<DynamicTextField <NameWrapper>
placeholder="Api name" <ActionNameEditor />
name="name" <a
singleLine style={{
setMaxHeight paddingTop: "7px",
link={providerURL && `http://${providerURL}`} }}
/> className="t--apiDocumentationLink"
target="_blank"
rel="noopener noreferrer"
href={providerURL && `http://${providerURL}`}
>
API documentation
</a>
</NameWrapper>
<ActionButtons> <ActionButtons>
<ActionButton <ActionButton
text="Delete" text="Delete"

View File

@ -25,6 +25,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { getActionById, getCurrentPageName } from "selectors/editorSelectors"; import { getActionById, getCurrentPageName } from "selectors/editorSelectors";
import { Plugin } from "api/PluginApi"; import { Plugin } from "api/PluginApi";
import { RapidApiAction, RestAction, PaginationType } from "entities/Action"; import { RapidApiAction, RestAction, PaginationType } from "entities/Action";
import { getApiName } from "selectors/formSelectors";
interface ReduxStateProps { interface ReduxStateProps {
actions: ActionDataState; actions: ActionDataState;
@ -32,6 +33,10 @@ interface ReduxStateProps {
isDeleting: Record<string, boolean>; isDeleting: Record<string, boolean>;
allowSave: boolean; allowSave: boolean;
apiName: string; apiName: string;
apiNameValidation: {
isValid: boolean;
validationMessage: string;
};
currentApplication: UserApplication; currentApplication: UserApplication;
currentPageName: string | undefined; currentPageName: string | undefined;
pages: any; pages: any;
@ -191,6 +196,8 @@ class ApiEditor extends React.Component<Props> {
? this.props.currentApplication.name ? this.props.currentApplication.name
: "" : ""
} }
apiName={this.props.apiName}
apiNameValidation={this.props.apiNameValidation}
onChange={this.onChangeHandler} onChange={this.onChangeHandler}
location={this.props.location} location={this.props.location}
/> />
@ -198,6 +205,9 @@ class ApiEditor extends React.Component<Props> {
{formUiComponent === "RapidApiEditorForm" && ( {formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm <RapidApiEditorForm
apiName={this.props.apiName}
apiNameValidation={this.props.apiNameValidation}
apiId={this.props.match.params.apiId}
paginationType={paginationType} paginationType={paginationType}
isRunning={isRunning[apiId]} isRunning={isRunning[apiId]}
isDeleting={isDeleting[apiId]} isDeleting={isDeleting[apiId]}
@ -225,6 +235,17 @@ class ApiEditor extends React.Component<Props> {
const mapStateToProps = (state: AppState, props: any): ReduxStateProps => { const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const formData = getFormValues(API_EDITOR_FORM_NAME)(state) as RestAction; const formData = getFormValues(API_EDITOR_FORM_NAME)(state) as RestAction;
const apiAction = getActionById(state, props); const apiAction = getActionById(state, props);
const apiName = getApiName(state, props.match.params.apiId);
const apiNameDraft =
state.ui.apiPane.apiName.drafts[props.match.params.apiId];
let apiNameValidation = {
isValid: true,
validationMessage: "",
};
if (apiNameDraft && apiNameDraft.validation) {
apiNameValidation = apiNameDraft.validation;
}
const { drafts, isDeleting, isRunning } = state.ui.apiPane; const { drafts, isDeleting, isRunning } = state.ui.apiPane;
let data: RestAction | ActionData | RapidApiAction | undefined; let data: RestAction | ActionData | RapidApiAction | undefined;
@ -245,7 +266,8 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
currentApplication: getCurrentApplication(state), currentApplication: getCurrentApplication(state),
currentPageName: getCurrentPageName(state), currentPageName: getCurrentPageName(state),
pages: state.entities.pageList.pages, pages: state.entities.pageList.pages,
apiName: formData?.name || "", apiName: apiName || "",
apiNameValidation: apiNameValidation,
plugins: state.entities.plugins.list, plugins: state.entities.plugins.list,
pluginId: _.get(data, "pluginId"), pluginId: _.get(data, "pluginId"),
paginationType: _.get(data, "actionConfiguration.paginationType"), paginationType: _.get(data, "actionConfiguration.paginationType"),

View File

@ -182,7 +182,7 @@ class ApiSidebar extends React.Component<Props> {
}; };
handleCreateNewApiClick = (selectedPageId: string) => { handleCreateNewApiClick = (selectedPageId: string) => {
const { history, createNewApiAction } = this.props; const { history } = this.props;
const { pageId, applicationId } = this.props.match.params; const { pageId, applicationId } = this.props.match.params;
history.push( history.push(
API_EDITOR_URL_WITH_SELECTED_PAGE_ID( API_EDITOR_URL_WITH_SELECTED_PAGE_ID(

View File

@ -74,7 +74,7 @@ const PluginImage = styled.img`
width: auto; width: auto;
`; `;
const FormTitleContainer = styled.div` export const FormTitleContainer = styled.div`
flex-direction: row; flex-direction: row;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -1,6 +1,8 @@
import React, { ReactNode, useState } from "react"; import React, { ReactNode, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import EditableText from "components/editorComponents/EditableText"; import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { PageListItemCSS } from "./PageListItem"; import { PageListItemCSS } from "./PageListItem";
import Button from "components/editorComponents/Button"; import Button from "components/editorComponents/Button";
@ -34,9 +36,10 @@ const CreatePageButton = (props: CreatePageProps) => {
className="t--page-name-input" className="t--page-name-input"
type="text" type="text"
placeholder="Enter page name" placeholder="Enter page name"
isEditing isEditingDefault
defaultValue={props.defaultValue} defaultValue={props.defaultValue}
onTextChanged={onChange} onTextChanged={onChange}
editInteractionKind={EditInteractionKind.SINGLE}
/> />
); );
} else { } else {

View File

@ -6,8 +6,11 @@ import ContextDropdown, {
} from "components/editorComponents/ContextDropdown"; } from "components/editorComponents/ContextDropdown";
import { MenuIcons } from "icons/MenuIcons"; import { MenuIcons } from "icons/MenuIcons";
import { Theme } from "constants/DefaultTheme"; import { Theme } from "constants/DefaultTheme";
import EditableText from "components/editorComponents/EditableText"; import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { Tooltip } from "@blueprintjs/core";
/** Page List Item */ /** Page List Item */
export const PageListItemCSS = css` export const PageListItemCSS = css`
@ -89,13 +92,16 @@ const PageListItem = withTheme((props: PageListItemProps) => {
> >
<div> <div>
{pageIcon} {pageIcon}
<Tooltip content="Double click to edit">
<EditableText <EditableText
type="text" type="text"
placeholder="Enter page name" placeholder="Enter page name"
defaultValue={props.name} defaultValue={props.name}
isEditing={false} editInteractionKind={EditInteractionKind.DOUBLE}
onTextChanged={onEditPageName} onTextChanged={onEditPageName}
hideEditIcon
/> />
</Tooltip>
</div> </div>
<ContextDropdown <ContextDropdown
options={props.contextActions} options={props.contextActions}

View File

@ -114,7 +114,11 @@ class PropertyPane extends Component<
const { widgetProperties } = this.props; const { widgetProperties } = this.props;
if (!widgetProperties) return <PropertyPaneWrapper />; if (!widgetProperties) return <PropertyPaneWrapper />;
return ( return (
<PropertyPaneWrapper> <PropertyPaneWrapper
onClick={(e: any) => {
e.stopPropagation();
}}
>
<PropertyPaneTitle <PropertyPaneTitle
key={this.props.widgetId} key={this.props.widgetId}
title={widgetProperties.widgetName} title={widgetProperties.widgetName}

View File

@ -1,14 +1,17 @@
import React, { useState, memo, useEffect } from "react"; import React, { useState, memo, useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import EditableText from "components/editorComponents/EditableText"; import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { updateWidgetName } from "actions/propertyPaneActions"; import { updateWidgetName } from "actions/propertyPaneActions";
import { AppState } from "reducers"; import { AppState } from "reducers";
import Spinner from "components/editorComponents/Spinner"; import Spinner from "components/editorComponents/Spinner";
import { getExistingWidgetNames } from "sagas/selectors"; import { getExistingWidgetNames } from "sagas/selectors";
import { convertToCamelCase } from "utils/helpers";
const Wrapper = styled.div` const Wrapper = styled.div`
display: inline-flex; display: flex;
justify-content: space-around; justify-content: flex-start;
align-items: center; align-items: center;
`; `;
@ -42,9 +45,6 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => {
dispatch(updateWidgetName(props.widgetId, value.trim())); dispatch(updateWidgetName(props.widgetId, value.trim()));
} }
}; };
const textChanged = (value: string) => {
setName(value.replace(/\W+/, "_").slice(0, 30));
};
useEffect(() => { useEffect(() => {
if (updateError) { if (updateError) {
setName(props.title); setName(props.title);
@ -55,12 +55,12 @@ const PropertyPaneTitle = memo((props: PropertyPaneTitleProps) => {
<Wrapper> <Wrapper>
<EditableText <EditableText
type="text" type="text"
valueTransform={convertToCamelCase}
defaultValue={name} defaultValue={name}
onTextChanged={updateTitle} onTextChanged={updateTitle}
onChange={textChanged}
isEditing={isEditing}
placeholder={props.title} placeholder={props.title}
value={name} updating={updating}
editInteractionKind={EditInteractionKind.SINGLE}
/> />
{updating && <Spinner size={16} />} {updating && <Spinner size={16} />}
</Wrapper> </Wrapper>

View File

@ -10,7 +10,7 @@ const Wrapper = styled.div`
grid-template-columns: 1fr 4fr; grid-template-columns: 1fr 4fr;
width: ${props => props.theme.sidebarWidth}; width: ${props => props.theme.sidebarWidth};
box-shadow: 0px 1px 3px ${props => props.theme.colors.paneBG}; box-shadow: 0px 1px 3px ${props => props.theme.colors.paneBG};
z-index: 2; z-index: 3;
`; `;
const NavBar = styled.div` const NavBar = styled.div`

View File

@ -1,12 +1,10 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Route } from "react-router-dom"; import { Route } from "react-router-dom";
import { useDispatch } from "react-redux";
import { import {
useShowPropertyPane, useShowPropertyPane,
useWidgetSelection, useWidgetSelection,
} from "utils/hooks/dragResizeHooks"; } from "utils/hooks/dragResizeHooks";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { setCurrentUserDetails } from "actions/userActions";
export const WrappedComponent = (props: any) => { export const WrappedComponent = (props: any) => {
const showPropertyPane = useShowPropertyPane(); const showPropertyPane = useShowPropertyPane();
@ -31,8 +29,6 @@ const AppRoute = ({
name: string; name: string;
location?: any; location?: any;
}) => { }) => {
const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
if (!rest.logDisable) { if (!rest.logDisable) {
AnalyticsUtil.logEvent("NAVIGATE_EDITOR", { AnalyticsUtil.logEvent("NAVIGATE_EDITOR", {

View File

@ -1,55 +1,8 @@
import React from "react";
import Badge from "./Badge";
import { Directions } from "utils/helpers"; import { Directions } from "utils/helpers";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getOnSelectAction, DropdownOnSelectActions } from "./dropdownHelpers"; import { getOnSelectAction, DropdownOnSelectActions } from "./dropdownHelpers";
import DropdownComponent, { CustomizedDropdownProps } from "./index"; import { CustomizedDropdownProps } from "./index";
import { Org } from "constants/orgConstants";
import { User } from "constants/userConstants"; import { User } from "constants/userConstants";
import FormDialogComponent from "components/editorComponents/form/FormDialogComponent";
import CreateOrganizationForm from "pages/organization/CreateOrganizationForm";
const switchdropdown = (
orgs: Org[],
currentOrg: Org,
): CustomizedDropdownProps => ({
sections: [
{
isSticky: true,
options: [
{
content: (
<FormDialogComponent
trigger="Create Organization"
Form={CreateOrganizationForm}
title="Create Organization"
/>
),
shouldCloseDropdown: false,
},
],
},
{
options: orgs
.filter(org => org.id !== currentOrg.id)
.map(org => ({
content: org.name,
onSelect: () =>
getOnSelectAction(DropdownOnSelectActions.DISPATCH, {
type: ReduxActionTypes.SWITCH_ORGANIZATION_INIT,
payload: {
orgId: org.id,
},
}),
})),
},
],
trigger: {
text: "Switch Organization",
},
openDirection: Directions.RIGHT,
openOnHover: true,
});
export const options = ( export const options = (
user: User, user: User,

View File

@ -39,7 +39,7 @@ export const PageHeader = (props: PageHeaderProps) => {
<StyledPageHeader> <StyledPageHeader>
<LogoContainer> <LogoContainer>
<a href="/applications"> <a href="/applications">
<img className="logoimg" src={Logo} /> <img className="logoimg" src={Logo} alt="Appsmith Logo" />
</a> </a>
</LogoContainer> </LogoContainer>
<StyledDropDownContainer> <StyledDropDownContainer>

View File

@ -1,24 +1,20 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import TagListField from "components/editorComponents/form/fields/TagListField"; import TagListField from "components/editorComponents/form/fields/TagListField";
import FormGroup from "components/editorComponents/form/FormGroup";
import { reduxForm } from "redux-form"; import { reduxForm } from "redux-form";
import SelectField from "components/editorComponents/form/fields/SelectField"; import SelectField from "components/editorComponents/form/fields/SelectField";
import Button from "components/editorComponents/Button"; import Button from "components/editorComponents/Button";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { import {
getRoles,
getDefaultRole, getDefaultRole,
getRolesForField, getRolesForField,
getAllUsers, getAllUsers,
} from "selectors/organizationSelectors"; } from "selectors/organizationSelectors";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { InviteUsersToOrgFormValues, inviteUsersToOrg } from "./helpers"; import { InviteUsersToOrgFormValues, inviteUsersToOrg } from "./helpers";
import { OrgRole } from "constants/orgConstants";
import { INVITE_USERS_TO_ORG_FORM } from "constants/forms"; import { INVITE_USERS_TO_ORG_FORM } from "constants/forms";
import { Classes } from "@blueprintjs/core"; import { Classes } from "@blueprintjs/core";
import { noop } from "lodash";
import FormMessage from "components/editorComponents/form/FormMessage"; import FormMessage from "components/editorComponents/form/FormMessage";
import { import {
INVITE_USERS_SUBMIT_SUCCESS, INVITE_USERS_SUBMIT_SUCCESS,
@ -89,11 +85,13 @@ const InviteUsersForm = (props: any) => {
submitFailed, submitFailed,
submitSucceeded, submitSucceeded,
error, error,
fetchUser,
fetchAllRoles,
} = props; } = props;
useEffect(() => { useEffect(() => {
props.fetchUser(props.orgId); fetchUser(props.orgId);
props.fetchAllRoles(props.orgId); fetchAllRoles(props.orgId);
}, [props.orgId]); }, [props.orgId, fetchUser, fetchAllRoles]);
return ( return (
<StyledForm> <StyledForm>

View File

@ -1,27 +1,23 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Icon } from "@blueprintjs/core"; import { Icon } from "@blueprintjs/core";
import _ from "lodash";
import { import {
GridComponent, GridComponent,
ColumnsDirective, ColumnsDirective,
ColumnDirective, ColumnDirective,
} from "@syncfusion/ej2-react-grids"; } from "@syncfusion/ej2-react-grids";
import { useHistory } from "react-router-dom";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { import {
getCurrentOrg,
getAllUsers, getAllUsers,
getAllRoles, getAllRoles,
getOrgs, getOrgs,
} from "selectors/organizationSelectors"; } from "selectors/organizationSelectors";
import { ORG_INVITE_USERS_PAGE_URL } from "constants/routes";
import PageSectionDivider from "pages/common/PageSectionDivider"; import PageSectionDivider from "pages/common/PageSectionDivider";
import PageSectionHeader from "pages/common/PageSectionHeader"; import PageSectionHeader from "pages/common/PageSectionHeader";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2"; import InviteUsersFormv2 from "pages/organization/InviteUsersFromv2";
import Button from "components/editorComponents/Button"; import Button from "components/editorComponents/Button";
import { Org, OrgUser, Organization } from "constants/orgConstants"; import { OrgUser, Organization } from "constants/orgConstants";
import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core"; import { Menu, MenuItem, Popover, Position } from "@blueprintjs/core";
import styled from "styled-components"; import styled from "styled-components";
import { FormIcons } from "icons/FormIcons"; import { FormIcons } from "icons/FormIcons";
@ -87,7 +83,6 @@ const StyledMenu = styled(Menu)`
`; `;
export const OrgSettings = (props: PageProps) => { export const OrgSettings = (props: PageProps) => {
const history = useHistory();
const { const {
match: { match: {
params: { orgId }, params: { orgId },
@ -95,6 +90,9 @@ export const OrgSettings = (props: PageProps) => {
deleteOrgUser, deleteOrgUser,
changeOrgUserRole, changeOrgUserRole,
allOrgs, allOrgs,
fetchUser,
fetchAllRoles,
getAllApplication,
} = props; } = props;
const userTableData = props.allUsers.map(user => ({ const userTableData = props.allUsers.map(user => ({
@ -105,10 +103,10 @@ export const OrgSettings = (props: PageProps) => {
const currentOrgName = currentOrg?.organization.name ?? ""; const currentOrgName = currentOrg?.organization.name ?? "";
useEffect(() => { useEffect(() => {
props.fetchUser(orgId); fetchUser(orgId);
props.fetchAllRoles(orgId); fetchAllRoles(orgId);
props.getAllApplication(); getAllApplication();
}, [orgId]); }, [orgId, fetchUser, fetchAllRoles, getAllApplication]);
const Dropdown = (props: DropdownProps) => { const Dropdown = (props: DropdownProps) => {
return ( return (

View File

@ -29,6 +29,7 @@ import { MetaState } from "./entityReducers/metaReducer";
import { ImportReduxState } from "reducers/uiReducers/importReducer"; import { ImportReduxState } from "reducers/uiReducers/importReducer";
import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer"; import { ActionDraftsState } from "reducers/entityReducers/actionDraftsReducer";
import { HelpReduxState } from "./uiReducers/helpReducer"; import { HelpReduxState } from "./uiReducers/helpReducer";
import { ApiNameReduxState } from "./uiReducers/apiNameReducer";
const appReducer = combineReducers({ const appReducer = combineReducers({
entities: entityReducer, entities: entityReducer,
@ -57,6 +58,7 @@ export interface AppState {
queryPane: QueryPaneReduxState; queryPane: QueryPaneReduxState;
datasourcePane: DatasourcePaneReduxState; datasourcePane: DatasourcePaneReduxState;
help: HelpReduxState; help: HelpReduxState;
apiName: ApiNameReduxState;
}; };
entities: { entities: {
canvasWidgets: CanvasWidgetsReduxState; canvasWidgets: CanvasWidgetsReduxState;

View File

@ -0,0 +1,70 @@
import { createReducer } from "utils/AppsmithUtils";
import {
ReduxAction,
ReduxActionTypes,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
const initialState: ApiNameReduxState = {
isSaving: {},
errors: {},
};
const apiNameReducer = createReducer(initialState, {
[ReduxActionErrorTypes.SAVE_API_NAME_ERROR]: (
state: ApiNameReduxState,
action: ReduxAction<{ actionId: string }>,
) => {
return {
...state,
isSaving: {
...state.isSaving,
[action.payload.actionId]: false,
},
errors: {
...state.errors,
[action.payload.actionId]: true,
},
};
},
[ReduxActionTypes.SAVE_API_NAME]: (
state: ApiNameReduxState,
action: ReduxAction<{ id: string }>,
) => {
return {
...state,
isSaving: {
...state.isSaving,
[action.payload.id]: true,
},
errors: {
...state.errors,
[action.payload.id]: false,
},
};
},
[ReduxActionTypes.SAVE_API_NAME_SUCCESS]: (
state: ApiNameReduxState,
action: ReduxAction<{ actionId: string }>,
) => {
return {
...state,
isSaving: {
...state.isSaving,
[action.payload.actionId]: false,
},
errors: {
...state.errors,
[action.payload.actionId]: false,
},
};
},
});
export interface ApiNameReduxState {
isSaving: Record<string, boolean>;
errors: Record<string, boolean>;
}
export default apiNameReducer;

View File

@ -19,8 +19,15 @@ const initialState: ApiPaneReduxState = {
lastSelectedPage: "", lastSelectedPage: "",
extraformData: {}, extraformData: {},
datasourceFieldText: {}, datasourceFieldText: {},
apiName: {
drafts: {},
isSaving: {},
},
}; };
export interface ApiNameValidation {
isValid: boolean;
validationMessage: string;
}
export interface ApiPaneReduxState { export interface ApiPaneReduxState {
lastUsed: string; lastUsed: string;
isFetching: boolean; isFetching: boolean;
@ -33,6 +40,16 @@ export interface ApiPaneReduxState {
datasourceFieldText: Record<string, string>; datasourceFieldText: Record<string, string>;
lastSelectedPage: string; lastSelectedPage: string;
extraformData: Record<string, any>; extraformData: Record<string, any>;
apiName: {
drafts: Record<
string,
{
value: string;
validation: ApiNameValidation;
}
>;
isSaving: Record<string, boolean>;
};
} }
const apiPaneReducer = createReducer(initialState, { const apiPaneReducer = createReducer(initialState, {
@ -216,6 +233,35 @@ const apiPaneReducer = createReducer(initialState, {
}, },
}; };
}, },
[ReduxActionTypes.UPDATE_API_NAME_DRAFT]: (
state: ApiPaneReduxState,
action: ReduxAction<{
id: string;
draft?: {
value: string;
validation: {
isValid: boolean;
validationMessage: string;
};
};
}>,
) => {
const { id, draft } = action.payload;
let nameDrafts = {
...state.apiName.drafts,
[id]: draft,
};
if (!draft) {
nameDrafts = _.omit(nameDrafts, id);
}
return {
...state,
apiName: {
drafts: nameDrafts,
},
};
},
}); });
export default apiPaneReducer; export default apiPaneReducer;

View File

@ -16,6 +16,7 @@ import providersReducer from "./providerReducer";
import importReducer from "./importReducer"; import importReducer from "./importReducer";
import queryPaneReducer from "./queryPaneReducer"; import queryPaneReducer from "./queryPaneReducer";
import helpReducer from "./helpReducer"; import helpReducer from "./helpReducer";
import apiNameReducer from "./apiNameReducer";
const uiReducer = combineReducers({ const uiReducer = combineReducers({
widgetSidebar: widgetSidebarReducer, widgetSidebar: widgetSidebarReducer,
@ -35,5 +36,6 @@ const uiReducer = combineReducers({
queryPane: queryPaneReducer, queryPane: queryPaneReducer,
datasourcePane: datasourcePaneReducer, datasourcePane: datasourcePaneReducer,
help: helpReducer, help: helpReducer,
apiName: apiNameReducer,
}); });
export default uiReducer; export default uiReducer;

View File

@ -30,6 +30,8 @@ import _ from "lodash";
import { mapToPropList } from "utils/AppsmithUtils"; import { mapToPropList } from "utils/AppsmithUtils";
import { AppToaster } from "components/editorComponents/ToastComponent"; import { AppToaster } from "components/editorComponents/ToastComponent";
import { GenericApiResponse } from "api/ApiResponses"; import { GenericApiResponse } from "api/ApiResponses";
import PageApi from "api/PageApi";
import { updateCanvasWithDSL } from "sagas/PageSagas";
import { import {
copyActionError, copyActionError,
copyActionSuccess, copyActionSuccess,
@ -42,6 +44,8 @@ import {
moveActionError, moveActionError,
moveActionSuccess, moveActionSuccess,
updateActionSuccess, updateActionSuccess,
updateApiNameDraft,
fetchActionsForPage,
} from "actions/actionActions"; } from "actions/actionActions";
import { import {
getDynamicBindings, getDynamicBindings,
@ -62,6 +66,7 @@ import {
import { import {
getCurrentApplicationId, getCurrentApplicationId,
getPageList, getPageList,
getCurrentPageId,
} from "selectors/editorSelectors"; } from "selectors/editorSelectors";
import history from "utils/history"; import history from "utils/history";
import { import {
@ -73,6 +78,9 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import * as log from "loglevel"; import * as log from "loglevel";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { RestAction } from "entities/Action"; import { RestAction } from "entities/Action";
import { validateEntityName } from "components/editorComponents/EntityNameComponent";
import { ActionData } from "reducers/entityReducers/actionsReducer";
import { getActions } from "selectors/entitiesSelector";
export const getAction = ( export const getAction = (
state: AppState, state: AppState,
@ -466,6 +474,10 @@ export function* updateActionSaga(
if (isApi) { if (isApi) {
action = transformRestAction(data); action = transformRestAction(data);
} }
action.name = (yield select(getActions)).find(
(act: any) => act.config.id === action.id,
)?.config.name;
const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI( const response: GenericApiResponse<RestAction> = yield ActionAPI.updateAPI(
action, action,
); );
@ -742,6 +754,103 @@ function* copyActionSaga(
} }
} }
function* editApiNameSaga(action: ReduxAction<{ id: string; value: string }>) {
const actionNames = yield select(state =>
state.entities.actions.map((action: ActionData) => action.config.name),
);
const draftActionNames = yield select(state =>
Object.values(state.ui.apiPane.apiName.drafts),
);
//TODO: If an api is in saving state, then it should not use that name as well.
const validation = validateEntityName(action.payload.value, [
...actionNames,
...draftActionNames,
]);
yield put(
updateApiNameDraft({
id: action.payload.id,
draft: {
value: action.payload.value,
validation: validation,
},
}),
);
}
export function* refactorActionName(
id: string,
pageId: string,
oldName: string,
newName: string,
) {
// fetch page of the action
const pageResponse = yield call(PageApi.fetchPage, {
pageId: pageId,
});
// check if page request is successful
const isPageRequestSuccessful = yield validateResponse(pageResponse);
if (isPageRequestSuccessful) {
// get the layoutId from the page response
const layoutId = pageResponse.data.layouts[0].id;
// call to refactor action
const refactorResponse = yield ActionAPI.updateActionName({
layoutId,
pageId: pageId,
oldName: oldName,
newName: newName,
});
const isRefactorSuccessful = yield validateResponse(refactorResponse);
const currentPageId = yield select(getCurrentPageId);
if (isRefactorSuccessful) {
yield put({
type: ReduxActionTypes.SAVE_API_NAME_SUCCESS,
payload: {
actionId: id,
},
});
if (currentPageId === pageId) {
yield updateCanvasWithDSL(refactorResponse.data, pageId, layoutId);
} else {
yield put(fetchActionsForPage(pageId));
}
}
}
}
function* saveApiNameSaga(action: ReduxAction<{ id: string; name: string }>) {
// Takes from drafts, checks if the name isValid, saves
try {
const apiId = action.payload.id;
const api = yield select(state =>
state.entities.actions.find(
(action: ActionData) => action.config.id === apiId,
),
);
yield refactorActionName(
api.config.id,
api.config.pageId,
api.config.name,
action.payload.name,
);
} catch (e) {
yield put({
type: ReduxActionErrorTypes.SAVE_API_NAME_ERROR,
payload: {
actionId: action.payload.id,
},
});
AppToaster.show({
message: `Unable to update API name`,
type: ToastType.ERROR,
});
console.error(e);
}
}
export function* watchActionSagas() { export function* watchActionSagas() {
yield all([ yield all([
takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga), takeEvery(ReduxActionTypes.FETCH_ACTIONS_INIT, fetchActionsSaga),
@ -750,6 +859,8 @@ export function* watchActionSagas() {
takeEvery(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga), takeEvery(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga),
takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga), takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga),
takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga), takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga),
takeLatest(ReduxActionTypes.EDIT_API_NAME, editApiNameSaga),
takeLatest(ReduxActionTypes.SAVE_API_NAME, saveApiNameSaga),
takeLatest( takeLatest(
ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS, ReduxActionTypes.EXECUTE_PAGE_LOAD_ACTIONS,
executePageLoadActionsSaga, executePageLoadActionsSaga,

View File

@ -231,7 +231,7 @@ function* changeApiSaga(actionPayload: ReduxAction<{ id: string }>) {
data = draft; data = draft;
} }
yield put(initialize(API_EDITOR_FORM_NAME, data)); yield put(initialize(API_EDITOR_FORM_NAME, _.omit(data, "name")));
history.push(API_EDITOR_ID_URL(applicationId, pageId, id)); history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
yield call(initializeExtraFormDataSaga); yield call(initializeExtraFormDataSaga);
@ -426,7 +426,7 @@ function* handleActionCreatedSaga(actionPayload: ReduxAction<RestAction>) {
const data = { ...action }; const data = { ...action };
if (pluginType === "API") { if (pluginType === "API") {
yield put(initialize(API_EDITOR_FORM_NAME, data)); yield put(initialize(API_EDITOR_FORM_NAME, _.omit(data, "name")));
const applicationId = yield select(getCurrentApplicationId); const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId); const pageId = yield select(getCurrentPageId);
history.push(API_EDITOR_ID_URL(applicationId, pageId, id)); history.push(API_EDITOR_ID_URL(applicationId, pageId, id));
@ -465,7 +465,7 @@ function* handleMoveOrCopySaga(actionPayload: ReduxAction<{ id: string }>) {
API_EDITOR_FORM_NAME, API_EDITOR_FORM_NAME,
); );
if (values.id === id) { if (values.id === id) {
yield put(initialize(API_EDITOR_FORM_NAME, action)); yield put(initialize(API_EDITOR_FORM_NAME, _.omit(action, "name")));
} else { } else {
yield put(changeApi(id)); yield put(changeApi(id));
} }

View File

@ -14,7 +14,6 @@ import ApplicationApi, {
ApplicationPagePayload, ApplicationPagePayload,
SetDefaultPageRequest, SetDefaultPageRequest,
DeleteApplicationRequest, DeleteApplicationRequest,
GetAllApplicationResponse,
FetchUsersApplicationsOrgsResponse, FetchUsersApplicationsOrgsResponse,
OrganizationApplicationObject, OrganizationApplicationObject,
ApplicationObject, ApplicationObject,

View File

@ -19,7 +19,6 @@ import OrgApi, {
CreateOrgRequest, CreateOrgRequest,
FetchAllUsersResponse, FetchAllUsersResponse,
FetchAllUsersRequest, FetchAllUsersRequest,
FetchAllRolesRequest,
FetchAllRolesResponse, FetchAllRolesResponse,
DeleteOrgUserRequest, DeleteOrgUserRequest,
ChangeUserRoleRequest, ChangeUserRoleRequest,

View File

@ -29,6 +29,7 @@ import PageApi, {
DeletePageRequest, DeletePageRequest,
UpdateWidgetNameRequest, UpdateWidgetNameRequest,
UpdateWidgetNameResponse, UpdateWidgetNameResponse,
PageLayout,
} from "api/PageApi"; } from "api/PageApi";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { import {
@ -369,23 +370,7 @@ export function* updateWidgetNameSaga(
); );
const isValidResponse = yield validateResponse(response); const isValidResponse = yield validateResponse(response);
if (isValidResponse) { if (isValidResponse) {
const normalizedWidgets = CanvasWidgetsNormalizer.normalize( yield updateCanvasWithDSL(response.data, pageId, layoutId);
response.data.dsl,
);
const currentPageName = yield select(getCurrentPageName);
const applicationId = yield select(getCurrentApplicationId);
const canvasWidgetsPayload: UpdateCanvasPayload = {
pageWidgetId: normalizedWidgets.result,
currentPageName,
currentPageId: pageId,
currentLayoutId: layoutId,
currentApplicationId: applicationId,
pageActions: response.data.layoutOnLoadActions,
widgets: normalizedWidgets.entities.canvasWidgets,
};
yield put(updateCanvas(canvasWidgetsPayload));
yield put(fetchActionsForPage(pageId));
yield put(updateWidgetNameSuccess()); yield put(updateWidgetNameSuccess());
} }
@ -409,6 +394,27 @@ export function* updateWidgetNameSaga(
} }
} }
export function* updateCanvasWithDSL(
data: PageLayout,
pageId: string,
layoutId: string,
) {
const normalizedWidgets = CanvasWidgetsNormalizer.normalize(data.dsl);
const currentPageName = yield select(getCurrentPageName);
const applicationId = yield select(getCurrentApplicationId);
const canvasWidgetsPayload: UpdateCanvasPayload = {
pageWidgetId: normalizedWidgets.result,
currentPageName,
currentPageId: pageId,
currentLayoutId: layoutId,
currentApplicationId: applicationId,
pageActions: data.layoutOnLoadActions,
widgets: normalizedWidgets.entities.canvasWidgets,
};
yield put(updateCanvas(canvasWidgetsPayload));
yield put(fetchActionsForPage(pageId));
}
export default function* pageSagas() { export default function* pageSagas() {
yield all([ yield all([
takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga), takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga),

View File

@ -12,8 +12,6 @@ import UserApi, {
ForgotPasswordRequest, ForgotPasswordRequest,
VerifyTokenRequest, VerifyTokenRequest,
TokenPasswordUpdateRequest, TokenPasswordUpdateRequest,
FetchUserRequest,
FetchUserResponse,
SwitchUserOrgRequest, SwitchUserOrgRequest,
AddUserToOrgRequest, AddUserToOrgRequest,
} from "api/UserApi"; } from "api/UserApi";
@ -25,11 +23,6 @@ import {
getResponseErrorMessage, getResponseErrorMessage,
callAPI, callAPI,
} from "./ErrorSagas"; } from "./ErrorSagas";
import * as Sentry from "@sentry/browser";
import { fetchOrgsSaga } from "./OrgSagas";
import { resetAuthExpiration } from "utils/storage";
import { import {
logoutUserSuccess, logoutUserSuccess,
logoutUserError, logoutUserError,

View File

@ -1,6 +1,7 @@
import { getFormValues, isValid, getFormInitialValues } from "redux-form"; import { getFormValues, isValid, getFormInitialValues } from "redux-form";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { RestAction } from "entities/Action"; import { RestAction } from "entities/Action";
import { ActionData } from "reducers/entityReducers/actionsReducer";
type GetFormData = ( type GetFormData = (
state: AppState, state: AppState,
@ -15,3 +16,16 @@ export const getFormData: GetFormData = (state, formName) => {
const valid = isValid(formName)(state); const valid = isValid(formName)(state);
return { initialValues, values, dirty, valid }; return { initialValues, values, dirty, valid };
}; };
export const getApiName = (state: AppState, id: string) => {
const apiNameDraft = state.ui.apiPane.apiName.drafts[id]?.value;
if (apiNameDraft === undefined) {
return state.entities.actions.find(
(action: ActionData) => action.config.id === id,
)?.config.name;
} else {
// If there is something in drafts, return draft value.
return apiNameDraft;
}
};

View File

@ -1,6 +1,6 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { OrgRole, Org, Organization } from "constants/orgConstants"; import { OrgRole, Organization } from "constants/orgConstants";
export const getRolesFromState = (state: AppState) => { export const getRolesFromState = (state: AppState) => {
return state.ui.orgs.roles; return state.ui.orgs.roles;

View File

@ -245,22 +245,24 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
if (!isValid) { if (!isValid) {
return { return {
isValid, isValid,
parsed, parsed: [],
message: `${WIDGET_TYPE_VALIDATION_ERROR}: Table Data`, transformed: parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`,
}; };
} else if ( }
!_.every(parsed, datum => { const isValidTableData = _.every(parsed, datum => {
return ( return (
_.isObject(datum) && _.isObject(datum) &&
Object.keys(datum).filter(key => _.isString(key) && key.length === 0) Object.keys(datum).filter(key => _.isString(key) && key.length === 0)
.length === 0 .length === 0
); );
}) });
) { if (!isValidTableData) {
return { return {
isValid: false, isValid: false,
parsed: [], parsed: [],
message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "key1" : "val1", "key2" : "val2" }, { "key1" : "val3", "key2" : "val4" }]`, transformed: parsed,
message: `${WIDGET_TYPE_VALIDATION_ERROR}: [{ "Col1" : "val1", "Col2" : "val2" }]`,
}; };
} }
return { isValid, parsed }; return { isValid, parsed };
@ -326,45 +328,6 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
} }
return { isValid, parsed, transformed: parsed }; return { isValid, parsed, transformed: parsed };
}, },
[VALIDATION_TYPES.SINGLE_CHART_DATA]: (value, props, dataTree) => {
const { isValid, parsed } = VALIDATORS[VALIDATION_TYPES.TABLE_DATA](
value,
props,
dataTree,
);
if (!isValid) {
return {
isValid: false,
parsed: [],
message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`,
};
}
const isValidChartData = _.every(
parsed,
(chartPoint: { x: string; y: any }) => {
return (
_.isObject(chartPoint) &&
_.isString(chartPoint.x) &&
!_.isUndefined(chartPoint.y)
);
},
);
if (!isValidChartData) {
return {
isValid: false,
parsed: [],
message: `${WIDGET_TYPE_VALIDATION_ERROR}: Chart Data`,
};
}
return {
isValid: true,
parsed,
message: "",
};
},
[VALIDATION_TYPES.MARKERS]: ( [VALIDATION_TYPES.MARKERS]: (
value: any, value: any,
props: WidgetProps, props: WidgetProps,

View File

@ -100,6 +100,37 @@ const chartDataMigration = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
return currentDSL; return currentDSL;
}; };
const singleChartDataMigration = (
currentDSL: ContainerWidgetProps<WidgetProps>,
) => {
currentDSL.children = currentDSL.children?.map(child => {
if (child.type === WidgetTypes.CHART_WIDGET) {
// Check if chart widget has the deprecated singleChartData property
if (child.hasOwnProperty("singleChartData")) {
// This is to make sure that the format of the chartData is accurate
if (
Array.isArray(child.singleChartData) &&
!child.singleChartData[0].hasOwnProperty("seriesName")
) {
child.singleChartData = {
seriesName: "Series 1",
data: child.singleChartData || [],
};
}
//TODO: other possibilities?
child.chartData = JSON.stringify([...child.singleChartData]);
delete child.singleChartData;
}
}
if (child.children && child.children.length > 0) {
child = singleChartDataMigration(child);
}
return child;
});
return currentDSL;
};
const mapDataMigration = (currentDSL: ContainerWidgetProps<WidgetProps>) => { const mapDataMigration = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
currentDSL.children = currentDSL.children?.map((children: WidgetProps) => { currentDSL.children = currentDSL.children?.map((children: WidgetProps) => {
if (children.type === WidgetTypes.MAP_WIDGET) { if (children.type === WidgetTypes.MAP_WIDGET) {
@ -199,6 +230,11 @@ const transformDSL = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
currentDSL = mapDataMigration(currentDSL); currentDSL = mapDataMigration(currentDSL);
currentDSL.version = 4; currentDSL.version = 4;
} }
if (currentDSL.version === 4) {
currentDSL = singleChartDataMigration(currentDSL);
currentDSL.version = 5;
}
return currentDSL; return currentDSL;
}; };

View File

@ -74,3 +74,17 @@ export const scrollElementIntoParentCanvasView = (
} }
} }
}; };
export const convertToCamelCase = (value: string, limit?: number) => {
const separatorRegex = /\W+/;
return value
.split(separatorRegex)
.map((token, index) => {
if (index > 0) {
return token.charAt(0).toLocaleUpperCase() + token.slice(1);
}
return token;
})
.join("_")
.slice(0, limit || 30);
};

View File

@ -41,7 +41,7 @@ class FormWidget extends ContainerWidget {
getFormData(formWidget: ContainerWidgetProps<WidgetProps>) { getFormData(formWidget: ContainerWidgetProps<WidgetProps>) {
const formData: any = {}; const formData: any = {};
if (formWidget.children) if (formWidget.children)
formWidget.children.map(widgetData => { formWidget.children.forEach(widgetData => {
if (widgetData.value) { if (widgetData.value) {
formData[widgetData.widgetName] = widgetData.value; formData[widgetData.widgetName] = widgetData.value;
} }

View File

@ -1,8 +1,8 @@
import React, { lazy, Suspense } from "react"; import React, { Suspense } from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants"; import { WidgetType } from "constants/WidgetConstants";
import { EventType } from "constants/ActionConstants"; import { EventType } from "constants/ActionConstants";
import { forIn } from "lodash"; // import { forIn } from "lodash";
import ReactTableComponent from "components/designSystems/appsmith/ReactTableComponent"; import ReactTableComponent from "components/designSystems/appsmith/ReactTableComponent";
import { VALIDATION_TYPES } from "constants/WidgetValidation"; import { VALIDATION_TYPES } from "constants/WidgetValidation";
@ -10,43 +10,44 @@ import {
WidgetPropertyValidationType, WidgetPropertyValidationType,
BASE_WIDGET_VALIDATION, BASE_WIDGET_VALIDATION,
} from "utils/ValidationFactory"; } from "utils/ValidationFactory";
import { ColumnModel } from "@syncfusion/ej2-grids"; // import { ColumnModel } from "@syncfusion/ej2-grids";
import { ColumnDirTypecast } from "@syncfusion/ej2-react-grids"; // import { ColumnDirTypecast } from "@syncfusion/ej2-react-grids";
import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl"; import { ColumnAction } from "components/propertyControls/ColumnActionSelectorControl";
import { TriggerPropertiesMap } from "utils/WidgetFactory"; import { TriggerPropertiesMap } from "utils/WidgetFactory";
import Skeleton from "components/utils/Skeleton"; import Skeleton from "components/utils/Skeleton";
import { Classes } from "@blueprintjs/core";
const TableComponent = lazy(() => // const TableComponent = React.lazy(() =>
import( // import(
/* webpackPrefetch: true, webpackChunkName: "table" */ "components/designSystems/syncfusion/TableComponent" // /* webpackPrefetch: true, webpackChunkName: "table" */ "components/designSystems/syncfusion/TableComponent"
), // ),
); // );
const ROW_HEIGHT = 37; // const ROW_HEIGHT = 37;
const TABLE_HEADER_HEIGHT = 39; // const TABLE_HEADER_HEIGHT = 39;
const TABLE_FOOTER_HEIGHT = 48; // const TABLE_FOOTER_HEIGHT = 48;
const TABLE_EXPORT_HEIGHT = 43; // const TABLE_EXPORT_HEIGHT = 43;
function constructColumns( // function constructColumns(
data: object[], // data: object[],
hiddenColumns?: string[], // hiddenColumns?: string[],
): ColumnModel[] | ColumnDirTypecast[] { // ): ColumnModel[] | ColumnDirTypecast[] {
let cols: ColumnModel[] | ColumnDirTypecast[] = []; // let cols: ColumnModel[] | ColumnDirTypecast[] = [];
const listItemWithAllProperties = {}; // const listItemWithAllProperties = {};
data.forEach(dataItem => { // data.forEach(dataItem => {
Object.assign(listItemWithAllProperties, dataItem); // Object.assign(listItemWithAllProperties, dataItem);
}); // });
forIn(listItemWithAllProperties, (value: any, key: string) => { // forIn(listItemWithAllProperties, (value: any, key: string) => {
cols.push({ // cols.push({
field: key, // field: key,
visible: !hiddenColumns?.includes(key), // visible: !hiddenColumns?.includes(key),
}); // });
}); // });
cols = (cols as any[]).filter(col => col.field !== "_color") as // cols = (cols as any[]).filter(col => col.field !== "_color") as
| ColumnModel[] // | ColumnModel[]
| ColumnDirTypecast[]; // | ColumnDirTypecast[];
return cols; // return cols;
} // }
class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> { class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
static getPropertyValidationMap(): WidgetPropertyValidationType { static getPropertyValidationMap(): WidgetPropertyValidationType {
@ -85,7 +86,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
getPageView() { getPageView() {
const { tableData, hiddenColumns } = this.props; const { tableData, hiddenColumns } = this.props;
const columns = constructColumns(tableData, hiddenColumns); // const columns = constructColumns(tableData, hiddenColumns);
const serverSidePaginationEnabled = (this.props const serverSidePaginationEnabled = (this.props
.serverSidePaginationEnabled && .serverSidePaginationEnabled &&
@ -98,14 +99,14 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
} }
const { componentWidth, componentHeight } = this.getComponentDimensions(); const { componentWidth, componentHeight } = this.getComponentDimensions();
const exportHeight = // const exportHeight =
this.props.exportCsv || this.props.exportPDF || this.props.exportCsv // this.props.exportCsv || this.props.exportPDF || this.props.exportCsv
? TABLE_EXPORT_HEIGHT // ? TABLE_EXPORT_HEIGHT
: 0; // : 0;
const tableHeaderHeight = // const tableHeaderHeight =
this.props.tableData.length === 0 ? 2 : TABLE_HEADER_HEIGHT; // this.props.tableData.length === 0 ? 2 : TABLE_HEADER_HEIGHT;
const tableContentHeight = // const tableContentHeight =
componentHeight - TABLE_FOOTER_HEIGHT - tableHeaderHeight - exportHeight; // componentHeight - TABLE_FOOTER_HEIGHT - tableHeaderHeight - exportHeight;
// Use below code to calculate page size for old table component // Use below code to calculate page size for old table component
// const pageSize = Math.floor(tableContentHeight / ROW_HEIGHT); // const pageSize = Math.floor(tableContentHeight / ROW_HEIGHT);
const pageSize = Math.floor((componentHeight - 104) / 52); const pageSize = Math.floor((componentHeight - 104) / 52);
@ -113,10 +114,10 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
if (pageSize !== this.props.pageSize) { if (pageSize !== this.props.pageSize) {
super.updateWidgetMetaProperty("pageSize", pageSize); super.updateWidgetMetaProperty("pageSize", pageSize);
} }
// /* // /*
return ( return (
<Suspense fallback={<Skeleton />}> <Suspense fallback={<Skeleton />}>
<div className={this.props.isLoading ? Classes.SKELETON : ""}>
<ReactTableComponent <ReactTableComponent
height={componentHeight} height={componentHeight}
width={componentWidth} width={componentWidth}
@ -166,6 +167,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
this.disableDrag(disable); this.disableDrag(disable);
}} }}
/> />
</div>
</Suspense> </Suspense>
); );
// */ // */