Merge branch 'release' of https://github.com/appsmithorg/appsmith into fix/rte-newline

This commit is contained in:
vicky-primathon.in 2021-02-23 18:11:14 +05:30
commit b474442ec7
74 changed files with 2343 additions and 429 deletions

View File

@ -297,6 +297,23 @@
"contributions": [
"code"
]
},
{
"login": "zegerhoogeboom",
"name": "Zeger Hoogeboom",
"avatar_url": "https://avatars.githubusercontent.com/u/5371096?v=4",
"profile": "https://github.com/zegerhoogeboom",
"contributions": [
"code"
]
},
"login": "Devedunkey",
"name": "Young Yoo",
"avatar_url": "https://avatars.githubusercontent.com/u/29448764?v=4",
"profile": "https://github.com/Devedunkey",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -139,6 +139,8 @@ The Appsmith platform is available under the [Apache License 2.0](https://www.ap
<td align="center"><a href="https://github.com/vicky-primathon"><img src="https://avatars2.githubusercontent.com/u/67091118?v=4?s=100" width="100px;" alt=""/><br /><sub><b>vicky-primathon</b></sub></a><br /><a href="https://github.com/appsmithorg/appsmith/commits?author=vicky-primathon" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/devrk96"><img src="https://avatars0.githubusercontent.com/u/68607686?v=4?s=100" width="100px;" alt=""/><br /><sub><b>devrk96</b></sub></a><br /><a href="https://github.com/appsmithorg/appsmith/commits?author=devrk96" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/trdillon"><img src="https://avatars.githubusercontent.com/u/26350151?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tim Dillon</b></sub></a><br /><a href="https://github.com/appsmithorg/appsmith/commits?author=trdillon" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/zegerhoogeboom"><img src="https://avatars.githubusercontent.com/u/5371096?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Zeger Hoogeboom</b></sub></a><br /><a href="https://github.com/appsmithorg/appsmith/commits?author=zegerhoogeboom" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Devedunkey"><img src="https://avatars.githubusercontent.com/u/29448764?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Young Yoo</b></sub></a><br /><a href="https://github.com/appsmithorg/appsmith/commits?author=Devedunkey" title="Code">💻</a></td>
</tr>
</table>

View File

@ -0,0 +1,129 @@
{
"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": 10,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"size": "MODAL_SMALL",
"canEscapeKeyClose": true,
"detachFromLayout": true,
"canOutsideClickClose": true,
"shouldScrollContents": true,
"widgetName": "Modal1",
"children": [
{
"isVisible": true,
"widgetName": "Canvas1",
"detachFromLayout": true,
"canExtend": true,
"isDisabled": false,
"shouldScrollContents": false,
"children": [
{
"isVisible": true,
"widgetName": "Icon1",
"iconName": "cross",
"iconSize": 24,
"color": "#040627",
"type": "ICON_WIDGET",
"isLoading": false,
"leftColumn": 14,
"rightColumn": 16,
"topRow": 0,
"bottomRow": 1,
"parentId": "yyyrxs383y",
"widgetId": "kxdvolusyp",
"onClick": "{{closeModal('Modal1')}}"
},
{
"isVisible": true,
"text": "Modal Title",
"textStyle": "HEADING",
"textAlign": "LEFT",
"widgetName": "Text1",
"type": "TEXT_WIDGET",
"isLoading": false,
"leftColumn": 0,
"rightColumn": 10,
"topRow": 0,
"bottomRow": 1,
"parentId": "yyyrxs383y",
"widgetId": "3fugqdtg8g"
},
{
"isVisible": true,
"text": "Cancel",
"buttonStyle": "SECONDARY_BUTTON",
"widgetName": "Button1",
"isDisabled": false,
"isDefaultClickDisabled": true,
"type": "BUTTON_WIDGET",
"isLoading": false,
"leftColumn": 9,
"rightColumn": 12,
"topRow": 4,
"bottomRow": 5,
"parentId": "yyyrxs383y",
"widgetId": "6cjfbnjfa4"
},
{
"isVisible": true,
"text": "Confirm",
"buttonStyle": "PRIMARY_BUTTON",
"widgetName": "Button2",
"isDisabled": false,
"isDefaultClickDisabled": true,
"type": "BUTTON_WIDGET",
"isLoading": false,
"leftColumn": 12,
"rightColumn": 16,
"topRow": 4,
"bottomRow": 5,
"parentId": "yyyrxs383y",
"widgetId": "3tmnukkm7m"
}
],
"minHeight": 240,
"type": "CANVAS_WIDGET",
"isLoading": false,
"parentColumnSpace": 1,
"parentRowSpace": 1,
"leftColumn": 0,
"rightColumn": 444,
"topRow": 0,
"bottomRow": 240,
"parentId": "287u6pannr",
"widgetId": "yyyrxs383y"
}
],
"type": "MODAL_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 5,
"rightColumn": 11,
"topRow": 15,
"bottomRow": 21,
"parentId": "0",
"widgetId": "287u6pannr"
}
]
}
}

View File

@ -0,0 +1,42 @@
{
"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": 9,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"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": 1,
"rightColumn": 9,
"topRow": 7,
"bottomRow": 14,
"parentId": "0",
"widgetId": "7miqot30xy",
"dynamicBindingPathList": []
}
]
}
}

View File

@ -0,0 +1,80 @@
{
"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": 9,
"minHeight": 1292,
"parentColumnSpace": 1,
"dynamicBindingPathList": [],
"leftColumn": 0,
"children": [
{
"isVisible": true,
"isDisabled": false,
"datePickerType": "DATE_PICKER",
"label": "",
"dateFormat": "DD/MM/YYYY HH:mm",
"widgetName": "DatePicker1",
"defaultDate": "2021-02-05T10:53:12.791Z",
"version": 2,
"type": "DATE_PICKER_WIDGET2",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 5,
"rightColumn": 10,
"topRow": 0,
"bottomRow": 1,
"parentId": "0",
"widgetId": "w4htilgv5t"
},
{
"isVisible": true,
"text": "Label",
"textStyle": "LABEL",
"textAlign": "LEFT",
"widgetName": "Text1",
"version": 1,
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 1,
"rightColumn": 5,
"topRow": 3,
"bottomRow": 4,
"parentId": "0",
"widgetId": "voohxsv4t2"
},
{
"isVisible": true,
"text": "Label",
"textStyle": "LABEL",
"textAlign": "LEFT",
"widgetName": "Text2",
"version": 1,
"type": "TEXT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 8,
"rightColumn": 12,
"topRow": 3,
"bottomRow": 4,
"parentId": "0",
"widgetId": "xif8wugzjv"
}
]
}
}

View File

@ -0,0 +1,91 @@
{
"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,
"shouldScrollContents": false,
"widgetName": "Tabs1",
"tabs": [
{
"label": "Tab 1",
"id": "tab1",
"widgetId": "uxle0mrg8t"
},
{
"label": "Tab 2",
"id": "tab2",
"widgetId": "9hy1rmqb2f"
}
],
"shouldShowTabs": true,
"defaultTab": "Tab 1",
"type": "TABS_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,
"leftColumn": 2,
"rightColumn": 10,
"topRow": 8,
"bottomRow": 15,
"parentId": "0",
"widgetId": "y6rla3dsd6",
"children": [
{
"type": "CANVAS_WIDGET",
"tabId": "tab1",
"tabName": "Tab 1",
"widgetId": "uxle0mrg8t",
"parentId": "y6rla3dsd6",
"detachFromLayout": true,
"children": [],
"parentRowSpace": 1,
"parentColumnSpace": 1,
"leftColumn": 0,
"rightColumn": 592,
"topRow": 0,
"bottomRow": 280,
"isLoading": false,
"widgetName": "Canvas1",
"renderMode": "CANVAS"
},
{
"type": "CANVAS_WIDGET",
"tabId": "tab2",
"tabName": "Tab 2",
"widgetId": "9hy1rmqb2f",
"parentId": "y6rla3dsd6",
"detachFromLayout": true,
"children": [],
"parentRowSpace": 1,
"parentColumnSpace": 1,
"leftColumn": 0,
"rightColumn": 592,
"topRow": 0,
"bottomRow": 280,
"isLoading": false,
"widgetName": "Canvas2",
"renderMode": "CANVAS"
}
]
}
]
}
}

View File

@ -24,10 +24,10 @@
"Post": "POST",
"Delete": "DELETE",
"Patch": "PATCH",
"methodput": "api/users/2",
"methodpost": "api/users",
"methodpatch": "api/users/2",
"methoddelete": "1",
"methodput": "echo/put",
"methodpost": "echo/post",
"methodpatch": "echo/patch",
"methoddelete": "echo/delete",
"putAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'PUT')]",
"postAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'POST')]",
"patchAction": "//div[contains(@class, 't--dropdown-option')]//span[contains(text(),'PATCH')]",
@ -97,7 +97,7 @@
"btoaInput": "{{btoa('A')",
"defaultInputBinding": "{{Input2.text",
"tabBinding": "{{Tabs1.selectedTab",
"pageloadBinding": "{{PageLoadApi.data.data[1].id}}{{Input1.text}}",
"pageloadBinding": "{{PageLoadApi.data.users[1].id}}{{Input1.text}}",
"currentRowEmail": "{{currentRow.email}}",
"currentRowOrderAmt": "{{currentRow.orderAmount}}",
"momentDate": "{{moment()}}",

View File

@ -15,7 +15,7 @@ describe("Binding the API with pageOnLoad and input Widgets", function() {
it("Will load an api on load", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("PageLoadApi");
cy.enterDatasourceAndPath("https://reqres.in/api/", "users");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.WaitAutoSave();
cy.get(apiwidget.settings).click({ force: true });
cy.get(apiwidget.onPageLoad).click({ force: true });
@ -25,9 +25,7 @@ describe("Binding the API with pageOnLoad and input Widgets", function() {
it("Input widget updated with deafult data", function() {
cy.SearchEntityandOpen("Input1");
cy.get(widgetsPage.defaultInput)
.type(testdata.command)
.type("3");
cy.get(widgetsPage.defaultInput).type("3");
cy.get(commonlocators.editPropCrossButton).click();
cy.wait("@updateLayout").should(
"have.nested.property",
@ -53,13 +51,13 @@ describe("Binding the API with pageOnLoad and input Widgets", function() {
);
cy.PublishtheApp();
cy.get(publish.inputWidget + " " + "input")
.last()
.first()
.invoke("attr", "value")
.should("contain", "3");
cy.get(publish.inputWidget + " " + "input")
.last()
.invoke("attr", "value")
.should("contain", "2");
.should("contain", "23");
cy.get(publish.backToEditor)
.first()
.click();

View File

@ -63,45 +63,45 @@ describe("DatePicker Widget Functionality", function() {
);
});
it("Datepicker min/max date validation", function() {
cy.get(formWidgetsPage.defaultDate).click({ force: true });
cy.SetDateToToday();
// it("Datepicker min/max date validation", function() {
// cy.get(formWidgetsPage.defaultDate).click({ force: true });
// cy.SetDateToToday();
cy.get(formWidgetsPage.minDate)
.first()
.click();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.setDate(-1, "ddd MMM DD YYYY");
// cy.get(formWidgetsPage.minDate)
// .first()
// .click();
// // eslint-disable-next-line cypress/no-unnecessary-waiting
// cy.wait(1000);
// cy.setDate(-1, "ddd MMM DD YYYY");
cy.get(formWidgetsPage.maxDate)
.first()
.click();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
cy.setDate(1, "ddd MMM DD YYYY");
// cy.get(formWidgetsPage.maxDate)
// .first()
// .click();
// // eslint-disable-next-line cypress/no-unnecessary-waiting
// cy.wait(1000);
// cy.setDate(1, "ddd MMM DD YYYY");
cy.PublishtheApp();
cy.get(publishPage.datepickerWidget + " .bp3-input").click();
// cy.PublishtheApp();
// cy.get(publishPage.datepickerWidget + " .bp3-input").click();
const minDate = Cypress.moment()
.add(2, "days")
.format("ddd MMM DD YYYY");
const maxDate = Cypress.moment()
.add(2, "days")
.format("ddd MMM DD YYYY");
// const minDate = Cypress.moment()
// .add(2, "days")
// .format("ddd MMM DD YYYY");
// const maxDate = Cypress.moment()
// .add(2, "days")
// .format("ddd MMM DD YYYY");
cy.get(`.DayPicker-Day[aria-label=\"${minDate}\"]`).should(
"have.attr",
"aria-disabled",
"true",
);
cy.get(`.DayPicker-Day[aria-label=\"${maxDate}\"]`).should(
"have.attr",
"aria-disabled",
"true",
);
});
// cy.get(`.DayPicker-Day[aria-label=\"${minDate}\"]`).should(
// "have.attr",
// "aria-disabled",
// "true",
// );
// cy.get(`.DayPicker-Day[aria-label=\"${maxDate}\"]`).should(
// "have.attr",
// "aria-disabled",
// "true",
// );
// });
// it("Datepicker default date validation", function() {
// cy.get(formWidgetsPage.defaultDate).click();

View File

@ -0,0 +1,25 @@
const dsl = require("../../../fixtures/ModalDsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
const explorer = require("../../../locators/explorerlocators.json");
describe("Modal Widget Functionality", function() {
beforeEach(() => {
cy.addDsl(dsl);
});
it("Add new Modal", () => {
cy.get(explorer.addWidget).click();
cy.dragAndDropToCanvas("modalwidget", { x: 300, y: -300 });
cy.get(".t--modal-widget").should("exist");
});
it("Open Existing Modal from created Widgets list", () => {
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({
multiple: true,
});
cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Modal1)").click({
multiple: true,
});
cy.get(".t--modal-widget").should("exist");
});
});

View File

@ -0,0 +1,28 @@
const commonlocators = require("../../../locators/commonlocators.json");
const Layoutpage = require("../../../locators/Layout.json");
const widgetsPage = require("../../../locators/Widgets.json");
const publish = require("../../../locators/publishWidgetspage.json");
const dsl = require("../../../fixtures/basicTabledsl.json");
const pages = require("../../../locators/Pages.json");
const tabname = "UpdatedTab";
describe("Tab widget test", function() {
const apiName = "Table1";
const tableName = "Table";
before(() => {
cy.addDsl(dsl);
});
it("Rename API with table widget name validation test", function() {
cy.log("Login Successful");
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateApiAndValidateUniqueEntityName(apiName);
});
it("Rename Table widget with api name validation test", function() {
cy.GlobalSearchEntity("Table1");
cy.RenameEntity(tableName);
cy.validateMessage(tableName);
});
});

View File

@ -0,0 +1,70 @@
const commonlocators = require("../../../locators/commonlocators.json");
const Layoutpage = require("../../../locators/Layout.json");
const widgetsPage = require("../../../locators/Widgets.json");
const publish = require("../../../locators/publishWidgetspage.json");
const dsl = require("../../../fixtures/tabdsl.json");
const pages = require("../../../locators/Pages.json");
const tabname = "UpdatedTab";
describe("Tab widget test", function() {
const tabname = "UpdatedTab";
before(() => {
cy.addDsl(dsl);
});
it("Tab Widget Functionality To rename Tabs from entity explorer", function() {
cy.GlobalSearchEntity("Tab 1");
cy.RenameEntity(tabname);
});
it("Tab name validation in properties and widget ", function() {
cy.openPropertyPane("tabswidget");
cy.closePropertyPane();
cy.get(Layoutpage.tabWidget)
.contains(tabname)
.click({ force: true })
.should("be.visible");
});
it("Tab Widget Functionality To delete Tabs from entity explorer", function() {
cy.GlobalSearchEntity("Tab 2");
cy.RenameEntity(tabname);
cy.validateMessage(tabname);
cy.deleteEntity();
cy.get(commonlocators.entityExplorersearch).should("be.visible");
cy.get(commonlocators.entityExplorersearch)
.clear()
.type("Tab 2");
cy.get(
commonlocators.entitySearchResult.concat("Tab 2").concat("')"),
).should("not.exist");
});
/* To be enabled once the bug is fixed
it("Publish app and check for the widget name", function() {
cy.PublishtheApp();
cy.get(publish.tabWidget)
.contains(tabname)
.click({ force: true })
.should("be.selected");
cy.get(publish.tabWidget)
.contains("Tab 2")
.click({ force: true })
.should("be.selected");
});
it("Tab Widget Functionality To Unchecked Visible Widget", function() {
cy.get(publish.backToEditor).click();
cy.openPropertyPane("tabswidget");
cy.closePropertyPane();
cy.get(Layoutpage.tabWidget)
.contains("Tab 2")
.click({ force: true })
.should("not.be.visible");
});
*/
});
afterEach(() => {
// put your clean up code if any
});

View File

@ -0,0 +1,40 @@
const commonlocators = require("../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../locators/FormWidgets.json");
const dsl = require("../../../fixtures/datePicker2dsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
const pages = require("../../../locators/Pages.json");
describe("DatePicker Widget Property pane tests with js bindings", function() {
before(() => {
cy.addDsl(dsl);
});
it("Datepicker default date validation with js binding and default date", function() {
cy.openPropertyPane("datepickerwidget2");
cy.get(".t--property-control-defaultdate .bp3-input").clear();
cy.get(formWidgetsPage.toggleJsDefaultDate).click();
cy.testJsontext("defaultdate", "{{ moment().add(-1,'days') }}");
});
it("Text widgets binding with datepicker", function() {
cy.SearchEntityandOpen("Text1");
cy.testJsontext("text", "{{DatePicker1.formattedDate}}");
cy.closePropertyPane();
cy.SearchEntityandOpen("Text2");
cy.testJsontext("text", "{{DatePicker1.selectedDate}}");
cy.closePropertyPane();
});
it("Text widgets binding with datepicker", function() {
cy.openPropertyPane("datepickerwidget2");
cy.selectDateFormat("DD/MM/YYYY");
cy.assertDateFormat();
cy.closePropertyPane();
cy.assertDateFormat();
});
it("Datepicker default date validation with js binding", function() {
cy.PublishtheApp();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000);
});
});

View File

@ -0,0 +1,80 @@
const commonlocators = require("../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../locators/FormWidgets.json");
const dsl = require("../../../fixtures/datePicker2dsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
const pages = require("../../../locators/Pages.json");
describe("DatePicker Widget Property pane tests with js bindings", function() {
before(() => {
cy.addDsl(dsl);
});
it("Datepicker default date validation with js binding", function() {
cy.openPropertyPane("datepickerwidget2");
cy.get(".t--property-control-defaultdate .bp3-input").clear();
cy.get(formWidgetsPage.toggleJsDefaultDate).click();
cy.testJsontext("defaultdate", "{{moment().toISOString()}}");
cy.get(formWidgetsPage.toggleJsMinDate).click();
cy.testJsontext(
"mindate",
"{{moment().subtract(10, 'days').toISOString()}}",
);
cy.get(formWidgetsPage.toggleJsMaxDate).click();
cy.testJsontext("maxdate", "{{moment().add(10, 'days').toISOString()}}");
/*
cy.get(formWidgetsPage.datepickerWidget + " .bp3-input").should(
"contain.value",
"14/02/2021",
);
cy.PublishtheApp();
cy.get(publishPage.datepickerWidget + " .bp3-input").should(
"contain.value",
"14/02/2021",
);
*/
});
it("Text widgets binding with datepicker", function() {
cy.SearchEntityandOpen("Text1");
cy.testJsontext("text", "{{DatePicker1.formattedDate}}");
cy.closePropertyPane();
cy.SearchEntityandOpen("Text2");
cy.testJsontext("text", "{{DatePicker1.selectedDate}}");
cy.closePropertyPane();
});
it("Text widgets binding with datepicker", function() {
cy.openPropertyPane("datepickerwidget2");
cy.selectDateFormat("YYYY-MM-DD");
cy.assertDateFormat();
cy.selectDateFormat("YYYY-MM-DD HH:mm");
cy.assertDateFormat();
cy.selectDateFormat("YYYY-MM-DDTHH:mm:ss.sssZ");
cy.assertDateFormat();
cy.selectDateFormat("DD/MM/YYYY");
cy.assertDateFormat();
cy.selectDateFormat("DD/MM/YYYY HH:mm");
cy.closePropertyPane();
cy.assertDateFormat();
});
it("Datepicker default date validation with strings", function() {
cy.openPropertyPane("datepickerwidget2");
cy.get(formWidgetsPage.toggleJsDefaultDate).click();
cy.get(".t--property-control-defaultdate .bp3-input").clear();
cy.get(".t--property-control-defaultdate .bp3-input").type("2020-02-01");
cy.closePropertyPane();
cy.openPropertyPane("datepickerwidget2");
cy.get(formWidgetsPage.toggleJsMinDate).click();
cy.get(".t--property-control-mindate .bp3-input").type("2020-01-01");
cy.get(formWidgetsPage.toggleJsMaxDate).click();
cy.get(".t--property-control-maxdate .bp3-input").type("2020-02-10");
cy.closePropertyPane();
});
it("Datepicker default date validation with js binding", function() {
cy.PublishtheApp();
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000);
});
});

View File

@ -0,0 +1,47 @@
const commonlocators = require("../../../locators/commonlocators.json");
const formWidgetsPage = require("../../../locators/FormWidgets.json");
const dsl = require("../../../fixtures/newFormDsl.json");
const publishPage = require("../../../locators/publishWidgetspage.json");
const pages = require("../../../locators/Pages.json");
describe("DatePicker Widget Property pane tests with js bindings", function() {
before(() => {
cy.addDsl(dsl);
});
beforeEach(() => {
cy.openPropertyPane("datepickerwidget");
});
it("Datepicker default date validation with js binding", function() {
cy.get(".t--property-control-defaultdate .bp3-input").clear();
cy.get(formWidgetsPage.toggleJsDefaultDate).click();
cy.testJsontext(
"defaultdate",
"{{moment('14/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}",
);
cy.get(formWidgetsPage.toggleJsMinDate).click();
cy.testJsontext(
"mindate",
"{{moment('12/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}",
);
cy.get(formWidgetsPage.toggleJsMaxDate).click();
cy.testJsontext(
"maxdate",
"{{moment('17/02/2021', 'DD/MM/YYYY').format('DD/MM/YYYY')}}",
);
cy.get(formWidgetsPage.datepickerWidget + " .bp3-input").should(
"contain.value",
"14/02/2021",
);
cy.PublishtheApp();
cy.get(publishPage.datepickerWidget + " .bp3-input").should(
"contain.value",
"14/02/2021",
);
});
afterEach(() => {
cy.get(publishPage.backToEditor).click({ force: true });
});
});

View File

@ -1,5 +1,6 @@
const dsl = require("../../../../fixtures/tableWidgetDsl.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const testdata = require("../../../../fixtures/testdata.json");
describe("API Panel Test Functionality", function() {
before(() => {
@ -8,7 +9,7 @@ describe("API Panel Test Functionality", function() {
it("Will load an api on load", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("PageLoadApi");
cy.enterDatasourceAndPath("https://reqres.in/api/", "users");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.WaitAutoSave();
cy.get("li:contains('Settings')").click({ force: true });
cy.get("[data-cy=executeOnLoad]").click({ force: true });

View File

@ -21,7 +21,7 @@ describe("API Panel Test Functionality", function() {
cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.putAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl2,
testdata.baseUrl,
testdata.methodput,
testdata.headerKey,
testdata.headerValue,
@ -35,7 +35,7 @@ describe("API Panel Test Functionality", function() {
.type(json, { force: true });
cy.WaitAutoSave();
cy.RunAPI();
cy.validateRequest(testdata.baseUrl2, testdata.methodput, testdata.Put);
cy.validateRequest(testdata.baseUrl, testdata.methodput, testdata.Put);
});
cy.ResponseStatusCheck("200 OK");
cy.log("Response code check successful");
@ -48,7 +48,7 @@ describe("API Panel Test Functionality", function() {
cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.postAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl2,
testdata.baseUrl,
testdata.methodpost,
testdata.headerKey,
testdata.headerValue,
@ -62,7 +62,7 @@ describe("API Panel Test Functionality", function() {
.type(json, { force: true });
cy.WaitAutoSave();
cy.RunAPI();
cy.validateRequest(testdata.baseUrl2, testdata.methodpost, testdata.Post);
cy.validateRequest(testdata.baseUrl, testdata.methodpost, testdata.Post);
});
cy.ResponseStatusCheck("201 CREATED");
cy.log("Response code check successful");
@ -75,7 +75,7 @@ describe("API Panel Test Functionality", function() {
cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.patchAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl2,
testdata.baseUrl,
testdata.methodpatch,
testdata.headerKey,
testdata.headerValue,
@ -90,7 +90,7 @@ describe("API Panel Test Functionality", function() {
cy.WaitAutoSave();
cy.RunAPI();
cy.validateRequest(
testdata.baseUrl2,
testdata.baseUrl,
testdata.methodpatch,
testdata.Patch,
);
@ -106,19 +106,19 @@ describe("API Panel Test Functionality", function() {
cy.log("Creation of FirstAPI Action successful");
cy.SelectAction(testdata.deleteAction);
cy.EnterSourceDetailsWithbody(
testdata.baseUrl2,
testdata.methodpatch,
testdata.baseUrl,
testdata.methoddelete,
testdata.headerKey,
testdata.headerValue,
);
cy.WaitAutoSave();
cy.RunAPI();
cy.validateRequest(
testdata.baseUrl2,
testdata.methodpatch,
testdata.baseUrl,
testdata.methoddelete,
testdata.Delete,
);
cy.ResponseStatusCheck("204 NO_CONTENT");
cy.ResponseStatusCheck("200");
cy.log("Response code check successful");
});

View File

@ -7,7 +7,7 @@ describe("Test curl import flow", function() {
cy.NavigateToApiEditor();
cy.get(ApiEditor.curlImage).click({ force: true });
cy.get("textarea").type(
'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://reqres.in/api/users',
'curl -d \'{"name":"morpheus","job":"leader"}\' -H Content-Type:application/json -X POST https://mock-api.appsmith.com/echo/post',
{
force: true,
parseSpecialCharSequences: false,

View File

@ -11,12 +11,14 @@ describe("Moustache test Functionality", function() {
it("Moustache test Functionality", function() {
cy.openPropertyPane("textwidget");
cy.widgetText("Api", widgetsPage.textWidget, widgetsPage.textInputval);
cy.testCodeMirror("/api/users/2");
cy.testCodeMirror("users");
cy.NavigateToAPI_Panel();
cy.log("Navigation to API Panel screen successful");
cy.CreateAPI("TestAPINew");
cy.log("Creation of API Action successful");
cy.enterDatasourceAndPath(testdata.baseUrl2, testdata.moustacheMethod);
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.moustacheMethod);
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300);
cy.RunAPI();
cy.ResponseStatusCheck(testdata.successStatusCode);
cy.log("Response code check successful");

View File

@ -1,8 +1,10 @@
const testdata = require("../../../../fixtures/testdata.json");
describe("Datasource form related tests", function() {
it("Check whether the delete button has the right color", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("Testapi");
cy.enterDatasourceAndPath("https://reqres.in/api/", "users");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.get(".t--store-as-datasource").click();

View File

@ -1,13 +1,15 @@
const testdata = require("../../../../fixtures/testdata.json");
describe("Create a rest datasource", function() {
it("Create a rest datasource", function() {
cy.NavigateToAPI_Panel();
cy.CreateAPI("Testapi");
cy.enterDatasourceAndPath("https://reqres.in/api/", "users");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.get(".t--store-as-datasource").click();
cy.saveDatasource();
cy.contains(".datasource-highlight", "https://reqres.in");
cy.contains(".datasource-highlight", "https://mock-api.appsmith.com");
cy.SaveAndRunAPI();
});

View File

@ -1,37 +1,36 @@
{
"checkboxWidget": ".t--draggable-checkboxwidget",
"dropdownWidget": ".t--draggable-dropdownwidget",
"dropdownSelectionType": ".t--property-control-selectiontype",
"radioWidget": ".t--draggable-radiogroupwidget",
"radioOnSelectionChangeDropdown": ".t--property-control-onselectionchange",
"nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day",
"datepickerWidget": ".t--draggable-datepickerwidget",
"defaultDate": ".t--property-control-defaultdate input",
"minDate": ".t--property-control-mindate input",
"maxDate": ".t--property-control-maxdate input",
"filepickerWidget": ".t--draggable-filepickerwidget",
"formWidget": ".t--draggable-formwidget",
"richTextEditorWidget": ".t--draggable-richtexteditorwidget",
"richEditorOnTextChange": ".t--property-control-ontextchange",
"optionvalue": ".t--property-control-defaultselectedvalue .kDwnRc",
"defselected": ".CodeMirror textarea",
"dropdowninner": ".bp3-button > .bp3-button-text",
"Textinput": ".t--property-control-options .CodeMirror-code",
"labelvalue": ".t--draggable-dropdownwidget label",
"dropdownInput": ".bp3-tag-input-values",
"labelradio": ".t--draggable-radiogroupwidget label",
"deleteradiovalue": ".t--property-control-options mask",
"inputRadio": ".t--draggable-radiogroupwidget input",
"defaultSelect": ".t--property-control-defaultselectedvalue .CodeMirror-code",
"formInner": ".t--draggable-formwidget span.t--widget-name",
"radioInput": ".t--draggable-radiogroupwidget span.t--widget-name",
"radioAddButton": ".t--property-control-options button",
"formD": "div[type='FORM_WIDGET']",
"datepickerFooter": ".bp3-datepicker-footer span",
"disableJs":".t--property-control-disable input[type='checkbox']",
"switchWidget": ".t--draggable-switchwidget",
"toggleJsDefaultDate": ".t--property-control-defaultdate .t--js-toggle",
"toggleJsMinDate": ".t--property-control-mindate .t--js-toggle",
"toggleJsMaxDate": ".t--property-control-maxdate .t--js-toggle"
}
"checkboxWidget": ".t--draggable-checkboxwidget",
"dropdownWidget": ".t--draggable-dropdownwidget",
"dropdownSelectionType": ".t--property-control-selectiontype",
"radioWidget": ".t--draggable-radiogroupwidget",
"radioOnSelectionChangeDropdown": ".t--property-control-onselectionchange",
"nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day",
"datepickerWidget": ".t--draggable-datepickerwidget",
"defaultDate": ".t--property-control-defaultdate input",
"minDate": ".t--property-control-mindate input",
"maxDate": ".t--property-control-maxdate input",
"filepickerWidget": ".t--draggable-filepickerwidget",
"formWidget": ".t--draggable-formwidget",
"richTextEditorWidget": ".t--draggable-richtexteditorwidget",
"richEditorOnTextChange": ".t--property-control-ontextchange",
"optionvalue": ".t--property-control-defaultselectedvalue .kDwnRc",
"defselected": ".CodeMirror textarea",
"dropdowninner": ".bp3-button > .bp3-button-text",
"Textinput": ".t--property-control-options .CodeMirror-code",
"labelvalue": ".t--draggable-dropdownwidget label",
"dropdownInput": ".bp3-tag-input-values",
"labelradio": ".t--draggable-radiogroupwidget label",
"deleteradiovalue": ".t--property-control-options mask",
"inputRadio": ".t--draggable-radiogroupwidget input",
"defaultSelect": ".t--property-control-defaultselectedvalue .CodeMirror-code",
"formInner": ".t--draggable-formwidget span.t--widget-name",
"radioInput": ".t--draggable-radiogroupwidget span.t--widget-name",
"radioAddButton": ".t--property-control-options button",
"formD": "div[type='FORM_WIDGET']",
"datepickerFooter": ".bp3-datepicker-footer span",
"disableJs": ".t--property-control-disable input[type='checkbox']",
"switchWidget": ".t--draggable-switchwidget",
"toggleJsDefaultDate": ".t--property-control-defaultdate .t--js-toggle",
"toggleJsMinDate": ".t--property-control-mindate .t--js-toggle",
"toggleJsMaxDate": ".t--property-control-maxdate .t--js-toggle"
}

View File

@ -51,5 +51,6 @@
"propertyList": ".t--entity-property",
"actionlist": ".action div div",
"settings": "li:contains('Settings')",
"onPageLoad": "[data-cy=executeOnLoad]"
"onPageLoad": "[data-cy=executeOnLoad]",
"renameEntity": ".single-select >div:contains('Edit Name')"
}

View File

@ -25,5 +25,6 @@
"addWidget":".widgets .t--entity-add-btn",
"dropHere":".appsmith_widget_0",
"closeWidgets":".t--close-widgets-sidebar",
"addDBQueryEntity": ".dbqueries .t--entity-add-btn"
"addDBQueryEntity": ".dbqueries .t--entity-add-btn",
"editEntity": ".t--entity-name input"
}

View File

@ -690,6 +690,37 @@ Cypress.Commands.add("switchToAPIInputTab", () => {
.click({ force: true });
});
Cypress.Commands.add("selectDateFormat", (value) => {
cy.get(".t--property-control-dateformat button")
.first()
.click({ force: true });
cy.get("ul.bp3-menu")
.children()
.contains(value)
.click();
});
Cypress.Commands.add("assertDateFormat", () => {
cy.get(".t--draggable-datepickerwidget2 input")
.first()
.invoke("attr", "value")
.then((text) => {
const firstTxt = text;
cy.log("date time : ", firstTxt);
cy.get(commonlocators.labelTextStyle)
.first()
.should("contain", firstTxt);
cy.get(commonlocators.labelTextStyle)
.last()
.invoke("text")
.then((text) => {
const secondText = text;
cy.log("date time : ", secondText);
expect(firstTxt).not.to.equal(secondText);
});
});
});
Cypress.Commands.add("selectPaginationType", (option) => {
cy.xpath(option).click({ force: true });
});
@ -825,6 +856,40 @@ Cypress.Commands.add("CopyAPIToHome", () => {
);
});
Cypress.Commands.add("RenameEntity", (value) => {
cy.xpath(apiwidget.popover)
.last()
.click({ force: true });
cy.get(apiwidget.renameEntity).click({ force: true });
cy.wait(2000);
cy.get(explorer.editEntity)
.last()
.type(value, { force: true });
cy.wait(3000);
});
Cypress.Commands.add("CreateApiAndValidateUniqueEntityName", (apiname) => {
cy.get(apiwidget.createapi).click({ force: true });
cy.wait("@createNewApi");
cy.get(apiwidget.resourceUrl).should("be.visible");
cy.get(apiwidget.ApiName).click({ force: true });
cy.get(apiwidget.apiTxt)
.clear()
.type(apiname, { force: true })
.should("have.value", apiname);
cy.get(".t--nameOfApi .error-message").should(($x) => {
console.log($x);
expect($x).contain(apiname.concat(" is already being used."));
});
});
Cypress.Commands.add("validateMessage", (value) => {
cy.get(".bp3-popover-content").should(($x) => {
console.log($x);
expect($x).contain(value.concat(" is already being used."));
});
});
Cypress.Commands.add("DeleteAPIFromSideBar", () => {
cy.deleteEntity();
cy.wait("@deleteAction").should(

View File

@ -8,7 +8,7 @@ import { ControlIcons } from "icons/ControlIcons";
import { AnyStyledComponent } from "styled-components";
import { Skin } from "constants/DefaultTheme";
import AutoToolTipComponent from "components/designSystems/appsmith/TableComponent/AutoToolTipComponent";
import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent";
import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent2";
import {
OperatorTypes,
Condition,

View File

@ -163,7 +163,7 @@ const ReactTableComponent = (props: ReactTableComponentProps) => {
row: { original: Record<string, unknown>; index: number },
isSelected: boolean,
) => {
if (!isSelected || !!props.multiRowSelection) {
if (!isSelected || props.multiRowSelection || !props.multiRowSelection) {
props.onRowClick(row.original, row.index);
}
};

View File

@ -1,6 +1,10 @@
import React from "react";
import styled from "styled-components";
import { getBorderCSSShorthand, labelStyle } from "constants/DefaultTheme";
import {
getBorderCSSShorthand,
labelStyle,
IntentColors,
} from "constants/DefaultTheme";
import { ControlGroup, Classes, Label } from "@blueprintjs/core";
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
import { DateInput } from "@blueprintjs/datetime";
@ -11,24 +15,30 @@ import { WIDGET_PADDING } from "constants/WidgetConstants";
import { TimePrecision } from "@blueprintjs/datetime";
import { Colors } from "constants/Colors";
import { ISO_DATE_FORMAT } from "constants/WidgetValidation";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import { DATE_WIDGET_DEFAULT_VALIDATION_ERROR } from "constants/messages";
const StyledControlGroup = styled(ControlGroup)`
const StyledControlGroup = styled(ControlGroup)<{ isValid: boolean }>`
&&& {
.${Classes.INPUT} {
box-shadow: none;
color: ${Colors.OXFORD_BLUE};
font-size: ${(props) => props.theme.fontSizes[3]}px;
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.GEYSER_LIGHT};
border-radius: 0;
width: 100%;
height: inherit;
align-items: center;
&:active {
border-color: ${Colors.HIT_GRAY};
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.HIT_GRAY};
}
&:focus {
border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])};
border-color: #80bdff;
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : "#80bdff"};
outline: 0;
box-shadow: 0 0 0 0.1rem rgba(0, 123, 255, 0.25);
}
@ -95,10 +105,17 @@ class DatePickerComponent extends React.Component<
.clone()
.set({ month: 11, date: 31, year: year + 20 })
.toDate();
const isValid = this.state.selectedDate
? this.isValidDate(this.parseDate(this.state.selectedDate))
: true;
const value =
isValid && this.state.selectedDate
? this.parseDate(this.state.selectedDate)
: null;
return (
<StyledControlGroup
fill
isValid={isValid}
onClick={(e: any) => {
e.stopPropagation();
}}
@ -115,29 +132,58 @@ class DatePickerComponent extends React.Component<
</Label>
)}
{
<DateInput
className={this.props.isLoading ? "bp3-skeleton" : ""}
formatDate={this.formatDate}
parseDate={this.parseDate}
placeholder={"Select Date"}
disabled={this.props.isDisabled}
showActionsBar={true}
timePrecision={TimePrecision.MINUTE}
closeOnSelection
onChange={this.onDateSelected}
value={
this.state.selectedDate
? this.parseDate(this.state.selectedDate)
: null
}
minDate={minDate}
maxDate={maxDate}
/>
<ErrorTooltip
isOpen={!isValid}
message={DATE_WIDGET_DEFAULT_VALIDATION_ERROR}
>
<DateInput
className={this.props.isLoading ? "bp3-skeleton" : ""}
formatDate={this.formatDate}
parseDate={this.parseDate}
placeholder={"Select Date"}
disabled={this.props.isDisabled}
showActionsBar={true}
timePrecision={TimePrecision.MINUTE}
closeOnSelection
onChange={this.onDateSelected}
value={value}
minDate={minDate}
maxDate={maxDate}
/>
</ErrorTooltip>
}
</StyledControlGroup>
);
}
isValidDate = (date: Date): boolean => {
let isValid = true;
const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT;
const parsedCurrentDate = moment(date);
if (this.props.minDate) {
const parsedMinDate = moment(this.props.minDate, dateFormat);
if (
this.props.minDate &&
parsedMinDate.isValid() &&
parsedCurrentDate.isBefore(parsedMinDate)
) {
isValid = false;
}
}
if (this.props.maxDate) {
const parsedMaxDate = moment(this.props.maxDate, dateFormat);
if (
isValid &&
this.props.maxDate &&
parsedMaxDate.isValid() &&
parsedCurrentDate.isAfter(parsedMaxDate)
) {
isValid = false;
}
}
return isValid;
};
formatDate = (date: Date): string => {
const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT;
return moment(date).format(dateFormat);
@ -165,9 +211,6 @@ class DatePickerComponent extends React.Component<
const date = selectedDate ? this.formatDate(selectedDate) : "";
this.setState({ selectedDate: date });
// if date is null ( if date is cleared ), don't call onDateSelected
if (!selectedDate) return false;
onDateSelected(date);
}
};

View File

@ -0,0 +1,236 @@
import React from "react";
import styled from "styled-components";
import { labelStyle, IntentColors } from "constants/DefaultTheme";
import { ControlGroup, Classes, Label } from "@blueprintjs/core";
import { ComponentProps } from "components/designSystems/appsmith/BaseComponent";
import { DateInput } from "@blueprintjs/datetime";
import moment from "moment-timezone";
import "../../../../node_modules/@blueprintjs/datetime/lib/css/blueprint-datetime.css";
import { DatePickerType } from "widgets/DatePickerWidget";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { TimePrecision } from "@blueprintjs/datetime";
import { Colors } from "constants/Colors";
import { ISO_DATE_FORMAT } from "constants/WidgetValidation";
import ErrorTooltip from "components/editorComponents/ErrorTooltip";
import { DATE_WIDGET_DEFAULT_VALIDATION_ERROR } from "constants/messages";
const StyledControlGroup = styled(ControlGroup)<{ isValid: boolean }>`
&&& {
.${Classes.INPUT} {
box-shadow: none;
border: 1px solid;
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.GEYSER_LIGHT};
border-radius: ${(props) => props.theme.radii[1]}px;
width: 100%;
height: inherit;
align-items: center;
&:active {
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.HIT_GRAY};
}
&:focus {
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.MYSTIC};
}
}
.${Classes.INPUT_GROUP} {
display: block;
margin: 0;
}
.${Classes.CONTROL_GROUP} {
justify-content: flex-start;
}
label {
${labelStyle}
flex: 0 1 30%;
margin: 7px ${WIDGET_PADDING * 2}px 0 0;
text-align: right;
align-self: flex-start;
max-width: calc(30% - ${WIDGET_PADDING}px);
}
}
&&& {
input {
border: 1px solid;
border-color: ${(props) =>
!props.isValid ? IntentColors.danger : Colors.HIT_GRAY};
border-radius: ${(props) => props.theme.radii[1]}px;
box-shadow: none;
color: ${Colors.OXFORD_BLUE};
font-size: ${(props) => props.theme.fontSizes[3]}px;
}
}
`;
class DatePickerComponent extends React.Component<
DatePickerComponentProps,
DatePickerComponentState
> {
constructor(props: DatePickerComponentProps) {
super(props);
this.state = {
selectedDate: props.selectedDate,
};
}
componentDidUpdate(prevProps: DatePickerComponentProps) {
if (
this.props.selectedDate !== this.state.selectedDate &&
!moment(this.props.selectedDate).isSame(
moment(prevProps.selectedDate),
"seconds",
)
) {
this.setState({ selectedDate: this.props.selectedDate });
}
}
getValidDate = (date: string, format: string) => {
const _date = moment(date, format);
return _date.isValid() ? _date.toDate() : undefined;
};
render() {
const now = moment();
const year = now.get("year");
const minDate = this.props.minDate
? new Date(this.props.minDate)
: now
.clone()
.set({ month: 0, date: 1, year: year - 100 })
.toDate();
const maxDate = this.props.maxDate
? new Date(this.props.maxDate)
: now
.clone()
.set({ month: 11, date: 31, year: year + 20 })
.toDate();
const isValid = this.state.selectedDate
? this.isValidDate(new Date(this.state.selectedDate))
: true;
const value =
isValid && this.state.selectedDate
? new Date(this.state.selectedDate)
: null;
return (
<StyledControlGroup
fill
isValid={isValid}
onClick={(e: any) => {
e.stopPropagation();
}}
>
{this.props.label && (
<Label
className={
this.props.isLoading
? Classes.SKELETON
: Classes.TEXT_OVERFLOW_ELLIPSIS
}
>
{this.props.label}
</Label>
)}
{
<ErrorTooltip
isOpen={!isValid}
message={DATE_WIDGET_DEFAULT_VALIDATION_ERROR}
>
<DateInput
className={this.props.isLoading ? "bp3-skeleton" : ""}
formatDate={this.formatDate}
parseDate={this.parseDate}
placeholder={"Select Date"}
disabled={this.props.isDisabled}
showActionsBar={true}
timePrecision={TimePrecision.MINUTE}
closeOnSelection
onChange={this.onDateSelected}
value={value}
minDate={minDate}
maxDate={maxDate}
/>
</ErrorTooltip>
}
</StyledControlGroup>
);
}
isValidDate = (date: Date): boolean => {
let isValid = true;
const parsedCurrentDate = moment(date);
if (this.props.minDate) {
const parsedMinDate = moment(this.props.minDate);
if (
this.props.minDate &&
parsedMinDate.isValid() &&
parsedCurrentDate.isBefore(parsedMinDate)
) {
isValid = false;
}
}
if (this.props.maxDate) {
const parsedMaxDate = moment(this.props.maxDate);
if (
isValid &&
this.props.maxDate &&
parsedMaxDate.isValid() &&
parsedCurrentDate.isAfter(parsedMaxDate)
) {
isValid = false;
}
}
return isValid;
};
formatDate = (date: Date): string => {
const dateFormat = this.props.dateFormat || ISO_DATE_FORMAT;
return moment(date).format(dateFormat);
};
parseDate = (dateStr: string): Date => {
const date = moment(dateStr);
if (date.isValid()) return moment(dateStr).toDate();
else return moment().toDate();
};
/**
* checks if selelectedDate is null or not,
* sets state and calls props onDateSelected
* if its null, don't call onDateSelected
*
* @param selectedDate
*/
onDateSelected = (selectedDate: Date, isUserChange: boolean) => {
if (isUserChange) {
const { onDateSelected } = this.props;
const date = selectedDate ? selectedDate.toISOString() : "";
this.setState({ selectedDate: date });
onDateSelected(date);
}
};
}
interface DatePickerComponentProps extends ComponentProps {
label: string;
dateFormat: string;
enableTimePicker?: boolean;
selectedDate?: string;
minDate?: string;
maxDate?: string;
timezone?: string;
datePickerType: DatePickerType;
isDisabled: boolean;
onDateSelected: (selectedDate: string) => void;
isLoading: boolean;
}
interface DatePickerComponentState {
selectedDate?: string;
}
export default DatePickerComponent;

View File

@ -28,7 +28,7 @@ export const HintStyles = createGlobalStyle<{
max-height: 20em;
overflow-y: auto;
background: ${(props) =>
props.theme.colors.codeMirror.background.defaultState};
props.editorTheme === EditorTheme.LIGHT ? "#FAFAFA" : "#262626"};
box-shadow: 0px 12px 28px -6px rgba(0, 0, 0, 0.32);
border-radius: 0px;
${(props) =>
@ -37,7 +37,8 @@ export const HintStyles = createGlobalStyle<{
.CodeMirror-hint {
height: 24px;
color: ${(props) => props.theme.colors.codeMirror.text};
color: ${(props) =>
props.editorTheme === EditorTheme.LIGHT ? "#090707" : "#FFFFFF"};
cursor: pointer;
display: flex;
width: 220px;
@ -47,7 +48,7 @@ export const HintStyles = createGlobalStyle<{
letter-spacing: -0.24px;
&:hover {
background: ${(props) =>
props.theme.colors.codeMirror.background.hoverState};
props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
border-radius: 0px;
color: #fff;
&:after {
@ -69,7 +70,7 @@ export const HintStyles = createGlobalStyle<{
padding-left: ${(props) => props.theme.spaces[11]}px !important;
&:hover{
background: ${(props) =>
props.theme.colors.codeMirror.background.hoverState};
props.editorTheme === EditorTheme.LIGHT ? "#6A86CE" : "#157A96"};
}
}
.CodeMirror-Tern-completion:before {
@ -281,9 +282,7 @@ export const EditorWrapper = styled.div<{
? `border-bottom: 1px solid ${Colors.MERCURY}`
: `border: 1px solid ${Colors.MERCURY}`};
background: ${(props) =>
props.isFocused || props.fill
? Colors.MERCURY
: props.theme.colors.codeMirror.background.defaultState};
props.isFocused || props.fill ? Colors.MERCURY : "#FAFAFA"};
color: ${Colors.CHARCOAL};
& {
span.cm-operator {
@ -303,9 +302,7 @@ export const EditorWrapper = styled.div<{
? `border-bottom: 1px solid ${Colors.NERO}`
: `border: 1px solid ${Colors.NERO}`};
background: ${(props) =>
props.isFocused || props.fill
? Colors.NERO
: props.theme.colors.codeMirror.background.defaultState};
props.isFocused || props.fill ? Colors.NERO : "#262626"};
color: ${Colors.LIGHT_GREY};
}
.cm-s-duotone-light .CodeMirror-linenumber,

View File

@ -5,8 +5,6 @@ import moment from "moment-timezone";
import styled from "styled-components";
import { TimePrecision } from "@blueprintjs/datetime";
import { WidgetProps } from "widgets/BaseWidget";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { ISO_DATE_FORMAT } from "constants/WidgetValidation";
const DatePickerControlWrapper = styled.div<{ isValid: boolean }>`
@ -63,18 +61,22 @@ class DatePickerControl extends BaseControl<
}
render() {
const version = this.props.widgetProperties.version;
const dateFormat =
this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
version === 2
? ISO_DATE_FORMAT
: this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
const isValid = this.state.selectedDate
? this.validateDate(moment(this.state.selectedDate, dateFormat).toDate())
: true;
const maxDate =
this.props.widgetProperties?.evaluatedValues?.maxDate ?? this.maxDate;
const minDate =
this.props.widgetProperties?.evaluatedValues?.minDate ?? this.minDate;
const value =
this.props.propertyValue && isValid
? version === 2
? new Date(this.props.propertyValue)
: this.parseDate(this.props.propertyValue)
: null;
return (
<DatePickerControlWrapper isValid={isValid}>
<DatePickerControlWrapper isValid={true}>
<StyledDatePicker
formatDate={this.formatDate}
parseDate={this.parseDate}
@ -83,21 +85,7 @@ class DatePickerControl extends BaseControl<
timePrecision={TimePrecision.MINUTE}
closeOnSelection
onChange={this.onDateSelected}
maxDate={
this.props.propertyName === "defaultDate"
? this.getValidDate(maxDate, dateFormat)
: undefined
}
minDate={
this.props.propertyName === "defaultDate"
? this.getValidDate(minDate, dateFormat)
: undefined
}
value={
this.props.propertyValue && isValid
? this.parseDate(this.props.propertyValue)
: null
}
value={value}
/>
</DatePickerControlWrapper>
);
@ -117,10 +105,13 @@ class DatePickerControl extends BaseControl<
*/
onDateSelected = (date: Date, isUserChange: boolean): void => {
if (isUserChange) {
const selectedDate = date ? this.formatDate(date) : undefined;
const selectedDate = date
? this.props.widgetProperties.version === 2
? date.toISOString()
: this.formatDate(date)
: undefined;
const isValid = this.validateDate(date);
if (!isValid) return;
// if everything is ok, put date in state
this.setState({ selectedDate: selectedDate });
this.updateProperty(this.props.propertyName, selectedDate);
@ -128,73 +119,14 @@ class DatePickerControl extends BaseControl<
};
/**
* checks:
* 1. if max date is greater than the default date
* 2. if default date is in range of min and max date
* checks if date is of valid date format
*/
validateDate = (date: Date): boolean => {
const dateFormat =
this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
const parsedSelectedDate = moment(date, dateFormat);
//validate defaultDate if both minDate and maxDate is already selected
if (this.props.propertyName === "defaultDate") {
if (
parsedSelectedDate.isValid() &&
this.props.widgetProperties?.evaluatedValues?.minDate &&
this.props.widgetProperties?.evaluatedValues?.maxDate
) {
const parsedMinDate = moment(
this.props.widgetProperties.evaluatedValues.minDate,
dateFormat,
);
const parsedMaxDate = moment(
this.props.widgetProperties.evaluatedValues.maxDate,
dateFormat,
);
if (
parsedSelectedDate.isBefore(parsedMinDate) ||
parsedSelectedDate.isAfter(parsedMaxDate)
) {
return false;
}
}
}
if (this.props.widgetProperties?.evaluatedValues?.value) {
const parsedWidgetDate = moment(
this.props.widgetProperties.evaluatedValues.value,
dateFormat,
);
// checking if widget date is after min date
if (this.props.propertyName === "minDate") {
if (
parsedSelectedDate.isValid() &&
parsedWidgetDate.isBefore(parsedSelectedDate)
) {
Toaster.show({
text: "Min date cannot be greater than current widget value.",
variant: Variant.danger,
});
return false;
}
}
// checking if widget date is before max date
if (this.props.propertyName === "maxDate") {
if (
parsedSelectedDate.isValid() &&
parsedWidgetDate.isAfter(parsedSelectedDate)
) {
Toaster.show({
text: "Max date cannot be less than current widget value.",
variant: Variant.danger,
});
return false;
}
}
}
return true;
this.props.widgetProperties.version === 2
? ISO_DATE_FORMAT
: this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
return date ? moment(date, dateFormat).isValid() : true;
};
formatDate = (date: Date): string => {
@ -205,7 +137,9 @@ class DatePickerControl extends BaseControl<
parseDate = (dateStr: string): Date => {
const dateFormat =
this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
this.props.widgetProperties.version === 2
? ISO_DATE_FORMAT
: this.props.widgetProperties.dateFormat || ISO_DATE_FORMAT;
const date = moment(dateStr, dateFormat);
if (date.isValid()) return moment(dateStr, dateFormat).toDate();

View File

@ -24,6 +24,13 @@ const FIELD_VALUES: Record<
isDisabled: "boolean",
// onDateSelected: "Function Call",
},
DATE_PICKER_WIDGET2: {
defaultDate: "string", //TODO:Vicky validate this property
isRequired: "boolean",
isVisible: "boolean",
isDisabled: "boolean",
// onDateSelected: "Function Call",
},
TABLE_WIDGET: {
tableData: "Array<Object>",
serverSidePaginationEnabled: "boolean",

View File

@ -27,6 +27,10 @@ export const HelpMap = {
path: "/widget-reference/datepicker",
searchKey: "DatePicker",
},
DATE_PICKER_WIDGET2: {
path: "/widget-reference/datepicker",
searchKey: "DatePicker",
},
TABLE_WIDGET: {
path: "/widget-reference/table",
searchKey: "Table",

View File

@ -47,6 +47,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
SAVE_PAGE_SUCCESS: "SAVE_PAGE_SUCCESS",
UPDATE_LAYOUT: "UPDATE_LAYOUT",
WIDGET_ADD_CHILD: "WIDGET_ADD_CHILD",
WIDGET_CHILD_ADDED: "WIDGET_CHILD_ADDED",
WIDGET_REMOVE_CHILD: "WIDGET_REMOVE_CHILD",
WIDGET_MOVE: "WIDGET_MOVE",
WIDGET_RESIZE: "WIDGET_RESIZE",

View File

@ -5,6 +5,7 @@ export enum WidgetTypes {
INPUT_WIDGET = "INPUT_WIDGET",
CONTAINER_WIDGET = "CONTAINER_WIDGET",
DATE_PICKER_WIDGET = "DATE_PICKER_WIDGET",
DATE_PICKER_WIDGET2 = "DATE_PICKER_WIDGET2",
TABLE_WIDGET = "TABLE_WIDGET",
DROP_DOWN_WIDGET = "DROP_DOWN_WIDGET",
CHECKBOX_WIDGET = "CHECKBOX_WIDGET",

View File

@ -42,7 +42,7 @@ export type Validator = (
dataTree?: DataTree,
) => ValidationResponse;
export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.Z";
export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.sssZ";
export const JAVASCRIPT_KEYWORDS = {
true: "true",

View File

@ -138,6 +138,8 @@ 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";
export const DATE_WIDGET_DEFAULT_VALIDATION_ERROR = "Date out of range";
export const AUTOFIT_ALL_COLUMNS = "Autofit all columns";
export const AUTOFIT_THIS_COLUMN = "Autofit this column";
export const AUTOFIT_COLUMN = "Autofit column";

View File

@ -29,6 +29,7 @@ describe("getAllPathsFromPropertyConfig", () => {
isLoading: false,
horizontalAlignment: "LEFT",
parentColumnSpace: 74,
version: 1,
dynamicTriggerPathList: [
{
key: "primaryColumns.status.onClick",

View File

@ -51,7 +51,7 @@ export const WidgetIcons: {
<ContainerIcon />
</IconWrapper>
),
DATE_PICKER_WIDGET: (props: IconProps) => (
DATE_PICKER_WIDGET2: (props: IconProps) => (
<IconWrapper {...props}>
<DatePickerIcon />
</IconWrapper>

View File

@ -14,6 +14,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
isDisabled: false,
isVisible: true,
isDefaultClickDisabled: true,
version: 1,
},
TEXT_WIDGET: {
text: "Label",
@ -22,6 +23,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
rows: 1,
columns: 4,
widgetName: "Text",
version: 1,
},
RICH_TEXT_EDITOR_WIDGET: {
defaultText: "This is the initial <b>content</b> of the editor",
@ -32,6 +34,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgetName: "RichTextEditor",
isDefaultClickDisabled: true,
inputType: "html",
version: 1,
},
IMAGE_WIDGET: {
defaultImage:
@ -42,6 +45,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
rows: 3,
columns: 4,
widgetName: "Image",
version: 1,
},
INPUT_WIDGET: {
inputType: "TEXT",
@ -49,6 +53,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
label: "",
columns: 5,
widgetName: "Input",
version: 1,
resetOnSubmit: true,
},
SWITCH_WIDGET: {
@ -58,11 +63,13 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
defaultSwitchState: true,
widgetName: "Switch",
alignWidget: "LEFT",
version: 1,
},
ICON_WIDGET: {
widgetName: "Icon",
rows: 1,
columns: 1,
version: 1,
},
CONTAINER_WIDGET: {
backgroundColor: "#FFFFFF",
@ -85,6 +92,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
},
],
},
version: 1,
},
DATE_PICKER_WIDGET: {
isDisabled: false,
@ -95,14 +103,26 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
columns: 5,
widgetName: "DatePicker",
defaultDate: moment().format("DD/MM/YYYY HH:mm"),
version: 1,
},
DATE_PICKER_WIDGET2: {
isDisabled: false,
datePickerType: "DATE_PICKER",
rows: 1,
label: "",
dateFormat: "DD/MM/YYYY HH:mm",
columns: 5,
widgetName: "DatePicker",
defaultDate: moment().toISOString(),
version: 2,
},
VIDEO_WIDGET: {
rows: 7,
columns: 7,
widgetName: "Video",
url: "https://www.youtube.com/watch?v=mzqK0QIZRLs",
autoPlay: false,
version: 1,
},
TABLE_WIDGET: {
rows: 7,
@ -138,6 +158,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
orderAmount: 19.99,
},
],
version: 1,
},
DROP_DOWN_WIDGET: {
rows: 1,
@ -151,6 +172,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
],
widgetName: "Dropdown",
defaultOptionValue: "VEG",
version: 1,
},
CHECKBOX_WIDGET: {
rows: 1,
@ -158,6 +180,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
label: "Label",
defaultCheckedState: true,
widgetName: "Checkbox",
version: 1,
alignWidget: "LEFT",
},
RADIO_GROUP_WIDGET: {
@ -170,6 +193,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
],
defaultOptionValue: "M",
widgetName: "RadioGroup",
version: 1,
},
ALERT_WIDGET: {
alertType: "NOTIFICATION",
@ -179,6 +203,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
header: "",
message: "",
widgetName: "Alert",
version: 1,
},
FILE_PICKER_WIDGET: {
rows: 1,
@ -189,6 +214,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
maxFileSize: 5,
widgetName: "FilePicker",
isDefaultClickDisabled: true,
version: 1,
},
TABS_WIDGET: {
rows: 7,
@ -225,10 +251,11 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
},
],
},
version: 1,
},
MODAL_WIDGET: {
rows: 456,
columns: 456,
rows: 6,
columns: 6,
size: "MODAL_SMALL",
canEscapeKeyClose: true,
detachFromLayout: true,
@ -236,6 +263,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
shouldScrollContents: true,
widgetName: "Modal",
children: [],
version: 1,
blueprint: {
view: [
{
@ -248,6 +276,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
isDisabled: false,
shouldScrollContents: false,
children: [],
version: 1,
blueprint: {
view: [
{
@ -258,6 +287,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
iconName: "cross",
iconSize: 24,
color: "#040627",
version: 1,
},
},
{
@ -267,6 +297,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
props: {
text: "Modal Title",
textStyle: "HEADING",
version: 1,
},
},
{
@ -276,6 +307,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
props: {
text: "Cancel",
buttonStyle: "SECONDARY_BUTTON",
version: 1,
},
},
{
@ -285,6 +317,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
props: {
text: "Confirm",
buttonStyle: "PRIMARY_BUTTON",
version: 1,
},
},
],
@ -323,6 +356,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
rows: 0,
columns: 0,
widgetName: "Canvas",
version: 1,
},
CHART_WIDGET: {
rows: 8,
@ -331,6 +365,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
chartType: "LINE_CHART",
chartName: "Sales on working days",
allowHorizontalScroll: false,
version: 1,
chartData: [
{
seriesName: "Sales",
@ -375,6 +410,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
widgetName: "FormButton",
text: "Submit",
isDefaultClickDisabled: true,
version: 1,
},
FORM_WIDGET: {
rows: 13,
@ -392,6 +428,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
canExtend: false,
detachFromLayout: true,
children: [],
version: 1,
blueprint: {
view: [
{
@ -401,6 +438,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
props: {
text: "Form",
textStyle: "HEADING",
version: 1,
},
},
{
@ -412,6 +450,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
buttonStyle: "PRIMARY_BUTTON",
disabledWhenInvalid: true,
resetFormOnClick: true,
version: 1,
},
},
{
@ -423,6 +462,7 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
buttonStyle: "SECONDARY_BUTTON",
disabledWhenInvalid: false,
resetFormOnClick: true,
version: 1,
},
},
],
@ -444,12 +484,14 @@ const WidgetConfigResponse: WidgetConfigReducerState = {
allowZoom: true,
mapCenter: { lat: -34.397, long: 150.644 },
defaultMarkers: [{ lat: -34.397, long: 150.644, title: "Test A" }],
version: 1,
},
SKELETON_WIDGET: {
isLoading: true,
rows: 1,
columns: 1,
widgetName: "Skeleton",
version: 1,
},
},
configVersion: 1,

View File

@ -29,7 +29,7 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
key: generateReactKey(),
},
{
type: "DATE_PICKER_WIDGET",
type: "DATE_PICKER_WIDGET2",
widgetCardName: "DatePicker",
key: generateReactKey(),
},
@ -93,6 +93,11 @@ const WidgetSidebarResponse: WidgetCardProps[] = [
widgetCardName: "Video",
key: generateReactKey(),
},
{
type: "MODAL_WIDGET",
widgetCardName: "Modal",
key: generateReactKey(),
},
];
export default WidgetSidebarResponse;

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import { connect, useSelector } from "react-redux";
import { reduxForm, InjectedFormProps, formValueSelector } from "redux-form";
import { formValueSelector, InjectedFormProps, reduxForm } from "redux-form";
import {
HTTP_METHOD_OPTIONS,
HTTP_METHODS,
@ -11,8 +11,8 @@ import FormRow from "components/editorComponents/FormRow";
import { PaginationField } from "api/ActionAPI";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import Pagination from "./Pagination";
import { PaginationType, Action } from "entities/Action";
import { HelpMap, HelpBaseURL } from "constants/HelpConstants";
import { Action, PaginationType } from "entities/Action";
import { HelpBaseURL, HelpMap } from "constants/HelpConstants";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import PostBodyData from "./PostBodyData";
import ApiResponseView from "components/editorComponents/ApiResponseView";
@ -255,8 +255,6 @@ const ApiEditorForm: React.FC<Props> = (props: Props) => {
e.stopPropagation();
history.replace(BUILDER_PAGE_URL(applicationId, pageId));
};
// Enforcing the light theme
const theme = EditorTheme.LIGHT;
return (

View File

@ -25,7 +25,7 @@ import { Plugin } from "api/PluginApi";
import { Action, PaginationType, RapidApiAction } from "entities/Action";
import { getApiName } from "selectors/formSelectors";
import Spinner from "components/editorComponents/Spinner";
import styled, { ThemeProvider } from "styled-components";
import styled from "styled-components";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import { changeApi } from "actions/apiPaneActions";
import PerformanceTracker, {
@ -178,55 +178,53 @@ class ApiEditor extends React.Component<Props> {
/>
);
return (
<ThemeProvider theme={this.props.lightTheme}>
<div
style={{
position: "relative",
height: "100%",
}}
>
{apiId ? (
<>
{formUiComponent === "ApiEditorForm" && (
<ApiEditorForm
pluginId={pluginId}
paginationType={paginationType}
isRunning={isRunning}
isDeleting={isDeleting}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
apiName={this.props.apiName}
/>
)}
<div
style={{
position: "relative",
height: "100%",
}}
>
{apiId ? (
<>
{formUiComponent === "ApiEditorForm" && (
<ApiEditorForm
pluginId={pluginId}
paginationType={paginationType}
isRunning={isRunning}
isDeleting={isDeleting}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
apiName={this.props.apiName}
/>
)}
{formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm
apiName={this.props.apiName}
apiId={this.props.match.params.apiId}
paginationType={paginationType}
isRunning={isRunning}
isDeleting={isDeleting}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
location={this.props.location}
/>
)}
</>
) : (
apiHomeScreen
)}
</div>
</ThemeProvider>
{formUiComponent === "RapidApiEditorForm" && (
<RapidApiEditorForm
apiName={this.props.apiName}
apiId={this.props.match.params.apiId}
paginationType={paginationType}
isRunning={isRunning}
isDeleting={isDeleting}
onDeleteClick={this.handleDeleteClick}
onRunClick={this.handleRunClick}
appName={
this.props.currentApplication
? this.props.currentApplication.name
: ""
}
location={this.props.location}
/>
)}
</>
) : (
apiHomeScreen
)}
</div>
);
}
}

View File

@ -35,7 +35,6 @@ import {
import { ControlProps } from "components/formControls/BaseControl";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import ActionSettings from "pages/Editor/ActionSettings";
import { queryActionSettingsConfig } from "mockResponses/ActionSettings";
import { addTableWidgetFromQuery } from "actions/widgetActions";
import { OnboardingStep } from "constants/OnboardingConstants";
import Boxed from "components/editorComponents/Onboarding/Boxed";
@ -279,6 +278,7 @@ type QueryFormProps = {
state: any;
};
editorConfig?: any;
settingConfig: any;
loadingFormConfigs: boolean;
};
@ -579,7 +579,7 @@ const QueryEditorForm: React.FC<Props> = (props: Props) => {
panelComponent: (
<SettingsWrapper>
<ActionSettings
actionSettingsConfig={queryActionSettingsConfig}
actionSettingsConfig={props.settingConfig}
formName={QUERY_EDITOR_FORM_NAME}
/>
</SettingsWrapper>

View File

@ -29,6 +29,7 @@ import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { queryActionSettingsConfig } from "mockResponses/ActionSettings";
const EmptyStateContainer = styled.div`
display: flex;
@ -58,6 +59,7 @@ type ReduxStateProps = {
isCreating: boolean;
pluginImages: Record<string, string>;
editorConfig: any;
settingConfig: any;
loadingFormConfigs: boolean;
isEditorInitialized: boolean;
};
@ -118,6 +120,7 @@ class QueryEditor extends React.Component<Props> {
runErrorMessage,
loadingFormConfigs,
editorConfig,
settingConfig,
isEditorInitialized,
} = this.props;
const { applicationId, pageId } = this.props.match.params;
@ -154,6 +157,7 @@ class QueryEditor extends React.Component<Props> {
onRunClick={this.handleRunClick}
dataSources={dataSources}
editorConfig={editorConfig}
settingConfig={settingConfig}
loadingFormConfigs={loadingFormConfigs}
DATASOURCES_OPTIONS={DATASOURCES_OPTIONS}
executedQueryData={responses[queryId]}
@ -178,7 +182,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
const { runErrorMessage } = state.ui.queryPane;
const { plugins } = state.entities;
const { editorConfigs, loadingFormConfigs } = plugins;
const { editorConfigs, loadingFormConfigs, settingConfigs } = plugins;
const formData = getFormValues(QUERY_EDITOR_FORM_NAME)(state) as QueryAction;
const queryAction = getAction(
state,
@ -194,6 +198,15 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
editorConfig = editorConfigs[pluginId];
}
let settingConfig: any;
if (settingConfigs && pluginId) {
settingConfig = settingConfigs[pluginId];
}
if (!settingConfig) {
settingConfig = queryActionSettingsConfig;
}
return {
pluginImages: getPluginImages(state),
plugins: getPlugins(state),
@ -205,6 +218,7 @@ const mapStateToProps = (state: AppState, props: any): ReduxStateProps => {
isDeleting: state.ui.queryPane.isDeleting[props.match.params.queryId],
formData,
editorConfig,
settingConfig,
loadingFormConfigs,
isCreating: state.ui.apiPane.isCreating,
isEditorInitialized: getIsEditorInitialized(state),

View File

@ -38,6 +38,9 @@ import { isMac } from "utils/helpers";
import { getSelectedWidget } from "selectors/ui";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import Welcome from "./Welcome";
import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
import { ThemeProvider } from "styled-components";
import { Theme } from "constants/DefaultTheme";
type EditorProps = {
currentApplicationId?: string;
@ -55,6 +58,7 @@ type EditorProps = {
cutSelectedWidget: () => void;
user?: User;
selectedWidget?: string;
lightTheme: Theme;
};
type Props = EditorProps & RouteComponentProps<BuilderRouteParams>;
@ -200,21 +204,23 @@ class Editor extends Component<Props> {
);
}
return (
<DndProvider
backend={TouchBackend}
options={{
enableMouseEvents: true,
}}
>
<div>
<Helmet>
<meta charSet="utf-8" />
<title>Editor | Appsmith</title>
</Helmet>
<MainContainer />
</div>
<ConfirmRunModal />
</DndProvider>
<ThemeProvider theme={this.props.lightTheme}>
<DndProvider
backend={TouchBackend}
options={{
enableMouseEvents: true,
}}
>
<div>
<Helmet>
<meta charSet="utf-8" />
<title>Editor | Appsmith</title>
</Helmet>
<MainContainer />
</div>
<ConfirmRunModal />
</DndProvider>
</ThemeProvider>
);
}
}
@ -229,6 +235,7 @@ const mapStateToProps = (state: AppState) => ({
user: getCurrentUser(state),
selectedWidget: getSelectedWidget(state),
creatingOnboardingDatabase: state.ui.onBoarding.showOnboardingLoader,
lightTheme: getThemeDetails(state, ThemeMode.LIGHT),
});
const mapDispatchToProps = (dispatch: any) => {

View File

@ -10,6 +10,7 @@ export interface PluginFormPayload {
id: string;
form: any[];
editor: any[];
setting: any[];
}
export interface PluginDataState {
@ -17,6 +18,7 @@ export interface PluginDataState {
loading: boolean;
formConfigs: Record<string, any[]>;
editorConfigs: Record<string, any[]>;
settingConfigs: Record<string, any[]>;
loadingFormConfigs: boolean;
loadingDBFormConfigs: boolean;
}
@ -26,6 +28,7 @@ const initialState: PluginDataState = {
loading: false,
formConfigs: {},
editorConfigs: {},
settingConfigs: {},
loadingFormConfigs: false,
loadingDBFormConfigs: false,
};
@ -71,6 +74,10 @@ const pluginsReducer = createReducer(initialState, {
...state.editorConfigs,
[action.payload.id]: action.payload.editor,
},
settingConfigs: {
...state.settingConfigs,
[action.payload.id]: action.payload.setting,
},
};
},
[ReduxActionErrorTypes.FETCH_PLUGIN_FORM_ERROR]: (state: PluginDataState) => {

View File

@ -9,6 +9,7 @@ import { ImageWidgetProps } from "widgets/ImageWidget";
import { InputWidgetProps } from "widgets/InputWidget";
import { RichTextEditorWidgetProps } from "widgets/RichTextEditorWidget";
import { DatePickerWidgetProps } from "../../widgets/DatePickerWidget";
import { DatePickerWidget2Props } from "../../widgets/DatePickerWidget2";
import { TableWidgetProps } from "../../widgets/TableWidget/TableWidgetConstants";
import { DropdownWidgetProps } from "../../widgets/DropdownWidget";
import { CheckboxWidgetProps } from "../../widgets/CheckboxWidget";
@ -59,6 +60,7 @@ export interface WidgetConfigReducerState {
CONTAINER_WIDGET: Partial<ContainerWidgetProps<WidgetProps>> &
WidgetConfigProps;
DATE_PICKER_WIDGET: Partial<DatePickerWidgetProps> & WidgetConfigProps;
DATE_PICKER_WIDGET2: Partial<DatePickerWidget2Props> & WidgetConfigProps;
TABLE_WIDGET: Partial<TableWidgetProps> & WidgetConfigProps;
VIDEO_WIDGET: Partial<VideoWidgetProps> & WidgetConfigProps;
DROP_DOWN_WIDGET: Partial<DropdownWidgetProps> & WidgetConfigProps;

View File

@ -49,6 +49,7 @@ import {
getAction,
getCurrentPageNameByActionId,
getPageNameByPageId,
getSettingConfig,
} from "selectors/entitiesSelector";
import { getDataSources } from "selectors/editorSelectors";
import { PLUGIN_TYPE_API } from "constants/ApiEditorConstants";
@ -77,13 +78,13 @@ export function* createActionSaga(
try {
let payload = actionPayload.payload;
if (actionPayload.payload.pluginId) {
let formConfig;
formConfig = yield select(
let editorConfig;
editorConfig = yield select(
getEditorConfig,
actionPayload.payload.pluginId,
);
if (!formConfig) {
if (!editorConfig) {
const formConfigResponse: GenericApiResponse<any> = yield PluginsApi.fetchFormConfig(
actionPayload.payload.pluginId,
);
@ -96,13 +97,24 @@ export function* createActionSaga(
},
});
formConfig = yield select(
editorConfig = yield select(
getEditorConfig,
actionPayload.payload.pluginId,
);
}
const settingConfig = yield select(
getSettingConfig,
actionPayload.payload.pluginId,
);
const initialValues = yield call(getConfigInitialValues, formConfig);
let initialValues = yield call(getConfigInitialValues, editorConfig);
if (settingConfig) {
const settingInitialValues = yield call(
getConfigInitialValues,
settingConfig,
);
initialValues = merge(initialValues, settingInitialValues);
}
payload = merge(initialValues, actionPayload.payload);
}

View File

@ -89,6 +89,17 @@ export function* showModalByNameSaga(
}
}
export function* showIfModalSaga(
action: ReduxAction<{ widgetId: string; type: string }>,
) {
if (action.payload.type === "MODAL_WIDGET") {
yield put({
type: ReduxActionTypes.SHOW_MODAL,
payload: { modalId: action.payload.widgetId },
});
}
}
export function* showModalSaga(action: ReduxAction<{ modalId: string }>) {
// First we close the currently open modals (if any)
// Notice the empty payload.
@ -184,5 +195,6 @@ export default function* modalSagas() {
takeLatest(ReduxActionTypes.CREATE_MODAL_INIT, createModalSaga),
takeLatest(ReduxActionTypes.SHOW_MODAL, showModalSaga),
takeLatest(ReduxActionTypes.SHOW_MODAL_BY_NAME, showModalByNameSaga),
takeLatest(ReduxActionTypes.WIDGET_CHILD_ADDED, showIfModalSaga),
]);
}

View File

@ -48,6 +48,7 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
const { id } = actionPayload.payload;
const state = yield select();
const editorConfigs = state.entities.plugins.editorConfigs;
const settingConfigs = state.entities.plugins.settingConfigs;
let configInitialValues = {};
// // Typescript says Element does not have blur function but it does;
// document.activeElement &&
@ -82,6 +83,7 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
currentEditorConfig = success.payload.editor;
}
}
const currentSettingConfig = settingConfigs[action.datasource.pluginId];
// If config exists
if (currentEditorConfig) {
@ -92,6 +94,14 @@ function* changeQuerySaga(actionPayload: ReduxAction<{ id: string }>) {
);
}
if (currentSettingConfig) {
const settingInitialValues = yield call(
getConfigInitialValues,
currentSettingConfig,
);
configInitialValues = merge(configInitialValues, settingInitialValues);
}
// Merge the initial values and action.
const formInitialValues = merge(configInitialValues, action);

View File

@ -143,6 +143,7 @@ function* getChildWidgetProps(
parentColumnSpace,
widgetName,
widgetProps,
restDefaultConfig.version,
);
widget.widgetId = newWidgetId;
@ -239,7 +240,6 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
const start = performance.now();
Toaster.clear();
const { widgetId } = addChildAction.payload;
// Get the current parent widget whose child will be the new widget.
const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
// const parent = Object.assign({}, stateParent);
@ -262,6 +262,13 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
widgets[parent.widgetId] = parent;
log.debug("add child computations took", performance.now() - start, "ms");
yield put({
type: ReduxActionTypes.WIDGET_CHILD_ADDED,
payload: {
widgetId: childWidgetPayload.widgetId,
type: addChildAction.payload.type,
},
});
yield put(updateAndSaveLayout(widgets));
} catch (error) {
yield put({
@ -1405,6 +1412,7 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
parentColumnSpace: 1,
isLoading: false,
version: 1,
props: {
tableData: `{{${queryName}.data}}`,
dynamicBindingPathList: [{ key: "tableData" }],

View File

@ -101,6 +101,10 @@ export const getEditorConfig = (state: AppState, pluginId: string): any[] => {
return state.entities.plugins.editorConfigs[pluginId];
};
export const getSettingConfig = (state: AppState, pluginId: string): any[] => {
return state.entities.plugins.settingConfigs[pluginId];
};
export const getActions = (state: AppState): ActionDataState =>
state.entities.actions;

View File

@ -259,6 +259,18 @@ const dynamicPathListMigration = (
return currentDSL;
};
const addVersionNumberMigration = (
currentDSL: ContainerWidgetProps<WidgetProps>,
) => {
if (currentDSL.children && currentDSL.children.length) {
currentDSL.children = currentDSL.children.map(addVersionNumberMigration);
}
if (currentDSL.version === undefined) {
currentDSL.version = 1;
}
return currentDSL;
};
const canvasNameConflictMigration = (
currentDSL: ContainerWidgetProps<WidgetProps>,
props = { counter: 1 },
@ -381,10 +393,15 @@ const transformDSL = (currentDSL: ContainerWidgetProps<WidgetProps>) => {
}
if (currentDSL.version === 10) {
currentDSL = rteDefaultValueMigration(currentDSL);
currentDSL = addVersionNumberMigration(currentDSL);
currentDSL.version = 11;
}
if (currentDSL.version === 11) {
currentDSL = rteDefaultValueMigration(currentDSL);
currentDSL.version = 12;
}
return currentDSL;
};
@ -608,6 +625,7 @@ export const generateWidgetProps = (
widgetId: string;
renderMode: RenderMode;
} & Partial<WidgetProps>,
version: number,
): ContainerWidgetProps<WidgetProps> => {
if (parent) {
const sizes = {
@ -629,6 +647,7 @@ export const generateWidgetProps = (
...sizes,
...others,
parentId: parent.widgetId,
version,
};
delete props.rows;
delete props.columns;

View File

@ -71,6 +71,10 @@ import DatePickerWidget, {
DatePickerWidgetProps,
ProfiledDatePickerWidget,
} from "widgets/DatePickerWidget";
import DatePickerWidget2, {
DatePickerWidget2Props,
ProfiledDatePickerWidget2,
} from "widgets/DatePickerWidget2";
import FormWidget, { ProfiledFormWidget } from "widgets/FormWidget";
import FormButtonWidget, {
FormButtonWidgetProps,
@ -286,6 +290,20 @@ export default class WidgetBuilderRegistry {
DatePickerWidget.getMetaPropertiesMap(),
DatePickerWidget.getPropertyPaneConfig(),
);
WidgetFactory.registerWidgetBuilder(
"DATE_PICKER_WIDGET2",
{
buildWidget(widgetData: DatePickerWidget2Props): JSX.Element {
return <ProfiledDatePickerWidget2 {...widgetData} />;
},
},
DatePickerWidget2.getPropertyValidationMap(),
DatePickerWidget2.getDerivedPropertiesMap(),
DatePickerWidget2.getTriggerPropertyMap(),
DatePickerWidget2.getDefaultPropertiesMap(),
DatePickerWidget2.getMetaPropertiesMap(),
DatePickerWidget.getPropertyPaneConfig(),
);
WidgetFactory.registerWidgetBuilder(
"TABS_WIDGET",
{
@ -435,5 +453,20 @@ export default class WidgetBuilderRegistry {
SkeletonWidget.getDefaultPropertiesMap(),
SkeletonWidget.getMetaPropertiesMap(),
);
WidgetFactory.registerWidgetBuilder(
WidgetTypes.MODAL_WIDGET,
{
buildWidget(widgetData: ModalWidgetProps): JSX.Element {
return <ProfiledModalWidget {...widgetData} />;
},
},
ModalWidget.getPropertyValidationMap(),
ModalWidget.getDerivedPropertiesMap(),
ModalWidget.getTriggerPropertyMap(),
ModalWidget.getDefaultPropertiesMap(),
ModalWidget.getMetaPropertiesMap(),
ModalWidget.getPropertyPaneConfig(),
);
}
}

View File

@ -124,6 +124,15 @@ export const entityDefinitions = {
selectedDate: "string",
isDisabled: "bool",
},
DATE_PICKER_WIDGET2: {
"!doc":
"Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth",
"!url": "https://docs.appsmith.com/widget-reference/datepicker",
isVisible: isVisible,
selectedDate: "string",
formattedDate: "string",
isDisabled: "bool",
},
CHECKBOX_WIDGET: {
"!doc":
"Checkbox is a simple UI widget you can use when you want users to make a binary choice",

View File

@ -24,6 +24,7 @@ describe("dataTreeTypeDefCreator", () => {
topRow: 1,
bottomRow: 2,
isLoading: false,
version: 1,
bindingPaths: {
defaultText: true,
},

View File

@ -44,6 +44,7 @@ const input1: ContainerWidgetProps<WidgetProps> = {
widgetId: "fs785w9gcy",
dynamicBindingPathList: [],
renderMode: "CANVAS",
version: 1,
},
],
};
@ -105,6 +106,7 @@ const input2: ContainerWidgetProps<WidgetProps> = {
},
],
renderMode: "CANVAS",
version: 1,
},
],
};
@ -157,6 +159,7 @@ const input3: ContainerWidgetProps<WidgetProps> = {
onRowSelected: "{{showAlert('test','success')}}",
onSearchTextChanged: "{{showAlert('fail','error')}}",
renderMode: "CANVAS",
version: 1,
},
],
};
@ -293,6 +296,7 @@ const output1 = {
horizontalAlignment: "LEFT",
verticalAlignment: "CENTER",
renderMode: "CANVAS",
version: 1,
},
],
};
@ -474,6 +478,7 @@ const output2 = {
horizontalAlignment: "LEFT",
verticalAlignment: "CENTER",
renderMode: "CANVAS",
version: 1,
},
],
};
@ -616,6 +621,7 @@ const output3 = {
horizontalAlignment: "LEFT",
verticalAlignment: "CENTER",
renderMode: "CANVAS",
version: 1,
},
],
};

View File

@ -319,6 +319,7 @@ export interface WidgetBaseProps {
widgetName: string;
parentId: string;
renderMode: RenderMode;
version: number;
}
export type WidgetRowCols = {

View File

@ -0,0 +1,223 @@
import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { EventType } from "constants/ActionConstants";
import DatePickerComponent from "components/designSystems/blueprint/DatePickerComponent2";
import {
WidgetPropertyValidationType,
BASE_WIDGET_VALIDATION,
} from "utils/WidgetValidation";
import { VALIDATION_TYPES } from "constants/WidgetValidation";
import {
DerivedPropertiesMap,
TriggerPropertiesMap,
} from "utils/WidgetFactory";
import * as Sentry from "@sentry/react";
import withMeta, { WithMeta } from "./MetaHOC";
class DatePickerWidget extends BaseWidget<DatePickerWidget2Props, WidgetState> {
static getPropertyPaneConfig() {
return [
{
sectionName: "General",
children: [
{
propertyName: "defaultDate",
label: "Default Date",
helpText:
"Sets the default date of the widget. The date is updated if the default date changes",
controlType: "DATE_PICKER",
placeholderText: "Enter Default Date",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
helpText: "Sets the format of the selected date",
propertyName: "dateFormat",
label: "Date Format",
controlType: "DROP_DOWN",
isJSConvertible: true,
options: [
{
label: "YYYY-MM-DD",
value: "YYYY-MM-DD",
},
{
label: "YYYY-MM-DD HH:mm",
value: "YYYY-MM-DD HH:mm",
},
{
label: "YYYY-MM-DDTHH:mm:ss.sssZ",
value: "YYYY-MM-DDTHH:mm:ss.sssZ",
},
{
label: "DD/MM/YYYY",
value: "DD/MM/YYYY",
},
{
label: "DD/MM/YYYY HH:mm",
value: "DD/MM/YYYY HH:mm",
},
],
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "isRequired",
label: "Required",
helpText: "Makes input to the widget mandatory",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "isDisabled",
label: "Disabled",
helpText: "Disables input to this widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "minDate",
label: "Min Date",
helpText: "Defines the min date for this widget",
controlType: "DATE_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
{
propertyName: "maxDate",
label: "Max Date",
helpText: "Defines the max date for this widget",
controlType: "DATE_PICKER",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
},
],
},
{
sectionName: "Actions",
children: [
{
propertyName: "onDateSelected",
label: "onDateSelected",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: true,
},
],
},
];
}
static getPropertyValidationMap(): WidgetPropertyValidationType {
return {
...BASE_WIDGET_VALIDATION,
defaultDate: VALIDATION_TYPES.DEFAULT_DATE,
timezone: VALIDATION_TYPES.TEXT,
enableTimePicker: VALIDATION_TYPES.BOOLEAN,
dateFormat: VALIDATION_TYPES.TEXT,
label: VALIDATION_TYPES.TEXT,
datePickerType: VALIDATION_TYPES.TEXT,
maxDate: VALIDATION_TYPES.DATE,
minDate: VALIDATION_TYPES.DATE,
isRequired: VALIDATION_TYPES.BOOLEAN,
// onDateSelected: VALIDATION_TYPES.ACTION_SELECTOR,
// onDateRangeSelected: VALIDATION_TYPES.ACTION_SELECTOR,
};
}
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return {
isValid: `{{ this.isRequired ? !!this.selectedDate : true }}`,
selectedDate: `{{ this.value ? moment(this.value).toISOString() : (this.defaultDate ? moment(this.defaultDate).toISOString() : "")}}`,
formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : (this.defaultDate ? moment(this.defaultDate).format(this.dateFormat) : "")}}`,
};
}
static getTriggerPropertyMap(): TriggerPropertiesMap {
return {
onDateSelected: true,
};
}
static getDefaultPropertiesMap(): Record<string, string> {
return {
value: "defaultDate",
};
}
static getMetaPropertiesMap(): Record<string, any> {
return {
value: undefined,
};
}
getPageView() {
return (
<DatePickerComponent
label={`${this.props.label}`}
dateFormat={this.props.dateFormat}
widgetId={this.props.widgetId}
isDisabled={this.props.isDisabled}
datePickerType={"DATE_PICKER"}
onDateSelected={this.onDateSelected}
selectedDate={this.props.value}
isLoading={this.props.isLoading}
minDate={this.props.minDate}
maxDate={this.props.maxDate}
/>
);
}
onDateSelected = (selectedDate: string) => {
this.props.updateWidgetMetaProperty("value", selectedDate, {
dynamicString: this.props.onDateSelected,
event: {
type: EventType.ON_DATE_SELECTED,
},
});
};
getWidgetType(): WidgetType {
return "DATE_PICKER_WIDGET2";
}
}
export type DatePickerType = "DATE_PICKER" | "DATE_RANGE_PICKER";
export interface DatePickerWidget2Props extends WidgetProps, WithMeta {
defaultDate: string;
selectedDate: string;
formattedDate: string;
isDisabled: boolean;
dateFormat: string;
label: string;
datePickerType: DatePickerType;
onDateSelected?: string;
onDateRangeSelected?: string;
maxDate: string;
minDate: string;
isRequired?: boolean;
}
export default DatePickerWidget;
export const ProfiledDatePickerWidget2 = Sentry.withProfiler(
withMeta(DatePickerWidget),
);

View File

@ -950,17 +950,24 @@ class TableWidget extends BaseWidget<TableWidgetProps, WidgetState> {
),
);
} else {
this.props.updateWidgetMetaProperty("selectedRowIndex", index);
this.props.updateWidgetMetaProperty(
"selectedRow",
this.props.filteredTableData[index],
{
dynamicString: this.props.onRowSelected,
event: {
type: EventType.ON_ROW_SELECTED,
const selectedRowIndex = isNumber(this.props.selectedRowIndex)
? this.props.selectedRowIndex
: -1;
if (selectedRowIndex === index) {
index = -1;
} else {
this.props.updateWidgetMetaProperty(
"selectedRow",
this.props.filteredTableData[index],
{
dynamicString: this.props.onRowSelected,
event: {
type: EventType.ON_ROW_SELECTED,
},
},
},
);
);
}
this.props.updateWidgetMetaProperty("selectedRowIndex", index);
}
};

View File

@ -289,6 +289,33 @@ const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = {
},
metaProperties: {},
},
DATE_PICKER_WIDGET2: {
validations: {
isLoading: "BOOLEAN",
isVisible: "BOOLEAN",
isDisabled: "BOOLEAN",
defaultDate: "DATE",
timezone: "TEXT",
enableTimePicker: "BOOLEAN",
dateFormat: "TEXT",
label: "TEXT",
datePickerType: "TEXT",
maxDate: "DATE",
minDate: "DATE",
isRequired: "BOOLEAN",
},
defaultProperties: {
selectedDate: "defaultDate",
},
derivedProperties: {
isValid: "{{ this.isRequired ? !!this.selectedDate : true }}",
value: "{{ this.selectedDate }}",
},
triggerProperties: {
onDateSelected: true,
},
metaProperties: {},
},
TABS_WIDGET: {
validations: {
tabs: "TABS_DATA",
@ -447,6 +474,7 @@ const BASE_WIDGET: DataTreeWidget = {
topRow: 0,
type: WidgetTypes.SKELETON_WIDGET,
parentId: "0",
version: 1,
bindingPaths: {},
triggerPaths: {},
ENTITY_TYPE: ENTITY_TYPE.WIDGET,

View File

@ -391,7 +391,10 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
dateString: string,
props: WidgetProps,
): ValidationResponse => {
const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT;
const dateFormat =
props.version === 2
? ISO_DATE_FORMAT
: props.dateFormat || ISO_DATE_FORMAT;
if (dateString === undefined) {
return {
@ -421,39 +424,22 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
dateString: string,
props: WidgetProps,
): ValidationResponse => {
const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT;
const dateFormat =
props.version === 2
? ISO_DATE_FORMAT
: props.dateFormat || ISO_DATE_FORMAT;
if (dateString === undefined) {
return {
isValid: false,
parsed: "",
message:
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat
? props.dateFormat
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat
? dateFormat
: "",
};
}
const parsedCurrentDate = moment(dateString, dateFormat);
let isValid = parsedCurrentDate.isValid();
const parsedMinDate = moment(props.minDate, dateFormat);
const parsedMaxDate = moment(props.maxDate, dateFormat);
// checking for max/min date range
if (isValid) {
if (
parsedMinDate.isValid() &&
parsedCurrentDate.isBefore(parsedMinDate)
) {
isValid = false;
}
if (
isValid &&
parsedMaxDate.isValid() &&
parsedCurrentDate.isAfter(parsedMaxDate)
) {
isValid = false;
}
}
const isValid = parsedCurrentDate.isValid();
if (!isValid) {
return {
isValid: isValid,
@ -471,14 +457,17 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
dateString: string,
props: WidgetProps,
): ValidationResponse => {
const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT;
const dateFormat =
props.version === 2
? ISO_DATE_FORMAT
: props.dateFormat || ISO_DATE_FORMAT;
if (dateString === undefined) {
return {
isValid: false,
parsed: "",
message:
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat
? props.dateFormat
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat
? dateFormat
: "",
};
}
@ -517,14 +506,17 @@ export const VALIDATORS: Record<ValidationType, Validator> = {
dateString: string,
props: WidgetProps,
): ValidationResponse => {
const dateFormat = props.dateFormat ? props.dateFormat : ISO_DATE_FORMAT;
const dateFormat =
props.version === 2
? ISO_DATE_FORMAT
: props.dateFormat || ISO_DATE_FORMAT;
if (dateString === undefined) {
return {
isValid: false,
parsed: "",
message:
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + props.dateFormat
? props.dateFormat
`${WIDGET_TYPE_VALIDATION_ERROR}: Date ` + dateFormat
? dateFormat
: "",
};
}

View File

@ -36,6 +36,8 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
@ -47,9 +49,33 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class DynamoPlugin extends BasePlugin {
private static final String SCAN_ACTION_VALUE = "Scan";
private static final String GET_ITEM_ACTION_VALUE = "GetItem";
private static final String BATCH_GET_ITEM_ACTION_VALUE = "BatchGetItem";
private static final String TRANSACT_GET_ITEMS_ACTION_VALUE = "TransactGetItems";
private static final String PUT_ITEM_ACTION_VALUE = "PutItem";
private static final String UPDATE_ITEM_ACTION_VALUE = "UpdateItem";
private static final String DELETE_ITEM_ACTION_VALUE = "DeleteItem";
private static final String ITEMS_KEY = "Items";
private static final String ITEM_KEY = "Item";
private static final String ATTRIBUTES_KEY = "Attributes";
private static final String RESPONSES_KEY = "Responses";
private static final String DYNAMO_TYPE_STRING_LABEL = "S";
private static final String DYNAMO_TYPE_NUMBER_LABEL = "N";
private static final String DYNAMO_TYPE_BINARY_LABEL = "B";
private static final String DYNAMO_TYPE_BOOLEAN_LABEL = "BOOL";
private static final String DYNAMO_TYPE_NULL_LABEL = "NUL";
private static final String DYNAMO_TYPE_STRING_SET_LABEL = "SS";
private static final String DYNAMO_TYPE_NUMBER_SET_LABEL = "NS";
private static final String DYNAMO_TYPE_BINARY_SET_LABEL = "BS";
private static final String DYNAMO_TYPE_MAP_LABEL = "M";
private static final String DYNAMO_TYPE_LIST_LABEL = "L";
private static final String RAW_RESPONSE_LABEL = "raw";
public DynamoPlugin(PluginWrapper wrapper) {
super(wrapper);
}
@ -71,6 +97,187 @@ public class DynamoPlugin extends BasePlugin {
private final Scheduler scheduler = Schedulers.elastic();
public Object extractValue(Object rawItem) {
if(!(rawItem instanceof List)
&& !(rawItem instanceof Map)) {
return rawItem;
}
if (rawItem instanceof List) {
return ((List<Object>) rawItem)
.stream()
.map(item -> extractValue(item))
.collect(Collectors.toList());
}
else { /* map type */
Map<String, Object> extractedValueMap = new HashMap<>();
Map<String, Object> rawItemAsMap = (Map<String, Object>) rawItem;
for (Map.Entry<String, Object> entry: rawItemAsMap.entrySet()) {
switch (entry.getKey()) {
case DYNAMO_TYPE_NUMBER_LABEL:
case DYNAMO_TYPE_STRING_LABEL:
case DYNAMO_TYPE_BINARY_LABEL:
case DYNAMO_TYPE_BOOLEAN_LABEL:
case DYNAMO_TYPE_NULL_LABEL:
if (entry.getValue() != null) {
return entry.getValue();
}
break;
case DYNAMO_TYPE_STRING_SET_LABEL:
case DYNAMO_TYPE_NUMBER_SET_LABEL:
case DYNAMO_TYPE_BINARY_SET_LABEL:
if (entry.getValue() != null && ((List<Object>)entry.getValue()).size() > 0) {
return entry.getValue();
}
break;
case DYNAMO_TYPE_LIST_LABEL:
/*
* - If size of rawValueAsList is 0, then we don't want to return.
*/
List<Object> rawValueAsList = (List<Object>) entry.getValue();
if (rawValueAsList.size() > 0) {
return rawValueAsList
.stream()
.map(listItem -> extractValue(listItem))
.collect(Collectors.toList());
}
break;
case DYNAMO_TYPE_MAP_LABEL:
/*
* - If size of rawValueAsMap is 0, then we don't want to return.
*/
Map<String, Object> rawValueAsMap = (Map<String, Object>) entry.getValue();
if (rawValueAsMap.size() > 0) {
return rawValueAsMap
.entrySet()
.stream()
.map(item -> Map.entry(item.getKey(), extractValue(item.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
break;
default:
extractedValueMap.put(entry.getKey(), extractValue(entry.getValue()));
}
}
return extractedValueMap;
}
}
/*
* - Transform response for easy consumption. For details please visit
* https://github.com/appsmithorg/appsmith/issues/3010
*/
public Object getTransformedResponse(Map<String, Object> rawResponse,
String action) throws AppsmithPluginException {
Map<String, Object> transformedResponse = new HashMap<>();
/*
* - Any action other than the following do not return transformed response:
* - Scan
* - GetItem
* - PutItem
* - UpdateItem
* - DeleteItem
* - BatchGetItem
* - TransactGetItem
*/
if (!SCAN_ACTION_VALUE.equals(action)
&& !GET_ITEM_ACTION_VALUE.equals(action)
&& !PUT_ITEM_ACTION_VALUE.equals(action)
&& !UPDATE_ITEM_ACTION_VALUE.equals(action)
&& !DELETE_ITEM_ACTION_VALUE.equals(action)
&& !BATCH_GET_ITEM_ACTION_VALUE.equals(action)
&& !TRANSACT_GET_ITEMS_ACTION_VALUE.equals(action)) {
return rawResponse;
}
/*
* - Transformed response has section "raw", under which raw response appears.
*/
transformedResponse.put(RAW_RESPONSE_LABEL, rawResponse);
/*
* - Transform response based on action.
*/
String topLevelKey;
switch (action) {
case SCAN_ACTION_VALUE:
topLevelKey = ITEMS_KEY;
break;
case GET_ITEM_ACTION_VALUE:
topLevelKey = ITEM_KEY;
break;
case PUT_ITEM_ACTION_VALUE:
case UPDATE_ITEM_ACTION_VALUE:
case DELETE_ITEM_ACTION_VALUE:
topLevelKey = ATTRIBUTES_KEY;
break;
case BATCH_GET_ITEM_ACTION_VALUE:
case TRANSACT_GET_ITEMS_ACTION_VALUE:
topLevelKey = RESPONSES_KEY;
break;
default:
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Appsmith has encountered an unexpected error when transforming raw DynamoDb response" +
". Please reach out to Appsmith customer support to resolve this."
);
}
for (Map.Entry<String, Object> responseEntry: rawResponse.entrySet()) {
if (!responseEntry.getKey().equals(topLevelKey)) {
transformedResponse.put(responseEntry.getKey(), responseEntry.getValue());
}
else {
if (rawResponse.get(topLevelKey) instanceof Collection) {
/*
* - Need to have an empty list if rawItems is null.
*/
List<Object> extractedResponse = new ArrayList<>();
Collection<Object> rawItems = (Collection<Object>) (rawResponse.get(topLevelKey));
if (rawItems != null) {
/*
* - Insert transformed values into extractedResponse list.
*/
extractedResponse = rawItems
.stream()
.map(item -> extractValue(item))
.collect(Collectors.toList());
}
transformedResponse.put(topLevelKey, extractedResponse);
}
else if (rawResponse.get(topLevelKey) instanceof Map) {
/*
* - Need to have an empty map if rawItems is null.
*/
Map<Object, Object> extractedResponse = new HashMap<>();
HashMap<String, Object> rawItems = (HashMap<String, Object>) rawResponse.get(topLevelKey);
if (rawItems != null) {
/*
* - Insert transformed values into extractedResponse map.
*/
extractedResponse = rawItems
.entrySet()
.stream()
.map(item -> Map.entry(item.getKey(), extractValue(item.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
transformedResponse.put(topLevelKey, extractedResponse);
}
}
}
return transformedResponse;
}
@Override
public Mono<ActionExecutionResult> execute(DynamoDbClient ddb,
DatasourceConfiguration datasourceConfiguration,
@ -115,8 +322,11 @@ public class DynamoPlugin extends BasePlugin {
toLowerCamelCase(action),
requestClass
);
final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, plainToSdk(parameters, requestClass));
result.setBody(sdkToPlain(response));
final Object sdkValue = plainToSdk(parameters, requestClass);
final DynamoDbResponse response = (DynamoDbResponse) actionExecuteMethod.invoke(ddb, sdkValue);
Object rawResponse = sdkToPlain(response);
Object transformedResponse = getTransformedResponse((Map<String, Object>)rawResponse, action);
result.setBody(transformedResponse);
} catch (AppsmithPluginException | InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
final String message = "Error executing the DynamoDB Action: " + (e.getCause() == null ? e : e.getCause()).getMessage();
log.warn(message, e);
@ -290,34 +500,43 @@ public class DynamoPlugin extends BasePlugin {
// For maps, we go recursive, applying this transformation to each value, and replacing with the
// result in the map. Generic types in the setter method's signature are used to convert the values.
final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName));
final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0];
final Map<String, Object> transformedMap = new HashMap<>();
for (final Map.Entry<String, Object> innerEntry : ((Map<String, Object>) value).entrySet()) {
Object innerValue = innerEntry.getValue();
if (innerValue instanceof Map) {
innerValue = plainToSdk((Map) innerValue, (Class<?>) valueType.getActualTypeArguments()[1]);
final Type parameterType = setterMethod.getGenericParameterTypes()[0];
if (parameterType instanceof ParameterizedType) {
final ParameterizedType valueType = (ParameterizedType) parameterType;
final Map<String, Object> transformedMap = new HashMap<>();
for (final Map.Entry<String, Object> innerEntry : ((Map<String, Object>) value).entrySet()) {
Object innerValue = innerEntry.getValue();
if (innerValue instanceof Map) {
innerValue = plainToSdk((Map) innerValue, (Class<?>) valueType.getActualTypeArguments()[1]);
}
transformedMap.put(innerEntry.getKey(), innerValue);
}
transformedMap.put(innerEntry.getKey(), innerValue);
value = transformedMap;
if (!Map.class.isAssignableFrom((Class<?>) valueType.getRawType())) {
// Some setters don't take a plain map. For example, some require an `AttributeValue` instance
// for objects that are just maps in JSON. So, we make that conversion here.
value = plainToSdk((Map) value, (Class<T>) valueType.getRawType());
}
setterMethod.invoke(builder, value);
} else if (parameterType instanceof Class) {
setterMethod.invoke(builder, plainToSdk((Map) value, (Class) parameterType));
}
value = transformedMap;
if (!Map.class.isAssignableFrom((Class<?>) valueType.getRawType())) {
// Some setters don't take a plain map. For example, some require an `AttributeValue` instance
// for objects that are just maps in JSON. So, we make that conversion here.
value = plainToSdk((Map) value, (Class<T>) valueType.getRawType());
}
setterMethod.invoke(builder, value);
} else if (value instanceof Collection) {
// For linear collections, the process is similar to that of maps.
final Collection<Object> valueAsCollection = (Collection) value;
// Find method by name and exclude the varargs version of the method.
final Method setterMethod = findMethod(builderType, m -> m.getName().equals(setterName) && !m.getParameterTypes()[0].getName().startsWith("[L"));
final ParameterizedType valueType = (ParameterizedType) setterMethod.getGenericParameterTypes()[0];
Type valueType = ((ParameterizedType) setterMethod.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
if (valueType instanceof WildcardType) {
// This occurs when the method's parameter is typed as `Collection<? extends Map<...>>`. Example op: `BatchGetItem`.
valueType = ((WildcardType) valueType).getUpperBounds()[0];
}
final Collection<Object> reTypedList = new ArrayList<>();
for (final Object innerValue : valueAsCollection) {
if (innerValue instanceof Map) {
reTypedList.add(plainToSdk((Map) innerValue, (Class<?>) valueType.getActualTypeArguments()[0]));
} else if (innerValue instanceof String && SdkBytes.class.isAssignableFrom((Class<?>) valueType.getActualTypeArguments()[0])) {
reTypedList.add(plainToSdk((Map) innerValue, valueType));
} else if (innerValue instanceof String && SdkBytes.class.isAssignableFrom((Class<?>) valueType)) {
reTypedList.add(SdkBytes.fromUtf8String((String) innerValue));
} else {
reTypedList.add(innerValue);
@ -338,6 +557,34 @@ public class DynamoPlugin extends BasePlugin {
return (T) builderType.getMethod("build").invoke(builder);
}
public static Object plainToSdk(Map<String, Object> mapping, Type type)
throws InvocationTargetException, NoSuchMethodException, ClassNotFoundException, AppsmithPluginException,
IllegalAccessException {
if (mapping == null) {
return null;
}
if (!(type instanceof ParameterizedType)) {
return plainToSdk(mapping, (Class) type);
}
final ParameterizedType ptype = (ParameterizedType) type;
if (Map.class.equals(ptype.getRawType())) {
final Map<String, Object> convertedMap = new HashMap<>();
for (final Map.Entry<String, Object> entry : mapping.entrySet()) {
convertedMap.put(entry.getKey(), plainToSdk((Map) entry.getValue(), (Class<?>) ptype.getActualTypeArguments()[1]));
}
return convertedMap;
}
throw new AppsmithPluginException(
AppsmithPluginError.PLUGIN_ERROR,
"Unknown type to convert to SDK style " + type.getTypeName()
);
}
private static Method findMethod(Class<?> builderType, Predicate<Method> predicate) {
return Arrays.stream(builderType.getMethods())
.filter(predicate)
@ -363,7 +610,7 @@ public class DynamoPlugin extends BasePlugin {
private static Object sdkToPlain(Object valueObj) {
if (valueObj instanceof SdkPojo) {
final SdkPojo response = (SdkPojo) valueObj;
SdkPojo response = (SdkPojo) valueObj;
final Map<String, Object> plain = new HashMap<>();
for (final SdkField<?> field : response.sdkFields()) {
@ -373,6 +620,10 @@ public class DynamoPlugin extends BasePlugin {
return plain;
} else if (valueObj instanceof SdkBytes) {
SdkBytes response = (SdkBytes) valueObj;
return new String(response.asByteArray());
} else if (valueObj instanceof Map) {
final Map<?, ?> valueAsMap = (Map<?, ?>) valueObj;
final Map<Object, Object> plainMap = new HashMap<>();
@ -392,7 +643,6 @@ public class DynamoPlugin extends BasePlugin {
}
return plainList;
}
return valueObj;

View File

@ -15,6 +15,7 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
@ -27,10 +28,13 @@ import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@ -93,6 +97,42 @@ public class DynamoPluginTest {
))
.build());
ddb.createTable(CreateTableRequest.builder()
.tableName("allTypes")
.attributeDefinitions(
AttributeDefinition.builder().attributeName("Id").attributeType(ScalarAttributeType.N).build()
)
.keySchema(
KeySchemaElement.builder().attributeName("Id").keyType(KeyType.HASH).build()
)
.provisionedThroughput(
ProvisionedThroughput.builder().readCapacityUnits(5L).writeCapacityUnits(5L).build()
)
.build());
String testPayload1 = "payload1";
SdkBytes bytesValue1 = SdkBytes.fromByteArray(testPayload1.getBytes());
String testPayload2 = "payload2";
SdkBytes bytesValue2 = SdkBytes.fromByteArray(testPayload2.getBytes());
AttributeValue mapValue = AttributeValue.builder().s("mapValue").build();
AttributeValue listValue1 = AttributeValue.builder().s("listValue1").build();
AttributeValue listValue2 = AttributeValue.builder().s("listValue2").build();
ddb.putItem(PutItemRequest.builder()
.tableName("allTypes")
.item(Map.of(
"Id", AttributeValue.builder().n("1").build(),
"StringType", AttributeValue.builder().s("str").build(),
"BooleanType", AttributeValue.builder().bool(true).build(),
"BinaryType", AttributeValue.builder().b(bytesValue1).build(),
"NullType", AttributeValue.builder().nul(true).build(),
"StringSetType", AttributeValue.builder().ss("str1", "str2").build(),
"NumberSetType", AttributeValue.builder().ns("1", "2").build(),
"BinarySetType", AttributeValue.builder().bs(bytesValue1, bytesValue2).build(),
"MapType", AttributeValue.builder().m(Map.of("mapKey", mapValue)).build(),
"ListType", AttributeValue.builder().l(listValue1, listValue2).build()
))
.build());
Endpoint endpoint = new Endpoint();
endpoint.setHost(host);
endpoint.setPort(port.longValue());
@ -121,10 +161,17 @@ public class DynamoPluginTest {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());
assertArrayEquals(
new String[]{"cities"},
((Map<String, List<String>>) result.getBody()).get("TableNames").toArray()
);
HashSet<String> expectedTables = new HashSet<>();
expectedTables.add("cities");
expectedTables.add("allTypes");
HashSet<String> actualTables = new HashSet<>();
actualTables.add(((Map<String, ArrayList<String>>) result.getBody()).get("TableNames").get(0));
actualTables.add(((Map<String, ArrayList<String>>) result.getBody()).get("TableNames").get(1));
assertTrue(expectedTables.equals(actualTables));
})
.verifyComplete();
}
@ -162,8 +209,13 @@ public class DynamoPluginTest {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());
final Map<String, Map<String, Object>> item = ((Map<String, Map<String, Map<String, Object>>>) result.getBody()).get("Item");
assertEquals("New Delhi", item.get("City").get("S"));
Map<String, Object> resultBody = (Map<String, Object>) result.getBody();
Map<String, Object> rawResponse = (Map<String, Object>) resultBody.get("raw");
Map<String, Map<String, String>> rawItem = (Map<String, Map<String, String>>) rawResponse.get("Item");
assertEquals("New Delhi", rawItem.get("City").get("S"));
Map<String, String> transformedItem = (Map<String, String>) resultBody.get("Item");
assertEquals("New Delhi", transformedItem.get("City"));
})
.verifyComplete();
}
@ -215,8 +267,14 @@ public class DynamoPluginTest {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());
final Map<String, Map<String, Object>> attributes = ((Map<String, Map<String, Map<String, Object>>>) result.getBody()).get("Attributes");
assertEquals("Bengaluru", attributes.get("City").get("S"));
Map<String, Object> resultBody = (Map<String, Object>) result.getBody();
Map<String, Object> rawResponse = (Map<String, Object>) resultBody.get("raw");
Map<String, Map<String, Object>> rawItem = (Map<String, Map<String, Object>>) rawResponse.get(
"Attributes");
assertEquals("Bengaluru", rawItem.get("City").get("S"));
Map<String, String> transformedItem = (Map<String, String>) resultBody.get("Attributes");
assertEquals("Bengaluru", transformedItem.get("City"));
})
.verifyComplete();
}
@ -232,8 +290,111 @@ public class DynamoPluginTest {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());
final List<Object> items = (List<Object>) ((Map<String, Object>) result.getBody()).get("Items");
List<Map<String, Object>> items = (List<Map<String, Object>>)
((Map<String, Object>)((Map<String, Object>) result.getBody()).get("raw")).get("Items");
assertEquals(2, items.size());
for (int i=0; i<items.size(); i++) {
Map<String, Object> item = items.get(i);
for (Map.Entry<String, Object> entry: item.entrySet()) {
Object rawValue = ((Map<String, Object>) entry.getValue()).get("S");
Object transformedValue =
((List<Map<String, Object>>)((Map<String, Object>) result.getBody()).get("Items"))
.get(i).get(entry.getKey());
assertTrue(rawValue.equals(transformedValue));
}
}
})
.verifyComplete();
}
@Test
public void testBatchGetItem() {
final String body = "{\n" +
" \"RequestItems\": {\n" +
" \"cities\": {\n" +
" \"Keys\": [\n" +
" {\n" +
" \"Id\": {\n" +
" \"S\": \"1\"\n" +
" }\n" +
" },\n" +
" {\n" +
" \"Id\": {\n" +
" \"S\": \"2\"\n" +
" }\n" +
" }\n" +
" ],\n" +
" \"ProjectionExpression\":\"City\"\n" +
" }\n" +
" },\n" +
" \"ReturnConsumedCapacity\": \"TOTAL\"\n" +
"}";
StepVerifier.create(execute("BatchGetItem", body))
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
final Map<String, ?> response = (Map) result.getBody();
assertEquals(
Collections.emptyMap(),
response.remove("UnprocessedKeys")
);
// Test raw response
Map<String, Object> rawResponse =
(Map<String, Object>) ((Map<String, Object>) response.get("raw")).get(
"Responses");
ArrayList<Map<String, Object>> rawCitiesList = (ArrayList<Map<String, Object>>) rawResponse.get("cities");
assertEquals("New Delhi", ((Map<String, Object>)(rawCitiesList.get(0)).get("City")).get("S"));
// Test transformed response
Map<String, Object> transformedResponse = (Map<String, Object>) response.get("Responses");
ArrayList<Map<String, Object>> transformedCitiesList = (ArrayList<Map<String, Object>>) transformedResponse.get("cities");
assertEquals("New Delhi", transformedCitiesList.get(0).get("City"));
})
.verifyComplete();
}
@Test
public void testTransactGetItems() {
final String body =
"{\n" +
" \"ReturnConsumedCapacity\": \"NONE\",\n" +
" \"TransactItems\": [\n" +
" {\n" +
" \"Get\": {\n" +
" \"Key\": {\n" +
" \"Id\": {\n" +
" \"S\": \"1\"\n" +
" }\n" +
" },\n" +
" \"TableName\": \"cities\"\n" +
" }\n" +
" }\n" +
" ]\n" +
"}";
StepVerifier.create(execute("TransactGetItems", body))
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
final Map<String, ?> response = (Map) result.getBody();
// Test raw response
ArrayList<Map<String, Object>> rawResponse =
(ArrayList<Map<String, Object>>) ((Map<String, Object>) response.get("raw")).get(
"Responses");
assertEquals("New Delhi",
((Map<String, Map<String, Object>>)rawResponse.get(0).get("Item")).get("City").get("S"));
// Test transformed response
ArrayList<Map<String, Object>> transformedResponse = (ArrayList<Map<String, Object>>) response.get("Responses");
assertEquals("New Delhi",
((Map<String, Object>)transformedResponse.get(0).get("Item")).get("City"));
})
.verifyComplete();
}
@ -248,14 +409,85 @@ public class DynamoPluginTest {
.assertNext(structure -> {
assertNotNull(structure);
assertNotNull(structure.getTables());
assertEquals(
List.of("cities"),
structure.getTables().stream()
.map(DatasourceStructure.Table::getName)
.collect(Collectors.toList())
);
HashSet<String> expectedTables = new HashSet<>();
expectedTables.add("cities");
expectedTables.add("allTypes");
HashSet<String> actualTables = new HashSet<>();
actualTables.add(structure.getTables().get(0).getName());
actualTables.add(structure.getTables().get(1).getName());
assertTrue(expectedTables.equals(actualTables));
})
.verifyComplete();
}
/*
* - "allTypes" table contains data of all type supported by DynamoDb.
* - This test aims to test the data type handling capability of the plugin.
*/
@Test
public void testParsingCapabilityForAllTypes() {
final String body = "{\n" +
" \"TableName\": \"allTypes\"\n" +
"}\n";
StepVerifier.create(execute("Scan", body))
.assertNext(result -> {
assertNotNull(result);
assertTrue(result.getIsExecutionSuccess());
assertNotNull(result.getBody());
Map<String, Object> resultBody = (Map<String, Object>) result.getBody();
Map<String, Object> rawResponse = (Map<String, Object>) resultBody.get("raw");
/*
* - Check if data under the "raw" section i.e. non-transformed data is correct.
*/
ArrayList<Map<String, Object>> rawItems = (ArrayList<Map<String, Object>>) rawResponse.get(
"Items");
Map<String, Object> rawItemMap = rawItems.get(0);
assertEquals("1", ((Map<String, Object>)rawItemMap.get("Id")).get("N"));
assertEquals("str", ((Map<String, Object>)rawItemMap.get("StringType")).get("S"));
assertEquals("true", ((Map<String, Object>)rawItemMap.get("BooleanType")).get("BOOL").toString());
assertEquals("payload1", ((Map<String, Object>)rawItemMap.get("BinaryType")).get("B"));
assertEquals("true", ((Map<String, Object>)rawItemMap.get("NullType")).get("NUL").toString());
assertArrayEquals(new String[]{"str1", "str2"},
((ArrayList<String>)((Map<String, Object>)rawItemMap.get("StringSetType")).get("SS")).toArray());
assertArrayEquals(new String[]{"payload1", "payload2"},
((ArrayList<String>)((Map<String, Object>)rawItemMap.get("BinarySetType")).get("BS")).toArray());
assertArrayEquals(new String[]{"1", "2"},
((ArrayList<String>)((Map<String, Object>)rawItemMap.get("NumberSetType")).get("NS")).toArray());
assertEquals("mapValue",
((Map<String, Map<String, Map<String, Object>>>)rawItemMap.get("MapType")).get("M")
.get("mapKey").get("S").toString());
assertEquals("listValue1",
((HashMap<String, ArrayList<HashMap<String, Object>>>)rawItemMap.get("ListType")).get("L").get(0).get("S"));
assertEquals("listValue2",
((HashMap<String, ArrayList<HashMap<String, Object>>>)rawItemMap.get("ListType")).get("L").get(1).get("S"));
/*
* - Check if the transformed data is correct.
*/
ArrayList<Map<String, Object>> transformedItems = (ArrayList<Map<String, Object>>) resultBody.get("Items");
Map<String, Object> transformedItemMap = transformedItems.get(0);
assertEquals("1", transformedItemMap.get("Id"));
assertEquals("str", transformedItemMap.get("StringType"));
assertEquals("true", transformedItemMap.get("BooleanType").toString());
assertEquals("payload1", transformedItemMap.get("BinaryType"));
assertEquals("true", transformedItemMap.get("NullType").toString());
assertArrayEquals(new String[]{"str1", "str2"},
((ArrayList<String>)transformedItemMap.get("StringSetType")).toArray());
assertArrayEquals(new String[]{"payload1", "payload2"},
((ArrayList<String>)transformedItemMap.get("BinarySetType")).toArray());
assertArrayEquals(new String[]{"1", "2"},
((ArrayList<String>)transformedItemMap.get("NumberSetType")).toArray());
assertEquals("mapValue",
((Map<String, Object>)transformedItemMap.get("MapType")).get("mapKey").toString());
assertEquals("listValue1", ((ArrayList<String>)transformedItemMap.get("ListType")).get(0));
assertEquals("listValue2", ((ArrayList<String>)transformedItemMap.get("ListType")).get(1));
})
.verifyComplete();
}
}

View File

@ -242,11 +242,17 @@ public class MongoPlugin extends BasePlugin {
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication != null) {
builder
.append(urlEncode(authentication.getUsername()))
.append(':')
.append(urlEncode(authentication.getPassword()))
.append('@');
final boolean hasUsername = StringUtils.hasText(authentication.getUsername());
final boolean hasPassword = StringUtils.hasText(authentication.getPassword());
if (hasUsername) {
builder.append(urlEncode(authentication.getUsername()));
}
if (hasPassword) {
builder.append(':').append(urlEncode(authentication.getPassword()));
}
if (hasUsername || hasPassword) {
builder.append('@');
}
}
for (Endpoint endpoint : endpoints) {
@ -313,25 +319,11 @@ public class MongoPlugin extends BasePlugin {
}
DBAuth authentication = (DBAuth) datasourceConfiguration.getAuthentication();
if (authentication == null) {
invalids.add("Missing authentication details.");
} else {
if (authentication != null) {
DBAuth.Type authType = authentication.getAuthType();
if (authType != null && VALID_AUTH_TYPES.contains(authType)) {
if (StringUtils.isEmpty(authentication.getUsername())) {
invalids.add("Missing username for authentication. Needed because authType is " + authType + ".");
}
if (StringUtils.isEmpty(authentication.getPassword())) {
invalids.add("Missing password for authentication. Needed because authType is " + authType + ".");
}
} else {
if (authType == null || !VALID_AUTH_TYPES.contains(authType)) {
invalids.add("Invalid authType. Must be one of " + VALID_AUTH_TYPES_STR);
}
if (StringUtils.isEmpty(authentication.getDatabaseName())) {

View File

@ -43,7 +43,8 @@
"configProperty": "datasourceConfiguration.endpoints[*].host",
"controlType": "KEYVALUE_ARRAY",
"validationMessage": "Please enter a valid host",
"validationRegex": "^((?![/:]).)*$"
"validationRegex": "^((?![/:]).)*$",
"placeholder": "myapp.abcde.mongodb.net"
},
{
"label": "Port",

View File

@ -150,10 +150,13 @@ public class PostgresPlugin extends BasePlugin {
Boolean isPreparedStatement;
final List<Property> properties = actionConfiguration.getPluginSpecifiedTemplates();
if (properties.get(PREPARED_STATEMENT_INDEX) == null) {
// If the configuration does not exist, default to true
// Note this is not possible today since the query editor sets a default value for this field.
isPreparedStatement = true;
if (properties == null || properties.get(PREPARED_STATEMENT_INDEX) == null) {
/**
* TODO :
* In case the prepared statement configuration is missing, default to true once PreparedStatement
* is no longer in beta.
*/
isPreparedStatement = false;
} else {
isPreparedStatement = Boolean.parseBoolean(properties.get(PREPARED_STATEMENT_INDEX).getValue());
}

View File

@ -4,13 +4,6 @@
"sectionName": "",
"id": 1,
"children": [
{
"label": "Use Prepared Statement",
"configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value",
"controlType": "SWITCH",
"isRequired": true,
"initialValue": true
},
{
"label": "",
"configProperty": "actionConfiguration.body",
@ -19,4 +12,4 @@
]
}
]
}
}

View File

@ -0,0 +1,36 @@
{
"setting": [
{
"sectionName": "",
"id": 1,
"children": [
{
"label": "Run query on page load",
"configProperty": "executeOnLoad",
"controlType": "SWITCH",
"info": "Will refresh data each time the page is loaded"
},
{
"label": "Request confirmation before running query",
"configProperty": "confirmBeforeExecute",
"controlType": "SWITCH",
"info": "Ask confirmation from the user each time before refreshing data"
},
{
"label": "[Beta]Use Prepared Statement",
"info": "Turning on Prepared Statement makes the query parametrized. This in turn makes it resilient against SQL injections",
"configProperty": "actionConfiguration.pluginSpecifiedTemplates[0].value",
"controlType": "SWITCH",
"initialValue": false
},
{
"label": "Query timeout (in milliseconds)",
"info": "Maximum time after which the query will return",
"configProperty": "actionConfiguration.timeoutInMillisecond",
"controlType": "INPUT_TEXT",
"dataType": "NUMBER"
}
]
}
]
}

View File

@ -74,6 +74,7 @@ public class Application extends BaseDomain {
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AppLayout implements Serializable {
Type type;

View File

@ -313,12 +313,21 @@ public class PluginServiceImpl extends BaseService<PluginRepository, Plugin, Str
)
.onErrorReturn(new HashMap())
.cache();
final Mono<Map> settingMono = loadPluginResource(pluginId, "setting.json")
.doOnError(throwable ->
// Remove this pluginId from the cache so it is tried again next time.
formCache.remove(pluginId)
)
.onErrorReturn(new HashMap())
.cache();
Mono<Map> resourceMono = Mono.zip(formMono, editorMono)
Mono<Map> resourceMono = Mono.zip(formMono, editorMono, settingMono)
.map(tuple -> {
Map formMap = tuple.getT1();
Map editorMap = tuple.getT2();
Map settingMap = tuple.getT3();
formMap.putAll(editorMap);
formMap.putAll(settingMap);
return formMap;
});

View File

@ -78,7 +78,7 @@ public class AuthenticationService {
// The state is used internally to calculate the redirect url when returning control to the client
.queryParam(STATE, String.join(",", pageId, datasourceId, redirectUri));
// Adding optional scope parameter
if (!oAuth2.getScope().isEmpty()) {
if (oAuth2.getScope() != null && !oAuth2.getScope().isEmpty()) {
uriComponentsBuilder
.queryParam(SCOPE, String.join(",", oAuth2.getScope()));
}

View File

@ -68,6 +68,8 @@ public class PluginServiceTest {
.thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL)));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json")))
.thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL)));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json")))
.thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL)));
Mono<Map> formConfig = pluginService.getFormConfig("random-plugin-id");
@ -86,6 +88,8 @@ public class PluginServiceTest {
.thenReturn(Mono.just(formMap));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json")))
.thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL)));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json")))
.thenReturn(Mono.error(new AppsmithException(AppsmithError.PLUGIN_LOAD_FORM_JSON_FAIL)));
Mono<Map> formConfig = pluginService.getFormConfig("random-plugin-id");
StepVerifier.create(formConfig)
@ -93,6 +97,7 @@ public class PluginServiceTest {
assertThat(form).isNotNull();
assertThat(form.get("form")).isNotNull();
assertThat(form.get("editor")).isNull();
assertThat(form.get("setting")).isNull();
})
.verifyComplete();
}
@ -105,10 +110,15 @@ public class PluginServiceTest {
Map editorMap = new HashMap();
editorMap.put("editor", new Object());
Map settingMap = new HashMap();
settingMap.put("setting", new Object());
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("form.json")))
.thenReturn(Mono.just(formMap));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("editor.json")))
.thenReturn(Mono.just(editorMap));
Mockito.when(pluginService.loadPluginResource(Mockito.anyString(), eq("setting.json")))
.thenReturn(Mono.just(settingMap));
Mono<Map> formConfig = pluginService.getFormConfig("random-plugin-id");
StepVerifier.create(formConfig)
@ -116,6 +126,7 @@ public class PluginServiceTest {
assertThat(form).isNotNull();
assertThat(form.get("form")).isNotNull();
assertThat(form.get("editor")).isNotNull();
assertThat(form.get("setting")).isNotNull();
})
.verifyComplete();
}