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. 🙌
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
@ -10,7 +10,7 @@ Read our [Code of Conduct](CODE_OF_CONDUCT.md) before contributing
## 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 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)!
#### ⚙️ 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

@ -1,94 +1,77 @@
{
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1280,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"dynamicBindingPathList": [],
"version": 6,
"minHeight": 1292,
"parentColumnSpace": 1,
"dsl": {
"widgetName": "MainContainer",
"backgroundColor": "none",
"rightColumn": 1224,
"snapColumns": 16,
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 1280,
"containerStyle": "none",
"snapRows": 33,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 8,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"isVisible": true,
"inputType": "TEXT",
"label": "",
"widgetName": "Input1",
"type": "INPUT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 6,
"rightColumn": 11,
"topRow": 13,
"bottomRow": 14,
"parentId": "0",
"widgetId": "1bek8n8byg"
},
{
"isVisible": true,
"label": "",
"selectionType": "SINGLE_SELECT",
"options": "",
"widgetName": "Dropdown1",
"defaultOptionValue": "VEG",
"type": "DROP_DOWN_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 0,
"children": [
{
"isVisible": true,
"inputType": "TEXT",
"label": "",
"widgetName": "Input1",
"type": "INPUT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 4,
"rightColumn": 9,
"topRow": 0,
"bottomRow": 1,
"parentId": "0",
"widgetId": "orkla1pg88"
},
{
"isVisible": true,
"label": "",
"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]",
"widgetName": "Dropdown1",
"defaultOptionValue": "VEG",
"type": "DROP_DOWN_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 4,
"rightColumn": 9,
"topRow": 2,
"bottomRow": 3,
"parentId": "0",
"widgetId": "9iofg44qjm",
"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,
"label": "Data",
"widgetName": "Table1",
"searchKey": "",
"tableData": "[\n {\n \"id\": 2381224,\n \"email\": \"michael.lawson@reqres.in\",\n \"userName\": \"Michael Lawson\",\n \"productName\": \"Chicken Sandwich\",\n \"orderAmount\": 4.99\n },\n {\n \"id\": 2736212,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"userName\": \"Lindsay Ferguson\",\n \"productName\": \"Tuna Salad\",\n \"orderAmount\": 9.99\n },\n {\n \"id\": 6788734,\n \"email\": \"tobias.funke@reqres.in\",\n \"userName\": \"Tobias Funke\",\n \"productName\": \"Beef steak\",\n \"orderAmount\": 19.99\n }\n]",
"type": "TABLE_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 3,
"rightColumn": 11,
"topRow": 12,
"bottomRow": 19,
"parentId": "0",
"widgetId": "iu9vqkj1rd",
"dynamicBindingPathList": []
}
]
}
}
"rightColumn": 5,
"topRow": 15,
"bottomRow": 16,
"parentId": "0",
"widgetId": "zd6jycngj7",
"dynamicBindingPathList": []
},
{
"isVisible": true,
"label": "Data",
"widgetName": "Table1",
"searchKey": "",
"tableData": "[\n {\n \"id\": 2381224,\n \"email\": \"michael.lawson@reqres.in\",\n \"userName\": \"Michael Lawson\",\n \"productName\": \"Chicken Sandwich\",\n \"orderAmount\": 4.99\n },\n {\n \"id\": 2736212,\n \"email\": \"lindsay.ferguson@reqres.in\",\n \"userName\": \"Lindsay Ferguson\",\n \"productName\": \"Tuna Salad\",\n \"orderAmount\": 9.99\n },\n {\n \"id\": 6788734,\n \"email\": \"tobias.funke@reqres.in\",\n \"userName\": \"Tobias Funke\",\n \"productName\": \"Beef steak\",\n \"orderAmount\": 19.99\n }\n]",
"type": "TABLE_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 4,
"rightColumn": 12,
"topRow": 19,
"bottomRow": 26,
"parentId": "0",
"widgetId": "ei38nqop3s",
"dynamicBindingPathList": []
}
]
}
}

View File

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

View File

@ -51,7 +51,7 @@
"value": "VEG"
},
{
"label": "{{Table1.tableData[1].email}}",
"label": "{{Table1.tableData[2].email}}",
"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() {
cy.openPropertyPane("inputwidget");
cy.get(widgetsPage.defaultInput).type(testdata.defaultInputWidget);
cy.get(widgetsPage.actionSelect).click();
cy.get(widgetsPage.actionSelect)
.first()
.click();
cy.get(commonlocators.chooseAction)
.children()
.contains("Navigate To")

View File

@ -1,88 +1,88 @@
// const commonlocators = require("../../../locators/commonlocators.json");
// const formWidgetsPage = require("../../../locators/FormWidgets.json");
// const dsl = require("../../../fixtures/rundsl.json");
// const pages = require("../../../locators/Pages.json");
// const widgetsPage = require("../../../locators/Widgets.json");
// const publish = require("../../../locators/publishWidgetspage.json");
// const queryLocators = require("../../../locators/QueryEditor.json");
// const datasource = require("../../../locators/DatasourcesEditor.json");
// const apiwidget = require("../../../locators/apiWidgetslocator.json");
// const testdata = require("../../../fixtures/testdata.json");
const commonlocators = require("../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../locators/FormWidgets.json");
const dsl = require("../../../fixtures/rundsl.json");
const pages = require("../../../locators/Pages.json");
const widgetsPage = require("../../../locators/Widgets.json");
const publish = require("../../../locators/publishWidgetspage.json");
const queryLocators = require("../../../locators/QueryEditor.json");
const datasource = require("../../../locators/DatasourcesEditor.json");
const apiwidget = require("../../../locators/apiWidgetslocator.json");
const testdata = require("../../../fixtures/testdata.json");
// const pageid = "MyPage";
// let updatedName;
// let datasourceName;
const pageid = "MyPage";
let updatedName;
let datasourceName;
// describe("Binding the multiple widgets and validating default data", function() {
// before(() => {
// cy.addDsl(dsl);
// });
// it("Create a postgres datasource", function() {
// cy.NavigateToDatasourceEditor();
// cy.get(datasource.PostgreSQL).click();
describe("Binding the multiple widgets and validating default data", function() {
before(() => {
cy.addDsl(dsl);
});
it("Create a postgres datasource", function() {
cy.NavigateToDatasourceEditor();
cy.get(datasource.PostgreSQL).click();
// cy.getPluginFormsAndCreateDatasource();
cy.getPluginFormsAndCreateDatasource();
// cy.fillPostgresDatasourceForm();
cy.fillPostgresDatasourceForm();
// cy.testSaveDatasource();
cy.testSaveDatasource();
// cy.get("@createDatasource").then(httpResponse => {
// datasourceName = httpResponse.response.body.data.name;
// });
// });
// it("Create and runs query", () => {
// cy.NavigateToQueryEditor();
// cy.contains(".t--datasource-name", datasourceName)
// .find(queryLocators.createQuery)
// .click();
cy.get("@createDatasource").then((httpResponse) => {
datasourceName = httpResponse.response.body.data.name;
});
});
it("Create and runs query", () => {
cy.NavigateToQueryEditor();
cy.contains(".t--datasource-name", datasourceName)
.find(queryLocators.createQuery)
.click();
// cy.get(queryLocators.templateMenu).click();
// cy.get(".CodeMirror textarea")
// .first()
// .focus()
// .type("select * from users limit 10");
cy.get(queryLocators.templateMenu).click();
cy.get(".CodeMirror textarea")
.first()
.focus()
.type("select * from users limit 10");
// cy.EvaluateCurrentValue("select * from users limit 10");
// cy.runQuery();
// });
cy.EvaluateCurrentValue("select * from users limit 10");
cy.runQuery();
});
// it("Button widget test with on action query run", function() {
// cy.SearchEntityandOpen("Button1");
// cy.executeDbQuery("Query1");
// cy.get(commonlocators.editPropCrossButton).click();
// cy.wait("@updateLayout").should(
// "have.nested.property",
// "response.body.responseMeta.status",
// 200,
// );
// });
it("Button widget test with on action query run", function() {
cy.SearchEntityandOpen("Button1");
cy.executeDbQuery("Query1");
cy.get(commonlocators.editPropCrossButton).click();
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
});
// it("Input widget test with default value update with query data", function() {
// cy.SearchEntityandOpen("Input1");
// cy.get(widgetsPage.defaultInput).type(testdata.defaultInputQuery);
// cy.get(commonlocators.editPropCrossButton).click();
// cy.wait("@updateLayout").should(
// "have.nested.property",
// "response.body.responseMeta.status",
// 200,
// );
// });
it("Input widget test with default value update with query data", function() {
cy.SearchEntityandOpen("Input1");
cy.get(widgetsPage.defaultInput).type(testdata.defaultInputQuery);
cy.get(commonlocators.editPropCrossButton).click();
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
});
// it("Publish App and validate loading functionalty", function() {
// cy.PublishtheApp();
// cy.wait(2000);
// cy.get(widgetsPage.widgetBtn)
// .first()
// .click({ force: true });
// cy.wait("@postExecute").should(
// "have.nested.property",
// "response.body.responseMeta.status",
// 200,
// );
// cy.get(publish.inputWidget + " " + "input")
// .first()
// .invoke("attr", "value")
// .should("contain", "7");
// });
// });
it("Publish App and validate loading functionalty", function() {
cy.PublishtheApp();
cy.wait(2000);
cy.get(widgetsPage.widgetBtn)
.first()
.click({ force: true });
cy.wait("@postExecute").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.get(publish.inputWidget + " " + "input")
.first()
.invoke("attr", "value")
.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() {
cy.openPropertyPane("dropdownwidget");
cy.testJsontext("options", JSON.stringify(testdata.deafultDropDownWidget));
@ -34,11 +33,10 @@ describe("Binding the multiple widgets and validating default data", function()
200,
);
});
*/
it("validation of default data displayed in all widgets based on row selected", function() {
cy.isSelectRow(1);
cy.readTabledataPublish("1", "0").then(tabData => {
cy.readTabledataPublish("1", "0").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("2736212");
cy.log("the value is" + tabValue);
@ -48,18 +46,17 @@ describe("Binding the multiple widgets and validating default data", function()
.invoke("attr", "value")
.should("contain", tabValue);
});
/*
cy.readTabledataPublish("1", "1").then(tabData => {
cy.readTabledataPublish("1", "1").then((tabData) => {
const tabValue = tabData;
expect(tabValue).to.be.equal("lindsay.ferguson@reqres.in");
cy.log("the value is" + tabValue);
cy.get(widgetsPage.defaultSingleSelectValue)
.invoke("text")
.then(text => {
.then((text) => {
const someText = text;
expect(someText).to.equal(tabValue);
});
});
*/
});
});

View File

@ -6,30 +6,37 @@ describe("FilePicker Widget Functionality", function() {
beforeEach(() => {
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() {
cy.openPropertyPane("filepickerwidget");
cy.SearchEntityandOpen("FilePicker1");
cy.wait(1000);
//Checking the edit props for FilePicker and also the properties of FilePicker widget
cy.testCodeMirror("Upload Files");
cy.get(commonlocators.editPropCrossButton).click();
});
it("It checks the loading state of filepicker on call the action", function() {
cy.openPropertyPane("filepickerwidget");
cy.SearchEntityandOpen("FilePicker1");
const fixturePath = "testFile.mov";
cy.getAlert(commonlocators.filePickerOnFilesSelected);
cy.addAPIFromLightningMenu("FirstAPI");
cy.get(commonlocators.filePickerButton).click();
cy.get(commonlocators.filePickerInput)
.first()
.attachFile(fixturePath);
cy.get(commonlocators.filePickerUploadButton).click();
cy.get(".bp3-spinner").should("have.length", 1);
cy.wait("@updateLayout").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.wait(500);
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']",
"password": "input[name='datasourceConfiguration.authentication.password']",
"authenticationAuthtype": "[data-cy=datasourceConfiguration\\.authentication\\.authType]",
"sslAuthtype": "[data-cy=datasourceConfiguration\\.connection\\.ssl\\.authType]",
"url": "input[name='datasourceConfiguration.url']",
"MongoDB": ".t--plugin-name:contains('MongoDB')",
"RESTAPI": ".t--plugin-name:contains('REST API')",
"PostgreSQL": ".t--plugin-name:contains('PostgreSQL')",
"sectionAuthentication": "[data-cy=section-Authentication]",
"sectionSSL": "[data-cy=section-SSL\\ \\(optional\\)]",
"PostgresEntity": ".t--entity-name:contains(PostgreSQL)",
"createQuerty": ".t--create-query",
"editDatasource": ".t--edit-datasource",

View File

@ -67,5 +67,7 @@
"uploadLogo": "//div/form/input",
"removeLogo": ".remove-button a span",
"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");
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
// 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"
});
it("Deleting the duplicated Application ", function() {
}
)
it("Deleting the duplicated Application ", function()
{
// Navigate to home Page
// Click on any application action icon (Three dots)
// 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 "Are You Sure?" option
// 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
}
)
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) => {
cy.get(commonlocators.dropdownSelectButton).click({ force: true });
cy.get(commonlocators.dropdownSelectButton)
.first()
.click({ force: true });
cy.get(widgetsPage.menubar)
.contains("Show Message")
.click({ force: true })
@ -1279,6 +1281,19 @@ Cypress.Commands.add("getAlert", (alertcss) => {
.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) => {
cy.get(widgetsPage.RadioInput)
.eq(index)
@ -1424,17 +1439,10 @@ Cypress.Commands.add("fillMongoDatasourceForm", () => {
cy.get(datasourceEditor["password"]).type(
datasourceFormData["mongo-password"],
);
cy.get(datasourceEditor.sectionSSL).click();
cy.get(datasourceEditor["authenticationAuthtype"]).click();
cy.contains(datasourceFormData["mongo-authenticationAuthtype"]).click({
force: true,
});
cy.get(datasourceEditor["sslAuthtype"]).click();
cy.contains(datasourceFormData["mongo-sslAuthtype"]).click({
force: true,
});
});
Cypress.Commands.add("fillPostgresDatasourceForm", () => {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import {
DEFAULT_EXECUTE_ACTION_TIMEOUT_MS,
} from "constants/ApiConstants";
import axios, { AxiosPromise, CancelTokenSource } from "axios";
import { Action } from "entities/Action";
import { Action, ActionViewMode } from "entities/Action";
export interface CreateActionRequest<T> extends APIRequest {
datasourceId: string;
@ -127,7 +127,7 @@ class ActionAPI extends API {
static fetchActionsForViewMode(
applicationId: string,
): AxiosPromise<GenericApiResponse<Action[]>> {
): AxiosPromise<GenericApiResponse<ActionViewMode[]>> {
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;
href?: string;
tag?: "a" | "button";
type?: "submit" | "reset" | "button";
};
const stateStyles = (

View File

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

View File

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

View File

@ -168,7 +168,8 @@ export const Toaster = {
/>,
{
toastId: toastId,
pauseOnHover: true,
pauseOnHover: !config.dispatchableAction && !config.hideProgressBar,
pauseOnFocusLoss: !config.dispatchableAction && !config.hideProgressBar,
autoClose: false,
closeOnClick: false,
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 { Tooltip } from "@blueprintjs/core";
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: {
isHidden?: boolean;
children: React.ReactNode;
title: string;
tableWidth?: number;
}) => {
const ref = createRef<HTMLDivElement>();
const [useToolTip, updateToolTip] = useState(false);
@ -23,7 +30,11 @@ const AutoToolTipComponent = (props: {
<Tooltip
autoFocus={false}
hoverOpenDelay={1000}
content={props.title}
content={
<TooltipContentWrapper width={(props.tableWidth || 300) - 32}>
{props.title}
</TooltipContentWrapper>
}
position="top"
>
{props.children}

View File

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

View File

@ -64,6 +64,7 @@ interface ReactTableComponentProps {
multiRowSelection?: boolean;
hiddenColumns?: string[];
columnNameMap?: { [key: string]: string };
triggerRowSelection: boolean;
columnTypeMap?: {
[key: string]: {
type: string;
@ -319,6 +320,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => {
pageNo={props.pageNo - 1}
updatePageNo={props.updatePageNo}
columnActions={props.columnActions}
triggerRowSelection={props.triggerRowSelection}
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[];
disableDrag: () => void;
enableDrag: () => void;
triggerRowSelection: boolean;
searchTableData: (searchKey: any) => void;
filters?: ReactTableFilter[];
applyFilter: (filters: ReactTableFilter[]) => void;
@ -120,6 +121,7 @@ export const Table = (props: TableProps) => {
height={props.height}
tableSizes={tableSizes}
id={`table${props.widgetId}`}
triggerRowSelection={props.triggerRowSelection}
backgroundColor={Colors.ATHENS_GRAY_DARKER}
>
<TableHeader

View File

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

View File

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

View File

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

View File

@ -136,6 +136,22 @@ class InputComponent extends React.Component<
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 = () => (
<NumericInput
value={this.props.value}
@ -155,6 +171,7 @@ class InputComponent extends React.Component<
stepSize={this.props.stepSize}
onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDown}
/>
);
private textAreaInputComponent = () => (
@ -169,6 +186,7 @@ class InputComponent extends React.Component<
growVertically={false}
onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDownTextArea}
/>
);
@ -199,6 +217,7 @@ class InputComponent extends React.Component<
type={this.getType(this.props.inputType)}
onFocus={() => this.setFocusState(true)}
onBlur={() => this.setFocusState(false)}
onKeyDown={this.onKeyDown}
/>
);
private renderInputComponent = (inputType: InputType, isTextArea: boolean) =>
@ -267,6 +286,12 @@ export interface InputComponentProps extends ComponentProps {
isInvalid: boolean;
showError: boolean;
onFocusChange: (state: boolean) => void;
disableNewLineOnPressEnterKey?: boolean;
onKeyDown?: (
e:
| React.KeyboardEvent<HTMLTextAreaElement>
| React.KeyboardEvent<HTMLInputElement>,
) => void;
}
export default InputComponent;

View File

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

View File

@ -25,6 +25,7 @@ import {
createNewApiAction,
createNewQueryAction,
} from "actions/apiPaneActions";
import { NavigationTargetType } from "../../../sagas/ActionExecutionSagas";
/* eslint-disable @typescript-eslint/ban-types */
/* 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" },
];
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 ACTION_TRIGGER_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_TYPE_FIELD: "DOWNLOAD_FILE_TYPE_FIELD",
COPY_TEXT_FIELD: "COPY_TEXT_FIELD",
NAVIGATION_TARGET_FIELD: "NAVIGATION_TARGET_FIELD",
};
type FieldType = typeof FieldType[keyof typeof FieldType];
@ -406,6 +421,15 @@ const fieldConfigs: FieldConfigs = {
},
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]: {
getter: (value: string) => {
return textGetter(value, 0);
@ -628,6 +652,9 @@ function getFieldFromValue(
fields.push({
field: FieldType.QUERY_PARAMS_FIELD,
});
fields.push({
field: FieldType.NAVIGATION_TARGET_FIELD,
});
}
if (value.indexOf("showModal") !== -1) {
@ -718,6 +745,7 @@ function renderField(props: {
case FieldType.PAGE_SELECTOR_FIELD:
case FieldType.ALERT_TYPE_SELECTOR_FIELD:
case FieldType.DOWNLOAD_FILE_TYPE_FIELD:
case FieldType.NAVIGATION_TARGET_FIELD:
let label = "";
let defaultText = "Select Action";
let options = props.apiOptionTree;
@ -776,6 +804,11 @@ function renderField(props: {
options = FILE_TYPE_OPTIONS;
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)({
options: options,
label: label,

View File

@ -18,6 +18,7 @@ export enum EventType {
ON_PAGE_LOAD = "ON_PAGE_LOAD",
ON_PREV_PAGE = "ON_PREV_PAGE",
ON_NEXT_PAGE = "ON_NEXT_PAGE",
ON_PAGE_SIZE_CHANGE = "ON_PAGE_SIZE_CHANGE",
ON_ERROR = "ON_ERROR",
ON_SUCCESS = "ON_SUCCESS",
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`
&& {
.${Classes.CONTROL} {
@ -312,11 +319,8 @@ export type Theme = {
};
authCard: {
width: number;
borderRadius: number;
background: Color;
padding: number;
dividerSpacing: number;
shadow: string;
formMessageWidth: number;
};
shadows: string[];
widgets: {
@ -735,6 +739,35 @@ type ColorType = {
bg: ShadeColor;
};
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 = {
@ -862,9 +895,9 @@ export const dark: ColorType = {
border: darkShades[2],
},
normal: {
bg: lightShades[10],
text: darkShades[9],
bg: darkShades[0],
border: darkShades[0],
text: darkShades[7],
},
placeholder: darkShades[5],
readOnly: {
@ -1019,6 +1052,8 @@ export const dark: ColorType = {
backgroundColor: darkShades[3],
iconColor: darkShades[6],
},
auth,
formMessage,
};
export const light: ColorType = {
@ -1303,6 +1338,8 @@ export const light: ColorType = {
backgroundColor: lightShades[3],
iconColor: lightShades[7],
},
auth,
formMessage,
};
export const theme: Theme = {
@ -1395,6 +1432,18 @@ export const theme: Theme = {
letterSpacing: -0.24,
fontWeight: "normal",
},
authCardHeader: {
fontStyle: "normal",
fontWeight: 600,
fontSize: 25,
lineHeight: 20,
},
authCardSubheader: {
fontStyle: "normal",
fontWeight: "normal",
fontSize: 15,
lineHeight: 20,
},
},
iconSizes: {
XXS: 8,
@ -1545,12 +1594,9 @@ export const theme: Theme = {
},
},
authCard: {
width: 612,
borderRadius: 16,
background: Colors.WHITE,
padding: 40,
width: 440,
dividerSpacing: 32,
shadow: "0px 4px 8px rgba(9, 30, 66, 0.25)",
formMessageWidth: 370,
},
shadows: [
/* 0. tab */

View File

@ -11,7 +11,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
FLUSH_AND_REDIRECT: "FLUSH_AND_REDIRECT",
SAFE_CRASH_APPSMITH: "SAFE_CRASH_APPSMITH",
SAFE_CRASH_APPSMITH_REQUEST: "SAFE_CRASH_APPSMITH_REQUEST",
UPDATE_CANVAS: "UPDATE_CANVAS",
INIT_CANVAS_LAYOUT: "INIT_CANVAS_LAYOUT",
FETCH_CANVAS: "FETCH_CANVAS",
CLEAR_CANVAS: "CLEAR_CANVAS",
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: "UPDATE_WIDGET_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_SUCCESS: "FETCH_PROPERTY_PANE_CONFIGS_SUCCESS",
FETCH_CONFIGS_INIT: "FETCH_CONFIGS_INIT",

View File

@ -1,5 +1,6 @@
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";
export type SocialLoginButtonProps = {
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 SIGNUP_FORM_NAME = "SignupForm";
export const SIGNUP_FORM_EMAIL_FIELD_NAME = "email";
export const FORGOT_PASSWORD_FORM_NAME = "ForgotPasswordForm";
export const RESET_PASSWORD_FORM_NAME = "ResetPasswordForm";
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_PASSWORD_RULE =
"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_TITLE = "Login";
export const LOGIN_PAGE_TITLE = "Sign In to your account";
export const LOGIN_PAGE_EMAIL_INPUT_LABEL = "Email";
export const LOGIN_PAGE_PASSWORD_INPUT_LABEL = "Password";
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 =
"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 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_REMEMBER_ME_LABEL = "Remember";
export const LOGIN_PAGE_SIGN_UP_LINK_TEXT = "New to Appsmith? Sign up";
export const SIGNUP_PAGE_TITLE = "Sign Up";
export const LOGIN_PAGE_SIGN_UP_LINK_TEXT = "Sign up";
export const SIGNUP_PAGE_TITLE = "Create your free account";
export const SIGNUP_PAGE_SUBTITLE = "Use your organization email";
export const SIGNUP_PAGE_EMAIL_INPUT_LABEL = "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_PASSWORD_INPUT_LABEL = "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_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_LOGIN_BUTTON_TEXT = "Login";
export const RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL = "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_SUBMIT_BUTTON_TEXT = "Reset";
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 NAVIGATE_TO_VALIDATION_ERROR =
"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 =
"Invalid Email address(es) found";
@ -129,7 +134,7 @@ export const DELETING_APPLICATION = "Deleting application...";
export const DUPLICATING_APPLICATION = "Duplicating application...";
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 INPUT_WIDGET_DEFAULT_VALIDATION_ERROR = "Invalid input";

View File

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

View File

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

View File

@ -79,3 +79,8 @@ const ThemedAppWithProps = connect(
)(ThemedApp);
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",
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",
label: "onSearchTextChanged",
controlType: "ACTION_SELECTOR",
@ -717,6 +725,15 @@ const PropertyPaneConfigResponse: PropertyPaneConfigsResponse["data"] = {
controlType: "ACTION_SELECTOR",
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 { isEllipsisActive } from "utils/helpers";
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 }>`
background: ${Colors.BALTIC_SEA};
@ -40,6 +42,13 @@ const HeaderWrapper = styled(StyledHeader)<{ hasPages: boolean }>`
color: white;
flex-direction: column;
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 }>`
@ -82,13 +91,6 @@ const ShareButton = styled(Button)`
color: white !important;
`;
const StyledApplicationName = styled.span`
font-size: 15px;
font-weight: 500;
font-size: 18px;
line-height: 14px;
`;
const PageTab = styled(NavLink)`
display: flex;
height: 30px;
@ -236,11 +238,9 @@ export const AppViewerHeader = (props: AppViewerHeaderProps) => {
<AppsmithLogoImg src={AppsmithLogo} alt="Appsmith logo" />
</Link>
</HeaderSection>
<HeaderSection justify={"center"}>
<HeaderSection justify={"center"} className="current-app-name">
{currentApplicationDetails && (
<StyledApplicationName>
{currentApplicationDetails.name}
</StyledApplicationName>
<Text type={TextType.H4}>{currentApplicationDetails.name}</Text>
)}
</HeaderSection>
<HeaderSection justify={"flex-end"}>

View File

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

View File

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

View File

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

View File

@ -27,6 +27,8 @@ import BackButton from "./BackButton";
import { PluginType } from "entities/Action";
import Boxed from "components/editorComponents/Onboarding/Boxed";
import { OnboardingStep } from "constants/OnboardingConstants";
import { isHidden } from "components/formControls/utils";
import log from "loglevel";
const { cloudHosting } = getAppsmithConfigs();
@ -412,6 +414,7 @@ class DatasourceDBEditor extends React.Component<
};
renderMainSection = (section: any, index: number) => {
if (isHidden(this.props.formData, section.hidden)) return null;
return (
<Collapsible title={section.sectionName} defaultIsOpen={index === 0}>
{this.renderEachConfig(section)}
@ -419,60 +422,73 @@ class DatasourceDBEditor extends React.Component<
);
};
renderEachConfig(section: any) {
const keyValueItems: any = [];
return (
<div key={section.id}>
<div>
{_.map(section.children, (propertyControlOrSection: ControlProps) => {
if ("children" in propertyControlOrSection) {
return this.renderEachConfig(propertyControlOrSection);
} else {
try {
const {
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 (
<div key={configProperty} style={{ marginTop: "16px" }}>
<FormControl
config={config}
formName={DATASOURCE_DB_FORM}
multipleConfig={multipleConfig}
/>
</div>
);
} catch (e) {
console.log(e);
}
}
})}
renderSingleConfig = (
config: ControlProps,
multipleConfig?: ControlProps[],
) => {
multipleConfig = multipleConfig || [];
try {
this.setupConfig(config);
return (
<div key={config.configProperty} style={{ marginTop: "16px" }}>
<FormControl
config={config}
formName={DATASOURCE_DB_FORM}
multipleConfig={multipleConfig}
/>
</div>
);
} catch (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>
);
}
};
}
export default reduxForm<Datasource, DatasourceDBEditorProps>({

View File

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

View File

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

View File

@ -37,7 +37,8 @@ export const EntityItem = styled.div<{
}>`
position: relative;
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")};
height: 30px;
width: 100%;

View File

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

View File

@ -17,7 +17,6 @@ import { useWidgetSelection } from "utils/hooks/dragResizeHooks";
import { AppState } from "reducers";
import { getWidgetIcon } from "../ExplorerIcons";
import { noop } from "lodash";
import WidgetContextMenu from "./WidgetContextMenu";
import { updateWidgetName } from "actions/propertyPaneActions";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
@ -133,7 +132,7 @@ export const WidgetEntity = memo((props: WidgetEntityProps) => {
name={props.widgetName}
entityId={props.widgetId}
step={props.step}
updateEntityName={props.pageId === pageId ? updateWidgetName : noop}
updateEntityName={props.pageId === pageId ? updateWidgetName : undefined}
searchKeyword={props.searchKeyword}
isDefaultExpanded={
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 StyledForm from "components/editorComponents/Form";
import {
AuthCardContainer,
AuthCardHeader,
AuthCardBody,
FormActions,
AuthCardNavLink,
FormMessagesContainer,
} from "./StyledComponents";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import {
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_LABEL,
FORGOT_PASSWORD_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT,
FORGOT_PASSWORD_PAGE_SUBTITLE,
FORGOT_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_INVALID_EMAIL,
@ -22,11 +22,12 @@ import {
FORGOT_PASSWORD_PAGE_LOGIN_LINK,
} from "constants/messages";
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 FormGroup from "components/editorComponents/form/FormGroup";
import Button from "components/editorComponents/Button";
import FormTextField from "components/editorComponents/form/FormTextField";
import FormGroup from "components/ads/formFields/FormGroup";
import Button, { Size } from "components/ads/Button";
import FormTextField from "components/ads/formFields/TextField";
import { Icon } from "@blueprintjs/core";
import { isEmail, isEmptyString } from "utils/formhelpers";
import {
ForgotPasswordFormValues,
@ -52,46 +53,56 @@ type ForgotPasswordProps = InjectedFormProps<
> &
RouteComponentProps<{ email: string }> & { emailValue: string };
export const ForgotPassword = (props: ForgotPasswordProps) => {
const {
error,
handleSubmit,
submitting,
submitFailed,
submitSucceeded,
} = props;
export const ForgotPassword = withTheme(
(props: ForgotPasswordProps & { theme: Theme }) => {
const {
error,
handleSubmit,
submitting,
submitFailed,
submitSucceeded,
} = props;
return (
<AuthCardContainer>
{submitSucceeded && (
<FormMessage
intent="primary"
message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`}
/>
)}
{!mailEnabled && (
<FormMessage
intent="warning"
message={
"You havent setup any email service yet. Please configure your email service to receive a reset link"
}
actions={[
{
url: "https://docs.appsmith.com/third-party-services/email",
text: "Configure Email service",
intent: "primary",
},
]}
/>
)}
{submitFailed && error && (
<FormMessage intent="warning" message={error} />
)}
<AuthCardHeader>
<h1>{FORGOT_PASSWORD_PAGE_TITLE}</h1>
<h5>{FORGOT_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
return (
<>
<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 && (
<FormMessage
intent="success"
message={`${FORGOT_PASSWORD_SUCCESS_TEXT} ${props.emailValue}`}
/>
)}
{!mailEnabled && (
<FormMessage
intent="warning"
message={
"You havent setup any email service yet. Please configure your email service to receive a reset link"
}
actions={[
{
url: "https://docs.appsmith.com/third-party-services/email",
text: "Configure Email service",
intent: "primary",
},
]}
/>
)}
{submitFailed && error && (
<FormMessage intent="warning" message={error} />
)}
</FormMessagesContainer>
<StyledForm onSubmit={handleSubmit(forgotPasswordSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
@ -105,23 +116,20 @@ export const ForgotPassword = (props: ForgotPasswordProps) => {
</FormGroup>
<FormActions>
<Button
tag="button"
type="submit"
text={FORGOT_PASSWORD_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary"
filled
size="large"
fill
size={Size.large}
disabled={!isEmail(props.emailValue)}
loading={submitting}
isLoading={submitting}
/>
</FormActions>
</StyledForm>
</AuthCardBody>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{FORGOT_PASSWORD_PAGE_LOGIN_LINK}
</AuthCardNavLink>
</AuthCardContainer>
);
};
</>
);
},
);
const selector = formValueSelector(FORGOT_PASSWORD_FORM_NAME);

View File

@ -9,13 +9,11 @@ import {
} from "constants/forms";
import { FORGOT_PASSWORD_URL, SIGN_UP_URL } from "constants/routes";
import {
LOGIN_PAGE_SUBTITLE,
LOGIN_PAGE_TITLE,
LOGIN_PAGE_EMAIL_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_LABEL,
LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER,
LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD,
@ -24,29 +22,28 @@ import {
LOGIN_PAGE_SIGN_UP_LINK_TEXT,
LOGIN_PAGE_INVALID_CREDS_ERROR,
LOGIN_PAGE_INVALID_CREDS_FORGOT_PASSWORD_LINK,
FORM_VALIDATION_PASSWORD_RULE,
NEW_TO_APPSMITH,
} from "constants/messages";
import Divider from "components/editorComponents/Divider";
import FormMessage from "components/editorComponents/form/FormMessage";
import FormGroup from "components/editorComponents/form/FormGroup";
import FormTextField from "components/editorComponents/form/FormTextField";
import Button from "components/editorComponents/Button";
import FormMessage from "components/ads/formFields/FormMessage";
import FormGroup from "components/ads/formFields/FormGroup";
import FormTextField from "components/ads/formFields/TextField";
import Button, { Size } from "components/ads/Button";
import ThirdPartyAuth, { SocialLoginTypes } from "./ThirdPartyAuth";
import { isEmail, isStrongPassword, isEmptyString } from "utils/formhelpers";
import { LoginFormValues } from "./helpers";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import {
AuthCardContainer,
SpacedSubmitForm,
FormActions,
AuthCardHeader,
AuthCardFooter,
AuthCardNavLink,
AuthCardBody,
SignUpLinkSection,
ForgotPasswordLink,
} from "./StyledComponents";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getAppsmithConfigs } from "configs";
import { TncPPLinks } from "./SignUp";
import { LOGIN_SUBMIT_PATH } from "constants/ApiConstants";
import PerformanceTracker, {
PerformanceTransactionName,
@ -55,16 +52,14 @@ const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs();
const validate = (values: 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];
if (!password || isEmptyString(password)) {
errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_EMPTY_PASSWORD;
} else if (!isStrongPassword(password)) {
errors[LOGIN_FORM_PASSWORD_FIELD_NAME] = FORM_VALIDATION_INVALID_PASSWORD;
}
if (!email || isEmptyString(email)) {
errors[LOGIN_FORM_EMAIL_FIELD_NAME] = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(email)) {
if (!isEmptyString(email) && !isEmail(email)) {
errors[LOGIN_FORM_EMAIL_FIELD_NAME] = FORM_VALIDATION_INVALID_EMAIL;
}
@ -74,14 +69,17 @@ const validate = (values: LoginFormValues) => {
type LoginFormProps = { emailValue: string } & InjectedFormProps<
LoginFormValues,
{ emailValue: string }
>;
> & {
theme: Theme;
};
const SocialLoginList: string[] = [];
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE);
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
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 queryParams = new URLSearchParams(location.search);
@ -103,7 +101,19 @@ export const Login = (props: LoginFormProps) => {
}
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 && (
<FormMessage
intent="warning"
@ -117,68 +127,56 @@ export const Login = (props: LoginFormProps) => {
]}
/>
)}
<AuthCardHeader>
<h1>{LOGIN_PAGE_TITLE}</h1>
<h5>{LOGIN_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<SpacedSubmitForm method="POST" action={loginURL}>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_EMAIL_INPUT_LABEL}
>
<FormTextField
name={LOGIN_FORM_EMAIL_FIELD_NAME}
type="email"
placeholder={LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER}
autoFocus
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_PASSWORD_INPUT_LABEL}
helperText={FORM_VALIDATION_PASSWORD_RULE}
>
<FormTextField
type="password"
name={LOGIN_FORM_PASSWORD_FIELD_NAME}
placeholder={LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER}
/>
</FormGroup>
<Link to={forgotPasswordURL}>{LOGIN_PAGE_FORGOT_PASSWORD_TEXT}</Link>
<FormActions>
<Button
type="submit"
disabled={!valid}
text={LOGIN_PAGE_LOGIN_BUTTON_TEXT}
intent="primary"
filled
size="large"
onClick={() => {
PerformanceTracker.startTracking(
PerformanceTransactionName.LOGIN_CLICK,
);
AnalyticsUtil.logEvent("LOGIN_CLICK", {
loginMethod: "EMAIL",
});
}}
/>
</FormActions>
</SpacedSubmitForm>
{SocialLoginList.length > 0 && (
<>
<Divider />
<ThirdPartyAuth type={"SIGNIN"} logins={SocialLoginList} />
</>
)}
</AuthCardBody>
<AuthCardNavLink to={signupURL}>
{LOGIN_PAGE_SIGN_UP_LINK_TEXT}
</AuthCardNavLink>
<AuthCardFooter>
<TncPPLinks />
</AuthCardFooter>
</AuthCardContainer>
{SocialLoginList.length > 0 && (
<ThirdPartyAuth type={"SIGNIN"} logins={SocialLoginList} />
)}
<SpacedSubmitForm method="POST" action={loginURL}>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_EMAIL_INPUT_LABEL}
>
<FormTextField
name={LOGIN_FORM_EMAIL_FIELD_NAME}
type="email"
placeholder={LOGIN_PAGE_EMAIL_INPUT_PLACEHOLDER}
autoFocus
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={LOGIN_PAGE_PASSWORD_INPUT_LABEL}
// helperText={FORM_VALIDATION_PASSWORD_RULE}
>
<FormTextField
type="password"
name={LOGIN_FORM_PASSWORD_FIELD_NAME}
placeholder={LOGIN_PAGE_PASSWORD_INPUT_PLACEHOLDER}
/>
</FormGroup>
<FormActions>
<Button
tag="button"
type="submit"
disabled={!isFormValid}
text={LOGIN_PAGE_LOGIN_BUTTON_TEXT}
fill
size={Size.large}
onClick={() => {
PerformanceTracker.startTracking(
PerformanceTransactionName.LOGIN_CLICK,
);
AnalyticsUtil.logEvent("LOGIN_CLICK", {
loginMethod: "EMAIL",
});
}}
/>
</FormActions>
</SpacedSubmitForm>
<ForgotPasswordLink>
<Link to={forgotPasswordURL}>{LOGIN_PAGE_FORGOT_PASSWORD_TEXT}</Link>
</ForgotPasswordLink>
</>
);
};
@ -190,5 +188,5 @@ export default connect((state) => ({
validate,
touchOnBlur: true,
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 { getIsTokenValid, getIsValidatingToken } from "selectors/authSelectors";
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, {
FormMessageProps,
MessageAction,
} from "components/editorComponents/form/FormMessage";
FormMessageProps,
} from "components/ads/formFields/FormMessage";
import Spinner from "components/editorComponents/Spinner";
import Button from "components/editorComponents/Button";
import FormGroup from "components/editorComponents/form/FormGroup";
import Button, { Size } from "components/ads/Button";
import StyledForm from "components/editorComponents/Form";
import { isEmptyString, isStrongPassword } from "utils/formhelpers";
import { ResetPasswordFormValues, resetPasswordSubmitHandler } from "./helpers";
import {
AuthCardHeader,
AuthCardFooter,
AuthCardContainer,
AuthCardBody,
AuthCardNavLink,
FormActions,
} from "./StyledComponents";
import { AUTH_LOGIN_URL, FORGOT_PASSWORD_URL } from "constants/routes";
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
import {
RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL,
RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER,
RESET_PASSWORD_LOGIN_LINK_TEXT,
RESET_PASSWORD_SUBMIT_BUTTON_TEXT,
RESET_PASSWORD_PAGE_SUBTITLE,
RESET_PASSWORD_PAGE_TITLE,
FORM_VALIDATION_INVALID_PASSWORD,
FORM_VALIDATION_EMPTY_PASSWORD,
@ -43,7 +42,6 @@ import {
RESET_PASSWORD_RESET_SUCCESS,
RESET_PASSWORD_RESET_SUCCESS_LOGIN_LINK,
} from "constants/messages";
import { TncPPLinks } from "./SignUp";
const validate = (values: ResetPasswordFormValues) => {
const errors: ResetPasswordFormValues = {};
@ -66,6 +64,7 @@ type ResetPasswordProps = InjectedFormProps<
verifyToken: (token: string, email: string) => void;
isTokenValid: boolean;
validatingToken: boolean;
theme: Theme;
} & RouteComponentProps<{ email: string; token: string }>;
export const ResetPassword = (props: ResetPasswordProps) => {
@ -154,50 +153,49 @@ export const ResetPassword = (props: ResetPasswordProps) => {
return <Spinner />;
}
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) && (
<FormMessage {...messageTagProps} />
)}
<AuthCardHeader>
<h1>{RESET_PASSWORD_PAGE_TITLE}</h1>
<h5>{RESET_PASSWORD_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<StyledForm onSubmit={handleSubmit(resetPasswordSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
label={RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL}
>
<FormTextField
name="password"
type="password"
placeholder={RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER}
disabled={submitSucceeded}
/>
</FormGroup>
<Field type="hidden" name="email" component="input" />
<Field type="hidden" name="token" component="input" />
<FormActions>
<Button
filled
size="large"
type="submit"
text={RESET_PASSWORD_SUBMIT_BUTTON_TEXT}
intent="primary"
disabled={pristine || submitSucceeded}
loading={submitting}
/>
</FormActions>
</StyledForm>
</AuthCardBody>
<AuthCardNavLink to={AUTH_LOGIN_URL}>
{RESET_PASSWORD_LOGIN_LINK_TEXT}
<Icon icon="arrow-right" intent="primary" />
</AuthCardNavLink>
<AuthCardFooter>
<TncPPLinks></TncPPLinks>
</AuthCardFooter>
</AuthCardContainer>
<StyledForm onSubmit={handleSubmit(resetPasswordSubmitHandler)}>
<FormGroup
intent={error ? "danger" : "none"}
label={RESET_PASSWORD_PAGE_PASSWORD_INPUT_LABEL}
>
<FormTextField
name="password"
type="password"
placeholder={RESET_PASSWORD_PAGE_PASSWORD_INPUT_PLACEHOLDER}
disabled={submitSucceeded}
/>
</FormGroup>
<Field type="hidden" name="email" component="input" />
<Field type="hidden" name="token" component="input" />
<FormActions>
<Button
tag="button"
fill
size={Size.large}
type="submit"
text={RESET_PASSWORD_SUBMIT_BUTTON_TEXT}
disabled={pristine || submitSucceeded}
isLoading={submitting}
/>
</FormActions>
</StyledForm>
</>
);
};
@ -232,5 +230,5 @@ export default connect(
validate,
form: RESET_PASSWORD_FORM_NAME,
touchOnBlur: true,
})(withRouter(ResetPassword)),
})(withRouter(withTheme(ResetPassword))),
);

View File

@ -1,45 +1,33 @@
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 { SIGNUP_FORM_NAME } from "constants/forms";
import {
Link,
RouteComponentProps,
useLocation,
withRouter,
} from "react-router-dom";
import Divider from "components/editorComponents/Divider";
import { RouteComponentProps, useLocation, withRouter } from "react-router-dom";
import {
AuthCardHeader,
AuthCardBody,
AuthCardFooter,
AuthCardNavLink,
SpacedSubmitForm,
FormActions,
AuthCardContainer,
SignUpLinkSection,
} from "./StyledComponents";
import {
SIGNUP_PAGE_TITLE,
SIGNUP_PAGE_SUBTITLE,
SIGNUP_PAGE_EMAIL_INPUT_LABEL,
SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER,
SIGNUP_PAGE_PASSWORD_INPUT_LABEL,
SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER,
SIGNUP_PAGE_LOGIN_LINK_TEXT,
FORM_VALIDATION_EMPTY_EMAIL,
FORM_VALIDATION_EMPTY_PASSWORD,
FORM_VALIDATION_INVALID_EMAIL,
FORM_VALIDATION_INVALID_PASSWORD,
SIGNUP_PAGE_SUBMIT_BUTTON_TEXT,
PRIVACY_POLICY_LINK,
TERMS_AND_CONDITIONS_LINK,
FORM_VALIDATION_PASSWORD_RULE,
ALREADY_HAVE_AN_ACCOUNT,
} from "constants/messages";
import FormMessage from "components/editorComponents/form/FormMessage";
import FormGroup from "components/editorComponents/form/FormGroup";
import FormTextField from "components/editorComponents/form/FormTextField";
import FormMessage from "components/ads/formFields/FormMessage";
import FormGroup from "components/ads/formFields/FormGroup";
import FormTextField from "components/ads/formFields/TextField";
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";
@ -54,28 +42,16 @@ import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { setOnboardingState } from "utils/storage";
const {
enableGithubOAuth,
enableGoogleOAuth,
enableTNCPP,
} = getAppsmithConfigs();
import { SIGNUP_FORM_EMAIL_FIELD_NAME } from "constants/forms";
const { enableGithubOAuth, enableGoogleOAuth } = getAppsmithConfigs();
const SocialLoginList: string[] = [];
if (enableGithubOAuth) SocialLoginList.push(SocialLoginTypes.GITHUB);
if (enableGoogleOAuth) SocialLoginList.push(SocialLoginTypes.GOOGLE);
export const TncPPLinks = () => {
if (!enableTNCPP) return null;
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>
</>
);
};
import { withTheme } from "styled-components";
import { Theme } from "constants/DefaultTheme";
const validate = (values: SignupFormValues) => {
const errors: SignupFormValues = {};
@ -84,19 +60,24 @@ const validate = (values: SignupFormValues) => {
} else if (!isStrongPassword(values.password)) {
errors.password = FORM_VALIDATION_INVALID_PASSWORD;
}
if (!values.email || isEmptyString(values.email)) {
errors.email = FORM_VALIDATION_EMPTY_EMAIL;
} else if (!isEmail(values.email)) {
const email = values.email || "";
if (!isEmptyString(email) && !isEmail(email)) {
errors.email = FORM_VALIDATION_INVALID_EMAIL;
}
return errors;
};
type SignUpFormProps = InjectedFormProps<SignupFormValues> &
RouteComponentProps<{ email: string }>;
type SignUpFormProps = InjectedFormProps<
SignupFormValues,
{ emailValue: string }
> &
RouteComponentProps<{ email: string }> & { theme: Theme; emailValue: string };
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();
let showError = false;
@ -115,85 +96,84 @@ export const SignUp = (props: SignUpFormProps) => {
}
return (
<AuthCardContainer>
<>
{showError && <FormMessage intent="danger" message={errorMessage} />}
<AuthCardHeader>
<h1>{SIGNUP_PAGE_TITLE}</h1>
<h5>{SIGNUP_PAGE_SUBTITLE}</h5>
</AuthCardHeader>
<AuthCardBody>
<SpacedSubmitForm method="POST" action={signupURL}>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_EMAIL_INPUT_LABEL}
>
<FormTextField
name="email"
type="email"
placeholder={SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER}
autoFocus
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_PASSWORD_INPUT_LABEL}
helperText={FORM_VALIDATION_PASSWORD_RULE}
>
<FormTextField
type="password"
name="password"
placeholder={SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER}
/>
</FormGroup>
<FormActions>
<Button
type="submit"
disabled={pristine || !valid}
loading={submitting}
text={SIGNUP_PAGE_SUBMIT_BUTTON_TEXT}
intent="primary"
filled
size="large"
onClick={() => {
AnalyticsUtil.logEvent("SIGNUP_CLICK", {
signupMethod: "EMAIL",
});
PerformanceTracker.startTracking(
PerformanceTransactionName.SIGN_UP,
);
setOnboardingState(true);
}}
/>
</FormActions>
</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>
<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}>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_EMAIL_INPUT_LABEL}
>
<FormTextField
name="email"
type="email"
placeholder={SIGNUP_PAGE_EMAIL_INPUT_PLACEHOLDER}
autoFocus
/>
</FormGroup>
<FormGroup
intent={error ? "danger" : "none"}
label={SIGNUP_PAGE_PASSWORD_INPUT_LABEL}
// helperText={FORM_VALIDATION_PASSWORD_RULE}
>
<FormTextField
type="password"
name="password"
placeholder={SIGNUP_PAGE_PASSWORD_INPUT_PLACEHOLDER}
/>
</FormGroup>
<FormActions>
<Button
tag="button"
type="submit"
disabled={pristine || !isFormValid}
isLoading={submitting}
text={SIGNUP_PAGE_SUBMIT_BUTTON_TEXT}
fill
size={Size.large}
onClick={() => {
AnalyticsUtil.logEvent("SIGNUP_CLICK", {
signupMethod: "EMAIL",
});
PerformanceTracker.startTracking(
PerformanceTransactionName.SIGN_UP,
);
setOnboardingState(true);
}}
/>
</FormActions>
</SpacedSubmitForm>
</>
);
};
const selector = formValueSelector(SIGNUP_FORM_NAME);
export default connect((state: AppState, props: SignUpFormProps) => {
const queryParams = new URLSearchParams(props.location.search);
return {
initialValues: {
email: queryParams.get("email"),
},
emailValue: selector(state, SIGNUP_FORM_EMAIL_FIELD_NAME),
};
}, null)(
reduxForm<SignupFormValues>({
reduxForm<SignupFormValues, { emailValue: string }>({
validate,
form: SIGNUP_FORM_NAME,
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 Form from "components/editorComponents/Form";
import { Card } from "@blueprintjs/core";
import { getTypographyByKey } from "constants/DefaultTheme";
import { Classes } from "@blueprintjs/core";
export const AuthContainer = styled.section`
position: absolute;
width: 100%;
height: 100vh;
will-change: transform, opacity;
`;
height: ${(props) => `calc(100vh - ${props.theme.headerHeight})`};
background-color: ${(props) => props.theme.colors.auth.background};
display: flex;
flex-direction: column;
align-items: center;
overflow: auto;
export const AuthCard = styled(Card)`
&& {
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;
& h1,
h5 {
padding: 0;
margin: 0;
font-weight: ${(props) => props.theme.fontWeights[1]};
}
& .${Classes.FORM_GROUP} {
margin: 0 0 ${(props) => props.theme.spaces[2]}px;
}
`;
export const AuthCardContainer = styled.div``;
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)`
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;
border: none;
h1 {
text-align: center;
padding: 0;
margin: 0;
${(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 AuthCardHeader = styled.header`
& {
h1 {
font-size: ${(props) => props.theme.fontSizes[6]}px;
white-space: nowrap;
}
h5 {
font-size: ${(props) => props.theme.fontSizes[4]}px;
@ -42,12 +73,10 @@ export const AuthCardHeader = styled.header`
`;
export const AuthCardNavLink = styled(Link)`
text-align: center;
margin: 0 auto;
display: block;
margin-top: ${(props) => props.theme.spaces[12]}px;
& span {
margin-left: ${(props) => props.theme.spaces[4]}px;
border-bottom: 1px solid transparent;
&:hover {
border-bottom: 1px solid ${(props) => props.theme.colors.auth.link};
text-decoration: none;
}
`;
@ -60,26 +89,15 @@ export const AuthCardFooter = styled.footer`
`;
export const AuthCardBody = styled.div`
display: flex;
justify-content: flex-start;
align-items: stretch;
& a {
margin-top: ${(props) => props.theme.spaces[8]}px;
font-size: ${(props) => props.theme.fontSizes[2]}px;
}
`;
const formSpacing = css`
flex-grow: 1;
margin-right: ${(props) => props.theme.authCard.dividerSpacing}px;
`;
export const SpacedForm = styled(Form)`
${formSpacing}
`;
export const SpacedForm = styled(Form)``;
export const SpacedSubmitForm = styled.form`
${formSpacing}
& a {
font-size: ${(props) => props.theme.fontSizes[3]}px;
}
@ -95,8 +113,29 @@ export const FormActions = styled.div`
}
justify-content: space-between;
align-items: baseline;
margin-top: ${(props) => props.theme.spaces[2]}px;
margin-top: ${(props) => props.theme.spaces[5]}px;
& > label {
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,
SocialLoginType,
} from "constants/SocialLogin";
import { IntentColors, getBorderCSSShorthand } from "constants/DefaultTheme";
import { getTypographyByKey } from "constants/DefaultTheme";
import AnalyticsUtil, { EventName } from "utils/AnalyticsUtil";
import { useLocation } from "react-router-dom";
import PerformanceTracker, {
@ -15,52 +15,42 @@ import { setOnboardingState } from "utils/storage";
const ThirdPartyAuthWrapper = styled.div`
display: flex;
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.
const StyledSocialLoginButton = styled.a`
width: 200px;
display: flex;
align-items: center;
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
padding: 8px;
color: ${(props) => props.theme.colors.textDefault};
border-radius: ${(props) => props.theme.radii[1]}px;
position: relative;
height: 42px;
justify-content: center;
border: solid 1px ${(props) => props.theme.colors.auth.socialBtnBorder};
padding: ${(props) => props.theme.spaces[2]}px;
&:first-child {
margin-bottom: ${(props) => props.theme.spaces[4]}px;
}
&:only-child {
margin-bottom: 0;
}
&:hover {
text-decoration: none;
background: ${IntentColors.success};
color: ${(props) => props.theme.colors.textOnDarkBG};
background-color: ${(props) => props.theme.colors.auth.socialBtnHighlight};
}
& > div {
width: 36px;
height: 36px;
padding: ${(props) => props.theme.radii[1]}px;
position: absolute;
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]};
& .login-method {
${(props) => getTypographyByKey(props, "btnLarge")}
color: ${(props) => props.theme.colors.auth.socialBtnText};
text-transform: uppercase;
}
`;
const ButtonLogo = styled.img`
margin: ${(props) => props.theme.spaces[2]}px;
width: 14px;
height: 14px;
`;
export const SocialLoginTypes: Record<string, string> = {
GOOGLE: "google",
GITHUB: "github",
@ -102,10 +92,8 @@ const SocialLoginButton = (props: {
});
}}
>
<div>
<img alt={` ${props.name} login`} src={props.logo} />
</div>
<p>{`Sign in with ${props.name}`}</p>
<ButtonLogo alt={` ${props.name} login`} src={props.logo} />
<div className="login-method">{`continue with ${props.name}`}</div>
</StyledSocialLoginButton>
);
};

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import {
} from "constants/ReduxActionConstants";
import { WidgetProps } from "widgets/BaseWidget";
import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import _ from "lodash";
const initialState: CanvasWidgetsReduxState = {};
@ -14,8 +15,7 @@ export type FlattenedWidgetProps = WidgetProps & {
};
const canvasWidgetsReducer = createImmerReducer(initialState, {
// TODO Rename to INIT_LAYOUT
[ReduxActionTypes.UPDATE_CANVAS]: (
[ReduxActionTypes.INIT_CANVAS_LAYOUT]: (
state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateCanvasPayload>,
) => {
@ -31,8 +31,13 @@ const canvasWidgetsReducer = createImmerReducer(initialState, {
state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateWidgetPropertyPayload>,
) => {
state[action.payload.widgetId][action.payload.propertyName] =
action.payload.propertyValue;
// We loop over all updates
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;
return { ...state };
},
[ReduxActionTypes.UPDATE_CANVAS]: (
[ReduxActionTypes.INIT_CANVAS_LAYOUT]: (
state: EditorReduxState,
action: ReduxAction<UpdateCanvasPayload>,
) => {

View File

@ -38,6 +38,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import history from "utils/history";
import {
BUILDER_PAGE_URL,
convertToQueryParams,
getApplicationViewerPageURL,
} from "constants/routes";
import {
@ -86,42 +87,61 @@ import {
} from "./EvaluationsSaga";
import copy from "copy-to-clipboard";
export enum NavigationTargetType {
SAME_WINDOW = "SAME_WINDOW",
NEW_WINDOW = "NEW_WINDOW",
}
function* navigateActionSaga(
action: { pageNameOrUrl: string; params: Record<string, string> },
action: {
pageNameOrUrl: string;
params: Record<string, string>;
target?: NavigationTargetType;
},
event: ExecuteActionPayloadEvent,
) {
const pageList = yield select(getPageList);
const applicationId = yield select(getCurrentApplicationId);
const {
pageNameOrUrl,
params,
target = NavigationTargetType.SAME_WINDOW,
} = action;
const page = _.find(
pageList,
(page: Page) => page.pageName === action.pageNameOrUrl,
(page: Page) => page.pageName === pageNameOrUrl,
);
if (page) {
AnalyticsUtil.logEvent("NAVIGATE", {
pageName: action.pageNameOrUrl,
pageParams: action.params,
pageName: pageNameOrUrl,
pageParams: params,
});
// TODO need to make this check via RENDER_MODE;
const appMode = yield select(getAppMode);
const path =
history.location.pathname.indexOf("/edit") !== -1
? BUILDER_PAGE_URL(applicationId, page.pageId, action.params)
: getApplicationViewerPageURL(
applicationId,
page.pageId,
action.params,
);
history.push(path);
appMode === APP_MODE.EDIT
? BUILDER_PAGE_URL(applicationId, page.pageId, params)
: getApplicationViewerPageURL(applicationId, page.pageId, params);
if (target === NavigationTargetType.SAME_WINDOW) {
history.push(path);
} else if (target === NavigationTargetType.NEW_WINDOW) {
window.open(path, "_blank");
}
if (event.callback) event.callback({ success: true });
} else {
AnalyticsUtil.logEvent("NAVIGATE", {
navUrl: action.pageNameOrUrl,
navUrl: pageNameOrUrl,
});
// Add a default protocol if it doesn't exist.
let url = action.pageNameOrUrl;
let url = pageNameOrUrl + convertToQueryParams(params);
if (url.indexOf("://") === -1) {
url = "https://" + url;
}
window.location.assign(url);
if (target === NavigationTargetType.SAME_WINDOW) {
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";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { QUERY_CONSTANT } from "constants/QueryEditorConstants";
import { Action } from "entities/Action";
import { Action, ActionViewMode } from "entities/Action";
import { ActionData } from "reducers/entityReducers/actionsReducer";
import {
getAction,
@ -145,14 +145,22 @@ export function* fetchActionsForViewModeSaga(
{ mode: "VIEWER", appId: applicationId },
);
try {
const response: GenericApiResponse<Action[]> = yield ActionAPI.fetchActionsForViewMode(
const response: GenericApiResponse<ActionViewMode[]> = yield ActionAPI.fetchActionsForViewMode(
applicationId,
);
const correctFormatResponse = response.data.map((action) => {
return {
...action,
actionConfiguration: {
timeoutInMillisecond: action.timeoutInMillisecond,
},
};
});
const isValidResponse = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.FETCH_ACTIONS_VIEW_MODE_SUCCESS,
payload: response.data,
payload: correctFormatResponse,
});
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_ACTIONS_API,

View File

@ -390,7 +390,9 @@ function* switchDatasourceSaga(action: ReduxAction<{ datasourceId: string }>) {
(datasource: Datasource) => datasource.id === datasourceId,
),
);
yield put(changeDatasource(datasource));
if (datasource) {
yield put(changeDatasource(datasource));
}
}
function* formValueChangeSaga(

View File

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

View File

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

View File

@ -1,42 +1,44 @@
import {
ReduxActionTypes,
ReduxActionErrorTypes,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
} from "constants/ReduxActionConstants";
import {
WidgetAddChild,
WidgetResize,
WidgetMove,
WidgetDelete,
updateAndSaveLayout,
WidgetAddChild,
WidgetAddChildren,
WidgetDelete,
WidgetMove,
WidgetResize,
} from "actions/pageActions";
import {
FlattenedWidgetProps,
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
getWidgets,
getWidget,
getSelectedWidget,
getWidget,
getWidgetMetaProps,
getWidgets,
} from "./selectors";
import {
generateWidgetProps,
updateWidgetPosition,
} from "utils/WidgetPropsUtils";
import {
all,
call,
put,
select,
takeEvery,
takeLatest,
all,
} from "redux-saga/effects";
import { convertToString, getNextEntityName } from "utils/AppsmithUtils";
import {
DeleteWidgetPropertyPayload,
SetWidgetDynamicPropertyPayload,
updateWidgetProperty,
UpdateWidgetPropertyPayload,
UpdateWidgetPropertyRequestPayload,
} from "actions/controlActions";
import {
@ -44,12 +46,13 @@ import {
getEntityDynamicBindingPathList,
getWidgetDynamicPropertyPathList,
getWidgetDynamicTriggerPathList,
isChildPropertyPath,
isDynamicValue,
isPathADynamicBinding,
isPathADynamicTrigger,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import _ from "lodash";
import _, { cloneDeep } from "lodash";
import WidgetFactory from "utils/WidgetFactory";
import {
buildWidgetBlueprint,
@ -58,24 +61,23 @@ import {
import { resetWidgetMetaProperty } from "actions/metaActions";
import {
GridDefaults,
WidgetTypes,
MAIN_CONTAINER_WIDGET_ID,
WIDGET_DELETE_UNDO_TIMEOUT,
RenderModes,
WIDGET_DELETE_UNDO_TIMEOUT,
WidgetType,
WidgetTypes,
} from "constants/WidgetConstants";
import WidgetConfigResponse from "mockResponses/WidgetConfigResponse";
import {
flushDeletedWidgets,
getCopiedWidgets,
getDeletedWidgets,
saveCopiedWidgets,
saveDeletedWidgets,
flushDeletedWidgets,
getDeletedWidgets,
getCopiedWidgets,
} from "utils/storage";
import { generateReactKey } from "utils/generators";
import { flashElementById } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { cloneDeep } from "lodash";
import log from "loglevel";
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/WidgetEntity";
import {
@ -86,8 +88,8 @@ import { forceOpenPropertyPane } from "actions/widgetActions";
import { getDataTree } from "selectors/dataTreeSelectors";
import { DataTreeWidget } from "entities/DataTree/dataTreeFactory";
import {
validateProperty,
clearEvalPropertyCacheOfWidget,
validateProperty,
} from "./EvaluationsSaga";
import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer";
import { Toaster } from "components/ads/Toast";
@ -221,7 +223,7 @@ function* generateChildWidgets(
// Add the parentId prop to this widget
widget.parentId = parent.widgetId;
// 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;
return { widgetId: widget.widgetId, widgets };
}
@ -461,21 +463,21 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets(
action.payload.widgetId,
);
// Find the parent in the list of deleted widgets
const deletedWidget = deletedWidgets.find(
(widget) => widget.widgetId === action.payload.widgetId,
);
if (deletedWidgets && Array.isArray(deletedWidgets)) {
// Find the parent in the list of deleted widgets
const deletedWidget = deletedWidgets.find(
(widget) => widget.widgetId === action.payload.widgetId,
);
// If the deleted widget is infact available.
if (deletedWidget) {
// Log an undo event
AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", {
widgetName: deletedWidget.widgetName,
widgetType: deletedWidget.type,
});
}
// If the deleted widget is in fact available.
if (deletedWidget) {
// Log an undo event
AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", {
widgetName: deletedWidget.widgetName,
widgetType: deletedWidget.type,
});
}
if (deletedWidgets) {
// Get the current list of widgets from reducer
const stateWidgets = yield select(getWidgets);
let widgets = { ...stateWidgets };
@ -522,7 +524,7 @@ export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
}
let newChildren = [widget.widgetId];
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);
}
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,
propertyPath: string,
propertyValue: string,
) {
// TODO WIDGETFACTORY
const triggerProperties = WidgetFactory.getWidgetTriggerPropertiesMap(
widget.type,
);
if (propertyPath in triggerProperties) {
let dynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
widget,
);
if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
dynamicTriggerPathList.push({
key: propertyPath,
});
}
if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
dynamicTriggerPathList = _.reject(dynamicTriggerPathList, {
key: propertyValue,
});
}
yield put(
updateWidgetProperty(
widget.widgetId,
"dynamicTriggerPathList",
dynamicTriggerPathList,
),
);
return true;
): DynamicPathUpdate {
if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
} else if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
}
return false;
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
function* updateDynamicBindings(
function getDynamicBindingPathListUpdate(
widget: WidgetProps,
propertyName: string,
propertyPath: string,
propertyValue: any,
) {
): DynamicPathUpdate {
let stringProp = propertyValue;
if (_.isObject(propertyValue)) {
// Stringify this because composite controls may have bindings in the sub controls
stringProp = JSON.stringify(propertyValue);
}
const isDynamic = isDynamicValue(stringProp);
let dynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
widget,
);
if (!isDynamic && isPathADynamicBinding(widget, propertyName)) {
dynamicBindingPathList = _.reject(dynamicBindingPathList, {
key: propertyName,
});
if (!isDynamic && isPathADynamicBinding(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.REMOVE,
};
} else if (isDynamic && !isPathADynamicBinding(widget, propertyPath)) {
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.ADD,
};
}
if (isDynamic && !isPathADynamicBinding(widget, propertyName)) {
dynamicBindingPathList.push({
key: propertyName,
return {
propertyPath,
effect: DynamicPathUpdateEffectEnum.NOOP,
};
}
function applyDynamicPathUpdates(
currentList: DynamicPath[],
update: DynamicPathUpdate,
): DynamicPath[] {
if (update.effect === DynamicPathUpdateEffectEnum.ADD) {
currentList.push({
key: update.propertyPath,
});
} else if (update.effect === DynamicPathUpdateEffectEnum.REMOVE) {
_.reject(currentList, { key: update.propertyPath });
}
yield put(
updateWidgetProperty(
widget.widgetId,
"dynamicBindingPathList",
dynamicBindingPathList,
),
);
return currentList;
}
function* updateWidgetPropertySaga(
updateAction: ReduxAction<UpdateWidgetPropertyRequestPayload>,
) {
const {
payload: { propertyValue, propertyName, widgetId },
payload: { propertyValue, propertyPath, widgetId },
} = updateAction;
if (!widgetId) {
// 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 widget = { ...stateWidget };
const dynamicTriggersUpdated = yield updateDynamicTriggers(
widget,
propertyName,
propertyValue,
// 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,
);
if (!dynamicTriggersUpdated) {
yield updateDynamicBindings(widget, propertyName, propertyValue);
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,
);
const effect = getDynamicTriggerPathListUpdate(
widget,
propertyPath,
propertyValue,
);
updates.dynamicTriggerPathList = applyDynamicPathUpdates(
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 widgets = { ...stateWidgets, [widgetId]: widget };
// Save the layout
yield put(updateAndSaveLayout(widgets));
}
function* setWidgetDynamicPropertySaga(
action: ReduxAction<SetWidgetDynamicPropertyPayload>,
) {
const { isDynamic, propertyName, widgetId } = action.payload;
const { isDynamic, propertyPath, widgetId } = action.payload;
const widget: WidgetProps = yield select(getWidget, widgetId);
// const tree = yield select(evaluateDataTree);
const propertyValue = _.get(widget, propertyName);
const propertyValue = _.get(widget, propertyPath);
let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget);
const propertyUpdates: Record<string, unknown> = {};
if (isDynamic) {
dynamicPropertyPathList.push({
key: propertyName,
key: propertyPath,
});
const value = convertToString(propertyValue);
yield put(updateWidgetProperty(widgetId, propertyName, value));
propertyUpdates[propertyPath] = convertToString(propertyValue);
} else {
dynamicPropertyPathList = _.reject(dynamicPropertyPathList, {
key: propertyName,
key: propertyPath,
});
const { parsed } = yield call(
validateProperty,
widget.type,
propertyName,
propertyPath,
propertyValue,
widget,
);
yield put(updateWidgetProperty(widgetId, propertyName, parsed));
propertyUpdates[propertyPath] = parsed;
}
yield put(
updateWidgetProperty(
widgetId,
"dynamicPropertyPathList",
dynamicPropertyPathList,
),
propertyUpdates.dynamicPropertyPathList = dynamicPropertyPathList;
yield put(updateWidgetProperty(widgetId, propertyUpdates));
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 {
@ -774,11 +918,13 @@ function* getWidgetChildren(widgetId: string): any {
const { children } = widget;
if (children && children.length) {
for (const childIndex in children) {
const child = children[childIndex];
childrenIds.push(child);
const grandChildren = yield call(getWidgetChildren, child);
if (grandChildren.length) {
childrenIds.push(...grandChildren);
if (children.hasOwnProperty(childIndex)) {
const child = children[childIndex];
childrenIds.push(child);
const grandChildren = yield call(getWidgetChildren, child);
if (grandChildren.length) {
childrenIds.push(...grandChildren);
}
}
}
}
@ -803,6 +949,10 @@ function* resetEvaluatedWidgetMetaProperties(widgetIds: string[]) {
for (const index in widgetIds) {
const widgetId = widgetIds[index];
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 metaPropsMap = WidgetFactory.getWidgetMetaPropertiesMap(widget.type);
const defaultPropertiesMap = WidgetFactory.getWidgetDefaultPropertiesMap(
@ -843,7 +993,9 @@ function* updateCanvasSize(
// TODO(abhinav): This considers that the topRow will always be zero
// Check this out when non canvas widgets are updating snapRows
// 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;
const widgets = yield select(getWidgets);
const widgetsToStore = getAllWidgetsInTree(selectedWidget.widgetId, widgets);
const saveResult = yield saveCopiedWidgets(
return yield saveCopiedWidgets(
JSON.stringify({ widgetId: selectedWidget.widgetId, list: widgetsToStore }),
);
return saveResult;
}
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
// Every time a widget is selected, we need to expand widget entities
// in the entity explorer so that the selected widget is visible
function* selectedWidgetAncestorySaga(
function* selectedWidgetAncestrySaga(
action: ReduxAction<{ widgetId: string }>,
) {
try {
@ -1259,7 +1409,7 @@ function* selectedWidgetAncestorySaga(
payload: widgetIdsExpandList,
});
} 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,
resetChildrenMetaSaga,
),
takeEvery(
ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
batchUpdateWidgetPropertySaga,
),
takeEvery(
ReduxActionTypes.DELETE_WIDGET_PROPERTY,
deleteWidgetPropertySaga,
),
takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize),
takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga),
takeEvery(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga),
takeEvery(ReduxActionTypes.UNDO_DELETE_WIDGET, undoDeleteSaga),
takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga),
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 {
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) => {
if (getShouldEnforceDarkTheme()) {
return {
mode: state.ui.theme.mode,
theme: { ...theme, colors: { ...theme.colors, ...dark } },
};
}
return {
theme: state.ui.theme.theme,
mode: state.ui.theme.mode,

View File

@ -249,3 +249,12 @@ export const unsafeFunctionForEval = [
"setInterval",
"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([
["{{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 = {
navigateTo: {
"!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: {
"!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 {
return {
onTextChanged: true,
onSubmit: true,
};
}
@ -136,6 +137,22 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
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() {
const value = this.props.text || "";
const isInvalid =
@ -149,6 +166,7 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
if (this.props.maxChars) conditionalProps.maxChars = this.props.maxChars;
if (this.props.maxNum) conditionalProps.maxNum = this.props.maxNum;
if (this.props.minNum) conditionalProps.minNum = this.props.minNum;
return (
<InputComponent
value={value}
@ -168,6 +186,8 @@ class InputWidget extends BaseWidget<InputWidgetProps, WidgetState> {
stepSize={1}
onFocusChange={this.handleFocusChange}
showError={!!this.props.isFocused}
disableNewLineOnPressEnterKey={!!this.props.onSubmit}
onKeyDown={this.handleKeyDown}
{...conditionalProps}
/>
);
@ -215,6 +235,7 @@ export interface InputWidgetProps extends WidgetProps, WithMeta {
isRequired?: boolean;
isFocused?: boolean;
isDirty?: boolean;
onSubmit?: string;
}
export default InputWidget;

View File

@ -132,6 +132,16 @@ class MapWidget extends BaseWidget<MapWidgetProps, WidgetState> {
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() {
return (
<>

View File

@ -90,13 +90,13 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
searchText: VALIDATION_TYPES.TEXT,
defaultSearchText: VALIDATION_TYPES.TEXT,
defaultSelectedRow: VALIDATION_TYPES.DEFAULT_SELECTED_ROW,
pageSize: VALIDATION_TYPES.NUMBER,
};
}
static getMetaPropertiesMap(): Record<string, any> {
return {
pageNo: 1,
pageSize: undefined,
selectedRowIndex: undefined,
selectedRowIndices: undefined,
searchText: undefined,
@ -120,10 +120,54 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
onRowSelected: true,
onPageChange: true,
onSearchTextChanged: true,
onPageSizeChange: 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>>) => {
let columns: ReactTableColumnProps[] = [];
const hiddenColumns: ReactTableColumnProps[] = [];
@ -135,6 +179,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
} = this.props;
if (tableData.length) {
const columnKeys: string[] = getAllTableColumnKeys(tableData);
const { componentWidth } = this.getComponentDimensions();
const sortedColumn = this.props.sortedColumn;
for (let index = 0; index < columnKeys.length; index++) {
const i = columnKeys[index];
@ -170,7 +215,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
inputFormat: columnType.inputFormat,
},
Cell: (props: any) => {
return renderCell(props.cell.value, columnType.type, isHidden);
return renderCell(
props.cell.value,
columnType.type,
isHidden,
componentWidth,
);
},
};
if (isHidden) {
@ -361,7 +411,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
const columnKeys: string[] = getAllTableColumnKeys(this.props.tableData);
const selectedRow: { [key: string]: any } = {};
for (let i = 0; i < columnKeys.length; i++) {
selectedRow[columnKeys[i]] = undefined;
selectedRow[columnKeys[i]] = "";
}
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) => {
@ -502,7 +560,12 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
};
getPageView() {
const { tableData, hiddenColumns, filteredTableData } = this.props;
const {
tableData,
hiddenColumns,
filteredTableData,
pageSize,
} = this.props;
const computedSelectedRowIndices = Array.isArray(
this.props.selectedRowIndices,
)
@ -524,28 +587,6 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
this.props.updateWidgetMetaProperty("pageNo", pageNo);
}
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 (
<Suspense fallback={<Skeleton />}>
<ReactTableComponent
@ -563,6 +604,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
columnNameMap={this.props.columnNameMap}
columnTypeMap={this.props.columnTypeMap}
columnOrder={this.props.columnOrder}
triggerRowSelection={this.props.triggerRowSelection}
pageSize={Math.max(1, pageSize)}
onCommandClick={this.onCommandClick}
selectedRowIndex={
@ -590,7 +632,14 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
super.updateWidgetProperty("columnNameMap", columnNameMap);
}}
handleResizeColumn={(columnSizeMap: { [key: string]: number }) => {
super.updateWidgetProperty("columnSizeMap", columnSizeMap);
if (this.props.renderMode === RenderModes.CANVAS) {
super.updateWidgetProperty("columnSizeMap", columnSizeMap);
} else {
this.props.updateWidgetMetaProperty(
"columnSizeMap",
columnSizeMap,
);
}
}}
handleReorderColumn={(columnOrder: string[]) => {
super.updateWidgetProperty("columnOrder", columnOrder);
@ -607,11 +656,7 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
}}
compactMode={this.props.compactMode || CompactModeTypes.DEFAULT}
updateCompactMode={(compactMode: CompactMode) => {
if (this.props.renderMode === RenderModes.CANVAS) {
this.props.updateWidgetMetaProperty("compactMode", compactMode);
} else {
this.props.updateWidgetMetaProperty("compactMode", compactMode);
}
this.props.updateWidgetMetaProperty("compactMode", compactMode);
}}
sortTableColumn={this.handleColumnSorting}
/>

View File

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