Merge pull request #2759 from appsmithorg/release

Release v1.3.2
This commit is contained in:
Arpit Mohan 2021-01-28 18:02:48 +05:30 committed by GitHub
commit 433461a347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
148 changed files with 3002 additions and 2511 deletions

View File

@ -2,7 +2,7 @@
Thank you for your interest in Appsmith and taking the time to contribute on this project. 🙌 Thank you for your interest in Appsmith and taking the time to contribute on this project. 🙌
Appsmith is a project by developers for developers and there are a lot of ways you can contribute. Appsmith is a project by developers for developers and there are a lot of ways you can contribute.
Feel free to propose changes to this document in a pull request. If you don't know where to start contributing, ask us on our [Discord channel](https://discord.gg/WN8b2W8j).
## Code of conduct ## Code of conduct
@ -10,7 +10,7 @@ Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing
## How can I contribute? ## How can I contribute?
There are many ways in which we/one can to contribute to Appsmith. All contributions are highly appreciated. There are many ways in which you can to contribute to Appsmith.
#### 🐛 Report a bug #### 🐛 Report a bug
Report all issues through GitHub Issues using the [Report a Bug](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Bug%2C+High&template=---bug-report.md&title=%5BBug%5D) template. Report all issues through GitHub Issues using the [Report a Bug](https://github.com/appsmithorg/appsmith/issues/new?assignees=Nikhil-Nandagopal&labels=Bug%2C+High&template=---bug-report.md&title=%5BBug%5D) template.
@ -24,4 +24,4 @@ File your feature request through GitHub Issues using the [Feature Request](http
In the process of shipping features quickly, we often forget to keep our docs up to date. You can help by suggesting improvements to our documentation or dive right in to our [Contribution Guide](contributions/docs/CONTRIBUTING.md)! In the process of shipping features quickly, we often forget to keep our docs up to date. You can help by suggesting improvements to our documentation or dive right in to our [Contribution Guide](contributions/docs/CONTRIBUTING.md)!
#### ⚙️ Close a Bug / Feature issue #### ⚙️ Close a Bug / Feature issue
We welcome contributions that help make appsmith bug free & improve the experience of our users. Check out our [Code Contribution Guide](contributions/CodeContributionsGuidelines.md) to begin. We welcome contributions that help make appsmith bug free & improve the experience of our users. You can also find issues tagged [Good First Issues](https://github.com/appsmithorg/appsmith/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22+bug). Check out our [Code Contribution Guide](contributions/CodeContributionsGuidelines.md) to begin.

View File

@ -13,10 +13,10 @@
"parentRowSpace": 1, "parentRowSpace": 1,
"type": "CANVAS_WIDGET", "type": "CANVAS_WIDGET",
"canExtend": true, "canExtend": true,
"dynamicBindingPathList": [], "version": 8,
"version": 6,
"minHeight": 1292, "minHeight": 1292,
"parentColumnSpace": 1, "parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0, "leftColumn": 0,
"children": [ "children": [
{ {
@ -28,49 +28,32 @@
"isLoading": false, "isLoading": false,
"parentColumnSpace": 74, "parentColumnSpace": 74,
"parentRowSpace": 40, "parentRowSpace": 40,
"leftColumn": 4, "leftColumn": 6,
"rightColumn": 9, "rightColumn": 11,
"topRow": 0, "topRow": 13,
"bottomRow": 1, "bottomRow": 14,
"parentId": "0", "parentId": "0",
"widgetId": "orkla1pg88" "widgetId": "1bek8n8byg"
}, },
{ {
"isVisible": true, "isVisible": true,
"label": "", "label": "",
"selectionType": "SINGLE_SELECT", "selectionType": "SINGLE_SELECT",
"options": "[\n {\n \"label\": \"Vegetarian\",\n \"value\": \"VEG\"\n },\n {\n \"label\": \"Non-Vegetarian\",\n \"value\": \"NON_VEG\"\n },\n {\n \"label\": \"Vegan\",\n \"value\": \"VEGAN\"\n }\n]", "options": "",
"widgetName": "Dropdown1", "widgetName": "Dropdown1",
"defaultOptionValue": "VEG", "defaultOptionValue": "VEG",
"type": "DROP_DOWN_WIDGET", "type": "DROP_DOWN_WIDGET",
"isLoading": false, "isLoading": false,
"parentColumnSpace": 74, "parentColumnSpace": 74,
"parentRowSpace": 40, "parentRowSpace": 40,
"leftColumn": 4, "leftColumn": 0,
"rightColumn": 9, "rightColumn": 5,
"topRow": 2, "topRow": 15,
"bottomRow": 3, "bottomRow": 16,
"parentId": "0", "parentId": "0",
"widgetId": "9iofg44qjm", "widgetId": "zd6jycngj7",
"dynamicBindingPathList": [] "dynamicBindingPathList": []
}, },
{
"isVisible": true,
"defaultText": "This is the initial <b>content</b> of the editor",
"isDisabled": false,
"widgetName": "RichTextEditor1",
"isDefaultClickDisabled": true,
"type": "RICH_TEXT_EDITOR_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 2,
"rightColumn": 10,
"topRow": 4,
"bottomRow": 9,
"parentId": "0",
"widgetId": "p4uowm5ds3"
},
{ {
"isVisible": true, "isVisible": true,
"label": "Data", "label": "Data",
@ -81,12 +64,12 @@
"isLoading": false, "isLoading": false,
"parentColumnSpace": 74, "parentColumnSpace": 74,
"parentRowSpace": 40, "parentRowSpace": 40,
"leftColumn": 3, "leftColumn": 4,
"rightColumn": 11, "rightColumn": 12,
"topRow": 12, "topRow": 19,
"bottomRow": 19, "bottomRow": 26,
"parentId": "0", "parentId": "0",
"widgetId": "iu9vqkj1rd", "widgetId": "ei38nqop3s",
"dynamicBindingPathList": [] "dynamicBindingPathList": []
} }
] ]

View File

@ -5,7 +5,6 @@
"mongo-username": "cypress-test", "mongo-username": "cypress-test",
"mongo-password": "RaopEmky505xYV4p", "mongo-password": "RaopEmky505xYV4p",
"mongo-authenticationAuthtype": "SCRAM-SHA-1", "mongo-authenticationAuthtype": "SCRAM-SHA-1",
"mongo-sslAuthtype": "No SSL",
"postgres-host": "postgres-test-db.cz8diybf9wdj.ap-south-1.rds.amazonaws.com", "postgres-host": "postgres-test-db.cz8diybf9wdj.ap-south-1.rds.amazonaws.com",
"postgres-port": 5432, "postgres-port": 5432,
"postgres-databaseName": "fakeapi", "postgres-databaseName": "fakeapi",

View File

@ -51,7 +51,7 @@
"value": "VEG" "value": "VEG"
}, },
{ {
"label": "{{Table1.tableData[1].email}}", "label": "{{Table1.tableData[2].email}}",
"value": "NONVEG" "value": "NONVEG"
} }
], ],

View File

@ -17,7 +17,9 @@ describe("Binding the multiple Widgets and validating NavigateTo Page", function
it("Input widget test with default value from table widget", function() { it("Input widget test with default value from table widget", function() {
cy.openPropertyPane("inputwidget"); cy.openPropertyPane("inputwidget");
cy.get(widgetsPage.defaultInput).type(testdata.defaultInputWidget); cy.get(widgetsPage.defaultInput).type(testdata.defaultInputWidget);
cy.get(widgetsPage.actionSelect).click(); cy.get(widgetsPage.actionSelect)
.first()
.click();
cy.get(commonlocators.chooseAction) cy.get(commonlocators.chooseAction)
.children() .children()
.contains("Navigate To") .contains("Navigate To")

View File

@ -1,88 +1,88 @@
// const commonlocators = require("../../../locators/commonlocators.json"); const commonlocators = require("../../../locators/commonlocators.json");
// const formWidgetsPage = require("../../../locators/FormWidgets.json"); const formWidgetsPage = require("../../../locators/FormWidgets.json");
// const dsl = require("../../../fixtures/rundsl.json"); const dsl = require("../../../fixtures/rundsl.json");
// const pages = require("../../../locators/Pages.json"); const pages = require("../../../locators/Pages.json");
// const widgetsPage = require("../../../locators/Widgets.json"); const widgetsPage = require("../../../locators/Widgets.json");
// const publish = require("../../../locators/publishWidgetspage.json"); const publish = require("../../../locators/publishWidgetspage.json");
// const queryLocators = require("../../../locators/QueryEditor.json"); const queryLocators = require("../../../locators/QueryEditor.json");
// const datasource = require("../../../locators/DatasourcesEditor.json"); const datasource = require("../../../locators/DatasourcesEditor.json");
// const apiwidget = require("../../../locators/apiWidgetslocator.json"); const apiwidget = require("../../../locators/apiWidgetslocator.json");
// const testdata = require("../../../fixtures/testdata.json"); const testdata = require("../../../fixtures/testdata.json");
// const pageid = "MyPage"; const pageid = "MyPage";
// let updatedName; let updatedName;
// let datasourceName; let datasourceName;
// describe("Binding the multiple widgets and validating default data", function() { describe("Binding the multiple widgets and validating default data", function() {
// before(() => { before(() => {
// cy.addDsl(dsl); cy.addDsl(dsl);
// }); });
// it("Create a postgres datasource", function() { it("Create a postgres datasource", function() {
// cy.NavigateToDatasourceEditor(); cy.NavigateToDatasourceEditor();
// cy.get(datasource.PostgreSQL).click(); cy.get(datasource.PostgreSQL).click();
// cy.getPluginFormsAndCreateDatasource(); cy.getPluginFormsAndCreateDatasource();
// cy.fillPostgresDatasourceForm(); cy.fillPostgresDatasourceForm();
// cy.testSaveDatasource(); cy.testSaveDatasource();
// cy.get("@createDatasource").then(httpResponse => { cy.get("@createDatasource").then((httpResponse) => {
// datasourceName = httpResponse.response.body.data.name; datasourceName = httpResponse.response.body.data.name;
// }); });
// }); });
// it("Create and runs query", () => { it("Create and runs query", () => {
// cy.NavigateToQueryEditor(); cy.NavigateToQueryEditor();
// cy.contains(".t--datasource-name", datasourceName) cy.contains(".t--datasource-name", datasourceName)
// .find(queryLocators.createQuery) .find(queryLocators.createQuery)
// .click(); .click();
// cy.get(queryLocators.templateMenu).click(); cy.get(queryLocators.templateMenu).click();
// cy.get(".CodeMirror textarea") cy.get(".CodeMirror textarea")
// .first() .first()
// .focus() .focus()
// .type("select * from users limit 10"); .type("select * from users limit 10");
// cy.EvaluateCurrentValue("select * from users limit 10"); cy.EvaluateCurrentValue("select * from users limit 10");
// cy.runQuery(); cy.runQuery();
// }); });
// it("Button widget test with on action query run", function() { it("Button widget test with on action query run", function() {
// cy.SearchEntityandOpen("Button1"); cy.SearchEntityandOpen("Button1");
// cy.executeDbQuery("Query1"); cy.executeDbQuery("Query1");
// cy.get(commonlocators.editPropCrossButton).click(); cy.get(commonlocators.editPropCrossButton).click();
// cy.wait("@updateLayout").should( cy.wait("@updateLayout").should(
// "have.nested.property", "have.nested.property",
// "response.body.responseMeta.status", "response.body.responseMeta.status",
// 200, 200,
// ); );
// }); });
// it("Input widget test with default value update with query data", function() { it("Input widget test with default value update with query data", function() {
// cy.SearchEntityandOpen("Input1"); cy.SearchEntityandOpen("Input1");
// cy.get(widgetsPage.defaultInput).type(testdata.defaultInputQuery); cy.get(widgetsPage.defaultInput).type(testdata.defaultInputQuery);
// cy.get(commonlocators.editPropCrossButton).click(); cy.get(commonlocators.editPropCrossButton).click();
// cy.wait("@updateLayout").should( cy.wait("@updateLayout").should(
// "have.nested.property", "have.nested.property",
// "response.body.responseMeta.status", "response.body.responseMeta.status",
// 200, 200,
// ); );
// }); });
// it("Publish App and validate loading functionalty", function() { it("Publish App and validate loading functionalty", function() {
// cy.PublishtheApp(); cy.PublishtheApp();
// cy.wait(2000); cy.wait(2000);
// cy.get(widgetsPage.widgetBtn) cy.get(widgetsPage.widgetBtn)
// .first() .first()
// .click({ force: true }); .click({ force: true });
// cy.wait("@postExecute").should( cy.wait("@postExecute").should(
// "have.nested.property", "have.nested.property",
// "response.body.responseMeta.status", "response.body.responseMeta.status",
// 200, 200,
// ); );
// cy.get(publish.inputWidget + " " + "input") cy.get(publish.inputWidget + " " + "input")
// .first() .first()
// .invoke("attr", "value") .invoke("attr", "value")
// .should("contain", "7"); .should("contain", "7");
// }); });
// }); });

View File

@ -22,8 +22,7 @@ describe("Binding the multiple widgets and validating default data", function()
); );
}); });
/* //To be enabled once the single select multi select issues are resolved
To be enabled once the single select multi select issues are resolved
it("Dropdown widget test with default value from table widget", function() { it("Dropdown widget test with default value from table widget", function() {
cy.openPropertyPane("dropdownwidget"); cy.openPropertyPane("dropdownwidget");
cy.testJsontext("options", JSON.stringify(testdata.deafultDropDownWidget)); cy.testJsontext("options", JSON.stringify(testdata.deafultDropDownWidget));
@ -34,11 +33,10 @@ describe("Binding the multiple widgets and validating default data", function()
200, 200,
); );
}); });
*/
it("validation of default data displayed in all widgets based on row selected", function() { it("validation of default data displayed in all widgets based on row selected", function() {
cy.isSelectRow(1); cy.isSelectRow(1);
cy.readTabledataPublish("1", "0").then(tabData => { cy.readTabledataPublish("1", "0").then((tabData) => {
const tabValue = tabData; const tabValue = tabData;
expect(tabValue).to.be.equal("2736212"); expect(tabValue).to.be.equal("2736212");
cy.log("the value is" + tabValue); cy.log("the value is" + tabValue);
@ -48,18 +46,17 @@ describe("Binding the multiple widgets and validating default data", function()
.invoke("attr", "value") .invoke("attr", "value")
.should("contain", tabValue); .should("contain", tabValue);
}); });
/*
cy.readTabledataPublish("1", "1").then(tabData => { cy.readTabledataPublish("1", "1").then((tabData) => {
const tabValue = tabData; const tabValue = tabData;
expect(tabValue).to.be.equal("lindsay.ferguson@reqres.in"); expect(tabValue).to.be.equal("lindsay.ferguson@reqres.in");
cy.log("the value is" + tabValue); cy.log("the value is" + tabValue);
cy.get(widgetsPage.defaultSingleSelectValue) cy.get(widgetsPage.defaultSingleSelectValue)
.invoke("text") .invoke("text")
.then(text => { .then((text) => {
const someText = text; const someText = text;
expect(someText).to.equal(tabValue); expect(someText).to.equal(tabValue);
}); });
}); });
*/
}); });
}); });

View File

@ -6,30 +6,37 @@ describe("FilePicker Widget Functionality", function() {
beforeEach(() => { beforeEach(() => {
cy.addDsl(dsl); cy.addDsl(dsl);
}); });
it("Create API to be used in Filepicker", function() {
cy.log("Login Successful");
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("FirstAPI");
cy.log("Creation of FirstAPI Action successful");
cy.enterDatasourceAndPath(
this.data.paginationUrl,
this.data.paginationParam,
);
cy.SaveAndRunAPI();
});
it("FilePicker Widget Functionality", function() { it("FilePicker Widget Functionality", function() {
cy.openPropertyPane("filepickerwidget"); cy.SearchEntityandOpen("FilePicker1");
cy.wait(1000);
//Checking the edit props for FilePicker and also the properties of FilePicker widget //Checking the edit props for FilePicker and also the properties of FilePicker widget
cy.testCodeMirror("Upload Files"); cy.testCodeMirror("Upload Files");
cy.get(commonlocators.editPropCrossButton).click();
}); });
it("It checks the loading state of filepicker on call the action", function() { it("It checks the loading state of filepicker on call the action", function() {
cy.openPropertyPane("filepickerwidget"); cy.SearchEntityandOpen("FilePicker1");
const fixturePath = "testFile.mov"; const fixturePath = "testFile.mov";
cy.getAlert(commonlocators.filePickerOnFilesSelected); cy.addAPIFromLightningMenu("FirstAPI");
cy.get(commonlocators.filePickerButton).click(); cy.get(commonlocators.filePickerButton).click();
cy.get(commonlocators.filePickerInput) cy.get(commonlocators.filePickerInput)
.first() .first()
.attachFile(fixturePath); .attachFile(fixturePath);
cy.get(commonlocators.filePickerUploadButton).click(); cy.get(commonlocators.filePickerUploadButton).click();
cy.get(".bp3-spinner").should("have.length", 1); cy.get(".bp3-spinner").should("have.length", 1);
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.wait(500); cy.wait(500);
cy.get("button").contains("1 files selected"); cy.get("button").contains("1 files selected");
}); });

View File

@ -0,0 +1,30 @@
/// <reference types="Cypress" />
const homePage = require("../../../locators/HomePage.json");
describe("Org name validation spec", function() {
it("create org with leading space validation", function() {
cy.NavigateToHome();
cy.get(homePage.createOrg)
.should("be.visible")
.first()
.click({ force: true });
cy.xpath(homePage.inputOrgName)
.should("be.visible")
.type(" ");
cy.get(homePage.submit).should("be.disabled");
cy.xpath(homePage.cancelBtn).click();
});
it("create org with special characters validation", function() {
cy.get(homePage.createOrg)
.should("be.visible")
.first()
.click({ force: true });
cy.xpath(homePage.inputOrgName)
.should("be.visible")
.type("Test & Org");
cy.get(homePage.submit).should("be.enabled");
cy.xpath(homePage.cancelBtn).click();
});
});

View File

@ -0,0 +1,29 @@
const commonlocators = require("../../../locators/commonlocators.json");
describe("Check for product updates button and modal", function() {
it("Check if we should show the product updates button and it opens the updates modal", function() {
cy.get(commonlocators.homeIcon).click({ force: true });
cy.wait(2000);
cy.window()
.its("store")
.invoke("getState")
.then((state) => {
const { releaseItems, newReleasesCount } = state.ui.releases;
if (Array.isArray(releaseItems) && releaseItems.length > 0) {
cy.get("[data-cy=t--product-updates-btn]")
.contains(newReleasesCount)
.click({ force: true });
cy.wait(500); // modal transition
cy.get(".bp3-dialog-container").contains("Product Updates");
cy.get("[data-cy=t--product-updates-close-btn]").click({
force: true,
});
cy.wait(500); // modal transition
cy.get(".bp3-dialog-container").should("not.exist");
} else {
cy.get("[data-cy=t--product-updates-btn]").should("not.exist");
}
});
});
});

View File

@ -6,13 +6,11 @@
"username": "input[name='datasourceConfiguration.authentication.username']", "username": "input[name='datasourceConfiguration.authentication.username']",
"password": "input[name='datasourceConfiguration.authentication.password']", "password": "input[name='datasourceConfiguration.authentication.password']",
"authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]", "authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]",
"sslAuthtype": "[data-cy=datasourceConfiguration\\.connection\\.ssl\\.authType]",
"url": "input[name='datasourceConfiguration.url']", "url": "input[name='datasourceConfiguration.url']",
"MongoDB": ".t--plugin-name:contains('MongoDB')", "MongoDB": ".t--plugin-name:contains('MongoDB')",
"RESTAPI": ".t--plugin-name:contains('REST API')", "RESTAPI": ".t--plugin-name:contains('REST API')",
"PostgreSQL": ".t--plugin-name:contains('PostgreSQL')", "PostgreSQL": ".t--plugin-name:contains('PostgreSQL')",
"sectionAuthentication": "[data-cy=section-Authentication]", "sectionAuthentication": "[data-cy=section-Authentication]",
"sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]",
"PostgresEntity": ".t--entity-name:contains(PostgreSQL)", "PostgresEntity": ".t--entity-name:contains(PostgreSQL)",
"createQuerty": ".t--create-query", "createQuerty": ".t--create-query",
"editDatasource": ".t--edit-datasource", "editDatasource": ".t--edit-datasource",

View File

@ -67,5 +67,7 @@
"uploadLogo": "//div/form/input", "uploadLogo": "//div/form/input",
"removeLogo": ".remove-button a span", "removeLogo": ".remove-button a span",
"generalTab": "//li//span[text()='General']", "generalTab": "//li//span[text()='General']",
"membersTab": "//li//span[text()='Members']" "membersTab": "//li//span[text()='Members']",
"cancelBtn": "//span[text()='Cancel']",
"submit": "button:contains('Submit')"
} }

View File

@ -0,0 +1,51 @@
const dsl = require("../../../fixtures/tableWidgetDsl.json");
describe("Test for Clipboard Copy", function() {
it(" Clipboard copy on selecting a row ", function()
{
// Add a table widget
// Click on the Property Pane
// Naviagte to Action Items
// Click on "onRow Selection" Dropdown
// Select "Copy to Clipboard"
// Add Text to be copied
// Select a Row from the table
// Add an Input Widget
// Now paste the copied text
// Ensure the text the same as written
}
)
it(" Clipboard copy by adding an action button", function()
{
// Add a table widget
// Click on the Property Pane
// Naviagte to Action Items
// Click on "Add button"
// Click on the dropdown
// Select on Copy to Clipboard
// Add a Text
// Click on the Action Button
// Add Input Widget
// Paste the text into the widget
// Ensure the text the same as written
}
)
it(" Clipboard copy function by converting it to JS ", function()
{
// Add a table widget
// Click on the Property Pane
// Naviagte to Action Items
// Click on "Add button"
// Click on the dropdown
// Click on the Js Option
// Add Copy to Clipboard FUNTION
// Add a Text
// Click on the Action Button
// Add Input Widget
// Paste the text into the widget
// Ensure the text the same as written
}
)
}
)

View File

@ -1,11 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() {
it("Duplicating an application", function() {
// Navigate to home Page
// Click on any application action icon (Three dots)
// Click on "Duplicate" option
// Ensure the application gets copied
// Ensure the name is appended with the word "Copy"
});
});

View File

@ -1,14 +1,17 @@
const homePage = require("../../../locators/HomePage.json"); const homePage = require("../../../locators/HomePage.json");
describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() { describe("Duplicate an application must duplicate every API ,Query widget and Datasource", function() {
it("Duplicating an application", function() { it("Duplicating an application", function()
{
// Navigate to home Page // Navigate to home Page
// Click on any application action icon (Three dots) // Click on any application action icon (Three dots)
// Click on "Duplicate" option // Click on "Duplicate" option
// Ensure the application gets copied // Ensure the application gets copied
// Ensure the name is appended with the word "Copy" // Ensure the name is appended with the word "Copy"
}); }
it("Deleting the duplicated Application ", function() { )
it("Deleting the duplicated Application ", function()
{
// Navigate to home Page // Navigate to home Page
// Click on any application action icon (Three dots) // Click on any application action icon (Three dots)
// Click on "Duplicate" option // Click on "Duplicate" option
@ -18,5 +21,38 @@ describe("Duplicate an application must duplicate every API ,Query widget and Da
// Click on Delete option // Click on Delete option
// Click on "Are You Sure?" option // Click on "Are You Sure?" option
// Ensure the App gets deleted // Ensure the App gets deleted
}); }
}); )
it(" Ensure only the original application is deleted and copy of it exists", function()
{
// Navigate to home Page
// Create an Application
// Add a name to the application
// Navigate to home page
// Now click on the action (Three Dots)
// Select "Duplicate" option
// Ensure App is created with App name prefixed with Copy
// Click on Delete option of Original Application
// Click on "Are You Sure?" option
// Ensure only Original Application is deleted and not the child application
}
)
it(" Ensure only the Duplicate application is deleted and original Application of it exists", function()
{
// Navigate to home Page
// Create an Application
// Add a name to the application
// Navigate to home page
// Now click on the action (Three Dots)
// Select "Duplicate" option
// Ensure App is created with App name prefixed with Copy
// Click on Delete option of Duplicate Application
// Click on "Are You Sure?" option
// Ensure only Duplicate Application is deleted and not the Orginal application
}
)
}
)

View File

@ -0,0 +1,61 @@
const homePage = require("../../../locators/HomePage.json");
describe("adding role without Email Id", function() {
it("Empty Email ID Invite flow", function()
{
// Navigate to Home Page
// Click on "Share" option
// Add Role from the dropdown
// Ensure the "Invite" option is "Inactive"
}
)
it("Error message must be dispalyed to user on inappropriate Email ID", function()
{
// Navigate to Home Page
// Click on "Share" option
// Add inappropriate Email Id
// Select the "Role"
// Ensure the "Invite" option is "Inactive" and error message is displayed to user
}
)
it("Clicking on the organisation list the user must be lead to organisation Station ", function()
{
// Navigate to Home Page
// Navigate to Organisation list
// Click on one of the organisation name
// Ensure user is directed to the organisation
}
)
it("Admin can only assign another Admin ", function()
{
// Navigate to Organisation Setting
// Navigate to Members
// Navigate to roles
// Ensure your also an "Admin"
// Change the role "Admin"
}
)
it("Ensure the user can not delete or create an application in the organisation", function()
{
// Navigate to Home page
// Navigate to Members
// Navigate to roles
// Ensure role is "App Viewer"
// Ensure user is not able to delete or add any user for the application
}
)
it("Ensure On invaild Email Id the box must get highlighted", function()
{
// Navigate to Home page
// Click on the Share option
// Ensure the pop up opens
// Enter an Invaild Email Id
// and ensure the Email ID box is highlight
}
)
}
)

View File

@ -0,0 +1,34 @@
const onboarding = require("../../../locators/Onboarding.json");
const explorer = require("../../../locators/explorerlocators.json");
const homePage = require("../../../locators/HomePage.json");
const loginPage = require("../../../locators/LoginPage.json");
describe("Onboarding flow", function()
{
it("Onboarding using Google Id ", function()
{
// Navigate to Login Page
// Click on "Sign In with Google"
// Ensure user is navigated to Google Account
// Do select the Google Id
// Ensure user is navigated into the Appsmith
// Click on the icon on the right with single letter (Profile)
// Check the Email Id
// Click on Logout
}
)
it("Onboarding using Github ID ", function()
{
// Navigate to Login Page
// Click on "Sign In with Github"
// Ensure user is navigated to Github Account
// Do select the Github Id
// Ensure user is navigated into the Appsmith
// Click on the icon on the right with single letter (Profile)
// Check the Email Id
// Click on Logout
}
)
}
)

View File

@ -1,19 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Deletion of organisational Logo ", function() {
it(" org logo upload ", function()
{
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
// Wait until it loads
// Switch between Tabs
// Click on the remove Icon
//Ensure the organisational Logo is deleted
}
)
}
)

View File

@ -1,18 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("insert organisational Logo ", function() {
it(" org logo upload ", function()
{
//Click on the dropdown next to organisational Name
// Navigate between tabs
// Naviagte to General Tab
// Add an Organisational Logo
//Wait until it loads
// Switch between Tabs
// Navigate to General Tab and ensure the logo exsits
//navigate back to Homepage
}
)
}
)

View File

@ -1,11 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Checking for error message on Organisation Name ", function() {
it("Ensure of Inactive Submit button ", function() {
// Navigate to home Page
// Click on Create Organisation
// Type "Space" as first character
// Ensure "Submit" button does not get Active
// Now click on "X" (Close icon) ensure the pop up closes
});
});

View File

@ -43,5 +43,35 @@ describe("Checking for error message on Organisation Name ", function() {
// Ensure the application can be created with the same name // Ensure the application can be created with the same name
} }
) )
it("User must not be able to add empty organisation name", function()
{
// Navigate to home Page
// Click on the "Create Organisation" button
// Ensure "Organisation Name" field is empty
// Ensure "Submit" is inactive
}
)
it("Cancel creating an Organisation when the Organisation name is empty", function()
{
// Navigate to home Page
// Click on the "Create Organisation" button
// Ensure "Organisation Name" field is empty
// Click on "Cancel" option
// Observe the organisation is not created
}
)
it("Cancel creating an Organisation when the Organisation name is dually filled", function()
{
// Navigate to home Page
// Click on the "Create Organisation" button
// Ensure "Organisation Name" field is enterd respectively
// Click on "Cancel" option
// Observe the organisation is not created
}
)
} }
) )

View File

@ -1,14 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Reuse the name of the deleted application name inside the same organisation", function() {
it("Reuse the name of the deleted application name ", function() {
// Navigate to home Page
// Create an Application by name "XYZ"
// Add some widgets
// Navigate back to the application
// Delete the Application
// Click on "Create New" option under samee organisation
// Enter the name "XYZ"
// Ensure the application can be created with the same name
});
});

View File

@ -1,17 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Shared user icon ", function() {
it(" User Icon is disaplyed to user ", function()
{
// Navigate to home Page
//Click on Share Icon
// Click on Field to add an Email Id
// Click on the Roles field
// Add an role from the Dropdown
// CLick on Invite
//Now observe the icon next to the Share Icon
}
)
}
)

View File

@ -1,11 +0,0 @@
const homePage = require("../../../locators/HomePage.json");
describe("Adding Special Character ", function() {
it("Adding Special Character ", function() {
// Navigate to home Page
// Click on Create Organisation
// Add special as first character
// Ensure "Submit" get Active
// Now click outside and ensure the pop up closes
});
});

View File

@ -1,15 +0,0 @@
const dsl = require("../../../fixtures/tableWidgetDsl.json");
describe("Test for Table Filter ", function() {
it("Table Filter", function()
{
//Add a table
// click on the column action item
// Click on Select a datatype
// Click on Filter option
// ensure to add filter
}
)
}
)

View File

@ -1262,7 +1262,9 @@ Cypress.Commands.add("togglebarDisable", (value) => {
}); });
Cypress.Commands.add("getAlert", (alertcss) => { Cypress.Commands.add("getAlert", (alertcss) => {
cy.get(commonlocators.dropdownSelectButton).click({ force: true }); cy.get(commonlocators.dropdownSelectButton)
.first()
.click({ force: true });
cy.get(widgetsPage.menubar) cy.get(widgetsPage.menubar)
.contains("Show Message") .contains("Show Message")
.click({ force: true }) .click({ force: true })
@ -1279,6 +1281,19 @@ Cypress.Commands.add("getAlert", (alertcss) => {
.click({ force: true }); .click({ force: true });
}); });
Cypress.Commands.add("addAPIFromLightningMenu", (ApiName) => {
cy.get(commonlocators.dropdownSelectButton)
.click({ force: true })
.get("ul.bp3-menu")
.children()
.contains("Call An API")
.click({ force: true })
.get("ul.bp3-menu")
.children()
.contains(ApiName)
.click({ force: true });
});
Cypress.Commands.add("radioInput", (index, text) => { Cypress.Commands.add("radioInput", (index, text) => {
cy.get(widgetsPage.RadioInput) cy.get(widgetsPage.RadioInput)
.eq(index) .eq(index)
@ -1424,17 +1439,10 @@ Cypress.Commands.add("fillMongoDatasourceForm", () => {
cy.get(datasourceEditor["password"]).type( cy.get(datasourceEditor["password"]).type(
datasourceFormData["mongo-password"], datasourceFormData["mongo-password"],
); );
cy.get(datasourceEditor.sectionSSL).click();
cy.get(datasourceEditor["authenticationAuthtype"]).click(); cy.get(datasourceEditor["authenticationAuthtype"]).click();
cy.contains(datasourceFormData["mongo-authenticationAuthtype"]).click({ cy.contains(datasourceFormData["mongo-authenticationAuthtype"]).click({
force: true, force: true,
}); });
cy.get(datasourceEditor["sslAuthtype"]).click();
cy.contains(datasourceFormData["mongo-sslAuthtype"]).click({
force: true,
});
}); });
Cypress.Commands.add("fillPostgresDatasourceForm", () => { Cypress.Commands.add("fillPostgresDatasourceForm", () => {

View File

@ -4,7 +4,7 @@ import { BatchAction, batchAction } from "actions/batchActions";
export const updateWidgetPropertyRequest = ( export const updateWidgetPropertyRequest = (
widgetId: string, widgetId: string,
propertyName: string, propertyPath: string,
propertyValue: any, propertyValue: any,
renderMode: RenderMode, renderMode: RenderMode,
): ReduxAction<UpdateWidgetPropertyRequestPayload> => { ): ReduxAction<UpdateWidgetPropertyRequestPayload> => {
@ -12,7 +12,7 @@ export const updateWidgetPropertyRequest = (
type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST, type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST,
payload: { payload: {
widgetId, widgetId,
propertyName, propertyPath,
propertyValue, propertyValue,
renderMode, renderMode,
}, },
@ -21,29 +21,49 @@ export const updateWidgetPropertyRequest = (
export const updateWidgetProperty = ( export const updateWidgetProperty = (
widgetId: string, widgetId: string,
propertyName: string, updates: Record<string, unknown>,
propertyValue: any,
): BatchAction<UpdateWidgetPropertyPayload> => { ): BatchAction<UpdateWidgetPropertyPayload> => {
return batchAction({ return batchAction({
type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY, type: ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
payload: { payload: {
widgetId, widgetId,
propertyName, updates,
propertyValue,
}, },
}); });
}; };
export const batchUpdateWidgetProperty = (
widgetId: string,
updates: Record<string, unknown>,
): ReduxAction<UpdateWidgetPropertyPayload> => ({
type: ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
payload: {
widgetId,
updates,
},
});
export const deleteWidgetProperty = (
widgetId: string,
propertyPath: string,
): ReduxAction<DeleteWidgetPropertyPayload> => ({
type: ReduxActionTypes.DELETE_WIDGET_PROPERTY,
payload: {
widgetId,
propertyPath,
},
});
export const setWidgetDynamicProperty = ( export const setWidgetDynamicProperty = (
widgetId: string, widgetId: string,
propertyName: string, propertyPath: string,
isDynamic: boolean, isDynamic: boolean,
): ReduxAction<SetWidgetDynamicPropertyPayload> => { ): ReduxAction<SetWidgetDynamicPropertyPayload> => {
return { return {
type: ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY, type: ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY,
payload: { payload: {
widgetId, widgetId,
propertyName, propertyPath,
isDynamic, isDynamic,
}, },
}; };
@ -51,19 +71,23 @@ export const setWidgetDynamicProperty = (
export interface UpdateWidgetPropertyRequestPayload { export interface UpdateWidgetPropertyRequestPayload {
widgetId: string; widgetId: string;
propertyName: string; propertyPath: string;
propertyValue: any; propertyValue: any;
renderMode: RenderMode; renderMode: RenderMode;
} }
export interface UpdateWidgetPropertyPayload { export interface UpdateWidgetPropertyPayload {
widgetId: string; widgetId: string;
propertyName: string; updates: Record<string, unknown>;
propertyValue: any;
} }
export interface SetWidgetDynamicPropertyPayload { export interface SetWidgetDynamicPropertyPayload {
widgetId: string; widgetId: string;
propertyName: string; propertyPath: string;
isDynamic: boolean; isDynamic: boolean;
} }
export interface DeleteWidgetPropertyPayload {
widgetId: string;
propertyPath: string;
}

View File

@ -77,11 +77,11 @@ export const updateCurrentPage = (id: string) => ({
payload: { id }, payload: { id },
}); });
export const updateCanvas = ( export const initCanvasLayout = (
payload: UpdateCanvasPayload, payload: UpdateCanvasPayload,
): ReduxAction<UpdateCanvasPayload> => { ): ReduxAction<UpdateCanvasPayload> => {
return { return {
type: ReduxActionTypes.UPDATE_CANVAS, type: ReduxActionTypes.INIT_CANVAS_LAYOUT,
payload, payload,
}; };
}; };

View File

@ -5,7 +5,7 @@ import {
DEFAULT_EXECUTE_ACTION_TIMEOUT_MS, DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
} from "constants/ApiConstants"; } from "constants/ApiConstants";
import axios, { AxiosPromise, CancelTokenSource } from "axios"; import axios, { AxiosPromise, CancelTokenSource } from "axios";
import { Action } from "entities/Action"; import { Action, ActionViewMode } from "entities/Action";
export interface CreateActionRequest<T> extends APIRequest { export interface CreateActionRequest<T> extends APIRequest {
datasourceId: string; datasourceId: string;
@ -127,7 +127,7 @@ class ActionAPI extends API {
static fetchActionsForViewMode( static fetchActionsForViewMode(
applicationId: string, applicationId: string,
): AxiosPromise<GenericApiResponse<Action[]>> { ): AxiosPromise<GenericApiResponse<ActionViewMode[]>> {
return API.get(`${ActionAPI.url}/view`, { applicationId }); return API.get(`${ActionAPI.url}/view`, { applicationId });
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -58,6 +58,7 @@ type ButtonProps = CommonComponentProps & {
fill?: boolean; fill?: boolean;
href?: string; href?: string;
tag?: "a" | "button"; tag?: "a" | "button";
type?: "submit" | "reset" | "button";
}; };
const stateStyles = ( const stateStyles = (

View File

@ -269,13 +269,15 @@ export const EditableText = (props: EditableTextProps) => {
onCancel={onConfirm} onCancel={onConfirm}
/> />
<IconWrapper className="icon-wrapper">
{savingState === SavingState.STARTED ? ( {savingState === SavingState.STARTED ? (
<IconWrapper className="icon-wrapper">
<Spinner size={IconSize.XL} /> <Spinner size={IconSize.XL} />
) : value ? (
<Icon name={iconName} size={IconSize.XL} />
) : null}
</IconWrapper> </IconWrapper>
) : value && !props.hideEditIcon ? (
<IconWrapper className="icon-wrapper">
<Icon name={iconName} size={IconSize.XL} />
</IconWrapper>
) : null}
</TextContainer> </TextContainer>
{isEditing && !!isInvalid ? ( {isEditing && !!isInvalid ? (
<Text className="error-message" type={TextType.P2}> <Text className="error-message" type={TextType.P2}>

View File

@ -15,13 +15,14 @@ const Container = styled.div<{
savingState: SavingState; savingState: SavingState;
isInvalid: boolean; isInvalid: boolean;
}>` }>`
position: relative;
.editable-text-container { .editable-text-container {
justify-content: center; justify-content: center;
} }
&&& .${Classes.EDITABLE_TEXT}, .icon-wrapper { &&& .${Classes.EDITABLE_TEXT}, .icon-wrapper {
padding: 5px 10px; padding: 5px 0px;
height: 25px; height: 31px;
background-color: ${(props) => background-color: ${(props) =>
(props.isInvalid && props.isEditing) || (props.isInvalid && props.isEditing) ||
props.savingState === SavingState.ERROR props.savingState === SavingState.ERROR
@ -29,15 +30,14 @@ const Container = styled.div<{
: "transparent"}; : "transparent"};
} }
&&&& .${Classes.EDITABLE_TEXT} { &&&& .${Classes.EDITABLE_TEXT}:hover {
${(props) => ${(props) =>
!props.isEditing !props.isEditing
? ` ? `
padding-left: 0px;
padding-right: 0px;
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-width: 1px; border-bottom-width: 1px;
width: fit-content; width: fit-content;
max-width: 194px;
` `
: null} : null}
} }
@ -47,6 +47,8 @@ const Container = styled.div<{
!props.isEditing !props.isEditing
? ` ? `
min-width: 0px !important; min-width: 0px !important;
height: auto !important;
line-height: ${props.theme.typography.h4.lineHeight}px !important;
` `
: null} : null}
} }
@ -57,15 +59,15 @@ const Container = styled.div<{
font-size: ${(props) => props.theme.typography.h4.fontSize}px; font-size: ${(props) => props.theme.typography.h4.fontSize}px;
line-height: ${(props) => props.theme.typography.h4.lineHeight}px; line-height: ${(props) => props.theme.typography.h4.lineHeight}px;
letter-spacing: ${(props) => props.theme.typography.h4.letterSpacing}px; letter-spacing: ${(props) => props.theme.typography.h4.letterSpacing}px;
font-weight: ${(props) => props.theme.typography.h4.fontWeight}px; font-weight: ${(props) => props.theme.typography.h4.fontWeight};
} padding-right: 0px;
.error-message {
margin-top: 2px;
} }
.icon-wrapper { .icon-wrapper {
padding-bottom: 0px; padding-bottom: 0px;
position: absolute;
right: 0;
top: 0;
} }
`; `;

View File

@ -91,6 +91,14 @@ const StyledInput = styled.input<
background-color: ${(props) => props.inputStyle.bgColor}; background-color: ${(props) => props.inputStyle.bgColor};
color: ${(props) => props.inputStyle.color}; color: ${(props) => props.inputStyle.color};
&:-internal-autofill-selected,
&:-webkit-autofill,
&:-webkit-autofill:hover,
&:-webkit-autofill:focus {
-webkit-box-shadow: 0 0 0 30px ${(props) => props.inputStyle.bgColor} inset !important;
-webkit-text-fill-color: ${(props) => props.inputStyle.color} !important;
}
&::placeholder { &::placeholder {
color: ${(props) => props.theme.colors.textInput.placeholder}; color: ${(props) => props.theme.colors.textInput.placeholder};
} }
@ -199,3 +207,5 @@ const TextInput = forwardRef(
TextInput.displayName = "TextInput"; TextInput.displayName = "TextInput";
export default TextInput; export default TextInput;
export type InputType = "text" | "password" | "number" | "email" | "tel";

View File

@ -168,7 +168,8 @@ export const Toaster = {
/>, />,
{ {
toastId: toastId, toastId: toastId,
pauseOnHover: true, pauseOnHover: !config.dispatchableAction && !config.hideProgressBar,
pauseOnFocusLoss: !config.dispatchableAction && !config.hideProgressBar,
autoClose: false, autoClose: false,
closeOnClick: false, closeOnClick: false,
hideProgressBar: config.hideProgressBar, hideProgressBar: config.hideProgressBar,

View File

@ -0,0 +1,34 @@
import React from "react";
import styled from "styled-components";
import { IntentColors } from "constants/DefaultTheme";
// Note: This component is only for the input fields which donot have the
// popover error tooltip. This is also only for Appsmith components
// Not to be used in widgets / canvas
const StyledError = styled.span<{ show: boolean }>`
text-align: left;
color: ${IntentColors.danger};
font-size: ${(props) => props.theme.fontSizes[3]}px;
opacity: ${(props) => (props.show ? 1 : 0)};
display: block;
position: relative;
margin-top: ${(props) => props.theme.spaces[1]}px;
`;
type FormFieldErrorProps = {
error?: string;
className?: string;
};
export const FormFieldError = (props: FormFieldErrorProps) => {
return (
<StyledError
className={props.className ? props.className : undefined}
show={!!props.error}
>
{props.error || "&nbsp;"}
</StyledError>
);
};
export default FormFieldError;

View File

@ -0,0 +1,22 @@
import styled from "styled-components";
import { FormGroup, Classes } from "@blueprintjs/core";
import { getTypographyByKey } from "constants/DefaultTheme";
type FormGroupProps = {
fill?: boolean;
};
const StyledFormGroup = styled(FormGroup)<FormGroupProps>`
& {
width: ${(props) => (props.fill ? "100%" : "auto")};
&.${Classes.FORM_GROUP} {
margin: 0 0 ${(props) => props.theme.spaces[5]}px;
}
&.${Classes.FORM_GROUP} .${Classes.FORM_HELPER_TEXT} {
font-size: ${(props) => props.theme.fontSizes[3]}px;
}
&.${Classes.FORM_GROUP} .${Classes.LABEL} {
${(props) => getTypographyByKey(props, "h5")}
color: ${(props) => props.theme.colors.textInput.normal.text};
}
}
`;
export default StyledFormGroup;

View File

@ -0,0 +1,85 @@
import React from "react";
import styled from "styled-components";
import { Intent } from "constants/DefaultTheme";
import { getTypographyByKey } from "constants/DefaultTheme";
import { Link } from "react-router-dom";
export type MessageAction = {
url?: string;
onClick?: () => void;
text: string;
intent: Intent;
};
const StyledMessage = styled.div<{ intent: Intent }>`
& {
${(props) => getTypographyByKey(props, "p1")}
width: 100%;
padding: ${(props) => props.theme.spaces[4]}px;
color: ${(props) => props.theme.colors.formMessage.text[props.intent]};
background-color: ${(props) =>
props.theme.colors.formMessage.background[props.intent]};
}
`;
export const ActionsContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
`;
const StyledAction = styled.div<{ intent: Intent }>`
margin-top: ${(props) => props.theme.spaces[5]}px;
${(props) => getTypographyByKey(props, "h5")}
font-weight: 600;
& a {
text-decoration: none;
color: ${(props) => props.theme.colors.formMessage.text[props.intent]};
}
`;
export const ActionButton = (props: MessageAction) => {
if (props.url) {
const isExternal = props.url.indexOf("//") !== -1;
return (
<StyledAction intent={props.intent}>
{isExternal ? (
<a href={props.url} target="_blank" rel="noreferrer">
{props.text}
</a>
) : (
<Link to={props.url}>{props.text}</Link>
)}
</StyledAction>
);
} else if (props.onClick) {
return (
<StyledAction onClick={props.onClick} intent={props.intent}>
{props.text}
</StyledAction>
);
}
return null;
};
export type FormMessageProps = {
intent: Intent;
message: string;
actions?: MessageAction[];
};
export const FormMessage = (props: FormMessageProps) => {
const actions =
props.actions &&
props.actions.map((action) => (
<ActionButton key={action.text} {...action} intent={props.intent} />
));
return (
<StyledMessage intent={props.intent} className="form-message-container">
{props.message}
{actions && <ActionsContainer>{actions}</ActionsContainer>}
</StyledMessage>
);
};
export default FormMessage;

View File

@ -0,0 +1,45 @@
import React from "react";
import {
Field,
WrappedFieldMetaProps,
WrappedFieldInputProps,
} from "redux-form";
import InputComponent, { InputType } from "../TextInput";
import { Intent } from "constants/DefaultTheme";
import FormFieldError from "./FieldError";
const renderComponent = (
componentProps: FormTextFieldProps & {
meta: Partial<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) => {
const showError = componentProps.meta.touched && !componentProps.meta.active;
return (
<React.Fragment>
<InputComponent {...componentProps} {...componentProps.input} fill />
<FormFieldError error={showError && componentProps.meta.error} />
</React.Fragment>
);
};
type FormTextFieldProps = {
name: string;
placeholder: string;
type?: InputType;
label?: string;
intent?: Intent;
disabled?: boolean;
autoFocus?: boolean;
};
const FormTextField = (props: FormTextFieldProps) => {
return (
<React.Fragment>
<Field component={renderComponent} {...props} asyncControl />
</React.Fragment>
);
};
export default FormTextField;

View File

@ -1,11 +1,18 @@
import React, { createRef, useEffect, useState } from "react"; import React, { createRef, useEffect, useState } from "react";
import { Tooltip } from "@blueprintjs/core"; import { Tooltip } from "@blueprintjs/core";
import { CellWrapper } from "components/designSystems/appsmith/TableStyledWrappers"; import { CellWrapper } from "components/designSystems/appsmith/TableStyledWrappers";
import styled from "styled-components";
const TooltipContentWrapper = styled.div<{ width: number }>`
word-break: break-all;
max-width: ${(props) => props.width}px;
`;
const AutoToolTipComponent = (props: { const AutoToolTipComponent = (props: {
isHidden?: boolean; isHidden?: boolean;
children: React.ReactNode; children: React.ReactNode;
title: string; title: string;
tableWidth?: number;
}) => { }) => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const [useToolTip, updateToolTip] = useState(false); const [useToolTip, updateToolTip] = useState(false);
@ -23,7 +30,11 @@ const AutoToolTipComponent = (props: {
<Tooltip <Tooltip
autoFocus={false} autoFocus={false}
hoverOpenDelay={1000} hoverOpenDelay={1000}
content={props.title} content={
<TooltipContentWrapper width={(props.tableWidth || 300) - 32}>
{props.title}
</TooltipContentWrapper>
}
position="top" position="top"
> >
{props.children} {props.children}

View File

@ -28,6 +28,9 @@ const StyledRemoveIcon = styled(
padding: 0; padding: 0;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
&.hide-icon {
display: none;
}
`; `;
const LabelWrapper = styled.div` const LabelWrapper = styled.div`
@ -246,7 +249,7 @@ const RenderOptions = (props: {
(i) => i.value === props.value, (i) => i.value === props.value,
); );
if (selectedOptions && selectedOptions.length) { if (selectedOptions && selectedOptions.length) {
selectValue(selectedOptions[0].value); selectValue(selectedOptions[0].label);
} else { } else {
selectValue(props.placeholder); selectValue(props.placeholder);
} }
@ -290,6 +293,7 @@ type CascadeFieldProps = {
value: any; value: any;
operator: Operator; operator: Operator;
index: number; index: number;
hasAnyFilters: boolean;
applyFilter: (filter: ReactTableFilter, index: number) => void; applyFilter: (filter: ReactTableFilter, index: number) => void;
removeFilter: (index: number) => void; removeFilter: (index: number) => void;
}; };
@ -447,7 +451,7 @@ const CascadeField = (props: CascadeFieldProps) => {
}; };
const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => { const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => {
const { index, removeFilter, applyFilter } = props; const { index, removeFilter, applyFilter, hasAnyFilters } = props;
const [state, dispatch] = React.useReducer(CaseCaseFieldReducer, props.state); const [state, dispatch] = React.useReducer(CaseCaseFieldReducer, props.state);
const handleRemoveFilter = () => { const handleRemoveFilter = () => {
dispatch({ type: CascadeFieldActionTypes.DELETE_FILTER }); dispatch({ type: CascadeFieldActionTypes.DELETE_FILTER });
@ -515,7 +519,9 @@ const Fields = (props: CascadeFieldProps & { state: CascadeFieldState }) => {
height={16} height={16}
width={16} width={16}
color={Colors.RIVER_BED} color={Colors.RIVER_BED}
className="t--table-filter-remove-btn" className={`t--table-filter-remove-btn ${
hasAnyFilters ? "" : "hide-icon"
}`}
/> />
{index === 1 ? ( {index === 1 ? (
<DropdownWrapper width={75}> <DropdownWrapper width={75}>

View File

@ -64,6 +64,7 @@ interface ReactTableComponentProps {
multiRowSelection?: boolean; multiRowSelection?: boolean;
hiddenColumns?: string[]; hiddenColumns?: string[];
columnNameMap?: { [key: string]: string }; columnNameMap?: { [key: string]: string };
triggerRowSelection: boolean;
columnTypeMap?: { columnTypeMap?: {
[key: string]: { [key: string]: {
type: string; type: string;
@ -319,6 +320,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => {
pageNo={props.pageNo - 1} pageNo={props.pageNo - 1}
updatePageNo={props.updatePageNo} updatePageNo={props.updatePageNo}
columnActions={props.columnActions} columnActions={props.columnActions}
triggerRowSelection={props.triggerRowSelection}
nextPageClick={() => { nextPageClick={() => {
props.nextPageClick(); props.nextPageClick();
}} }}

View File

@ -0,0 +1,94 @@
import React, { useEffect, useState, useRef } from "react";
import styled from "styled-components";
import _ from "lodash";
import { useSpring, animated, interpolate } from "react-spring";
const ScrollTrack = styled.div<{
isVisible: boolean;
}>`
position: absolute;
z-index: 100;
top: 0;
right: 2px;
width: 4px;
height: 100%;
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
overflow: hidden;
opacity: ${(props) => (props.isVisible ? 1 : 0)};
transition: opacity 0.15s ease-in;
`;
const ScrollThumb = styled(animated.div)`
width: 4px;
background-color: #ebeef0aa;
border-radius: 3px;
transform: translate3d(0, 0, 0);
`;
interface Props {
containerRef: React.RefObject<HTMLDivElement>;
}
const ScrollIndicator = ({ containerRef }: Props) => {
const [{ thumbPosition }, setThumbPosition] = useSpring(() => ({
thumbPosition: 0,
config: {
clamp: true,
friction: 10,
precision: 0.1,
tension: 800,
},
}));
const [isScrollVisible, setIsScrollVisible] = useState(false);
const thumbRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleContainerScroll = (e: any): void => {
setIsScrollVisible(true);
const thumbHeight =
e.target.offsetHeight / (e.target.scrollHeight / e.target.offsetHeight);
const thumbPosition = (e.target.scrollTop / e.target.offsetHeight) * 100;
/* set scroll thumb height */
if (thumbRef.current) {
thumbRef.current.style.height = thumbHeight + "px";
}
setThumbPosition({
thumbPosition,
});
};
containerRef.current?.addEventListener("scroll", handleContainerScroll);
return () => {
containerRef.current?.removeEventListener(
"scroll",
handleContainerScroll,
);
};
}, []);
useEffect(() => {
if (isScrollVisible) {
hideScrollbar();
}
}, [isScrollVisible]);
const hideScrollbar = _.debounce(() => {
setIsScrollVisible(false);
}, 1500);
return (
<ScrollTrack isVisible={isScrollVisible}>
<ScrollThumb
ref={thumbRef}
style={{
transform: interpolate(
[thumbPosition],
(top: number) => `translate3d(0px, ${top}%, 0)`,
),
}}
/>
</ScrollTrack>
);
};
export default ScrollIndicator;

View File

@ -53,6 +53,7 @@ interface TableProps {
selectedRowIndices: number[]; selectedRowIndices: number[];
disableDrag: () => void; disableDrag: () => void;
enableDrag: () => void; enableDrag: () => void;
triggerRowSelection: boolean;
searchTableData: (searchKey: any) => void; searchTableData: (searchKey: any) => void;
filters?: ReactTableFilter[]; filters?: ReactTableFilter[];
applyFilter: (filters: ReactTableFilter[]) => void; applyFilter: (filters: ReactTableFilter[]) => void;
@ -120,6 +121,7 @@ export const Table = (props: TableProps) => {
height={props.height} height={props.height}
tableSizes={tableSizes} tableSizes={tableSizes}
id={`table${props.widgetId}`} id={`table${props.widgetId}`}
triggerRowSelection={props.triggerRowSelection}
backgroundColor={Colors.ATHENS_GRAY_DARKER} backgroundColor={Colors.ATHENS_GRAY_DARKER}
> >
<TableHeader <TableHeader

View File

@ -151,8 +151,11 @@ const TableFilters = (props: TableFilterProps) => {
}; };
}, },
); );
const showAddFilter = const hasAnyFilters = !!(
filters.length >= 1 && filters[0].column && filters[0].condition; filters.length >= 1 &&
filters[0].column &&
filters[0].condition
);
return ( return (
<Popover <Popover
minimal minimal
@ -170,7 +173,7 @@ const TableFilters = (props: TableFilterProps) => {
className="t--table-filter-toggle-btn" className="t--table-filter-toggle-btn"
selected={selected} selected={selected}
icon={ icon={
showAddFilter ? ( hasAnyFilters ? (
<SelectedFilterWrapper>{filters.length}</SelectedFilterWrapper> <SelectedFilterWrapper>{filters.length}</SelectedFilterWrapper>
) : null ) : null
} }
@ -194,6 +197,7 @@ const TableFilters = (props: TableFilterProps) => {
condition={filter.condition} condition={filter.condition}
value={filter.value} value={filter.value}
columns={columns} columns={columns}
hasAnyFilters={hasAnyFilters}
applyFilter={(filter: ReactTableFilter, index: number) => { applyFilter={(filter: ReactTableFilter, index: number) => {
const updatedFilters = props.filters const updatedFilters = props.filters
? [...props.filters] ? [...props.filters]
@ -215,7 +219,7 @@ const TableFilters = (props: TableFilterProps) => {
/> />
); );
})} })}
{showAddFilter ? ( {hasAnyFilters ? (
<ButtonWrapper className={Classes.POPOVER_DISMISS}> <ButtonWrapper className={Classes.POPOVER_DISMISS}>
<Button <Button
intent="primary" intent="primary"

View File

@ -8,6 +8,7 @@ export const TableWrapper = styled.div<{
height: number; height: number;
tableSizes: TableSizes; tableSizes: TableSizes;
backgroundColor?: Color; backgroundColor?: Color;
triggerRowSelection: boolean;
}>` }>`
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -41,12 +42,8 @@ export const TableWrapper = styled.div<{
} }
.tr { .tr {
overflow: hidden; overflow: hidden;
:nth-child(even) { cursor: ${(props) => props.triggerRowSelection && "pointer"};
background: ${Colors.ATHENS_GRAY_DARKER};
}
:nth-child(odd) {
background: ${Colors.WHITE}; background: ${Colors.WHITE};
}
&.selected-row { &.selected-row {
background: ${Colors.POLAR}; background: ${Colors.POLAR};
&:hover { &:hover {

View File

@ -463,6 +463,7 @@ export const renderCell = (
value: any, value: any,
columnType: string, columnType: string,
isHidden: boolean, isHidden: boolean,
tableWidth: number,
) => { ) => {
if (!value) { if (!value) {
return <CellWrapper isHidden={isHidden}></CellWrapper>; return <CellWrapper isHidden={isHidden}></CellWrapper>;
@ -519,7 +520,11 @@ export const renderCell = (
} }
default: default:
return ( return (
<AutoToolTipComponent title={value.toString()} isHidden={isHidden}> <AutoToolTipComponent
title={value.toString()}
isHidden={isHidden}
tableWidth={tableWidth}
>
{value.toString()} {value.toString()}
</AutoToolTipComponent> </AutoToolTipComponent>
); );
@ -883,7 +888,7 @@ export const ConditionFunctions: {
[key: string]: (a: any, b: any) => boolean; [key: string]: (a: any, b: any) => boolean;
} = { } = {
isExactly: (a: any, b: any) => { isExactly: (a: any, b: any) => {
return a === b; return a.toString() === b.toString();
}, },
empty: (a: any) => { empty: (a: any) => {
return a === "" || a === undefined || a === null; return a === "" || a === undefined || a === null;

View File

@ -136,6 +136,22 @@ class InputComponent extends React.Component<
return "text"; return "text";
} }
} }
onKeyDownTextArea = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isEnterKey = e.key === "Enter" || e.keyCode === 13;
const { disableNewLineOnPressEnterKey } = this.props;
if (isEnterKey && disableNewLineOnPressEnterKey && !e.shiftKey) {
e.preventDefault();
}
if (typeof this.props.onKeyDown === "function") {
this.props.onKeyDown(e);
}
};
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (typeof this.props.onKeyDown === "function") {
this.props.onKeyDown(e);
}
};
private numericInputComponent = () => ( private numericInputComponent = () => (
<NumericInput <NumericInput
value={this.props.value} value={this.props.value}
@ -155,6 +171,7 @@ class InputComponent extends React.Component<
stepSize={this.props.stepSize} stepSize={this.props.stepSize}
onFocus={() => this.setFocusState(true)} onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)} onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDown}
/> />
); );
private textAreaInputComponent = () => ( private textAreaInputComponent = () => (
@ -169,6 +186,7 @@ class InputComponent extends React.Component<
growVertically={false} growVertically={false}
onFocus={() => this.setFocusState(true)} onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)} onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDownTextArea}
/> />
); );
@ -199,6 +217,7 @@ class InputComponent extends React.Component<
type={this.getType(this.props.inputType)} type={this.getType(this.props.inputType)}
onFocus={() => this.setFocusState(true)} onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)} onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDown}
/> />
); );
private renderInputComponent = (inputType: InputType, isTextArea: boolean) => private renderInputComponent = (inputType: InputType, isTextArea: boolean) =>
@ -267,6 +286,12 @@ export interface InputComponentProps extends ComponentProps {
isInvalid: boolean; isInvalid: boolean;
showError: boolean; showError: boolean;
onFocusChange: (state: boolean) => void; onFocusChange: (state: boolean) => void;
disableNewLineOnPressEnterKey?: boolean;
onKeyDown?: (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => void;
} }
export default InputComponent; export default InputComponent;

View File

@ -10,7 +10,7 @@ import PerformanceTracker, {
const SidebarWrapper = styled.div` const SidebarWrapper = styled.div`
background-color: ${Colors.MINE_SHAFT}; background-color: ${Colors.MINE_SHAFT};
padding: 0px 0 0 6px; padding: 0;
width: ${(props) => props.theme.sidebarWidth}; width: ${(props) => props.theme.sidebarWidth};
z-index: 3; z-index: 3;

View File

@ -25,6 +25,7 @@ import {
createNewApiAction, createNewApiAction,
createNewQueryAction, createNewQueryAction,
} from "actions/apiPaneActions"; } from "actions/apiPaneActions";
import { NavigationTargetType } from "../../../sagas/ActionExecutionSagas";
/* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/ban-types */
/* TODO: Function and object types need to be updated to enable the lint rule */ /* TODO: Function and object types need to be updated to enable the lint rule */
@ -47,6 +48,19 @@ const FILE_TYPE_OPTIONS = [
{ label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" }, { label: "SVG", value: "'image/svg+xml'", id: "image/svg+xml" },
]; ];
const NAVIGATION_TARGET_FIELD_OPTIONS = [
{
label: "Same window",
value: `'${NavigationTargetType.SAME_WINDOW}'`,
id: NavigationTargetType.SAME_WINDOW,
},
{
label: "New window",
value: `'${NavigationTargetType.NEW_WINDOW}'`,
id: NavigationTargetType.NEW_WINDOW,
},
];
const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[].*[\]])|([\{].*[\}])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi; const FUNC_ARGS_REGEX = /((["][^"]*["])|([\[].*[\]])|([\{].*[\}])|(['][^']*['])|([\(].*[\)[=][>][{].*[}])|([^'",][^,"+]*[^'",]*))*/gi;
const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g; const ACTION_TRIGGER_REGEX = /^{{([\s\S]*?)\(([\s\S]*?)\)}}$/g;
//Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g; //Old Regex:: /\(\) => ([\s\S]*?)(\([\s\S]*?\))/g;
@ -316,6 +330,7 @@ const FieldType = {
DOWNLOAD_FILE_NAME_FIELD: "DOWNLOAD_FILE_NAME_FIELD", DOWNLOAD_FILE_NAME_FIELD: "DOWNLOAD_FILE_NAME_FIELD",
DOWNLOAD_FILE_TYPE_FIELD: "DOWNLOAD_FILE_TYPE_FIELD", DOWNLOAD_FILE_TYPE_FIELD: "DOWNLOAD_FILE_TYPE_FIELD",
COPY_TEXT_FIELD: "COPY_TEXT_FIELD", COPY_TEXT_FIELD: "COPY_TEXT_FIELD",
NAVIGATION_TARGET_FIELD: "NAVIGATION_TARGET_FIELD",
}; };
type FieldType = typeof FieldType[keyof typeof FieldType]; type FieldType = typeof FieldType[keyof typeof FieldType];
@ -406,6 +421,15 @@ const fieldConfigs: FieldConfigs = {
}, },
view: ViewTypes.TEXT_VIEW, view: ViewTypes.TEXT_VIEW,
}, },
[FieldType.NAVIGATION_TARGET_FIELD]: {
getter: (value: any) => {
return enumTypeGetter(value, 2, NavigationTargetType.SAME_WINDOW);
},
setter: (option: any, currentValue: string) => {
return enumTypeSetter(option.value, currentValue, 2);
},
view: ViewTypes.SELECTOR_VIEW,
},
[FieldType.ALERT_TEXT_FIELD]: { [FieldType.ALERT_TEXT_FIELD]: {
getter: (value: string) => { getter: (value: string) => {
return textGetter(value, 0); return textGetter(value, 0);
@ -628,6 +652,9 @@ function getFieldFromValue(
fields.push({ fields.push({
field: FieldType.QUERY_PARAMS_FIELD, field: FieldType.QUERY_PARAMS_FIELD,
}); });
fields.push({
field: FieldType.NAVIGATION_TARGET_FIELD,
});
} }
if (value.indexOf("showModal") !== -1) { if (value.indexOf("showModal") !== -1) {
@ -718,6 +745,7 @@ function renderField(props: {
case FieldType.PAGE_SELECTOR_FIELD: case FieldType.PAGE_SELECTOR_FIELD:
case FieldType.ALERT_TYPE_SELECTOR_FIELD: case FieldType.ALERT_TYPE_SELECTOR_FIELD:
case FieldType.DOWNLOAD_FILE_TYPE_FIELD: case FieldType.DOWNLOAD_FILE_TYPE_FIELD:
case FieldType.NAVIGATION_TARGET_FIELD:
let label = ""; let label = "";
let defaultText = "Select Action"; let defaultText = "Select Action";
let options = props.apiOptionTree; let options = props.apiOptionTree;
@ -776,6 +804,11 @@ function renderField(props: {
options = FILE_TYPE_OPTIONS; options = FILE_TYPE_OPTIONS;
defaultText = "Select file type (optional)"; defaultText = "Select file type (optional)";
} }
if (fieldType === FieldType.NAVIGATION_TARGET_FIELD) {
label = "Target";
options = NAVIGATION_TARGET_FIELD_OPTIONS;
defaultText = "Navigation target";
}
viewElement = (view as (props: SelectorViewProps) => JSX.Element)({ viewElement = (view as (props: SelectorViewProps) => JSX.Element)({
options: options, options: options,
label: label, label: label,

View File

@ -18,6 +18,7 @@ export enum EventType {
ON_PAGE_LOAD = "ON_PAGE_LOAD", ON_PAGE_LOAD = "ON_PAGE_LOAD",
ON_PREV_PAGE = "ON_PREV_PAGE", ON_PREV_PAGE = "ON_PREV_PAGE",
ON_NEXT_PAGE = "ON_NEXT_PAGE", ON_NEXT_PAGE = "ON_NEXT_PAGE",
ON_PAGE_SIZE_CHANGE = "ON_PAGE_SIZE_CHANGE",
ON_ERROR = "ON_ERROR", ON_ERROR = "ON_ERROR",
ON_SUCCESS = "ON_SUCCESS", ON_SUCCESS = "ON_SUCCESS",
ON_ROW_SELECTED = "ON_ROW_SELECTED", ON_ROW_SELECTED = "ON_ROW_SELECTED",

View File

@ -60,6 +60,13 @@ export const scrollbarDark = css`
} }
`; `;
export const getTypographyByKey = (props: Record<string, any>, key: string) => `
font-weight: ${props.theme.typography[key].fontWeight};
font-size: ${props.theme.typography[key].fontSize}px;
line-height: ${props.theme.typography[key].lineHeight}px;
letter-spacing: ${props.theme.typography[key].letterSpacing}px;
`;
export const BlueprintControlTransform = css` export const BlueprintControlTransform = css`
&& { && {
.${Classes.CONTROL} { .${Classes.CONTROL} {
@ -312,11 +319,8 @@ export type Theme = {
}; };
authCard: { authCard: {
width: number; width: number;
borderRadius: number;
background: Color;
padding: number;
dividerSpacing: number; dividerSpacing: number;
shadow: string; formMessageWidth: number;
}; };
shadows: string[]; shadows: string[];
widgets: { widgets: {
@ -735,6 +739,35 @@ type ColorType = {
bg: ShadeColor; bg: ShadeColor;
}; };
floatingBtn: any; floatingBtn: any;
auth: any;
formMessage: Record<string, Record<Intent, string>>;
};
const auth: any = {
background: darkShades[1],
cardBackground: lightShades[10],
btnPrimary: "#F86A2B",
inputBackground: darkShades[1],
headingText: "#FFF",
link: "#106ba3",
text: darkShades[7],
placeholder: darkShades[5],
socialBtnText: darkShades[8],
socialBtnBorder: darkShades[8],
socialBtnHighlight: darkShades[1],
};
const formMessage = {
background: {
danger: "rgba(226,44,44,0.08)",
success: "#172320",
warning: "rgba(224, 179, 14, 0.08)",
},
text: {
danger: "#E22C2C",
success: "#03B365",
warning: "#E0B30E",
},
}; };
export const dark: ColorType = { export const dark: ColorType = {
@ -862,9 +895,9 @@ export const dark: ColorType = {
border: darkShades[2], border: darkShades[2],
}, },
normal: { normal: {
bg: lightShades[10], bg: darkShades[0],
text: darkShades[9],
border: darkShades[0], border: darkShades[0],
text: darkShades[7],
}, },
placeholder: darkShades[5], placeholder: darkShades[5],
readOnly: { readOnly: {
@ -1019,6 +1052,8 @@ export const dark: ColorType = {
backgroundColor: darkShades[3], backgroundColor: darkShades[3],
iconColor: darkShades[6], iconColor: darkShades[6],
}, },
auth,
formMessage,
}; };
export const light: ColorType = { export const light: ColorType = {
@ -1303,6 +1338,8 @@ export const light: ColorType = {
backgroundColor: lightShades[3], backgroundColor: lightShades[3],
iconColor: lightShades[7], iconColor: lightShades[7],
}, },
auth,
formMessage,
}; };
export const theme: Theme = { export const theme: Theme = {
@ -1395,6 +1432,18 @@ export const theme: Theme = {
letterSpacing: -0.24, letterSpacing: -0.24,
fontWeight: "normal", fontWeight: "normal",
}, },
authCardHeader: {
fontStyle: "normal",
fontWeight: 600,
fontSize: 25,
lineHeight: 20,
},
authCardSubheader: {
fontStyle: "normal",
fontWeight: "normal",
fontSize: 15,
lineHeight: 20,
},
}, },
iconSizes: { iconSizes: {
XXS: 8, XXS: 8,
@ -1545,12 +1594,9 @@ export const theme: Theme = {
}, },
}, },
authCard: { authCard: {
width: 612, width: 440,
borderRadius: 16,
background: Colors.WHITE,
padding: 40,
dividerSpacing: 32, dividerSpacing: 32,
shadow: "0px 4px 8px rgba(9, 30, 66, 0.25)", formMessageWidth: 370,
}, },
shadows: [ shadows: [
/* 0. tab */ /* 0. tab */

View File

@ -11,7 +11,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
FLUSH_AND_REDIRECT: "FLUSH_AND_REDIRECT", FLUSH_AND_REDIRECT: "FLUSH_AND_REDIRECT",
SAFE_CRASH_APPSMITH: "SAFE_CRASH_APPSMITH", SAFE_CRASH_APPSMITH: "SAFE_CRASH_APPSMITH",
SAFE_CRASH_APPSMITH_REQUEST: "SAFE_CRASH_APPSMITH_REQUEST", SAFE_CRASH_APPSMITH_REQUEST: "SAFE_CRASH_APPSMITH_REQUEST",
UPDATE_CANVAS: "UPDATE_CANVAS", INIT_CANVAS_LAYOUT: "INIT_CANVAS_LAYOUT",
FETCH_CANVAS: "FETCH_CANVAS", FETCH_CANVAS: "FETCH_CANVAS",
CLEAR_CANVAS: "CLEAR_CANVAS", CLEAR_CANVAS: "CLEAR_CANVAS",
FETCH_PAGE_INIT: "FETCH_PAGE_INIT", FETCH_PAGE_INIT: "FETCH_PAGE_INIT",
@ -55,6 +55,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
UPDATE_WIDGET_PROPERTY_REQUEST: "UPDATE_WIDGET_PROPERTY_REQUEST", UPDATE_WIDGET_PROPERTY_REQUEST: "UPDATE_WIDGET_PROPERTY_REQUEST",
UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY", UPDATE_WIDGET_PROPERTY: "UPDATE_WIDGET_PROPERTY",
UPDATE_WIDGET_DYNAMIC_PROPERTY: "UPDATE_WIDGET_DYNAMIC_PROPERTY", UPDATE_WIDGET_DYNAMIC_PROPERTY: "UPDATE_WIDGET_DYNAMIC_PROPERTY",
BATCH_UPDATE_WIDGET_PROPERTY: "BATCH_UPDATE_WIDGET_PROPERTY",
DELETE_WIDGET_PROPERTY: "DELETE_WIDGET_PROPERTY",
FETCH_PROPERTY_PANE_CONFIGS_INIT: "FETCH_PROPERTY_PANE_CONFIGS_INIT", FETCH_PROPERTY_PANE_CONFIGS_INIT: "FETCH_PROPERTY_PANE_CONFIGS_INIT",
FETCH_PROPERTY_PANE_CONFIGS_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS", FETCH_PROPERTY_PANE_CONFIGS_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS",
FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT", FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT",

View File

@ -1,5 +1,6 @@
import { GoogleOAuthURL, GithubOAuthURL } from "constants/ApiConstants"; import { GoogleOAuthURL, GithubOAuthURL } from "constants/ApiConstants";
import GithubLogo from "assets/images/Github.png";
import GithubLogo from "assets/images/Github_inverted.png";
import GoogleLogo from "assets/images/Google.png"; import GoogleLogo from "assets/images/Google.png";
export type SocialLoginButtonProps = { export type SocialLoginButtonProps = {
url: string; url: string;

View File

@ -7,6 +7,7 @@ export const LOGIN_FORM_EMAIL_FIELD_NAME = "username";
export const LOGIN_FORM_PASSWORD_FIELD_NAME = "password"; export const LOGIN_FORM_PASSWORD_FIELD_NAME = "password";
export const SIGNUP_FORM_NAME = "SignupForm"; export const SIGNUP_FORM_NAME = "SignupForm";
export const SIGNUP_FORM_EMAIL_FIELD_NAME = "email";
export const FORGOT_PASSWORD_FORM_NAME = "ForgotPasswordForm"; export const FORGOT_PASSWORD_FORM_NAME = "ForgotPasswordForm";
export const RESET_PASSWORD_FORM_NAME = "ResetPasswordForm"; export const RESET_PASSWORD_FORM_NAME = "ResetPasswordForm";
export const CREATE_PASSWORD_FORM_NAME = "CreatePasswordForm"; export const CREATE_PASSWORD_FORM_NAME = "CreatePasswordForm";

View File

@ -18,10 +18,10 @@ export const ENTER_VIDEO_URL = "Please provide a valid url";
export const FORM_VALIDATION_EMPTY_PASSWORD = "Please enter the password"; export const FORM_VALIDATION_EMPTY_PASSWORD = "Please enter the password";
export const FORM_VALIDATION_PASSWORD_RULE = export const FORM_VALIDATION_PASSWORD_RULE =
"Please provide a password with a minimum of 6 characters"; "Please provide a password with a minimum of 6 characters";
export const FORM_VALIDATION_INVALID_PASSWORD = "Please enter a valid password"; export const FORM_VALIDATION_INVALID_PASSWORD = FORM_VALIDATION_PASSWORD_RULE;
export const LOGIN_PAGE_SUBTITLE = "Use your organization email"; export const LOGIN_PAGE_SUBTITLE = "Use your organization email";
export const LOGIN_PAGE_TITLE = "Login"; export const LOGIN_PAGE_TITLE = "Sign In to your account";
export const LOGIN_PAGE_EMAIL_INPUT_LABEL = "Email"; export const LOGIN_PAGE_EMAIL_INPUT_LABEL = "Email";
export const LOGIN_PAGE_PASSWORD_INPUT_LABEL = "Password"; export const LOGIN_PAGE_PASSWORD_INPUT_LABEL = "Password";
export const LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER = "Email"; export const LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER = "Email";
@ -29,12 +29,13 @@ export const LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER = "Password";
export const LOGIN_PAGE_INVALID_CREDS_ERROR = export const LOGIN_PAGE_INVALID_CREDS_ERROR =
"It looks like you may have entered incorrect/invalid credentials. Please try again or reset password using the button below."; "It looks like you may have entered incorrect/invalid credentials. Please try again or reset password using the button below.";
export const LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK = "Reset Password"; export const LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK = "Reset Password";
export const NEW_TO_APPSMITH = "New to Appsmith?";
export const LOGIN_PAGE_LOGIN_BUTTON_TEXT = "Login"; export const LOGIN_PAGE_LOGIN_BUTTON_TEXT = "sign in";
export const LOGIN_PAGE_FORGOT_PASSWORD_TEXT = "Forgot Password"; export const LOGIN_PAGE_FORGOT_PASSWORD_TEXT = "Forgot Password";
export const LOGIN_PAGE_REMEMBER_ME_LABEL = "Remember"; export const LOGIN_PAGE_REMEMBER_ME_LABEL = "Remember";
export const LOGIN_PAGE_SIGN_UP_LINK_TEXT = "New to Appsmith? Sign up"; export const LOGIN_PAGE_SIGN_UP_LINK_TEXT = "Sign up";
export const SIGNUP_PAGE_TITLE = "Sign Up"; export const SIGNUP_PAGE_TITLE = "Create your free account";
export const SIGNUP_PAGE_SUBTITLE = "Use your organization email"; export const SIGNUP_PAGE_SUBTITLE = "Use your organization email";
export const SIGNUP_PAGE_EMAIL_INPUT_LABEL = "Email"; export const SIGNUP_PAGE_EMAIL_INPUT_LABEL = "Email";
export const SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER = " Email"; export const SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER = " Email";
@ -42,16 +43,17 @@ export const SIGNUP_PAGE_NAME_INPUT_PLACEHOLDER = "Name";
export const SIGNUP_PAGE_NAME_INPUT_LABEL = "Name"; export const SIGNUP_PAGE_NAME_INPUT_LABEL = "Name";
export const SIGNUP_PAGE_PASSWORD_INPUT_LABEL = "Password"; export const SIGNUP_PAGE_PASSWORD_INPUT_LABEL = "Password";
export const SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER = "Password"; export const SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER = "Password";
export const SIGNUP_PAGE_LOGIN_LINK_TEXT = "Have an account? Login"; export const SIGNUP_PAGE_LOGIN_LINK_TEXT = "Sign In";
export const SIGNUP_PAGE_NAME_INPUT_SUBTEXT = "How should we call you?"; export const SIGNUP_PAGE_NAME_INPUT_SUBTEXT = "How should we call you?";
export const SIGNUP_PAGE_SUBMIT_BUTTON_TEXT = "Sign Up"; export const SIGNUP_PAGE_SUBMIT_BUTTON_TEXT = "Sign Up";
export const ALREADY_HAVE_AN_ACCOUNT = "Already have an account?";
export const SIGNUP_PAGE_SUCCESS = "Awesome! You have successfully registered."; export const SIGNUP_PAGE_SUCCESS = "Awesome! You have successfully registered.";
export const SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT = "Login"; export const SIGNUP_PAGE_SUCCESS_LOGIN_BUTTON_TEXT = "Login";
export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL = "New Password"; export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL = "New Password";
export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER = "New Password"; export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER = "New Password";
export const RESET_PASSWORD_LOGIN_LINK_TEXT = "Changed your mind? Login"; export const RESET_PASSWORD_LOGIN_LINK_TEXT = "Back to Sign In";
export const RESET_PASSWORD_PAGE_TITLE = "Reset Password"; export const RESET_PASSWORD_PAGE_TITLE = "Reset Password";
export const RESET_PASSWORD_SUBMIT_BUTTON_TEXT = "Reset"; export const RESET_PASSWORD_SUBMIT_BUTTON_TEXT = "Reset";
export const RESET_PASSWORD_PAGE_SUBTITLE = export const RESET_PASSWORD_PAGE_SUBTITLE =
@ -90,6 +92,9 @@ export const WIDGET_TYPE_VALIDATION_ERROR = "Value does not match type";
export const URL_HTTP_VALIDATION_ERROR = "Please enter a valid URL"; export const URL_HTTP_VALIDATION_ERROR = "Please enter a valid URL";
export const NAVIGATE_TO_VALIDATION_ERROR = export const NAVIGATE_TO_VALIDATION_ERROR =
"Please enter a valid URL or page name"; "Please enter a valid URL or page name";
export const PAGE_NOT_FOUND_ERROR =
"The page youre looking for either does not exist, or cannot be found";
export const INVALID_URL_ERROR = "Invalid URL";
export const INVITE_USERS_VALIDATION_EMAIL_LIST = export const INVITE_USERS_VALIDATION_EMAIL_LIST =
"Invalid Email address(es) found"; "Invalid Email address(es) found";
@ -129,7 +134,7 @@ export const DELETING_APPLICATION = "Deleting application...";
export const DUPLICATING_APPLICATION = "Duplicating application..."; export const DUPLICATING_APPLICATION = "Duplicating application...";
export const CURL_IMPORT_SUCCESS = "Curl Import Successfull"; export const CURL_IMPORT_SUCCESS = "Curl Import Successfull";
export const FORGOT_PASSWORD_PAGE_LOGIN_LINK = "Back to Login"; export const FORGOT_PASSWORD_PAGE_LOGIN_LINK = "Back to Sign In";
export const ADD_API_TO_PAGE_SUCCESS_MESSAGE = "Api added to page."; export const ADD_API_TO_PAGE_SUCCESS_MESSAGE = "Api added to page.";
export const INPUT_WIDGET_DEFAULT_VALIDATION_ERROR = "Invalid input"; export const INPUT_WIDGET_DEFAULT_VALIDATION_ERROR = "Invalid input";

View File

@ -127,7 +127,9 @@ export const getApplicationViewerPageURL = (
return url + queryParams; return url + queryParams;
}; };
function convertToQueryParams(params: Record<string, string> = {}): string { export function convertToQueryParams(
params: Record<string, string> = {},
): string {
const paramKeys = Object.keys(params); const paramKeys = Object.keys(params);
const queryParams: string[] = []; const queryParams: string[] = [];
if (paramKeys) { if (paramKeys) {

View File

@ -108,4 +108,13 @@ export interface QueryAction extends BaseAction {
datasource: StoredDatasource; datasource: StoredDatasource;
} }
export type ActionViewMode = {
id: string;
name: string;
pageId: string;
jsonPathKeys: string[];
confirmBeforeExecute?: boolean;
timeoutInMillisecond?: number;
};
export type Action = ApiAction | QueryAction; export type Action = ApiAction | QueryAction;

View File

@ -79,3 +79,8 @@ const ThemedAppWithProps = connect(
)(ThemedApp); )(ThemedApp);
ReactDOM.render(<App />, document.getElementById("root")); ReactDOM.render(<App />, document.getElementById("root"));
// expose store when run in Cypress
if ((window as any).Cypress) {
(window as any).store = store;
}

View File

@ -281,6 +281,14 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
}, },
{ {
id: "7.2.4", id: "7.2.4",
helpText: "Triggers an action when a table page size is changed",
propertyName: "onPageSizeChange",
label: "onPageSizeChange",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
},
{
id: "7.2.5",
propertyName: "onSearchTextChanged", propertyName: "onSearchTextChanged",
label: "onSearchTextChanged", label: "onSearchTextChanged",
controlType: "ACTION_SELECTOR", controlType: "ACTION_SELECTOR",
@ -717,6 +725,15 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
controlType: "ACTION_SELECTOR", controlType: "ACTION_SELECTOR",
isJSConvertible: true, isJSConvertible: true,
}, },
{
id: "5.11.3",
helpText:
"Triggers an action on submit (when the enter key is pressed)",
propertyName: "onSubmit",
label: "onSubmit",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
},
], ],
}, },
], ],

View File

@ -33,6 +33,8 @@ import { getCurrentUser } from "selectors/usersSelectors";
import { ANONYMOUS_USERNAME, User } from "constants/userConstants"; import { ANONYMOUS_USERNAME, User } from "constants/userConstants";
import { isEllipsisActive } from "utils/helpers"; import { isEllipsisActive } from "utils/helpers";
import TooltipComponent from "components/ads/Tooltip"; import TooltipComponent from "components/ads/Tooltip";
import Text, { TextType } from "components/ads/Text";
import { Classes } from "components/ads/common";
const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>` const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
background: ${Colors.BALTIC_SEA}; background: ${Colors.BALTIC_SEA};
@ -40,6 +42,13 @@ const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
color: white; color: white;
flex-direction: column; flex-direction: column;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05);
.${Classes.TEXT} {
max-width: 194px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #d4d4d4;
}
`; `;
const HeaderRow = styled.div<{ justify: string }>` const HeaderRow = styled.div<{ justify: string }>`
@ -82,13 +91,6 @@ const ShareButton = styled(Button)`
color: white !important; color: white !important;
`; `;
const StyledApplicationName = styled.span`
font-size: 15px;
font-weight: 500;
font-size: 18px;
line-height: 14px;
`;
const PageTab = styled(NavLink)` const PageTab = styled(NavLink)`
display: flex; display: flex;
height: 30px; height: 30px;
@ -236,11 +238,9 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
<AppsmithLogoImg src={AppsmithLogo} alt="Appsmith logo" /> <AppsmithLogoImg src={AppsmithLogo} alt="Appsmith logo" />
</Link> </Link>
</HeaderSection> </HeaderSection>
<HeaderSection justify={"center"}> <HeaderSection justify={"center"} className="current-app-name">
{currentApplicationDetails && ( {currentApplicationDetails && (
<StyledApplicationName> <Text type={TextType.H4}>{currentApplicationDetails.name}</Text>
{currentApplicationDetails.name}
</StyledApplicationName>
)} )}
</HeaderSection> </HeaderSection>
<HeaderSection justify={"flex-end"}> <HeaderSection justify={"flex-end"}>

View File

@ -56,7 +56,7 @@ const UpdatesIcon = withTheme(({ theme }) => (
)); ));
const UpdatesButton = ({ newReleasesCount }: { newReleasesCount: string }) => ( const UpdatesButton = ({ newReleasesCount }: { newReleasesCount: string }) => (
<StyledUpdatesButton> <StyledUpdatesButton data-cy="t--product-updates-btn">
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<UpdatesIcon /> <UpdatesIcon />
<UpdatesButtonTextContainer>What&apos;s New?</UpdatesButtonTextContainer> <UpdatesButtonTextContainer>What&apos;s New?</UpdatesButtonTextContainer>

View File

@ -74,7 +74,10 @@ const Header = withTheme(
> >
View on Github View on Github
</ViewInGithubLink> </ViewInGithubLink>
<CloseIconContainer onClick={onClose}> <CloseIconContainer
onClick={onClose}
data-cy="t--product-updates-close-btn"
>
<CloseIcon <CloseIcon
height={20} height={20}
width={20} width={20}

View File

@ -32,6 +32,7 @@ import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
} from "utils/PerformanceTracker"; } from "utils/PerformanceTracker";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import { ApplicationPayload } from "constants/ReduxActionConstants"; import { ApplicationPayload } from "constants/ReduxActionConstants";
const LoadingContainer = styled(CenteredWrapper)` const LoadingContainer = styled(CenteredWrapper)`
@ -144,6 +145,9 @@ class ApiEditor extends React.Component<Props> {
paginationType, paginationType,
isEditorInitialized, isEditorInitialized,
} = this.props; } = this.props;
if (!this.props.pluginId && this.props.match.params.apiId) {
return <EntityNotFoundPane />;
}
if (isCreating || !isEditorInitialized) { if (isCreating || !isEditorInitialized) {
return ( return (
<LoadingContainer> <LoadingContainer>

View File

@ -27,6 +27,8 @@ import BackButton from "./BackButton";
import { PluginType } from "entities/Action"; import { PluginType } from "entities/Action";
import Boxed from "components/editorComponents/Onboarding/Boxed"; import Boxed from "components/editorComponents/Onboarding/Boxed";
import { OnboardingStep } from "constants/OnboardingConstants"; import { OnboardingStep } from "constants/OnboardingConstants";
import { isHidden } from "components/formControls/utils";
import log from "loglevel";
const { cloudHosting } = getAppsmithConfigs(); const { cloudHosting } = getAppsmithConfigs();
@ -412,6 +414,7 @@ class DatasourceDBEditor extends React.Component<
}; };
renderMainSection = (section: any, index: number) => { renderMainSection = (section: any, index: number) => {
if (isHidden(this.props.formData, section.hidden)) return null;
return ( return (
<Collapsible title={section.sectionName} defaultIsOpen={index === 0}> <Collapsible title={section.sectionName} defaultIsOpen={index === 0}>
{this.renderEachConfig(section)} {this.renderEachConfig(section)}
@ -419,44 +422,15 @@ class DatasourceDBEditor extends React.Component<
); );
}; };
renderEachConfig(section: any) { renderSingleConfig = (
const keyValueItems: any = []; config: ControlProps,
multipleConfig?: ControlProps[],
return ( ) => {
<div key={section.id}> multipleConfig = multipleConfig || [];
<div>
{_.map(section.children, (propertyControlOrSection: ControlProps) => {
if ("children" in propertyControlOrSection) {
return this.renderEachConfig(propertyControlOrSection);
} else {
try { try {
const { this.setupConfig(config);
controlType,
isRequired,
configProperty,
} = propertyControlOrSection;
const config = { ...propertyControlOrSection };
const multipleConfig = keyValueItems;
this.configDetails[configProperty] = controlType;
if (isRequired) {
this.requiredFields[
configProperty
] = propertyControlOrSection;
}
if (
controlType === "KEYVALUE_ARRAY" &&
keyValueItems.length < 2
) {
keyValueItems.push(config);
if (keyValueItems.length < 2) return undefined;
}
return ( return (
<div key={configProperty} style={{ marginTop: "16px" }}> <div key={config.configProperty} style={{ marginTop: "16px" }}>
<FormControl <FormControl
config={config} config={config}
formName={DATASOURCE_DB_FORM} formName={DATASOURCE_DB_FORM}
@ -465,14 +439,56 @@ class DatasourceDBEditor extends React.Component<
</div> </div>
); );
} catch (e) { } catch (e) {
console.log(e); log.error(e);
} }
};
setupConfig = (config: ControlProps) => {
const { controlType, isRequired, configProperty } = config;
this.configDetails[configProperty] = controlType;
if (isRequired) {
this.requiredFields[configProperty] = config;
}
};
isKVArray = (children: Array<ControlProps>) => {
if (!Array.isArray(children) || children.length < 2) return false;
return (
children[0].controlType && children[0].controlType === "KEYVALUE_ARRAY"
);
};
renderKVArray = (children: Array<ControlProps>) => {
try {
// setup config for each child
children.forEach((c) => this.setupConfig(c));
// We pass last child for legacy reasons, to keep the logic here exactly same as before.
return this.renderSingleConfig(children[children.length - 1], children);
} catch (e) {
log.error(e);
}
};
renderEachConfig = (section: any) => {
return (
<div key={section.sectionName}>
{_.map(section.children, (propertyControlOrSection: ControlProps) => {
// If the section is hidden, skip rendering
if (isHidden(this.props.formData, section.hidden)) return null;
if ("children" in propertyControlOrSection) {
const { children } = propertyControlOrSection;
if (this.isKVArray(children)) {
return this.renderKVArray(children);
}
return this.renderEachConfig(propertyControlOrSection);
} else {
return this.renderSingleConfig(propertyControlOrSection);
} }
})} })}
</div> </div>
</div>
); );
} };
} }
export default reduxForm<Datasource, DatasourceDBEditorProps>({ export default reduxForm<Datasource, DatasourceDBEditorProps>({

View File

@ -3,6 +3,8 @@ import React from "react";
import { map, get } from "lodash"; import { map, get } from "lodash";
import { Colors } from "constants/Colors"; import { Colors } from "constants/Colors";
import styled from "styled-components"; import styled from "styled-components";
import { isHidden } from "components/formControls/utils";
import log from "loglevel";
const Key = styled.div` const Key = styled.div`
color: ${Colors.DOVE_GRAY}; color: ${Colors.DOVE_GRAY};
@ -14,7 +16,6 @@ const Value = styled.div`
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
display: inline-block; display: inline-block;
text-transform: uppercase;
margin-left: 5px; margin-left: 5px;
`; `;
@ -34,6 +35,7 @@ export const renderDatasourceSection = (
return ( return (
<React.Fragment key={datasource.id}> <React.Fragment key={datasource.id}>
{map(config.children, (section) => { {map(config.children, (section) => {
if (isHidden(datasource, section.hidden)) return null;
if ("children" in section) { if ("children" in section) {
return renderDatasourceSection(section, datasource); return renderDatasourceSection(section, datasource);
} else { } else {
@ -85,12 +87,25 @@ export const renderDatasourceSection = (
); );
} }
if (controlType === "DROP_DOWN") {
if (Array.isArray(section.options)) {
const option = section.options.find(
(el: any) => el.value === value,
);
if (option && option.label) {
value = option.label;
}
}
}
return ( return (
<FieldWrapper key={reactKey}> <FieldWrapper key={reactKey}>
<Key>{label}: </Key> <Value>{value}</Value> <Key>{label}: </Key> <Value>{value}</Value>
</FieldWrapper> </FieldWrapper>
); );
} catch (e) {} } catch (e) {
log.error(e);
}
} }
})} })}
</React.Fragment> </React.Fragment>

View File

@ -21,6 +21,7 @@ import DatasourceHome from "./DatasourceHome";
import DataSourceEditorForm from "./DBForm"; import DataSourceEditorForm from "./DBForm";
import { Datasource } from "entities/Datasource"; import { Datasource } from "entities/Datasource";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
interface ReduxStateProps { interface ReduxStateProps {
formData: Datasource; formData: Datasource;
@ -88,7 +89,9 @@ class DataSourceEditor extends React.Component<Props> {
setDatasourceEditorMode, setDatasourceEditorMode,
pluginType, pluginType,
} = this.props; } = this.props;
if (!pluginId && datasourceId) {
return <EntityNotFoundPane />;
}
return ( return (
<React.Fragment> <React.Fragment>
{datasourceId ? ( {datasourceId ? (

View File

@ -0,0 +1,71 @@
import React from "react";
import styled from "styled-components";
import Button, { Size, Category } from "components/ads/Button";
import PageUnavailableImage from "assets/images/invalid-page.png";
import { PAGE_NOT_FOUND_ERROR, INVALID_URL_ERROR } from "constants/messages";
import { useHistory } from "react-router-dom";
const Wrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
align-items: center;
justify-content: flex-start;
padding-top: 15%;
background: #fcfcfc;
position: absolute;
width: 100%;
height: 100%;
.page-details {
display: flex;
flex-direction: column;
align-items: center;
width: 450px;
}
.bold-text {
font-weight: ${(props) => props.theme.fontWeights[3]};
font-size: 24px;
margin-top: 20px;
}
.page-message {
margin-top: 14px;
color: #716e6e;
font-size: 14px;
line-height: 17px;
letter-spacing: 0.733333px;
}
.page-unavailable-img {
width: 72px;
}
.button-position {
margin-top: 14px;
}
`;
const EntityNotFoundPane = () => {
const history = useHistory();
return (
<Wrapper>
<img
src={PageUnavailableImage}
alt="Page Unavailable"
className="page-unavailable-img"
/>
<div className="page-details">
<p className="bold-text">{INVALID_URL_ERROR}</p>
<p className="page-message">{PAGE_NOT_FOUND_ERROR}</p>
<Button
tag="button"
text="Go Back"
cypressSelector="t--invalid-page-go-back"
className="button-position"
size={Size.large}
category={Category.secondary}
onClick={history.goBack}
/>
</div>
</Wrapper>
);
};
export default EntityNotFoundPane;

View File

@ -62,7 +62,7 @@ export const EntityProperties = memo(
actionProperty = actionProperty + "()"; actionProperty = actionProperty + "()";
} }
if (actionProperty === "data") { if (actionProperty === "data") {
value = entity.data; value = entity.data?.body;
} }
return { return {
propertyName: actionProperty, propertyName: actionProperty,

View File

@ -37,7 +37,8 @@ export const EntityItem = styled.div<{
}>` }>`
position: relative; position: relative;
font-size: 12px; font-size: 12px;
padding-left: ${(props) => props.step * props.theme.spaces[2]}px; padding-left: ${(props) =>
props.step * props.theme.spaces[2] + props.theme.spaces[2]}px;
background: ${(props) => (props.active ? Colors.TUNDORA : "none")}; background: ${(props) => (props.active ? Colors.TUNDORA : "none")};
height: 30px; height: 30px;
width: 100%; width: 100%;

View File

@ -9,7 +9,6 @@ import {
} from "./hooks"; } from "./hooks";
import Search from "./ExplorerSearch"; import Search from "./ExplorerSearch";
import ExplorerPageGroup from "./Pages/PageGroup"; import ExplorerPageGroup from "./Pages/PageGroup";
import { scrollbarDark } from "constants/DefaultTheme";
import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core"; import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core";
import WidgetSidebar from "../WidgetSidebar"; import WidgetSidebar from "../WidgetSidebar";
import { BUILDER_PAGE_URL } from "constants/routes"; import { BUILDER_PAGE_URL } from "constants/routes";
@ -22,11 +21,17 @@ import PerformanceTracker, {
} from "utils/PerformanceTracker"; } from "utils/PerformanceTracker";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { getPlugins } from "selectors/entitiesSelector"; import { getPlugins } from "selectors/entitiesSelector";
import ScrollIndicator from "components/designSystems/appsmith/ScrollIndicator";
const Wrapper = styled.div` const Wrapper = styled.div`
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: auto;
${scrollbarDark}; scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
width: 0px;
-webkit-appearance: none;
}
`; `;
const NoResult = styled(NonIdealState)` const NoResult = styled(NonIdealState)`
@ -41,6 +46,7 @@ const StyledDivider = styled(Divider)`
const EntityExplorer = (props: IPanelProps) => { const EntityExplorer = (props: IPanelProps) => {
const { applicationId } = useParams<ExplorerURLParams>(); const { applicationId } = useParams<ExplorerURLParams>();
const searchInputRef: MutableRefObject<HTMLInputElement | null> = useRef( const searchInputRef: MutableRefObject<HTMLInputElement | null> = useRef(
null, null,
); );
@ -99,6 +105,7 @@ const EntityExplorer = (props: IPanelProps) => {
)} )}
<StyledDivider /> <StyledDivider />
<JSDependencies /> <JSDependencies />
<ScrollIndicator containerRef={explorerRef} />
</Wrapper> </Wrapper>
); );
}; };

View File

@ -17,7 +17,6 @@ import { useWidgetSelection } from "utils/hooks/dragResizeHooks";
import { AppState } from "reducers"; import { AppState } from "reducers";
import { getWidgetIcon } from "../ExplorerIcons"; import { getWidgetIcon } from "../ExplorerIcons";
import { noop } from "lodash";
import WidgetContextMenu from "./WidgetContextMenu"; import WidgetContextMenu from "./WidgetContextMenu";
import { updateWidgetName } from "actions/propertyPaneActions"; import { updateWidgetName } from "actions/propertyPaneActions";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
@ -133,7 +132,7 @@ export const WidgetEntity = memo((props: WidgetEntityProps) => {
name={props.widgetName} name={props.widgetName}
entityId={props.widgetId} entityId={props.widgetId}
step={props.step} step={props.step}
updateEntityName={props.pageId === pageId ? updateWidgetName : noop} updateEntityName={props.pageId === pageId ? updateWidgetName : undefined}
searchKeyword={props.searchKeyword} searchKeyword={props.searchKeyword}
isDefaultExpanded={ isDefaultExpanded={
shouldExpand || shouldExpand ||

View File

@ -0,0 +1,39 @@
import React from "react";
import styled from "styled-components";
const FooterLink = styled.a`
cursor: pointer;
text-decoration: none;
:hover {
text-decoration: underline;
color: ${(props) => props.theme.colors.text.normal};
}
font-weight: ${(props) => props.theme.typography.releaseList.fontWeight};
font-size: ${(props) => props.theme.typography.releaseList.fontSize}px;
line-height: ${(props) => props.theme.typography.releaseList.lineHeight}px;
letter-spacing: ${(props) =>
props.theme.typography.releaseList.letterSpacing}px;
color: ${(props) => props.theme.colors.text.normal};
`;
const FooterLinksContainer = styled.div`
padding: ${(props) => props.theme.spaces[9]}px;
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
max-width: ${(props) => props.theme.authCard.width}px;
`;
const FooterLinks = () => (
<FooterLinksContainer>
<FooterLink target="_blank" href="/privacy-policy.html">
Privacy Policy
</FooterLink>
<FooterLink target="_blank" href="/terms-and-conditions.html">
Terms and conditions
</FooterLink>
</FooterLinksContainer>
);
export default FooterLinks;

View File

@ -4,17 +4,17 @@ import { withRouter, RouteComponentProps } from "react-router-dom";
import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form"; import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form";
import StyledForm from "components/editorComponents/Form"; import StyledForm from "components/editorComponents/Form";
import { import {
AuthCardContainer,
AuthCardHeader, AuthCardHeader,
AuthCardBody,
FormActions, FormActions,
AuthCardNavLink, AuthCardNavLink,
FormMessagesContainer,
} from "./StyledComponents"; } from "./StyledComponents";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import { import {
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL, FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL,
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER, FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT, FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT,
FORGOT_PASSWORD_PAGE_SUBTITLE,
FORGOT_PASSWORD_PAGE_TITLE, FORGOT_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_EMPTY_EMAIL, FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_INVALID_EMAIL, FORM_VALIDATION_INVALID_EMAIL,
@ -22,11 +22,12 @@ import {
FORGOT_PASSWORD_PAGE_LOGIN_LINK, FORGOT_PASSWORD_PAGE_LOGIN_LINK,
} from "constants/messages"; } from "constants/messages";
import { AUTH_LOGIN_URL } from "constants/routes"; import { AUTH_LOGIN_URL } from "constants/routes";
import FormMessage from "components/editorComponents/form/FormMessage"; import FormMessage from "components/ads/formFields/FormMessage";
import { FORGOT_PASSWORD_FORM_NAME } from "constants/forms"; import { FORGOT_PASSWORD_FORM_NAME } from "constants/forms";
import FormGroup from "components/editorComponents/form/FormGroup"; import FormGroup from "components/ads/formFields/FormGroup";
import Button from "components/editorComponents/Button"; import Button, { Size } from "components/ads/Button";
import FormTextField from "components/editorComponents/form/FormTextField"; import FormTextField from "components/ads/formFields/TextField";
import { Icon } from "@blueprintjs/core";
import { isEmail, isEmptyString } from "utils/formhelpers"; import { isEmail, isEmptyString } from "utils/formhelpers";
import { import {
ForgotPasswordFormValues, ForgotPasswordFormValues,
@ -52,7 +53,8 @@ type ForgotPasswordProps = InjectedFormProps<
> & > &
RouteComponentProps<{ email: string }> & { emailValue: string }; RouteComponentProps<{ email: string }> & { emailValue: string };
export const ForgotPassword = (props: ForgotPasswordProps) => { export const ForgotPassword = withTheme(
(props: ForgotPasswordProps & { theme: Theme }) => {
const { const {
error, error,
handleSubmit, handleSubmit,
@ -62,10 +64,23 @@ export const ForgotPassword = (props: ForgotPasswordProps) => {
} = props; } = props;
return ( return (
<AuthCardContainer> <>
<AuthCardHeader>
<h1>{FORGOT_PASSWORD_PAGE_TITLE}</h1>
</AuthCardHeader>
<div style={{ display: "flex", justifyContent: "center" }}>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
<Icon
icon="arrow-left"
style={{ marginRight: props.theme.spaces[3] }}
/>
{FORGOT_PASSWORD_PAGE_LOGIN_LINK}
</AuthCardNavLink>
</div>
<FormMessagesContainer>
{submitSucceeded && ( {submitSucceeded && (
<FormMessage <FormMessage
intent="primary" intent="success"
message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`} message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`}
/> />
)} )}
@ -87,11 +102,7 @@ export const ForgotPassword = (props: ForgotPasswordProps) => {
{submitFailed && error && ( {submitFailed && error && (
<FormMessage intent="warning" message={error} /> <FormMessage intent="warning" message={error} />
)} )}
<AuthCardHeader> </FormMessagesContainer>
<h1>{FORGOT_PASSWORD_PAGE_TITLE}</h1>
<h5>{FORGOT_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<StyledForm onSubmit={handleSubmit(forgotPasswordSubmitHandler)}> <StyledForm onSubmit={handleSubmit(forgotPasswordSubmitHandler)}>
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
@ -105,23 +116,20 @@ export const ForgotPassword = (props: ForgotPasswordProps) => {
</FormGroup> </FormGroup>
<FormActions> <FormActions>
<Button <Button
tag="button"
type="submit" type="submit"
text={FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT} text={FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary" fill
filled size={Size.large}
size="large"
disabled={!isEmail(props.emailValue)} disabled={!isEmail(props.emailValue)}
loading={submitting} isLoading={submitting}
/> />
</FormActions> </FormActions>
</StyledForm> </StyledForm>
</AuthCardBody> </>
<AuthCardNavLink to={AUTH_LOGIN_URL}> );
{FORGOT_PASSWORD_PAGE_LOGIN_LINK} },
</AuthCardNavLink>
</AuthCardContainer>
); );
};
const selector = formValueSelector(FORGOT_PASSWORD_FORM_NAME); const selector = formValueSelector(FORGOT_PASSWORD_FORM_NAME);

View File

@ -9,13 +9,11 @@ import {
} from "constants/forms"; } from "constants/forms";
import { FORGOT_PASSWORD_URL, SIGN_UP_URL } from "constants/routes"; import { FORGOT_PASSWORD_URL, SIGN_UP_URL } from "constants/routes";
import { import {
LOGIN_PAGE_SUBTITLE,
LOGIN_PAGE_TITLE, LOGIN_PAGE_TITLE,
LOGIN_PAGE_EMAIL_INPUT_LABEL, LOGIN_PAGE_EMAIL_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_LABEL, LOGIN_PAGE_PASSWORD_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER, LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER,
LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER, LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD, FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL, FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD, FORM_VALIDATION_INVALID_PASSWORD,
@ -24,29 +22,28 @@ import {
LOGIN_PAGE_SIGN_UP_LINK_TEXT, LOGIN_PAGE_SIGN_UP_LINK_TEXT,
LOGIN_PAGE_INVALID_CREDS_ERROR, LOGIN_PAGE_INVALID_CREDS_ERROR,
LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK, LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK,
FORM_VALIDATION_PASSWORD_RULE, NEW_TO_APPSMITH,
} from "constants/messages"; } from "constants/messages";
import Divider from "components/editorComponents/Divider"; import FormMessage from "components/ads/formFields/FormMessage";
import FormMessage from "components/editorComponents/form/FormMessage"; import FormGroup from "components/ads/formFields/FormGroup";
import FormGroup from "components/editorComponents/form/FormGroup"; import FormTextField from "components/ads/formFields/TextField";
import FormTextField from "components/editorComponents/form/FormTextField"; import Button, { Size } from "components/ads/Button";
import Button from "components/editorComponents/Button";
import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth"; import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth";
import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers"; import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
import { LoginFormValues } from "./helpers"; import { LoginFormValues } from "./helpers";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import { import {
AuthCardContainer,
SpacedSubmitForm, SpacedSubmitForm,
FormActions, FormActions,
AuthCardHeader, AuthCardHeader,
AuthCardFooter,
AuthCardNavLink, AuthCardNavLink,
AuthCardBody, SignUpLinkSection,
ForgotPasswordLink,
} from "./StyledComponents"; } from "./StyledComponents";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { getAppsmithConfigs } from "configs"; import { getAppsmithConfigs } from "configs";
import { TncPPLinks } from "./SignUp";
import { LOGIN_SUBMIT_PATH } from "constants/ApiConstants"; import { LOGIN_SUBMIT_PATH } from "constants/ApiConstants";
import PerformanceTracker, { import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
@ -55,16 +52,14 @@ const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs();
const validate = (values: LoginFormValues) => { const validate = (values: LoginFormValues) => {
const errors: LoginFormValues = {}; const errors: LoginFormValues = {};
const email = values[LOGIN_FORM_EMAIL_FIELD_NAME]; const email = values[LOGIN_FORM_EMAIL_FIELD_NAME] || "";
const password = values[LOGIN_FORM_PASSWORD_FIELD_NAME]; const password = values[LOGIN_FORM_PASSWORD_FIELD_NAME];
if (!password || isEmptyString(password)) { if (!password || isEmptyString(password)) {
errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_EMPTY_PASSWORD; errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_EMPTY_PASSWORD;
} else if (!isStrongPassword(password)) { } else if (!isStrongPassword(password)) {
errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_INVALID_PASSWORD; errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_INVALID_PASSWORD;
} }
if (!email || isEmptyString(email)) { if (!isEmptyString(email) && !isEmail(email)) {
errors[LOGIN_FORM_EMAIL_FIELD_NAME] = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(email)) {
errors[LOGIN_FORM_EMAIL_FIELD_NAME] = FORM_VALIDATION_INVALID_EMAIL; errors[LOGIN_FORM_EMAIL_FIELD_NAME] = FORM_VALIDATION_INVALID_EMAIL;
} }
@ -74,14 +69,17 @@ const validate = (values: LoginFormValues) => {
type LoginFormProps = { emailValue: string } & InjectedFormProps< type LoginFormProps = { emailValue: string } & InjectedFormProps<
LoginFormValues, LoginFormValues,
{ emailValue: string } { emailValue: string }
>; > & {
theme: Theme;
};
const SocialLoginList: string[] = []; const SocialLoginList: string[] = [];
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE); if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE);
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
export const Login = (props: LoginFormProps) => { export const Login = (props: LoginFormProps) => {
const { error, valid } = props; const { error, valid, emailValue: email } = props;
const isFormValid = valid && email && !isEmptyString(email);
const location = useLocation(); const location = useLocation();
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
@ -103,7 +101,19 @@ export const Login = (props: LoginFormProps) => {
} }
return ( return (
<AuthCardContainer> <>
<AuthCardHeader>
<h1>{LOGIN_PAGE_TITLE}</h1>
</AuthCardHeader>
<SignUpLinkSection>
{NEW_TO_APPSMITH}
<AuthCardNavLink
to={signupURL}
style={{ marginLeft: props.theme.spaces[3] }}
>
{LOGIN_PAGE_SIGN_UP_LINK_TEXT}
</AuthCardNavLink>
</SignUpLinkSection>
{showError && ( {showError && (
<FormMessage <FormMessage
intent="warning" intent="warning"
@ -117,11 +127,9 @@ export const Login = (props: LoginFormProps) => {
]} ]}
/> />
)} )}
<AuthCardHeader> {SocialLoginList.length > 0 && (
<h1>{LOGIN_PAGE_TITLE}</h1> <ThirdPartyAuth type={"SIGNIN"} logins={SocialLoginList} />
<h5>{LOGIN_PAGE_SUBTITLE}</h5> )}
</AuthCardHeader>
<AuthCardBody>
<SpacedSubmitForm method="POST" action={loginURL}> <SpacedSubmitForm method="POST" action={loginURL}>
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
@ -137,7 +145,7 @@ export const Login = (props: LoginFormProps) => {
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
label={LOGIN_PAGE_PASSWORD_INPUT_LABEL} label={LOGIN_PAGE_PASSWORD_INPUT_LABEL}
helperText={FORM_VALIDATION_PASSWORD_RULE} // helperText={FORM_VALIDATION_PASSWORD_RULE}
> >
<FormTextField <FormTextField
type="password" type="password"
@ -145,15 +153,15 @@ export const Login = (props: LoginFormProps) => {
placeholder={LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER} placeholder={LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER}
/> />
</FormGroup> </FormGroup>
<Link to={forgotPasswordURL}>{LOGIN_PAGE_FORGOT_PASSWORD_TEXT}</Link>
<FormActions> <FormActions>
<Button <Button
tag="button"
type="submit" type="submit"
disabled={!valid} disabled={!isFormValid}
text={LOGIN_PAGE_LOGIN_BUTTON_TEXT} text={LOGIN_PAGE_LOGIN_BUTTON_TEXT}
intent="primary" fill
filled size={Size.large}
size="large"
onClick={() => { onClick={() => {
PerformanceTracker.startTracking( PerformanceTracker.startTracking(
PerformanceTransactionName.LOGIN_CLICK, PerformanceTransactionName.LOGIN_CLICK,
@ -165,20 +173,10 @@ export const Login = (props: LoginFormProps) => {
/> />
</FormActions> </FormActions>
</SpacedSubmitForm> </SpacedSubmitForm>
{SocialLoginList.length > 0 && ( <ForgotPasswordLink>
<> <Link to={forgotPasswordURL}>{LOGIN_PAGE_FORGOT_PASSWORD_TEXT}</Link>
<Divider /> </ForgotPasswordLink>
<ThirdPartyAuth type={"SIGNIN"} logins={SocialLoginList} />
</> </>
)}
</AuthCardBody>
<AuthCardNavLink to={signupURL}>
{LOGIN_PAGE_SIGN_UP_LINK_TEXT}
</AuthCardNavLink>
<AuthCardFooter>
<TncPPLinks />
</AuthCardFooter>
</AuthCardContainer>
); );
}; };
@ -190,5 +188,5 @@ export default connect((state) => ({
validate, validate,
touchOnBlur: true, touchOnBlur: true,
form: LOGIN_FORM_NAME, form: LOGIN_FORM_NAME,
})(Login), })(withTheme(Login)),
); );

View File

@ -7,33 +7,32 @@ import { RESET_PASSWORD_FORM_NAME } from "constants/forms";
import { ReduxActionTypes } from "constants/ReduxActionConstants"; import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { getIsTokenValid, getIsValidatingToken } from "selectors/authSelectors"; import { getIsTokenValid, getIsValidatingToken } from "selectors/authSelectors";
import { Icon } from "@blueprintjs/core"; import { Icon } from "@blueprintjs/core";
import FormTextField from "components/editorComponents/form/fields/TextField"; import FormGroup from "components/ads/formFields/FormGroup";
import FormTextField from "components/ads/formFields/TextField";
import FormMessage, { import FormMessage, {
FormMessageProps,
MessageAction, MessageAction,
} from "components/editorComponents/form/FormMessage"; FormMessageProps,
} from "components/ads/formFields/FormMessage";
import Spinner from "components/editorComponents/Spinner"; import Spinner from "components/editorComponents/Spinner";
import Button from "components/editorComponents/Button"; import Button, { Size } from "components/ads/Button";
import FormGroup from "components/editorComponents/form/FormGroup";
import StyledForm from "components/editorComponents/Form"; import StyledForm from "components/editorComponents/Form";
import { isEmptyString, isStrongPassword } from "utils/formhelpers"; import { isEmptyString, isStrongPassword } from "utils/formhelpers";
import { ResetPasswordFormValues, resetPasswordSubmitHandler } from "./helpers"; import { ResetPasswordFormValues, resetPasswordSubmitHandler } from "./helpers";
import { import {
AuthCardHeader, AuthCardHeader,
AuthCardFooter,
AuthCardContainer,
AuthCardBody,
AuthCardNavLink, AuthCardNavLink,
FormActions, FormActions,
} from "./StyledComponents"; } from "./StyledComponents";
import { AUTH_LOGIN_URL, FORGOT_PASSWORD_URL } from "constants/routes"; import { AUTH_LOGIN_URL, FORGOT_PASSWORD_URL } from "constants/routes";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import { import {
RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL, RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL,
RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER, RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER,
RESET_PASSWORD_LOGIN_LINK_TEXT, RESET_PASSWORD_LOGIN_LINK_TEXT,
RESET_PASSWORD_SUBMIT_BUTTON_TEXT, RESET_PASSWORD_SUBMIT_BUTTON_TEXT,
RESET_PASSWORD_PAGE_SUBTITLE,
RESET_PASSWORD_PAGE_TITLE, RESET_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_INVALID_PASSWORD, FORM_VALIDATION_INVALID_PASSWORD,
FORM_VALIDATION_EMPTY_PASSWORD, FORM_VALIDATION_EMPTY_PASSWORD,
@ -43,7 +42,6 @@ import {
RESET_PASSWORD_RESET_SUCCESS, RESET_PASSWORD_RESET_SUCCESS,
RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK, RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK,
} from "constants/messages"; } from "constants/messages";
import { TncPPLinks } from "./SignUp";
const validate = (values: ResetPasswordFormValues) => { const validate = (values: ResetPasswordFormValues) => {
const errors: ResetPasswordFormValues = {}; const errors: ResetPasswordFormValues = {};
@ -66,6 +64,7 @@ type ResetPasswordProps = InjectedFormProps<
verifyToken: (token: string, email: string) => void; verifyToken: (token: string, email: string) => void;
isTokenValid: boolean; isTokenValid: boolean;
validatingToken: boolean; validatingToken: boolean;
theme: Theme;
} & RouteComponentProps<{ email: string; token: string }>; } & RouteComponentProps<{ email: string; token: string }>;
export const ResetPassword = (props: ResetPasswordProps) => { export const ResetPassword = (props: ResetPasswordProps) => {
@ -154,15 +153,22 @@ export const ResetPassword = (props: ResetPasswordProps) => {
return <Spinner />; return <Spinner />;
} }
return ( return (
<AuthCardContainer> <>
<AuthCardHeader>
<h1>{RESET_PASSWORD_PAGE_TITLE}</h1>
</AuthCardHeader>
<div style={{ display: "flex", justifyContent: "center" }}>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
<Icon
icon="arrow-left"
style={{ marginRight: props.theme.spaces[3] }}
/>
{RESET_PASSWORD_LOGIN_LINK_TEXT}
</AuthCardNavLink>
</div>
{(showSuccessMessage || showFailureMessage) && ( {(showSuccessMessage || showFailureMessage) && (
<FormMessage {...messageTagProps} /> <FormMessage {...messageTagProps} />
)} )}
<AuthCardHeader>
<h1>{RESET_PASSWORD_PAGE_TITLE}</h1>
<h5>{RESET_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<StyledForm onSubmit={handleSubmit(resetPasswordSubmitHandler)}> <StyledForm onSubmit={handleSubmit(resetPasswordSubmitHandler)}>
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
@ -179,25 +185,17 @@ export const ResetPassword = (props: ResetPasswordProps) => {
<Field type="hidden" name="token" component="input" /> <Field type="hidden" name="token" component="input" />
<FormActions> <FormActions>
<Button <Button
filled tag="button"
size="large" fill
size={Size.large}
type="submit" type="submit"
text={RESET_PASSWORD_SUBMIT_BUTTON_TEXT} text={RESET_PASSWORD_SUBMIT_BUTTON_TEXT}
intent="primary"
disabled={pristine || submitSucceeded} disabled={pristine || submitSucceeded}
loading={submitting} isLoading={submitting}
/> />
</FormActions> </FormActions>
</StyledForm> </StyledForm>
</AuthCardBody> </>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{RESET_PASSWORD_LOGIN_LINK_TEXT}
<Icon icon="arrow-right" intent="primary" />
</AuthCardNavLink>
<AuthCardFooter>
<TncPPLinks></TncPPLinks>
</AuthCardFooter>
</AuthCardContainer>
); );
}; };
@ -232,5 +230,5 @@ export default connect(
validate, validate,
form: RESET_PASSWORD_FORM_NAME, form: RESET_PASSWORD_FORM_NAME,
touchOnBlur: true, touchOnBlur: true,
})(withRouter(ResetPassword)), })(withRouter(withTheme(ResetPassword))),
); );

View File

@ -1,45 +1,33 @@
import React from "react"; import React from "react";
import { reduxForm, InjectedFormProps } from "redux-form"; import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form";
import { AUTH_LOGIN_URL } from "constants/routes"; import { AUTH_LOGIN_URL } from "constants/routes";
import { SIGNUP_FORM_NAME } from "constants/forms"; import { SIGNUP_FORM_NAME } from "constants/forms";
import { import { RouteComponentProps, useLocation, withRouter } from "react-router-dom";
Link,
RouteComponentProps,
useLocation,
withRouter,
} from "react-router-dom";
import Divider from "components/editorComponents/Divider";
import { import {
AuthCardHeader, AuthCardHeader,
AuthCardBody,
AuthCardFooter,
AuthCardNavLink, AuthCardNavLink,
SpacedSubmitForm, SpacedSubmitForm,
FormActions, FormActions,
AuthCardContainer, SignUpLinkSection,
} from "./StyledComponents"; } from "./StyledComponents";
import { import {
SIGNUP_PAGE_TITLE, SIGNUP_PAGE_TITLE,
SIGNUP_PAGE_SUBTITLE,
SIGNUP_PAGE_EMAIL_INPUT_LABEL, SIGNUP_PAGE_EMAIL_INPUT_LABEL,
SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER, SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER,
SIGNUP_PAGE_PASSWORD_INPUT_LABEL, SIGNUP_PAGE_PASSWORD_INPUT_LABEL,
SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER, SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER,
SIGNUP_PAGE_LOGIN_LINK_TEXT, SIGNUP_PAGE_LOGIN_LINK_TEXT,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD, FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL, FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD, FORM_VALIDATION_INVALID_PASSWORD,
SIGNUP_PAGE_SUBMIT_BUTTON_TEXT, SIGNUP_PAGE_SUBMIT_BUTTON_TEXT,
PRIVACY_POLICY_LINK, ALREADY_HAVE_AN_ACCOUNT,
TERMS_AND_CONDITIONS_LINK,
FORM_VALIDATION_PASSWORD_RULE,
} from "constants/messages"; } from "constants/messages";
import FormMessage from "components/editorComponents/form/FormMessage"; import FormMessage from "components/ads/formFields/FormMessage";
import FormGroup from "components/editorComponents/form/FormGroup"; import FormGroup from "components/ads/formFields/FormGroup";
import FormTextField from "components/editorComponents/form/FormTextField"; import FormTextField from "components/ads/formFields/TextField";
import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth"; import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth";
import Button from "components/editorComponents/Button"; import Button, { Size } from "components/ads/Button";
import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers"; import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
@ -54,28 +42,16 @@ import PerformanceTracker, {
PerformanceTransactionName, PerformanceTransactionName,
} from "utils/PerformanceTracker"; } from "utils/PerformanceTracker";
import { setOnboardingState } from "utils/storage"; import { setOnboardingState } from "utils/storage";
const {
enableGithubOAuth, import { SIGNUP_FORM_EMAIL_FIELD_NAME } from "constants/forms";
enableGoogleOAuth,
enableTNCPP, const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs();
} = getAppsmithConfigs();
const SocialLoginList: string[] = []; const SocialLoginList: string[] = [];
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB); if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE); if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE);
export const TncPPLinks = () => { import { withTheme } from "styled-components";
if (!enableTNCPP) return null; import { Theme } from "constants/DefaultTheme";
return (
<>
<Link target="_blank" to="/privacy-policy.html">
{PRIVACY_POLICY_LINK}
</Link>
<Link target="_blank" to="/terms-and-conditions.html">
{TERMS_AND_CONDITIONS_LINK}
</Link>
</>
);
};
const validate = (values: SignupFormValues) => { const validate = (values: SignupFormValues) => {
const errors: SignupFormValues = {}; const errors: SignupFormValues = {};
@ -84,19 +60,24 @@ const validate = (values: SignupFormValues) => {
} else if (!isStrongPassword(values.password)) { } else if (!isStrongPassword(values.password)) {
errors.password = FORM_VALIDATION_INVALID_PASSWORD; errors.password = FORM_VALIDATION_INVALID_PASSWORD;
} }
if (!values.email || isEmptyString(values.email)) {
errors.email = FORM_VALIDATION_EMPTY_EMAIL; const email = values.email || "";
} else if (!isEmail(values.email)) { if (!isEmptyString(email) && !isEmail(email)) {
errors.email = FORM_VALIDATION_INVALID_EMAIL; errors.email = FORM_VALIDATION_INVALID_EMAIL;
} }
return errors; return errors;
}; };
type SignUpFormProps = InjectedFormProps<SignupFormValues> & type SignUpFormProps = InjectedFormProps<
RouteComponentProps<{ email: string }>; SignupFormValues,
{ emailValue: string }
> &
RouteComponentProps<{ email: string }> & { theme: Theme; emailValue: string };
export const SignUp = (props: SignUpFormProps) => { export const SignUp = (props: SignUpFormProps) => {
const { error, submitting, pristine, valid } = props; const { error, submitting, pristine, valid, emailValue: email } = props;
const isFormValid = valid && email && !isEmptyString(email);
const location = useLocation(); const location = useLocation();
let showError = false; let showError = false;
@ -115,13 +96,23 @@ export const SignUp = (props: SignUpFormProps) => {
} }
return ( return (
<AuthCardContainer> <>
{showError && <FormMessage intent="danger" message={errorMessage} />} {showError && <FormMessage intent="danger" message={errorMessage} />}
<AuthCardHeader> <AuthCardHeader>
<h1>{SIGNUP_PAGE_TITLE}</h1> <h1>{SIGNUP_PAGE_TITLE}</h1>
<h5>{SIGNUP_PAGE_SUBTITLE}</h5>
</AuthCardHeader> </AuthCardHeader>
<AuthCardBody> <SignUpLinkSection>
{ALREADY_HAVE_AN_ACCOUNT}
<AuthCardNavLink
to={AUTH_LOGIN_URL}
style={{ marginLeft: props.theme.spaces[3] }}
>
{SIGNUP_PAGE_LOGIN_LINK_TEXT}
</AuthCardNavLink>
</SignUpLinkSection>
{SocialLoginList.length > 0 && (
<ThirdPartyAuth type={"SIGNUP"} logins={SocialLoginList} />
)}
<SpacedSubmitForm method="POST" action={signupURL}> <SpacedSubmitForm method="POST" action={signupURL}>
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
@ -137,7 +128,7 @@ export const SignUp = (props: SignUpFormProps) => {
<FormGroup <FormGroup
intent={error ? "danger" : "none"} intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_PASSWORD_INPUT_LABEL} label={SIGNUP_PAGE_PASSWORD_INPUT_LABEL}
helperText={FORM_VALIDATION_PASSWORD_RULE} // helperText={FORM_VALIDATION_PASSWORD_RULE}
> >
<FormTextField <FormTextField
type="password" type="password"
@ -147,13 +138,13 @@ export const SignUp = (props: SignUpFormProps) => {
</FormGroup> </FormGroup>
<FormActions> <FormActions>
<Button <Button
tag="button"
type="submit" type="submit"
disabled={pristine || !valid} disabled={pristine || !isFormValid}
loading={submitting} isLoading={submitting}
text={SIGNUP_PAGE_SUBMIT_BUTTON_TEXT} text={SIGNUP_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary" fill
filled size={Size.large}
size="large"
onClick={() => { onClick={() => {
AnalyticsUtil.logEvent("SIGNUP_CLICK", { AnalyticsUtil.logEvent("SIGNUP_CLICK", {
signupMethod: "EMAIL", signupMethod: "EMAIL",
@ -166,34 +157,23 @@ export const SignUp = (props: SignUpFormProps) => {
/> />
</FormActions> </FormActions>
</SpacedSubmitForm> </SpacedSubmitForm>
{SocialLoginList.length > 0 && (
<>
<Divider />
<ThirdPartyAuth type={"SIGNUP"} logins={SocialLoginList} />
</> </>
)}
</AuthCardBody>
<AuthCardFooter>
<TncPPLinks />
</AuthCardFooter>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{SIGNUP_PAGE_LOGIN_LINK_TEXT}
</AuthCardNavLink>
</AuthCardContainer>
); );
}; };
const selector = formValueSelector(SIGNUP_FORM_NAME);
export default connect((state: AppState, props: SignUpFormProps) => { export default connect((state: AppState, props: SignUpFormProps) => {
const queryParams = new URLSearchParams(props.location.search); const queryParams = new URLSearchParams(props.location.search);
return { return {
initialValues: { initialValues: {
email: queryParams.get("email"), email: queryParams.get("email"),
}, },
emailValue: selector(state, SIGNUP_FORM_EMAIL_FIELD_NAME),
}; };
}, null)( }, null)(
reduxForm<SignupFormValues>({ reduxForm<SignupFormValues, { emailValue: string }>({
validate, validate,
form: SIGNUP_FORM_NAME, form: SIGNUP_FORM_NAME,
touchOnBlur: true, touchOnBlur: true,
})(withRouter(SignUp)), })(withRouter(withTheme(SignUp))),
); );

View File

@ -1,38 +1,69 @@
import styled, { css } from "styled-components"; import styled from "styled-components";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Form from "components/editorComponents/Form"; import Form from "components/editorComponents/Form";
import { Card } from "@blueprintjs/core"; import { Card } from "@blueprintjs/core";
import { getTypographyByKey } from "constants/DefaultTheme";
import { Classes } from "@blueprintjs/core";
export const AuthContainer = styled.section` export const AuthContainer = styled.section`
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100vh; height: ${(props) => `calc(100vh - ${props.theme.headerHeight})`};
will-change: transform, opacity; background-color: ${(props) => props.theme.colors.auth.background};
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
& .${Classes.FORM_GROUP} {
margin: 0 0 ${(props) => props.theme.spaces[2]}px;
}
`;
export const AuthCardContainer = styled.div`
display: flex;
flex-grow: 1;
flex-direction: column;
justify-content: center;
padding: ${(props) => props.theme.authCard.padding}px 0;
`; `;
export const AuthCard = styled(Card)` export const AuthCard = styled(Card)`
&& { display: flex;
flex-direction: column;
background-color: ${(props) => props.theme.colors.auth.cardBackground};
padding: ${(props) => props.theme.spaces[15]}px 64px;
width: ${(props) => props.theme.authCard.width}px; width: ${(props) => props.theme.authCard.width}px;
background: ${(props) => props.theme.authCard.background};
border-radius: ${(props) => props.theme.authCard.borderRadius}px;
padding: ${(props) => props.theme.authCard.padding}px;
box-shadow: ${(props) => props.theme.authCard.shadow};
border: none; border: none;
& h1, h1 {
h5 { text-align: center;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-weight: ${(props) => props.theme.fontWeights[1]}; ${(props) => getTypographyByKey(props, "authCardHeader")}
color: ${(props) => props.theme.colors.auth.headingText};
} }
& .form-message-container {
width: ${(props) => props.theme.authCard.formMessageWidth}px;
align-self: center;
text-align: center;
}
.form-message-container ~ .form-message-container {
margin-top: ${(props) => props.theme.spaces[4]}px;
}
& > div {
margin-bottom: ${(props) => props.theme.spaces[14]}px;
}
& > div:last-child,
& > div:empty {
margin-bottom: 0;
} }
`; `;
export const AuthCardContainer = styled.div``;
export const AuthCardHeader = styled.header` export const AuthCardHeader = styled.header`
& { & {
h1 { h1 {
font-size: ${(props) => props.theme.fontSizes[6]}px; font-size: ${(props) => props.theme.fontSizes[6]}px;
white-space: nowrap;
} }
h5 { h5 {
font-size: ${(props) => props.theme.fontSizes[4]}px; font-size: ${(props) => props.theme.fontSizes[4]}px;
@ -42,12 +73,10 @@ export const AuthCardHeader = styled.header`
`; `;
export const AuthCardNavLink = styled(Link)` export const AuthCardNavLink = styled(Link)`
text-align: center; border-bottom: 1px solid transparent;
margin: 0 auto; &:hover {
display: block; border-bottom: 1px solid ${(props) => props.theme.colors.auth.link};
margin-top: ${(props) => props.theme.spaces[12]}px; text-decoration: none;
& span {
margin-left: ${(props) => props.theme.spaces[4]}px;
} }
`; `;
@ -60,26 +89,15 @@ export const AuthCardFooter = styled.footer`
`; `;
export const AuthCardBody = styled.div` export const AuthCardBody = styled.div`
display: flex;
justify-content: flex-start;
align-items: stretch;
& a { & a {
margin-top: ${(props) => props.theme.spaces[8]}px; margin-top: ${(props) => props.theme.spaces[8]}px;
font-size: ${(props) => props.theme.fontSizes[2]}px; font-size: ${(props) => props.theme.fontSizes[2]}px;
} }
`; `;
const formSpacing = css` export const SpacedForm = styled(Form)``;
flex-grow: 1;
margin-right: ${(props) => props.theme.authCard.dividerSpacing}px;
`;
export const SpacedForm = styled(Form)`
${formSpacing}
`;
export const SpacedSubmitForm = styled.form` export const SpacedSubmitForm = styled.form`
${formSpacing}
& a { & a {
font-size: ${(props) => props.theme.fontSizes[3]}px; font-size: ${(props) => props.theme.fontSizes[3]}px;
} }
@ -95,8 +113,29 @@ export const FormActions = styled.div`
} }
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
margin-top: ${(props) => props.theme.spaces[2]}px; margin-top: ${(props) => props.theme.spaces[5]}px;
& > label { & > label {
margin-right: ${(props) => props.theme.spaces[11]}px; margin-right: ${(props) => props.theme.spaces[11]}px;
} }
`; `;
export const SignUpLinkSection = styled.div`
${(props) => getTypographyByKey(props, "authCardSubheader")}
color: ${(props) => props.theme.colors.auth.text};
text-align: center;
`;
export const ForgotPasswordLink = styled.div`
${(props) => getTypographyByKey(props, "authCardSubheader")}
color: ${(props) => props.theme.colors.auth.text};
text-align: center;
margin-top: ${(props) => props.theme.spaces[11]}px;
& a {
color: ${(props) => props.theme.colors.auth.text};
}
`;
export const FormMessagesContainer = styled.div`
display: flex;
flex-direction: column;
`;

View File

@ -4,7 +4,7 @@ import {
getSocialLoginButtonProps, getSocialLoginButtonProps,
SocialLoginType, SocialLoginType,
} from "constants/SocialLogin"; } from "constants/SocialLogin";
import { IntentColors, getBorderCSSShorthand } from "constants/DefaultTheme"; import { getTypographyByKey } from "constants/DefaultTheme";
import AnalyticsUtil, { EventName } from "utils/AnalyticsUtil"; import AnalyticsUtil, { EventName } from "utils/AnalyticsUtil";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import PerformanceTracker, { import PerformanceTracker, {
@ -15,52 +15,42 @@ import { setOnboardingState } from "utils/storage";
const ThirdPartyAuthWrapper = styled.div` const ThirdPartyAuthWrapper = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start;
align-items: flex-end;
margin-left: ${(props) => props.theme.authCard.dividerSpacing}px;
`; `;
//TODO(abhinav): Port this to use themes. //TODO(abhinav): Port this to use themes.
const StyledSocialLoginButton = styled.a` const StyledSocialLoginButton = styled.a`
width: 200px;
display: flex; display: flex;
align-items: center; align-items: center;
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])}; justify-content: center;
padding: 8px; border: solid 1px ${(props) => props.theme.colors.auth.socialBtnBorder};
color: ${(props) => props.theme.colors.textDefault}; padding: ${(props) => props.theme.spaces[2]}px;
border-radius: ${(props) => props.theme.radii[1]}px;
position: relative; &:first-child {
height: 42px; margin-bottom: ${(props) => props.theme.spaces[4]}px;
}
&:only-child {
margin-bottom: 0;
}
&:hover { &:hover {
text-decoration: none; text-decoration: none;
background: ${IntentColors.success}; background-color: ${(props) => props.theme.colors.auth.socialBtnHighlight};
color: ${(props) => props.theme.colors.textOnDarkBG};
} }
& > div {
width: 36px; & .login-method {
height: 36px; ${(props) => getTypographyByKey(props, "btnLarge")}
padding: ${(props) => props.theme.radii[1]}px; color: ${(props) => props.theme.colors.auth.socialBtnText};
position: absolute; text-transform: uppercase;
left: 2px;
top: 2px;
background: white;
display: flex;
justify-content: center;
align-items: center;
& img {
width: 80%;
height: 80%;
}
}
& p {
display: block;
margin: 0 0 0 36px;
font-size: ${(props) => props.theme.fontSizes[3]}px;
font-weight: ${(props) => props.theme.fontWeights[3]};
} }
`; `;
const ButtonLogo = styled.img`
margin: ${(props) => props.theme.spaces[2]}px;
width: 14px;
height: 14px;
`;
export const SocialLoginTypes: Record<string, string> = { export const SocialLoginTypes: Record<string, string> = {
GOOGLE: "google", GOOGLE: "google",
GITHUB: "github", GITHUB: "github",
@ -102,10 +92,8 @@ const SocialLoginButton = (props: {
}); });
}} }}
> >
<div> <ButtonLogo alt={` ${props.name} login`} src={props.logo} />
<img alt={` ${props.name} login`} src={props.logo} /> <div className="login-method">{`continue with ${props.name}`}</div>
</div>
<p>{`Sign in with ${props.name}`}</p>
</StyledSocialLoginButton> </StyledSocialLoginButton>
); );
}; };

View File

@ -1,30 +1,22 @@
import React from "react"; import React from "react";
import { Switch, useRouteMatch, useLocation, Route } from "react-router-dom"; import { Switch, useRouteMatch, useLocation, Route } from "react-router-dom";
import Login from "./Login"; import Login from "./Login";
import Centered from "components/designSystems/appsmith/CenteredWrapper"; import { AuthContainer, AuthCard, AuthCardContainer } from "./StyledComponents";
import { animated, useTransition } from "react-spring";
import { AuthContainer, AuthCard } from "./StyledComponents";
import SignUp from "./SignUp"; import SignUp from "./SignUp";
import ForgotPassword from "./ForgotPassword"; import ForgotPassword from "./ForgotPassword";
import ResetPassword from "./ResetPassword"; import ResetPassword from "./ResetPassword";
import PageNotFound from "pages/common/PageNotFound"; import PageNotFound from "pages/common/PageNotFound";
import FooterLinks from "./FooterLinks";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
const SentryRoute = Sentry.withSentryRouting(Route); const SentryRoute = Sentry.withSentryRouting(Route);
const AnimatedAuthCard = animated(AuthContainer);
export const UserAuth = () => { export const UserAuth = () => {
const { path } = useRouteMatch(); const { path } = useRouteMatch();
const location = useLocation(); const location = useLocation();
const transitions = useTransition(location, (location) => location.pathname, {
from: { opacity: 0, transform: "translate3d(50%,0,0)" }, return (
enter: { opacity: 1, transform: "translate3d(0%,0,0)" }, <AuthContainer>
leave: { opacity: 0, transform: "translate3d(-50%,0,0)" }, <AuthCardContainer>
reset: true,
});
const renderTransitions = transitions.map(
({ item: location, props, key }) => (
<AnimatedAuthCard key={key} style={props}>
<Centered>
<AuthCard> <AuthCard>
<Switch location={location}> <Switch location={location}>
<SentryRoute exact path={`${path}/login`} component={Login} /> <SentryRoute exact path={`${path}/login`} component={Login} />
@ -42,11 +34,10 @@ export const UserAuth = () => {
<SentryRoute component={PageNotFound} /> <SentryRoute component={PageNotFound} />
</Switch> </Switch>
</AuthCard> </AuthCard>
</Centered> </AuthCardContainer>
</AnimatedAuthCard> <FooterLinks />
), </AuthContainer>
); );
return <React.Fragment>{renderTransitions}</React.Fragment>;
}; };
export default UserAuth; export default UserAuth;

View File

@ -14,7 +14,7 @@ const StyledPageHeader = styled(StyledHeader)`
height: 48px; height: 48px;
background: ${Colors.BALTIC_SEA}; background: ${Colors.BALTIC_SEA};
display: flex; display: flex;
justify-content: space-between; justify-content: center;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05); box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.05);
padding: 0px ${(props) => props.theme.spaces[12]}; padding: 0px ${(props) => props.theme.spaces[12]};
`; `;

View File

@ -128,6 +128,7 @@ export default function MemberSettings(props: PageProps) {
{ {
Header: "Delete", Header: "Delete",
accessor: "delete", accessor: "delete",
disableSortBy: true,
Cell: function DeleteCell(cellProps: any) { Cell: function DeleteCell(cellProps: any) {
if ( if (
cellProps.cell.row.values.username === cellProps.cell.row.values.username ===

View File

@ -6,6 +6,7 @@ import {
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import { UpdateWidgetPropertyPayload } from "actions/controlActions"; import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import _ from "lodash";
const initialState: CanvasWidgetsReduxState = {}; const initialState: CanvasWidgetsReduxState = {};
@ -14,8 +15,7 @@ export type FlattenedWidgetProps = WidgetProps & {
}; };
const canvasWidgetsReducer = createImmerReducer(initialState, { const canvasWidgetsReducer = createImmerReducer(initialState, {
// TODO Rename to INIT_LAYOUT [ReduxActionTypes.INIT_CANVAS_LAYOUT]: (
[ReduxActionTypes.UPDATE_CANVAS]: (
state: CanvasWidgetsReduxState, state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateCanvasPayload>, action: ReduxAction<UpdateCanvasPayload>,
) => { ) => {
@ -31,8 +31,13 @@ const canvasWidgetsReducer = createImmerReducer(initialState, {
state: CanvasWidgetsReduxState, state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateWidgetPropertyPayload>, action: ReduxAction<UpdateWidgetPropertyPayload>,
) => { ) => {
state[action.payload.widgetId][action.payload.propertyName] = // We loop over all updates
action.payload.propertyValue; Object.entries(action.payload.updates).forEach(
([propertyPath, propertyValue]) => {
// since property paths could be nested, we use lodash set method
_.set(state[action.payload.widgetId], propertyPath, propertyValue);
},
);
}, },
}); });

View File

@ -93,7 +93,7 @@ const editorReducer = createReducer(initialState, {
state.loadingStates.savingError = true; state.loadingStates.savingError = true;
return { ...state }; return { ...state };
}, },
[ReduxActionTypes.UPDATE_CANVAS]: ( [ReduxActionTypes.INIT_CANVAS_LAYOUT]: (
state: EditorReduxState, state: EditorReduxState,
action: ReduxAction<UpdateCanvasPayload>, action: ReduxAction<UpdateCanvasPayload>,
) => { ) => {

View File

@ -38,6 +38,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import history from "utils/history"; import history from "utils/history";
import { import {
BUILDER_PAGE_URL, BUILDER_PAGE_URL,
convertToQueryParams,
getApplicationViewerPageURL, getApplicationViewerPageURL,
} from "constants/routes"; } from "constants/routes";
import { import {
@ -86,42 +87,61 @@ import {
} from "./EvaluationsSaga"; } from "./EvaluationsSaga";
import copy from "copy-to-clipboard"; import copy from "copy-to-clipboard";
export enum NavigationTargetType {
SAME_WINDOW = "SAME_WINDOW",
NEW_WINDOW = "NEW_WINDOW",
}
function* navigateActionSaga( function* navigateActionSaga(
action: { pageNameOrUrl: string; params: Record<string, string> }, action: {
pageNameOrUrl: string;
params: Record<string, string>;
target?: NavigationTargetType;
},
event: ExecuteActionPayloadEvent, event: ExecuteActionPayloadEvent,
) { ) {
const pageList = yield select(getPageList); const pageList = yield select(getPageList);
const applicationId = yield select(getCurrentApplicationId); const applicationId = yield select(getCurrentApplicationId);
const {
pageNameOrUrl,
params,
target = NavigationTargetType.SAME_WINDOW,
} = action;
const page = _.find( const page = _.find(
pageList, pageList,
(page: Page) => page.pageName === action.pageNameOrUrl, (page: Page) => page.pageName === pageNameOrUrl,
); );
if (page) { if (page) {
AnalyticsUtil.logEvent("NAVIGATE", { AnalyticsUtil.logEvent("NAVIGATE", {
pageName: action.pageNameOrUrl, pageName: pageNameOrUrl,
pageParams: action.params, pageParams: params,
}); });
// TODO need to make this check via RENDER_MODE; const appMode = yield select(getAppMode);
const path = const path =
history.location.pathname.indexOf("/edit") !== -1 appMode === APP_MODE.EDIT
? BUILDER_PAGE_URL(applicationId, page.pageId, action.params) ? BUILDER_PAGE_URL(applicationId, page.pageId, params)
: getApplicationViewerPageURL( : getApplicationViewerPageURL(applicationId, page.pageId, params);
applicationId, if (target === NavigationTargetType.SAME_WINDOW) {
page.pageId,
action.params,
);
history.push(path); history.push(path);
} else if (target === NavigationTargetType.NEW_WINDOW) {
window.open(path, "_blank");
}
if (event.callback) event.callback({ success: true }); if (event.callback) event.callback({ success: true });
} else { } else {
AnalyticsUtil.logEvent("NAVIGATE", { AnalyticsUtil.logEvent("NAVIGATE", {
navUrl: action.pageNameOrUrl, navUrl: pageNameOrUrl,
}); });
// Add a default protocol if it doesn't exist. // Add a default protocol if it doesn't exist.
let url = action.pageNameOrUrl; let url = pageNameOrUrl + convertToQueryParams(params);
if (url.indexOf("://") === -1) { if (url.indexOf("://") === -1) {
url = "https://" + url; url = "https://" + url;
} }
if (target === NavigationTargetType.SAME_WINDOW) {
window.location.assign(url); window.location.assign(url);
} else if (target === NavigationTargetType.NEW_WINDOW) {
window.open(url, "_blank");
}
if (event.callback) event.callback({ success: true });
} }
} }

View File

@ -44,7 +44,7 @@ import {
} from "selectors/editorSelectors"; } from "selectors/editorSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants"; import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { Action } from "entities/Action"; import { Action, ActionViewMode } from "entities/Action";
import { ActionData } from "reducers/entityReducers/actionsReducer"; import { ActionData } from "reducers/entityReducers/actionsReducer";
import { import {
getAction, getAction,
@ -145,14 +145,22 @@ export function* fetchActionsForViewModeSaga(
{ mode: "VIEWER", appId: applicationId }, { mode: "VIEWER", appId: applicationId },
); );
try { try {
const response: GenericApiResponse<Action[]> = yield ActionAPI.fetchActionsForViewMode( const response: GenericApiResponse<ActionViewMode[]> = yield ActionAPI.fetchActionsForViewMode(
applicationId, applicationId,
); );
const correctFormatResponse = response.data.map((action) => {
return {
...action,
actionConfiguration: {
timeoutInMillisecond: action.timeoutInMillisecond,
},
};
});
const isValidResponse = yield validateResponse(response); const isValidResponse = yield validateResponse(response);
if (isValidResponse) { if (isValidResponse) {
yield put({ yield put({
type: ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS, type: ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
payload: response.data, payload: correctFormatResponse,
}); });
PerformanceTracker.stopAsyncTracking( PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_ACTIONS_API, PerformanceTransactionName.FETCH_ACTIONS_API,

View File

@ -390,8 +390,10 @@ function* switchDatasourceSaga(action: ReduxAction<{ datasourceId: string }>) {
(datasource: Datasource) => datasource.id === datasourceId, (datasource: Datasource) => datasource.id === datasourceId,
), ),
); );
if (datasource) {
yield put(changeDatasource(datasource)); yield put(changeDatasource(datasource));
} }
}
function* formValueChangeSaga( function* formValueChangeSaga(
actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>, actionPayload: ReduxActionWithMeta<string, { field: string; form: string }>,

View File

@ -43,6 +43,7 @@ import {
} from "constants/OnboardingConstants"; } from "constants/OnboardingConstants";
import AnalyticsUtil from "../utils/AnalyticsUtil"; import AnalyticsUtil from "../utils/AnalyticsUtil";
import { get } from "lodash"; import { get } from "lodash";
import { updateWidgetProperty } from "../actions/controlActions";
export const getCurrentStep = (state: AppState) => export const getCurrentStep = (state: AppState) =>
state.ui.onBoarding.currentStep; state.ui.onBoarding.currentStep;
@ -85,14 +86,9 @@ function* listenForWidgetAdditions() {
canvasWidgets[selectedWidget.widgetId] canvasWidgets[selectedWidget.widgetId]
) { ) {
if (selectedWidget.tableData === initialTableData) { if (selectedWidget.tableData === initialTableData) {
yield put({ yield put(
type: "UPDATE_WIDGET_PROPERTY", updateWidgetProperty(selectedWidget.widgetId, { tableData: [] }),
payload: { );
widgetId: selectedWidget.widgetId,
propertyName: "tableData",
propertyValue: [],
},
});
} }
AnalyticsUtil.logEvent("ONBOARDING_ADD_WIDGET"); AnalyticsUtil.logEvent("ONBOARDING_ADD_WIDGET");
@ -107,11 +103,11 @@ function* listenForWidgetAdditions() {
} }
} }
function* listenForSuccessfullBinding() { function* listenForSuccessfulBinding() {
while (true) { while (true) {
yield take(); yield take();
let bindSuccessfull = true; let bindSuccessful = true;
const selectedWidget = yield select(getSelectedWidget); const selectedWidget = yield select(getSelectedWidget);
if (selectedWidget && selectedWidget.type === "TABLE_WIDGET") { if (selectedWidget && selectedWidget.type === "TABLE_WIDGET") {
const dataTree = yield select(getDataTree); const dataTree = yield select(getDataTree);
@ -124,19 +120,19 @@ function* listenForSuccessfullBinding() {
const hasBinding = const hasBinding =
dynamicBindingPathList && !!dynamicBindingPathList.length; dynamicBindingPathList && !!dynamicBindingPathList.length;
bindSuccessfull = bindSuccessful =
bindSuccessfull && hasBinding && tableHasData && tableHasData.length; bindSuccessful && hasBinding && tableHasData && tableHasData.length;
if (widgetProperties.invalidProps) { if (widgetProperties.invalidProps) {
bindSuccessfull = bindSuccessful =
bindSuccessfull && bindSuccessful &&
!( !(
"tableData" in widgetProperties.invalidProps && "tableData" in widgetProperties.invalidProps &&
widgetProperties.invalidProps.tableData widgetProperties.invalidProps.tableData
); );
} }
if (bindSuccessfull) { if (bindSuccessful) {
yield put(showTooltip(OnboardingStep.NONE)); yield put(showTooltip(OnboardingStep.NONE));
AnalyticsUtil.logEvent("ONBOARDING_SUCCESSFUL_BINDING"); AnalyticsUtil.logEvent("ONBOARDING_SUCCESSFUL_BINDING");
yield put(setCurrentStep(OnboardingStep.SUCCESSFUL_BINDING)); yield put(setCurrentStep(OnboardingStep.SUCCESSFUL_BINDING));
@ -320,7 +316,7 @@ export default function* onboardingSagas() {
takeEvery(ReduxActionTypes.LISTEN_FOR_ADD_WIDGET, listenForWidgetAdditions), takeEvery(ReduxActionTypes.LISTEN_FOR_ADD_WIDGET, listenForWidgetAdditions),
takeEvery( takeEvery(
ReduxActionTypes.LISTEN_FOR_TABLE_WIDGET_BINDING, ReduxActionTypes.LISTEN_FOR_TABLE_WIDGET_BINDING,
listenForSuccessfullBinding, listenForSuccessfulBinding,
), ),
takeEvery(ReduxActionTypes.SET_CURRENT_STEP, setupOnboardingStep), takeEvery(ReduxActionTypes.SET_CURRENT_STEP, setupOnboardingStep),
takeEvery(ReduxActionTypes.LISTEN_FOR_DEPLOY, listenForDeploySaga), takeEvery(ReduxActionTypes.LISTEN_FOR_DEPLOY, listenForDeploySaga),

View File

@ -16,7 +16,7 @@ import {
fetchPublishedPageSuccess, fetchPublishedPageSuccess,
savePageSuccess, savePageSuccess,
setUrlData, setUrlData,
updateCanvas, initCanvasLayout,
updateCurrentPage, updateCurrentPage,
updateWidgetNameSuccess, updateWidgetNameSuccess,
} from "actions/pageActions"; } from "actions/pageActions";
@ -179,7 +179,7 @@ export function* fetchPageSaga(
// Get Canvas payload // Get Canvas payload
const canvasWidgetsPayload = getCanvasWidgetsPayload(fetchPageResponse); const canvasWidgetsPayload = getCanvasWidgetsPayload(fetchPageResponse);
// Update the canvas // Update the canvas
yield put(updateCanvas(canvasWidgetsPayload)); yield put(initCanvasLayout(canvasWidgetsPayload));
// set current page // set current page
yield put(updateCurrentPage(id)); yield put(updateCurrentPage(id));
// dispatch fetch page success // dispatch fetch page success
@ -242,7 +242,7 @@ export function* fetchPublishedPageSaga(
// Get Canvas payload // Get Canvas payload
const canvasWidgetsPayload = getCanvasWidgetsPayload(response); const canvasWidgetsPayload = getCanvasWidgetsPayload(response);
// Update the canvas // Update the canvas
yield put(updateCanvas(canvasWidgetsPayload)); yield put(initCanvasLayout(canvasWidgetsPayload));
// set current page // set current page
yield put(updateCurrentPage(pageId)); yield put(updateCurrentPage(pageId));
// dispatch fetch page success // dispatch fetch page success
@ -613,7 +613,7 @@ export function* updateCanvasWithDSL(
pageActions: data.layoutOnLoadActions, pageActions: data.layoutOnLoadActions,
widgets: normalizedWidgets.entities.canvasWidgets, widgets: normalizedWidgets.entities.canvasWidgets,
}; };
yield put(updateCanvas(canvasWidgetsPayload)); yield put(initCanvasLayout(canvasWidgetsPayload));
yield put(fetchActionsForPage(pageId)); yield put(fetchActionsForPage(pageId));
} }

View File

@ -1,42 +1,44 @@
import { import {
ReduxActionTypes,
ReduxActionErrorTypes,
ReduxAction, ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants"; } from "constants/ReduxActionConstants";
import { import {
WidgetAddChild,
WidgetResize,
WidgetMove,
WidgetDelete,
updateAndSaveLayout, updateAndSaveLayout,
WidgetAddChild,
WidgetAddChildren, WidgetAddChildren,
WidgetDelete,
WidgetMove,
WidgetResize,
} from "actions/pageActions"; } from "actions/pageActions";
import { import {
FlattenedWidgetProps,
CanvasWidgetsReduxState, CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer"; } from "reducers/entityReducers/canvasWidgetsReducer";
import { import {
getWidgets,
getWidget,
getSelectedWidget, getSelectedWidget,
getWidget,
getWidgetMetaProps, getWidgetMetaProps,
getWidgets,
} from "./selectors"; } from "./selectors";
import { import {
generateWidgetProps, generateWidgetProps,
updateWidgetPosition, updateWidgetPosition,
} from "utils/WidgetPropsUtils"; } from "utils/WidgetPropsUtils";
import { import {
all,
call, call,
put, put,
select, select,
takeEvery, takeEvery,
takeLatest, takeLatest,
all,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { convertToString, getNextEntityName } from "utils/AppsmithUtils"; import { convertToString, getNextEntityName } from "utils/AppsmithUtils";
import { import {
DeleteWidgetPropertyPayload,
SetWidgetDynamicPropertyPayload, SetWidgetDynamicPropertyPayload,
updateWidgetProperty, updateWidgetProperty,
UpdateWidgetPropertyPayload,
UpdateWidgetPropertyRequestPayload, UpdateWidgetPropertyRequestPayload,
} from "actions/controlActions"; } from "actions/controlActions";
import { import {
@ -44,12 +46,13 @@ import {
getEntityDynamicBindingPathList, getEntityDynamicBindingPathList,
getWidgetDynamicPropertyPathList, getWidgetDynamicPropertyPathList,
getWidgetDynamicTriggerPathList, getWidgetDynamicTriggerPathList,
isChildPropertyPath,
isDynamicValue, isDynamicValue,
isPathADynamicBinding, isPathADynamicBinding,
isPathADynamicTrigger, isPathADynamicTrigger,
} from "utils/DynamicBindingUtils"; } from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget"; import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash"; import _, { cloneDeep } from "lodash";
import WidgetFactory from "utils/WidgetFactory"; import WidgetFactory from "utils/WidgetFactory";
import { import {
buildWidgetBlueprint, buildWidgetBlueprint,
@ -58,24 +61,23 @@ import {
import { resetWidgetMetaProperty } from "actions/metaActions"; import { resetWidgetMetaProperty } from "actions/metaActions";
import { import {
GridDefaults, GridDefaults,
WidgetTypes,
MAIN_CONTAINER_WIDGET_ID, MAIN_CONTAINER_WIDGET_ID,
WIDGET_DELETE_UNDO_TIMEOUT,
RenderModes, RenderModes,
WIDGET_DELETE_UNDO_TIMEOUT,
WidgetType, WidgetType,
WidgetTypes,
} from "constants/WidgetConstants"; } from "constants/WidgetConstants";
import WidgetConfigResponse from "mockResponses/WidgetConfigResponse"; import WidgetConfigResponse from "mockResponses/WidgetConfigResponse";
import { import {
flushDeletedWidgets,
getCopiedWidgets,
getDeletedWidgets,
saveCopiedWidgets, saveCopiedWidgets,
saveDeletedWidgets, saveDeletedWidgets,
flushDeletedWidgets,
getDeletedWidgets,
getCopiedWidgets,
} from "utils/storage"; } from "utils/storage";
import { generateReactKey } from "utils/generators"; import { generateReactKey } from "utils/generators";
import { flashElementById } from "utils/helpers"; import { flashElementById } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil"; import AnalyticsUtil from "utils/AnalyticsUtil";
import { cloneDeep } from "lodash";
import log from "loglevel"; import log from "loglevel";
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/WidgetEntity"; import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/WidgetEntity";
import { import {
@ -86,8 +88,8 @@ import { forceOpenPropertyPane } from "actions/widgetActions";
import { getDataTree } from "selectors/dataTreeSelectors"; import { getDataTree } from "selectors/dataTreeSelectors";
import { DataTreeWidget } from "entities/DataTree/dataTreeFactory"; import { DataTreeWidget } from "entities/DataTree/dataTreeFactory";
import { import {
validateProperty,
clearEvalPropertyCacheOfWidget, clearEvalPropertyCacheOfWidget,
validateProperty,
} from "./EvaluationsSaga"; } from "./EvaluationsSaga";
import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer"; import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer";
import { Toaster } from "components/ads/Toast"; import { Toaster } from "components/ads/Toast";
@ -221,7 +223,7 @@ function* generateChildWidgets(
// Add the parentId prop to this widget // Add the parentId prop to this widget
widget.parentId = parent.widgetId; widget.parentId = parent.widgetId;
// Remove the blueprint from the widget (if any) // Remove the blueprint from the widget (if any)
// as blueprints are not useful beyont this point. // as blueprints are not useful beyond this point.
delete widget.blueprint; delete widget.blueprint;
return { widgetId: widget.widgetId, widgets }; return { widgetId: widget.widgetId, widgets };
} }
@ -461,6 +463,7 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets( const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets(
action.payload.widgetId, action.payload.widgetId,
); );
if (deletedWidgets && Array.isArray(deletedWidgets)) {
// Find the parent in the list of deleted widgets // Find the parent in the list of deleted widgets
const deletedWidget = deletedWidgets.find( const deletedWidget = deletedWidgets.find(
(widget) => widget.widgetId === action.payload.widgetId, (widget) => widget.widgetId === action.payload.widgetId,
@ -475,7 +478,6 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
}); });
} }
if (deletedWidgets) {
// Get the current list of widgets from reducer // Get the current list of widgets from reducer
const stateWidgets = yield select(getWidgets); const stateWidgets = yield select(getWidgets);
let widgets = { ...stateWidgets }; let widgets = { ...stateWidgets };
@ -522,7 +524,7 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
} }
let newChildren = [widget.widgetId]; let newChildren = [widget.widgetId];
if (widgets[widget.parentId].children) { if (widgets[widget.parentId].children) {
// Concatenate the list of paren't children with the current widgetId // Concatenate the list of parents children with the current widgetId
newChildren = newChildren.concat(widgets[widget.parentId].children); newChildren = newChildren.concat(widgets[widget.parentId].children);
} }
widgets = { widgets = {
@ -636,79 +638,86 @@ export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
} }
} }
function* updateDynamicTriggers( enum DynamicPathUpdateEffectEnum {
ADD = "ADD",
REMOVE = "REMOVE",
NOOP = "NOOP",
}
type DynamicPathUpdate = {
propertyPath: string;
effect: DynamicPathUpdateEffectEnum;
};
function getDynamicTriggerPathListUpdate(
widget: WidgetProps, widget: WidgetProps,
propertyPath: string, propertyPath: string,
propertyValue: string, propertyValue: string,
) { ): DynamicPathUpdate {
// TODO WIDGETFACTORY
const triggerProperties = WidgetFactory.getWidgetTriggerPropertiesMap(
widget.type,
);
if (propertyPath in triggerProperties) {
let dynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
widget,
);
if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) { if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
dynamicTriggerPathList.push({ return {
key: propertyPath, propertyPath,
}); effect: DynamicPathUpdateEffectEnum.ADD,
};
} else if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
} }
if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) { return {
dynamicTriggerPathList = _.reject(dynamicTriggerPathList, { propertyPath,
key: propertyValue, effect: DynamicPathUpdateEffectEnum.NOOP,
}); };
}
yield put(
updateWidgetProperty(
widget.widgetId,
"dynamicTriggerPathList",
dynamicTriggerPathList,
),
);
return true;
}
return false;
} }
function* updateDynamicBindings( function getDynamicBindingPathListUpdate(
widget: WidgetProps, widget: WidgetProps,
propertyName: string, propertyPath: string,
propertyValue: any, propertyValue: any,
) { ): DynamicPathUpdate {
let stringProp = propertyValue; let stringProp = propertyValue;
if (_.isObject(propertyValue)) { if (_.isObject(propertyValue)) {
// Stringify this because composite controls may have bindings in the sub controls // Stringify this because composite controls may have bindings in the sub controls
stringProp = JSON.stringify(propertyValue); stringProp = JSON.stringify(propertyValue);
} }
const isDynamic = isDynamicValue(stringProp); const isDynamic = isDynamicValue(stringProp);
let dynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList( if (!isDynamic && isPathADynamicBinding(widget, propertyPath)) {
widget, return {
); propertyPath,
if (!isDynamic && isPathADynamicBinding(widget, propertyName)) { effect: DynamicPathUpdateEffectEnum.REMOVE,
dynamicBindingPathList = _.reject(dynamicBindingPathList, { };
key: propertyName, } else if (isDynamic && !isPathADynamicBinding(widget, propertyPath)) {
}); return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
} }
if (isDynamic && !isPathADynamicBinding(widget, propertyName)) { return {
dynamicBindingPathList.push({ propertyPath,
key: propertyName, effect: DynamicPathUpdateEffectEnum.NOOP,
}); };
} }
yield put(
updateWidgetProperty( function applyDynamicPathUpdates(
widget.widgetId, currentList: DynamicPath[],
"dynamicBindingPathList", update: DynamicPathUpdate,
dynamicBindingPathList, ): DynamicPath[] {
), if (update.effect === DynamicPathUpdateEffectEnum.ADD) {
); currentList.push({
key: update.propertyPath,
});
} else if (update.effect === DynamicPathUpdateEffectEnum.REMOVE) {
_.reject(currentList, { key: update.propertyPath });
}
return currentList;
} }
function* updateWidgetPropertySaga( function* updateWidgetPropertySaga(
updateAction: ReduxAction<UpdateWidgetPropertyRequestPayload>, updateAction: ReduxAction<UpdateWidgetPropertyRequestPayload>,
) { ) {
const { const {
payload: { propertyValue, propertyName, widgetId }, payload: { propertyValue, propertyPath, widgetId },
} = updateAction; } = updateAction;
if (!widgetId) { if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here // Handling the case where sometimes widget id is not passed through here
@ -717,55 +726,190 @@ function* updateWidgetPropertySaga(
const stateWidget: WidgetProps = yield select(getWidget, widgetId); const stateWidget: WidgetProps = yield select(getWidget, widgetId);
const widget = { ...stateWidget }; const widget = { ...stateWidget };
const dynamicTriggersUpdated = yield updateDynamicTriggers( // Holder object to collect all updates
const updates: Record<string, unknown> = {
[propertyPath]: propertyValue,
};
// Check if the path is a of a dynamic trigger property
const triggerProperties = WidgetFactory.getWidgetTriggerPropertiesMap(
widget.type,
);
const isTriggerProperty = propertyPath in triggerProperties;
// If it is a trigger property, it will go in a different list than the general
// dynamicBindingPathList.
if (isTriggerProperty) {
const currentDynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
widget, widget,
propertyName, );
const effect = getDynamicTriggerPathListUpdate(
widget,
propertyPath,
propertyValue, propertyValue,
); );
if (!dynamicTriggersUpdated) { updates.dynamicTriggerPathList = applyDynamicPathUpdates(
yield updateDynamicBindings(widget, propertyName, propertyValue); currentDynamicTriggerPathList,
effect,
);
} else {
const currentDynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
widget,
);
const effect = getDynamicBindingPathListUpdate(
widget,
propertyPath,
propertyValue,
);
updates.dynamicBindingPathList = applyDynamicPathUpdates(
currentDynamicBindingPathList,
effect,
);
} }
yield put(updateWidgetProperty(widgetId, propertyName, propertyValue)); // Send the updates
yield put(updateWidgetProperty(widgetId, updates));
const stateWidgets = yield select(getWidgets); const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget }; const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets)); yield put(updateAndSaveLayout(widgets));
} }
function* setWidgetDynamicPropertySaga( function* setWidgetDynamicPropertySaga(
action: ReduxAction<SetWidgetDynamicPropertyPayload>, action: ReduxAction<SetWidgetDynamicPropertyPayload>,
) { ) {
const { isDynamic, propertyName, widgetId } = action.payload; const { isDynamic, propertyPath, widgetId } = action.payload;
const widget: WidgetProps = yield select(getWidget, widgetId); const widget: WidgetProps = yield select(getWidget, widgetId);
// const tree = yield select(evaluateDataTree); const propertyValue = _.get(widget, propertyPath);
const propertyValue = _.get(widget, propertyName);
let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget); let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget);
const propertyUpdates: Record<string, unknown> = {};
if (isDynamic) { if (isDynamic) {
dynamicPropertyPathList.push({ dynamicPropertyPathList.push({
key: propertyName, key: propertyPath,
}); });
const value = convertToString(propertyValue); propertyUpdates[propertyPath] = convertToString(propertyValue);
yield put(updateWidgetProperty(widgetId, propertyName, value));
} else { } else {
dynamicPropertyPathList = _.reject(dynamicPropertyPathList, { dynamicPropertyPathList = _.reject(dynamicPropertyPathList, {
key: propertyName, key: propertyPath,
}); });
const { parsed } = yield call( const { parsed } = yield call(
validateProperty, validateProperty,
widget.type, widget.type,
propertyName, propertyPath,
propertyValue, propertyValue,
widget, widget,
); );
yield put(updateWidgetProperty(widgetId, propertyName, parsed)); propertyUpdates[propertyPath] = parsed;
} }
yield put( propertyUpdates.dynamicPropertyPathList = dynamicPropertyPathList;
updateWidgetProperty(
widgetId, yield put(updateWidgetProperty(widgetId, propertyUpdates));
"dynamicPropertyPathList",
dynamicPropertyPathList, const stateWidgets = yield select(getWidgets);
), const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
function* batchUpdateWidgetPropertySaga(
action: ReduxAction<UpdateWidgetPropertyPayload>,
) {
const { updates, widgetId } = action.payload;
if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here
return;
}
const widget: WidgetProps = yield select(getWidget, widgetId);
const triggerProperties = WidgetFactory.getWidgetTriggerPropertiesMap(
widget.type,
); );
const propertyUpdates: Record<string, unknown> = {};
const currentDynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
widget,
);
const currentDynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
widget,
);
const dynamicTriggerPathListUpdates: DynamicPathUpdate[] = [];
const dynamicBindingPathListUpdates: DynamicPathUpdate[] = [];
Object.entries(updates).forEach(([propertyPath, propertyValue]) => {
// Set the actual property update
propertyUpdates[propertyPath] = propertyValue;
// Check if the path is a of a dynamic trigger property
const isTriggerProperty = propertyPath in triggerProperties;
// If it is a trigger property, it will go in a different list than the general
// dynamicBindingPathList.
if (isTriggerProperty && _.isString(propertyValue)) {
dynamicTriggerPathListUpdates.push(
getDynamicTriggerPathListUpdate(widget, propertyPath, propertyValue),
);
} else {
dynamicBindingPathListUpdates.push(
getDynamicBindingPathListUpdate(widget, propertyPath, propertyValue),
);
}
});
propertyUpdates.dynamicTriggerPathList = dynamicTriggerPathListUpdates.reduce(
applyDynamicPathUpdates,
currentDynamicTriggerPathList,
);
propertyUpdates.dynamicBindingPathList = dynamicBindingPathListUpdates.reduce(
applyDynamicPathUpdates,
currentDynamicBindingPathList,
);
// Send the updates
yield put(updateWidgetProperty(widgetId, updates));
const stateWidgets = yield select(getWidgets);
const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
function* deleteWidgetPropertySaga(
action: ReduxAction<DeleteWidgetPropertyPayload>,
) {
const { widgetId, propertyPath } = action.payload;
if (!widgetId) {
// Handling the case where sometimes widget id is not passed through here
return;
}
const stateWidget: WidgetProps = yield select(getWidget, widgetId);
const dynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
stateWidget,
);
const dynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
stateWidget,
);
dynamicTriggerPathList.filter((dynamicPath) => {
return !isChildPropertyPath(propertyPath, dynamicPath.key);
});
dynamicBindingPathList.forEach((dynamicPath) => {
return !isChildPropertyPath(propertyPath, dynamicPath.key);
});
yield put(
updateWidgetProperty(widgetId, {
dynamicTriggerPathList,
dynamicBindingPathList,
}),
);
const stateWidgets = yield select(getWidgets);
const widget = { ...stateWidget };
_.unset(widget, propertyPath);
const widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
} }
function* getWidgetChildren(widgetId: string): any { function* getWidgetChildren(widgetId: string): any {
@ -774,6 +918,7 @@ function* getWidgetChildren(widgetId: string): any {
const { children } = widget; const { children } = widget;
if (children && children.length) { if (children && children.length) {
for (const childIndex in children) { for (const childIndex in children) {
if (children.hasOwnProperty(childIndex)) {
const child = children[childIndex]; const child = children[childIndex];
childrenIds.push(child); childrenIds.push(child);
const grandChildren = yield call(getWidgetChildren, child); const grandChildren = yield call(getWidgetChildren, child);
@ -782,6 +927,7 @@ function* getWidgetChildren(widgetId: string): any {
} }
} }
} }
}
return childrenIds; return childrenIds;
} }
@ -803,6 +949,10 @@ function* resetEvaluatedWidgetMetaProperties(widgetIds: string[]) {
for (const index in widgetIds) { for (const index in widgetIds) {
const widgetId = widgetIds[index]; const widgetId = widgetIds[index];
const widget = _.find(evaluatedDataTree, { widgetId }) as DataTreeWidget; const widget = _.find(evaluatedDataTree, { widgetId }) as DataTreeWidget;
// the widget was not found in the data tree, so don't do anything
if (!widget) continue;
const widgetToUpdate = { ...widget }; const widgetToUpdate = { ...widget };
const metaPropsMap = WidgetFactory.getWidgetMetaPropertiesMap(widget.type); const metaPropsMap = WidgetFactory.getWidgetMetaPropertiesMap(widget.type);
const defaultPropertiesMap = WidgetFactory.getWidgetDefaultPropertiesMap( const defaultPropertiesMap = WidgetFactory.getWidgetDefaultPropertiesMap(
@ -843,7 +993,9 @@ function* updateCanvasSize(
// TODO(abhinav): This considers that the topRow will always be zero // TODO(abhinav): This considers that the topRow will always be zero
// Check this out when non canvas widgets are updating snapRows // Check this out when non canvas widgets are updating snapRows
// erstwhile: Math.round((rows * props.snapRowSpace) / props.parentRowSpace), // erstwhile: Math.round((rows * props.snapRowSpace) / props.parentRowSpace),
yield put(updateWidgetProperty(canvasWidgetId, "bottomRow", newBottomRow)); yield put(
updateWidgetProperty(canvasWidgetId, { bottomRow: newBottomRow }),
);
} }
} }
@ -852,11 +1004,9 @@ function* createWidgetCopy() {
if (!selectedWidget) return; if (!selectedWidget) return;
const widgets = yield select(getWidgets); const widgets = yield select(getWidgets);
const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets); const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets);
const saveResult = yield saveCopiedWidgets( return yield saveCopiedWidgets(
JSON.stringify({ widgetId: selectedWidget.widgetId, list: widgetsToStore }), JSON.stringify({ widgetId: selectedWidget.widgetId, list: widgetsToStore }),
); );
return saveResult;
} }
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) { function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
@ -1232,7 +1382,7 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
// The following is computed to be used in the entity explorer // The following is computed to be used in the entity explorer
// Every time a widget is selected, we need to expand widget entities // Every time a widget is selected, we need to expand widget entities
// in the entity explorer so that the selected widget is visible // in the entity explorer so that the selected widget is visible
function* selectedWidgetAncestorySaga( function* selectedWidgetAncestrySaga(
action: ReduxAction<{ widgetId: string }>, action: ReduxAction<{ widgetId: string }>,
) { ) {
try { try {
@ -1259,7 +1409,7 @@ function* selectedWidgetAncestorySaga(
payload: widgetIdsExpandList, payload: widgetIdsExpandList,
}); });
} catch (error) { } catch (error) {
log.debug("Could not compute selected widget's ancestory", error); log.debug("Could not compute selected widget's ancestry", error);
} }
} }
@ -1285,12 +1435,20 @@ export default function* widgetOperationSagas() {
ReduxActionTypes.RESET_CHILDREN_WIDGET_META, ReduxActionTypes.RESET_CHILDREN_WIDGET_META,
resetChildrenMetaSaga, resetChildrenMetaSaga,
), ),
takeEvery(
ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
batchUpdateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.DELETE_WIDGET_PROPERTY,
deleteWidgetPropertySaga,
),
takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize), takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize),
takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga), takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga),
takeEvery(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga), takeEvery(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga),
takeEvery(ReduxActionTypes.UNDO_DELETE_WIDGET, undoDeleteSaga), takeEvery(ReduxActionTypes.UNDO_DELETE_WIDGET, undoDeleteSaga),
takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga), takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga),
takeEvery(ReduxActionTypes.WIDGET_ADD_CHILDREN, addChildrenSaga), takeEvery(ReduxActionTypes.WIDGET_ADD_CHILDREN, addChildrenSaga),
takeLatest(ReduxActionTypes.SELECT_WIDGET, selectedWidgetAncestorySaga), takeLatest(ReduxActionTypes.SELECT_WIDGET, selectedWidgetAncestrySaga),
]); ]);
} }

View File

@ -1,6 +1,33 @@
import { AppState } from "reducers"; import { AppState } from "reducers";
import {
AUTH_LOGIN_URL,
SIGN_UP_URL,
RESET_PASSWORD_URL,
FORGOT_PASSWORD_URL,
} from "constants/routes";
import { theme, dark } from "constants/DefaultTheme";
const enforceDarkThemeRoutes = [
AUTH_LOGIN_URL,
SIGN_UP_URL,
RESET_PASSWORD_URL,
FORGOT_PASSWORD_URL,
];
const getShouldEnforceDarkTheme = () => {
const currentPath = window.location.pathname;
return enforceDarkThemeRoutes.some(
(path: string) => currentPath.indexOf(path) !== -1,
);
};
export const getThemeDetails = (state: AppState) => { export const getThemeDetails = (state: AppState) => {
if (getShouldEnforceDarkTheme()) {
return {
mode: state.ui.theme.mode,
theme: { ...theme, colors: { ...theme.colors, ...dark } },
};
}
return { return {
theme: state.ui.theme.theme, theme: state.ui.theme.theme,
mode: state.ui.theme.mode, mode: state.ui.theme.mode,

View File

@ -249,3 +249,12 @@ export const unsafeFunctionForEval = [
"setInterval", "setInterval",
"Promise", "Promise",
]; ];
export const isChildPropertyPath = (
parentPropertyPath: string,
childPropertyPath: string,
): boolean => {
const regexTest = new RegExp(
`^${parentPropertyPath.replace(".", "\\.")}(\\.\\S+)?$`,
);
return regexTest.test(childPropertyPath);
};

View File

@ -1,4 +1,7 @@
import { getDynamicStringSegments } from "./DynamicBindingUtils"; import {
getDynamicStringSegments,
isChildPropertyPath,
} from "./DynamicBindingUtils";
describe.each([ describe.each([
["{{A}}", ["{{A}}"]], ["{{A}}", ["{{A}}"]],
@ -30,3 +33,20 @@ describe.each([
); );
}); });
}); });
describe("isChildPropertyPath function", () => {
it("works", () => {
const cases: Array<[string, string, boolean]> = [
["Table1.selectedRow", "Table1.selectedRow", true],
["Table1.selectedRow", "Table1.selectedRows", false],
["Table1.selectedRow", "Table1.selectedRow.email", true],
["Table1.selectedRow", "1Table1.selectedRow", false],
["Table1.selectedRow", "Table11selectedRow", false],
["Table1.selectedRow", "Table1.selectedRow", true],
];
cases.forEach((testCase) => {
const result = isChildPropertyPath(testCase[0], testCase[1]);
expect(result).toBe(testCase[2]);
});
});
});

View File

@ -231,7 +231,7 @@ export const GLOBAL_DEFS = {
export const GLOBAL_FUNCTIONS = { export const GLOBAL_FUNCTIONS = {
navigateTo: { navigateTo: {
"!doc": "Action to navigate the user to another page or url", "!doc": "Action to navigate the user to another page or url",
"!type": "fn(pageNameOrUrl: string, params: {}) -> void", "!type": "fn(pageNameOrUrl: string, params: {}, target?: string) -> void",
}, },
showAlert: { showAlert: {
"!doc": "Show a temporary notification style message to the user", "!doc": "Show a temporary notification style message to the user",

View File

@ -44,6 +44,7 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
static getTriggerPropertyMap(): TriggerPropertiesMap { static getTriggerPropertyMap(): TriggerPropertiesMap {
return { return {
onTextChanged: true, onTextChanged: true,
onSubmit: true,
}; };
} }
@ -136,6 +137,22 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
this.props.updateWidgetMetaProperty("isFocused", focusState); this.props.updateWidgetMetaProperty("isFocused", focusState);
}; };
handleKeyDown = (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => {
const isEnterKey = e.key === "Enter" || e.keyCode === 13;
if (isEnterKey && this.props.onSubmit) {
super.executeAction({
dynamicString: this.props.onSubmit,
event: {
type: EventType.ON_SUBMIT,
},
});
}
};
getPageView() { getPageView() {
const value = this.props.text || ""; const value = this.props.text || "";
const isInvalid = const isInvalid =
@ -149,6 +166,7 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
if (this.props.maxChars) conditionalProps.maxChars = this.props.maxChars; if (this.props.maxChars) conditionalProps.maxChars = this.props.maxChars;
if (this.props.maxNum) conditionalProps.maxNum = this.props.maxNum; if (this.props.maxNum) conditionalProps.maxNum = this.props.maxNum;
if (this.props.minNum) conditionalProps.minNum = this.props.minNum; if (this.props.minNum) conditionalProps.minNum = this.props.minNum;
return ( return (
<InputComponent <InputComponent
value={value} value={value}
@ -168,6 +186,8 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
stepSize={1} stepSize={1}
onFocusChange={this.handleFocusChange} onFocusChange={this.handleFocusChange}
showError={!!this.props.isFocused} showError={!!this.props.isFocused}
disableNewLineOnPressEnterKey={!!this.props.onSubmit}
onKeyDown={this.handleKeyDown}
{...conditionalProps} {...conditionalProps}
/> />
); );
@ -215,6 +235,7 @@ export interface InputWidgetProps extends WidgetProps, WithMeta {
isRequired?: boolean; isRequired?: boolean;
isFocused?: boolean; isFocused?: boolean;
isDirty?: boolean; isDirty?: boolean;
onSubmit?: string;
} }
export default InputWidget; export default InputWidget;

View File

@ -132,6 +132,16 @@ class MapWidget extends BaseWidget<MapWidgetProps, WidgetState> {
return this.props.center || this.props.mapCenter || DefaultCenter; return this.props.center || this.props.mapCenter || DefaultCenter;
} }
componentDidUpdate(prevProps: MapWidgetProps) {
//remove selectedMarker when map initial location is updated
if (
JSON.stringify(prevProps.center) !== JSON.stringify(this.props.center) &&
this.props.selectedMarker
) {
this.unselectMarker();
}
}
getPageView() { getPageView() {
return ( return (
<> <>

View File

@ -90,13 +90,13 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
searchText: VALIDATION_TYPES.TEXT, searchText: VALIDATION_TYPES.TEXT,
defaultSearchText: VALIDATION_TYPES.TEXT, defaultSearchText: VALIDATION_TYPES.TEXT,
defaultSelectedRow: VALIDATION_TYPES.DEFAULT_SELECTED_ROW, defaultSelectedRow: VALIDATION_TYPES.DEFAULT_SELECTED_ROW,
pageSize: VALIDATION_TYPES.NUMBER,
}; };
} }
static getMetaPropertiesMap(): Record<string, any> { static getMetaPropertiesMap(): Record<string, any> {
return { return {
pageNo: 1, pageNo: 1,
pageSize: undefined,
selectedRowIndex: undefined, selectedRowIndex: undefined,
selectedRowIndices: undefined, selectedRowIndices: undefined,
searchText: undefined, searchText: undefined,
@ -120,10 +120,54 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
onRowSelected: true, onRowSelected: true,
onPageChange: true, onPageChange: true,
onSearchTextChanged: true, onSearchTextChanged: true,
onPageSizeChange: true,
columnActions: true, columnActions: true,
}; };
} }
static getDerivedPropertiesMap() {
return {
pageSize: `{{function() {
const TABLE_SIZES = {
${CompactModeTypes.DEFAULT}: {
COLUMN_HEADER_HEIGHT: 38,
TABLE_HEADER_HEIGHT: 42,
ROW_HEIGHT: 40,
ROW_FONT_SIZE: 14,
},
${CompactModeTypes.SHORT}: {
COLUMN_HEADER_HEIGHT: 38,
TABLE_HEADER_HEIGHT: 42,
ROW_HEIGHT: 20,
ROW_FONT_SIZE: 12,
},
${CompactModeTypes.TALL}: {
COLUMN_HEADER_HEIGHT: 38,
TABLE_HEADER_HEIGHT: 42,
ROW_HEIGHT: 60,
ROW_FONT_SIZE: 18,
},
};
const compactMode = this.compactMode || "${CompactModeTypes.DEFAULT}";
const componentHeight = (this.bottomRow - this.topRow) * this.parentRowSpace;
const tableSizes = TABLE_SIZES[compactMode];
let pageSize= Math.floor((componentHeight - tableSizes.TABLE_HEADER_HEIGHT - tableSizes.COLUMN_HEADER_HEIGHT) / tableSizes.ROW_HEIGHT);
if (
componentHeight -
(tableSizes.TABLE_HEADER_HEIGHT +
tableSizes.COLUMN_HEADER_HEIGHT +
tableSizes.ROW_HEIGHT * pageSize) >
0
) {
pageSize += 1;
}
return pageSize;
}()
}}`,
triggerRowSelection: "{{!!this.onRowSelected}}",
};
}
getTableColumns = (tableData: Array<Record<string, unknown>>) => { getTableColumns = (tableData: Array<Record<string, unknown>>) => {
let columns: ReactTableColumnProps[] = []; let columns: ReactTableColumnProps[] = [];
const hiddenColumns: ReactTableColumnProps[] = []; const hiddenColumns: ReactTableColumnProps[] = [];
@ -135,6 +179,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
} = this.props; } = this.props;
if (tableData.length) { if (tableData.length) {
const columnKeys: string[] = getAllTableColumnKeys(tableData); const columnKeys: string[] = getAllTableColumnKeys(tableData);
const { componentWidth } = this.getComponentDimensions();
const sortedColumn = this.props.sortedColumn; const sortedColumn = this.props.sortedColumn;
for (let index = 0; index < columnKeys.length; index++) { for (let index = 0; index < columnKeys.length; index++) {
const i = columnKeys[index]; const i = columnKeys[index];
@ -170,7 +215,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
inputFormat: columnType.inputFormat, inputFormat: columnType.inputFormat,
}, },
Cell: (props: any) => { Cell: (props: any) => {
return renderCell(props.cell.value, columnType.type, isHidden); return renderCell(
props.cell.value,
columnType.type,
isHidden,
componentWidth,
);
}, },
}; };
if (isHidden) { if (isHidden) {
@ -361,7 +411,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
const columnKeys: string[] = getAllTableColumnKeys(this.props.tableData); const columnKeys: string[] = getAllTableColumnKeys(this.props.tableData);
const selectedRow: { [key: string]: any } = {}; const selectedRow: { [key: string]: any } = {};
for (let i = 0; i < columnKeys.length; i++) { for (let i = 0; i < columnKeys.length; i++) {
selectedRow[columnKeys[i]] = undefined; selectedRow[columnKeys[i]] = "";
} }
return selectedRow; return selectedRow;
} }
@ -493,6 +543,14 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
); );
} }
} }
if (this.props.pageSize !== prevProps.pageSize) {
super.executeAction({
dynamicString: this.props.onPageSizeChange,
event: {
type: EventType.ON_PAGE_SIZE_CHANGE,
},
});
}
} }
getSelectedRowIndexes = (selectedRowIndices: string) => { getSelectedRowIndexes = (selectedRowIndices: string) => {
@ -502,7 +560,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
}; };
getPageView() { getPageView() {
const { tableData, hiddenColumns, filteredTableData } = this.props; const {
tableData,
hiddenColumns,
filteredTableData,
pageSize,
} = this.props;
const computedSelectedRowIndices = Array.isArray( const computedSelectedRowIndices = Array.isArray(
this.props.selectedRowIndices, this.props.selectedRowIndices,
) )
@ -524,28 +587,6 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
this.props.updateWidgetMetaProperty("pageNo", pageNo); this.props.updateWidgetMetaProperty("pageNo", pageNo);
} }
const { componentWidth, componentHeight } = this.getComponentDimensions(); const { componentWidth, componentHeight } = this.getComponentDimensions();
const tableSizes =
TABLE_SIZES[this.props.compactMode || CompactModeTypes.DEFAULT];
let pageSize = Math.floor(
(componentHeight -
tableSizes.TABLE_HEADER_HEIGHT -
tableSizes.COLUMN_HEADER_HEIGHT) /
tableSizes.ROW_HEIGHT,
);
if (
componentHeight -
(tableSizes.TABLE_HEADER_HEIGHT +
tableSizes.COLUMN_HEADER_HEIGHT +
tableSizes.ROW_HEIGHT * pageSize) >
0
)
pageSize += 1;
if (pageSize !== this.props.pageSize) {
this.props.updateWidgetMetaProperty("pageSize", pageSize);
}
return ( return (
<Suspense fallback={<Skeleton />}> <Suspense fallback={<Skeleton />}>
<ReactTableComponent <ReactTableComponent
@ -563,6 +604,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
columnNameMap={this.props.columnNameMap} columnNameMap={this.props.columnNameMap}
columnTypeMap={this.props.columnTypeMap} columnTypeMap={this.props.columnTypeMap}
columnOrder={this.props.columnOrder} columnOrder={this.props.columnOrder}
triggerRowSelection={this.props.triggerRowSelection}
pageSize={Math.max(1, pageSize)} pageSize={Math.max(1, pageSize)}
onCommandClick={this.onCommandClick} onCommandClick={this.onCommandClick}
selectedRowIndex={ selectedRowIndex={
@ -590,7 +632,14 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
super.updateWidgetProperty("columnNameMap", columnNameMap); super.updateWidgetProperty("columnNameMap", columnNameMap);
}} }}
handleResizeColumn={(columnSizeMap: { [key: string]: number }) => { handleResizeColumn={(columnSizeMap: { [key: string]: number }) => {
if (this.props.renderMode === RenderModes.CANVAS) {
super.updateWidgetProperty("columnSizeMap", columnSizeMap); super.updateWidgetProperty("columnSizeMap", columnSizeMap);
} else {
this.props.updateWidgetMetaProperty(
"columnSizeMap",
columnSizeMap,
);
}
}} }}
handleReorderColumn={(columnOrder: string[]) => { handleReorderColumn={(columnOrder: string[]) => {
super.updateWidgetProperty("columnOrder", columnOrder); super.updateWidgetProperty("columnOrder", columnOrder);
@ -607,11 +656,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
}} }}
compactMode={this.props.compactMode || CompactModeTypes.DEFAULT} compactMode={this.props.compactMode || CompactModeTypes.DEFAULT}
updateCompactMode={(compactMode: CompactMode) => { updateCompactMode={(compactMode: CompactMode) => {
if (this.props.renderMode === RenderModes.CANVAS) {
this.props.updateWidgetMetaProperty("compactMode", compactMode); this.props.updateWidgetMetaProperty("compactMode", compactMode);
} else {
this.props.updateWidgetMetaProperty("compactMode", compactMode);
}
}} }}
sortTableColumn={this.handleColumnSorting} sortTableColumn={this.handleColumnSorting}
/> />

View File

@ -15,6 +15,7 @@ import {
extraLibraries, extraLibraries,
getDynamicBindings, getDynamicBindings,
getEntityDynamicBindingPathList, getEntityDynamicBindingPathList,
isChildPropertyPath,
isPathADynamicBinding, isPathADynamicBinding,
isPathADynamicTrigger, isPathADynamicTrigger,
unsafeFunctionForEval, unsafeFunctionForEval,
@ -33,7 +34,6 @@ import {
CrashingError, CrashingError,
DataTreeDiffEvent, DataTreeDiffEvent,
getValidatedTree, getValidatedTree,
isChildPropertyPath,
makeParentsDependOnChildren, makeParentsDependOnChildren,
removeFunctions, removeFunctions,
removeFunctionsFromDataTree, removeFunctionsFromDataTree,
@ -148,9 +148,10 @@ ctx.addEventListener(
true, true,
callbackData, callbackData,
); );
const cleanTriggers = removeFunctions(triggers);
const errors = dataTreeEvaluator.errors; const errors = dataTreeEvaluator.errors;
dataTreeEvaluator.clearErrors(); dataTreeEvaluator.clearErrors();
return { triggers, errors }; return { triggers: cleanTriggers, errors };
} }
case EVAL_WORKER_ACTIONS.CLEAR_CACHE: { case EVAL_WORKER_ACTIONS.CLEAR_CACHE: {
dataTreeEvaluator = undefined; dataTreeEvaluator = undefined;
@ -180,12 +181,14 @@ ctx.addEventListener(
value, value,
props, props,
} = requestData; } = requestData;
return validateWidgetProperty( return removeFunctions(
validateWidgetProperty(
widgetTypeConfigMap, widgetTypeConfigMap,
widgetType, widgetType,
property, property,
value, value,
props, props,
),
); );
} }
default: { default: {
@ -1373,10 +1376,11 @@ const addFunctions = (dataTree: Readonly<DataTree>): DataTree => {
withFunction.navigateTo = function( withFunction.navigateTo = function(
pageNameOrUrl: string, pageNameOrUrl: string,
params: Record<string, string>, params: Record<string, string>,
target?: string,
) { ) {
return { return {
type: "NAVIGATE_TO", type: "NAVIGATE_TO",
payload: { pageNameOrUrl, params }, payload: { pageNameOrUrl, params, target },
}; };
}; };
withFunction.actionPaths.push("navigateTo"); withFunction.actionPaths.push("navigateTo");

View File

@ -1,17 +0,0 @@
import { isChildPropertyPath } from "./evaluationUtils";
describe("isChildPropertyPath function", () => {
it("works", () => {
const cases: Array<[string, string, boolean]> = [
["Table1.selectedRow", "Table1.selectedRows", false],
["Table1.selectedRow", "Table1.selectedRow.email", true],
["Table1.selectedRow", "1Table1.selectedRow", false],
["Table1.selectedRow", "Table11selectedRow", false],
["Table1.selectedRow", "Table1.selectedRow", true],
];
cases.forEach((testCase) => {
const result = isChildPropertyPath(testCase[0], testCase[1]);
expect(result).toBe(testCase[2]);
});
});
});

Some files were not shown because too many files have changed in this diff Show More