Merge branch 'release' into feat/13412-improve-import-app-flow-ui

This commit is contained in:
haojin111 2022-05-23 13:31:28 +08:00
commit 7e5332ceb7
36 changed files with 1361 additions and 770 deletions

2
.github/config.json vendored

File diff suppressed because one or more lines are too long

View File

@ -7,13 +7,13 @@
"detachFromLayout": true,
"widgetId": "0",
"topRow": 0,
"bottomRow": 820,
"bottomRow": 1050,
"containerStyle": "none",
"snapRows": 125,
"parentRowSpace": 1,
"type": "CANVAS_WIDGET",
"canExtend": true,
"version": 23,
"version": 58,
"minHeight": 830,
"parentColumnSpace": 1,
"dynamicTriggerPathList": [],
@ -21,9 +21,10 @@
"leftColumn": 0,
"children": [
{
"labelTextSize": "0.875rem",
"boxShadow": "none",
"widgetName": "Chart1",
"rightColumn": 26,
"allowScroll": false,
"widgetId": "ypstklohw5",
"topRow": 4,
"bottomRow": 36,
@ -72,6 +73,7 @@
"parentColumnSpace": 19.65625,
"chartName": "Last week's revenue",
"leftColumn": 2,
"borderRadius": "0px",
"xAxisName": "Last Week",
"customFusionChartConfig": {
"type": "column2d",
@ -128,6 +130,8 @@
"chartType": "LINE_CHART"
},
{
"labelTextSize": "0.875rem",
"boxShadow": "none",
"backgroundColor": "#FFFFFF",
"widgetName": "Container1",
"rightColumn": 62,
@ -143,8 +147,11 @@
"isLoading": false,
"parentColumnSpace": 19.65625,
"leftColumn": 30,
"borderRadius": "0px",
"children": [
{
"labelTextSize": "0.875rem",
"boxShadow": "none",
"widgetName": "Canvas1",
"rightColumn": 629,
"detachFromLayout": true,
@ -162,12 +169,15 @@
"isLoading": false,
"parentColumnSpace": 1,
"leftColumn": 0,
"borderRadius": "0px",
"children": []
}
]
},
{
"labelTextSize": "0.875rem",
"image": "",
"boxShadow": "none",
"widgetName": "Image1",
"rightColumn": 22,
"widgetId": "1t50avy6f1",
@ -183,26 +193,63 @@
"parentColumnSpace": 19.65625,
"imageShape": "RECTANGLE",
"leftColumn": 6,
"borderRadius": "0px",
"defaultImage": "https://res.cloudinary.com/drako999/image/upload/v1589196259/default.png"
},
{
"labelTextSize": "0.875rem",
"boxShadow": "none",
"widgetName": "Button1",
"rightColumn": 18,
"isDefaultClickDisabled": true,
"buttonColor": "#03B365",
"widgetId": "41wgbhd5vp",
"buttonStyle": "PRIMARY_BUTTON",
"topRow": 44,
"bottomRow": 48,
"parentRowSpace": 10,
"isVisible": true,
"type": "BUTTON_WIDGET",
"version": 1,
"recaptchaType": "V3",
"parentId": "0",
"isLoading": false,
"parentColumnSpace": 19.65625,
"leftColumn": 10,
"borderRadius": "0px",
"buttonVariant": "PRIMARY",
"text": "Submit",
"isDisabled": false
},
{
"boxShadow": "none",
"widgetName": "Camera1",
"isCanvas": false,
"displayName": "Camera",
"iconSVG": "/static/media/icon.79c0d6de.svg",
"topRow": 70,
"bottomRow": 103,
"parentRowSpace": 10,
"type": "CAMERA_WIDGET",
"hideCard": false,
"mode": "CAMERA",
"parentColumnSpace": 12.5625,
"leftColumn": 2,
"dynamicBindingPathList": [
{
"key": "borderRadius"
}
],
"isDisabled": false,
"key": "bybcx1x9lk",
"isMirrored": true,
"rightColumn": 27,
"widgetId": "wv1qtmzsbm",
"isVisible": true,
"version": 1,
"parentId": "0",
"renderMode": "CANVAS",
"isLoading": false,
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
}
]
}

View File

@ -10,7 +10,6 @@ let homePage = ObjectsRegistry.HomePage,
locator = ObjectsRegistry.CommonLocators;
describe("AForce - Community Issues page validations", function() {
before(function() {
agHelper.clearLocalStorageCache();
});
@ -28,231 +27,267 @@ describe("AForce - Community Issues page validations", function () {
cy.visit("/applications");
homePage.ImportApp("CommunityIssuesExport.json");
cy.wait("@importNewApplication").then((interception: any) => {
agHelper.Sleep()
agHelper.Sleep();
const { isPartialImport } = interception.response.body.data;
if (isPartialImport) {
// should reconnect modal
dataSources.ReconnectDataSourcePostgres("AForceDB")
dataSources.ReconnectDataSourcePostgres("AForceDB");
} else {
homePage.AssertImport()
homePage.AssertImport();
}
//Validate table is not empty!
table.WaitUntilTableLoad()
table.WaitUntilTableLoad();
//Validating order of header columns!
table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at")
table.AssertTableHeaderOrder(
"TypeTitleStatus+1CommentorsVotesAnswerUpVoteStatesupvote_ididgithub_issue_idauthorcreated_atdescriptionlabelsstatelinkupdated_at",
);
//Validating hidden columns:
table.AssertHiddenColumns(['States', 'upvote_id', 'id', 'github_issue_id', 'author', 'created_at', 'description', 'labels', 'state', 'link', 'updated_at'])
table.AssertHiddenColumns([
"States",
"upvote_id",
"id",
"github_issue_id",
"author",
"created_at",
"description",
"labels",
"state",
"link",
"updated_at",
]);
});
});
it("2. Validate table navigation with Server Side pagination enabled with Default selected row", () => {
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.AssertExistingToggleState("serversidepagination", 'checked')
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.AssertExistingToggleState("serversidepagination", "checked");
agHelper.EvaluateExistingPropertyFieldValue("Default Selected Row")
.then($selectedRow => {
agHelper
.EvaluateExistingPropertyFieldValue("Default Selected Row")
.then(($selectedRow) => {
selectedRow = Number($selectedRow);
table.AssertSelectedRow(selectedRow)
table.AssertSelectedRow(selectedRow);
});
agHelper.DeployApp()
table.WaitUntilTableLoad()
agHelper.DeployApp();
table.WaitUntilTableLoad();
//Verify hidden columns are infact hidden in deployed app!
table.AssertTableHeaderOrder("TypeTitleStatus+1CommentorsVotesAnswerUpVote")//from case #1
table.AssertTableHeaderOrder(
"TypeTitleStatus+1CommentorsVotesAnswerUpVote",
); //from case #1
table.AssertSelectedRow(selectedRow)//Assert default selected row
table.AssertSelectedRow(selectedRow); //Assert default selected row
table.AssertPageNumber(1);
table.NavigateToNextPage()//page 2
agHelper.Sleep(3000)//wait for table navigation to take effect!
table.WaitUntilTableLoad()
table.AssertSelectedRow(selectedRow)
table.NavigateToNextPage(); //page 2
agHelper.Sleep(3000); //wait for table navigation to take effect!
table.WaitUntilTableLoad();
table.AssertSelectedRow(selectedRow);
table.NavigateToNextPage(); //page 3
agHelper.Sleep(3000); //wait for table navigation to take effect!
table.WaitForTableEmpty(); //page 3
table.NavigateToPreviousPage(); //page 2
agHelper.Sleep(3000); //wait for table navigation to take effect!
table.WaitUntilTableLoad();
table.AssertSelectedRow(selectedRow);
table.NavigateToNextPage()//page 3
agHelper.Sleep(3000)//wait for table navigation to take effect!
table.WaitForTableEmpty()//page 3
table.NavigateToPreviousPage()//page 2
agHelper.Sleep(3000)//wait for table navigation to take effect!
table.WaitUntilTableLoad()
table.AssertSelectedRow(selectedRow)
table.NavigateToPreviousPage()//page 1
agHelper.Sleep(3000)//wait for table navigation to take effect!
table.WaitUntilTableLoad()
table.AssertSelectedRow(selectedRow)
table.NavigateToPreviousPage(); //page 1
agHelper.Sleep(3000); //wait for table navigation to take effect!
table.WaitUntilTableLoad();
table.AssertSelectedRow(selectedRow);
table.AssertPageNumber(1);
})
});
it("3. Validate table navigation with Server Side pagination disabled with Default selected row selection", () => {
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.ToggleOnOrOff('serversidepagination', 'Off')
agHelper.DeployApp()
table.WaitUntilTableLoad()
table.AssertPageNumber(1, 'Off');
table.AssertSelectedRow(selectedRow)
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.ToggleOnOrOff('serversidepagination', 'On')
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.ToggleOnOrOff("serversidepagination", "Off");
agHelper.DeployApp();
table.WaitUntilTableLoad();
table.AssertPageNumber(1, "Off");
table.AssertSelectedRow(selectedRow);
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.ToggleOnOrOff("serversidepagination", "On");
});
it("4. Change Default selected row in table and verify", () => {
jsEditor.EnterJSContext("Default Selected Row", "1")
agHelper.DeployApp()
table.WaitUntilTableLoad()
jsEditor.EnterJSContext("Default Selected Row", "1");
agHelper.DeployApp();
table.WaitUntilTableLoad();
table.AssertPageNumber(1);
table.AssertSelectedRow(1)
table.NavigateToNextPage()//page 2
table.AssertSelectedRow(1);
table.NavigateToNextPage(); //page 2
table.AssertPageNumber(2);
table.AssertSelectedRow(1)
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
table.AssertSelectedRow(1);
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
});
it.skip("5. Verify Default search text in table as per 'Default Search Text' property set + Bug 12228", () => {
ee.SelectEntityByName("Table1", "WIDGETS");
jsEditor.EnterJSContext("Default Search Text", "Bug", false);
agHelper.DeployApp();
table.AssertSearchText("Bug");
table.WaitUntilTableLoad();
table.WaitUntilTableLoad();
agHelper.NavigateBacktoEditor();
ee.SelectEntityByName("Table1", 'WIDGETS')
jsEditor.EnterJSContext("Default Search Text", "Bug", false)
agHelper.DeployApp()
table.AssertSearchText('Bug')
table.WaitUntilTableLoad()
table.WaitUntilTableLoad()
agHelper.NavigateBacktoEditor()
ee.SelectEntityByName("Table1", "WIDGETS");
jsEditor.EnterJSContext("Default Search Text", "Question", false);
agHelper.DeployApp();
table.AssertSearchText("Question");
table.WaitUntilTableLoad();
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", 'WIDGETS')
jsEditor.EnterJSContext("Default Search Text", "Question", false)
agHelper.DeployApp()
table.AssertSearchText('Question')
table.WaitUntilTableLoad()
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", 'WIDGETS')
jsEditor.EnterJSContext("Default Search Text", "Epic", false)//Bug 12228 - Searching based on hidden column value should not be allowed
agHelper.DeployApp()
table.AssertSearchText('Epic')
table.WaitForTableEmpty()
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", 'WIDGETS')
jsEditor.RemoveText('defaultsearchtext')
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", "WIDGETS");
jsEditor.EnterJSContext("Default Search Text", "Epic", false); //Bug 12228 - Searching based on hidden column value should not be allowed
agHelper.DeployApp();
table.AssertSearchText("Epic");
table.WaitForTableEmpty();
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", "WIDGETS");
jsEditor.RemoveText("defaultsearchtext");
table.WaitUntilTableLoad();
});
it.skip("6. Validate Search table with Client Side Search enabled & disabled", () => {
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.AssertExistingToggleState("enableclientsidesearch", 'checked')
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.AssertExistingToggleState("enableclientsidesearch", "checked");
agHelper.DeployApp()
table.WaitUntilTableLoad()
agHelper.DeployApp();
table.WaitUntilTableLoad();
table.SearchTable('Bug')
table.WaitUntilTableLoad()
cy.xpath(table._searchBoxCross).click()
table.SearchTable("Bug");
table.WaitUntilTableLoad();
cy.xpath(table._searchBoxCross).click();
table.SearchTable('Question')
table.WaitUntilTableLoad()
cy.xpath(table._searchBoxCross).click()
table.SearchTable("Question");
table.WaitUntilTableLoad();
cy.xpath(table._searchBoxCross).click();
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.ToggleOnOrOff("enableclientsidesearch", 'Off')
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.ToggleOnOrOff("enableclientsidesearch", "Off");
agHelper.DeployApp()
table.WaitUntilTableLoad()
agHelper.DeployApp();
table.WaitUntilTableLoad();
table.SearchTable('Bug')
table.WaitForTableEmpty()
cy.xpath(table._searchBoxCross).click()
table.SearchTable("Bug");
table.WaitForTableEmpty();
cy.xpath(table._searchBoxCross).click();
table.SearchTable('Question')
table.WaitForTableEmpty()
cy.xpath(table._searchBoxCross).click()
table.SearchTable("Question");
table.WaitForTableEmpty();
cy.xpath(table._searchBoxCross).click();
agHelper.NavigateBacktoEditor()
table.WaitUntilTableLoad()
ee.SelectEntityByName("Table1", 'WIDGETS')
agHelper.ToggleOnOrOff("enableclientsidesearch", 'On')
})
agHelper.NavigateBacktoEditor();
table.WaitUntilTableLoad();
ee.SelectEntityByName("Table1", "WIDGETS");
agHelper.ToggleOnOrOff("enableclientsidesearch", "On");
});
it("7. Validate Filter table", () => {
agHelper.DeployApp()
table.WaitUntilTableLoad()
var filterTitle = new Array();
agHelper.DeployApp();
table.WaitUntilTableLoad();
//One filter
table.OpenNFilterTable("Type", "is exactly", "Bug")
table.ReadTableRowColumnData(0, 1).then(($cellData) => {
expect($cellData).to.eq("[Bug]: Postgres queries unable to execute with more than 9 placeholders");
table.OpenNFilterTable("Type", "is exactly", "Bug");
for (let i = 0; i < 3; i++) {
table.ReadTableRowColumnData(i, 0, 200).then(($cellData) => {
expect($cellData).to.eq("Bug");
});
table.ReadTableRowColumnData(2, 1).then(($cellData) => {
expect($cellData).to.eq("[Bug]: Input updates with default values are not captured");
});
table.RemoveFilterNVerify("Question", true, false)
}
table.RemoveFilterNVerify("Question", true, false);
//Two filters - OR
table.OpenNFilterTable("Type", "starts with", "Trouble")
table.ReadTableRowColumnData(0, 0).then(($cellData) => {
table.OpenNFilterTable("Type", "starts with", "Trouble");
for (let i = 0; i < 5; i++) {
table.ReadTableRowColumnData(i, 0, 200).then(($cellData) => {
expect($cellData).to.eq("Troubleshooting");
});
table.ReadTableRowColumnData(0, 1).then(($cellData) => {
expect($cellData).to.eq("Renew expired SSL certificate on a self-hosted instance");
}
table.OpenNFilterTable("Title", "contains", "query", "OR", 1);
table.ReadTableRowColumnData(1, 0, 200).then(($cellData) => {
expect($cellData).to.be.oneOf(["Troubleshooting", "Question"]);
});
table.OpenNFilterTable("Title", "contains", "query", 'OR', 1)
table.ReadTableRowColumnData(1, 0).then(($cellData) => {
expect($cellData).to.be.oneOf(['Troubleshooting','Question'])
for (let i = 0; i < 8; i++) {
table.ReadTableRowColumnData(i, 1, 100).then(($cellData) => {
if ($cellData.toLowerCase().includes("query"))
filterTitle.push($cellData);
});
table.ReadTableRowColumnData(6, 1).then(($cellData) => {
expect($cellData).to.eq("Run storeValue commands before a Query.run()");
});
table.RemoveFilterNVerify("Question", true, false)
}
cy.wrap(filterTitle).as("filterTitleText"); // alias it for later
cy.get("@filterTitleText")
.its("length")
.should("eq", 2);
table.RemoveFilterNVerify("Question", true, false);
//Two filters - AND
table.OpenNFilterTable("Votes", "greater than", "3")
table.ReadTableRowColumnData(1, 1).then(($cellData) => {
table.OpenNFilterTable("Votes", "greater than", "2");
table.ReadTableRowColumnData(0, 1).then(($cellData) => {
expect($cellData).to.eq("Combine queries from different datasources");
});
table.OpenNFilterTable("Title", "contains", "button", 'AND', 1)
table.OpenNFilterTable("Title", "contains", "button", "AND", 1);
table.ReadTableRowColumnData(0, 1).then(($cellData) => {
expect($cellData).to.eq("Change the video in the video player with a button click");
expect($cellData).to.eq(
"Change the video in the video player with a button click",
);
});
table.RemoveFilterNVerify("Question", true, false);
});
table.RemoveFilterNVerify("Question", true, false)
})
it("8. Validate Adding a New issue from Add Modal", () => {
// agHelper.DeployApp()
// table.WaitUntilTableLoad()
cy.get(table._addIcon).closest('div').click()
agHelper.AssertElementPresence(locator._modal)
agHelper.SelectFromDropDown('Suggestion', 't--modal-widget')
cy.get(table._addIcon)
.closest("div")
.click();
agHelper.AssertElementPresence(locator._modal);
agHelper.SelectFromDropDown("Suggestion", "t--modal-widget");
cy.get(locator._inputWidgetv1InDeployed).eq(3).type("Adding Title Suggestion via script")
cy.get(locator._textAreainputWidgetv1InDeployed).eq(1).type("Adding Description Suggestion via script")
cy.get(locator._inputWidgetv1InDeployed).eq(4).type("https://github.com/appsmithorg/appsmith/issues/12532")
agHelper.SelectFromMultiSelect(['Epic', 'Task'], 1)
cy.xpath(table._visibleTextSpan('Labels')).click()
cy.get(locator._inputWidgetv1InDeployed).eq(5).type("https://release.app.appsmith.com/applications/62486d45ab307a026918639e/pages/62486d45ab307a02691863a7")
agHelper.SelectFromMultiSelect(['Documented', 'Needs App'], 1, true, 'multiselectwidget')
cy.get(locator._inputWidgetv1InDeployed)
.eq(3)
.type("Adding Title Suggestion via script");
cy.get(locator._textAreainputWidgetv1InDeployed)
.eq(1)
.type("Adding Description Suggestion via script");
cy.get(locator._inputWidgetv1InDeployed)
.eq(4)
.type("https://github.com/appsmithorg/appsmith/issues/12532");
agHelper.SelectFromMultiSelect(["Epic", "Task"], 1);
cy.xpath(table._visibleTextSpan("Labels")).click();
cy.get(locator._inputWidgetv1InDeployed)
.eq(5)
.type(
"https://release.app.appsmith.com/applications/62486d45ab307a026918639e/pages/62486d45ab307a02691863a7",
);
agHelper.SelectFromMultiSelect(
["Documented", "Needs App"],
1,
true,
"multiselectwidget",
);
agHelper.ClickButton('Confirm')
agHelper.Sleep(3000)
table.SearchTable('Suggestion', 2)
table.WaitUntilTableLoad()
agHelper.ClickButton("Confirm");
agHelper.Sleep(3000);
table.SearchTable("Suggestion", 2);
table.WaitUntilTableLoad();
table.ReadTableRowColumnData(0, 0, 1000).then((cellData) => {
expect(cellData).to.be.equal("Suggestion");
@ -261,22 +296,32 @@ describe("AForce - Community Issues page validations", function () {
table.ReadTableRowColumnData(0, 1, 1000).then((cellData) => {
expect(cellData).to.be.equal("Adding Title Suggestion via script");
});
})
});
it("9. Validate Updating issue from Details tab", () => {
agHelper.AssertElementAbsence(locator._widgetInDeployed('tabswidget'))
table.SelectTableRow(0)
agHelper.AssertElementPresence(locator._widgetInDeployed('tabswidget'))
agHelper.GetNClick(locator._inputWidgetv1InDeployed).type("-updating title")
agHelper.GetNClick(locator._textAreainputWidgetv1InDeployed).type("-updating desc")
agHelper.GetNClick(locator._inputWidgetv1InDeployed, 1).type("-updating issue link")
agHelper.SelectFromDropDown('Troubleshooting', 't--widget-tabswidget')
agHelper.SelectFromMultiSelect(['Epic', 'Task'], 0, false)
agHelper.SelectFromMultiSelect(['High', 'Dependencies'], 0, true)
agHelper.SelectFromDropDown('[Bug] TypeError: o is undefined', 't--widget-tabswidget', 1)
agHelper.GetNClick(locator._inputWidgetv1InDeployed, 2).type("-updating answer link")
agHelper.AssertElementAbsence(locator._widgetInDeployed("tabswidget"));
table.SelectTableRow(0);
agHelper.AssertElementPresence(locator._widgetInDeployed("tabswidget"));
agHelper
.GetNClick(locator._inputWidgetv1InDeployed)
.type("-updating title");
agHelper
.GetNClick(locator._textAreainputWidgetv1InDeployed)
.type("-updating desc");
agHelper
.GetNClick(locator._inputWidgetv1InDeployed, 1)
.type("-updating issue link");
agHelper.SelectFromDropDown("Troubleshooting", "t--widget-tabswidget");
agHelper.SelectFromMultiSelect(["Epic", "Task"], 0, false);
agHelper.SelectFromMultiSelect(["High", "Dependencies"], 0, true);
agHelper.SelectFromDropDown(
"[Bug] TypeError: o is undefined",
"t--widget-tabswidget",
1,
);
agHelper
.GetNClick(locator._inputWidgetv1InDeployed, 2)
.type("-updating answer link");
//cy.get("body").tab().type("{enter}")
@ -288,34 +333,41 @@ describe("AForce - Community Issues page validations", function () {
// key: 'Enter',
// })
//agHelper.Sleep(2000)
//cy.get("body").type("{enter}")
agHelper.RemoveMultiSelectItems(['Documented', 'Needs App'])
agHelper.RemoveMultiSelectItems(["Documented", "Needs App"]);
//agHelper.SelectFromMultiSelect(['Documented', 'Needs App', 'App Built'], 0, false, 'multiselectwidget')
agHelper.SelectFromMultiSelect(['Needs Product'], 0, true, 'multiselectwidget')
agHelper.ClickButton('Save')
agHelper.SelectFromMultiSelect(
["Needs Product"],
0,
true,
"multiselectwidget",
);
agHelper.ClickButton("Save");
table.ReadTableRowColumnData(0, 0, 1000).then((cellData) => {
expect(cellData).to.be.equal("Troubleshooting");
});
table.ReadTableRowColumnData(0, 1, 1000).then((cellData) => {
expect(cellData).to.be.equal("Adding Title Suggestion via script-updating title");
expect(cellData).to.be.equal(
"Adding Title Suggestion via script-updating title",
);
});
});
})
it("10. Validate Deleting the newly created issue", () => {
agHelper.AssertElementAbsence(locator._widgetInDeployed('tabswidget'))
table.SelectTableRow(0)
agHelper.AssertElementPresence(locator._widgetInDeployed('tabswidget'))
agHelper.Sleep()
cy.get(table._trashIcon).closest('div').click()
agHelper.AssertElementAbsence(locator._widgetInDeployed('tabswidget'))
table.WaitForTableEmpty()
agHelper.AssertElementAbsence(locator._widgetInDeployed("tabswidget"));
table.SelectTableRow(0);
agHelper.AssertElementPresence(locator._widgetInDeployed("tabswidget"));
agHelper.Sleep();
cy.get(table._trashIcon)
.closest("div")
.click();
agHelper.AssertElementAbsence(locator._widgetInDeployed("tabswidget"));
table.WaitForTableEmpty();
//2nd search is not working, hence commenting below
// cy.xpath(table._searchBoxCross).click()

View File

@ -113,7 +113,7 @@ describe("Validate API request body panel", () => {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);

View File

@ -19,7 +19,7 @@ describe("Validate JSObjects binding to Input widget", () => {
paste: false,
completeReplace: false,
toRun: true,
shouldNavigate: true,
shouldCreateNewJSObj: true,
});
ee.expandCollapseEntity("WIDGETS"); //to expand widgets
ee.expandCollapseEntity("Form1");

View File

@ -32,7 +32,7 @@ describe("Validate JSObj binding to Table widget", () => {
paste: false,
completeReplace: false,
toRun: true,
shouldNavigate: true,
shouldCreateNewJSObj: true,
});
cy.get("@jsObjName").then((jsObj) => {
jsName = jsObj;

View File

@ -319,7 +319,7 @@ showAlert("Wonderful! all apis executed", "success")).catch(() => showAlert("Ple
return Promise.any([this.func2(), this.func3(), this.func1()]).then((value) => showAlert("Resolved promise is:" + value))
}
}`,
{ paste: true, completeReplace: true, toRun: true, shouldNavigate: true },
{ paste: true, completeReplace: true, toRun: true, shouldCreateNewJSObj: true },
);
ee.SelectEntityByName("Button1", "WIDGETS");
cy.get("@jsObjName").then((jsObjName) => {

View File

@ -15,7 +15,7 @@ describe("JS Function Execution", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
@ -33,7 +33,7 @@ describe("JS Function Execution", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
@ -58,7 +58,7 @@ describe("JS Function Execution", function() {
paste: true,
completeReplace: true,
toRun: true,
shouldNavigate: true,
shouldCreateNewJSObj: true,
});
// Assert presence of function execution parse error callout
@ -69,7 +69,7 @@ describe("JS Function Execution", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: false,
shouldCreateNewJSObj: false,
});
// Assert presence of parse error callout (entire JS Object is invalid)

View File

@ -33,5 +33,17 @@ describe("Widget Grouping", function() {
.should("have.length", 2);
cy.get(`@group`).find(`.t--draggable-buttonwidget`);
cy.get(`@group`).find(`.t--draggable-imagewidget`);
// verify the position so that the camera widget is still below the newly grouped container
cy.get(`.t--widget-containerwidget`)
.eq(1)
.then((element) => {
const elementTop = parseFloat(element.css("top"));
const elementHeight = parseFloat(element.css("height"));
const containerBottom = (elementTop + elementHeight).toString() + "px";
cy.get(`.t--widget-camerawidget`)
.invoke("attr", "style")
.should("contain", `top: ${containerBottom}`);
});
});
});

View File

@ -43,7 +43,7 @@ describe("Validate basic operations on Entity explorer JSEditor structure", () =
it("5. Validate Move JSObject", function() {
const newPageId = "Page2";
agHelper.AddNewPage();
ee.AddNewPage();
ee.AssertEntityPresenceInExplorer(newPageId);
ee.SelectEntityByName(pageId);
ee.expandCollapseEntity("QUERIES/JS");

View File

@ -39,7 +39,7 @@ describe("JSObjects OnLoad Actions tests", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
jsEditor.EnableDisableAsyncFuncSettings("getId", false, true); //Only before calling confirmation is enabled by User here
@ -189,7 +189,7 @@ describe("JSObjects OnLoad Actions tests", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
@ -251,7 +251,7 @@ describe("JSObjects OnLoad Actions tests", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
@ -440,7 +440,7 @@ describe("JSObjects OnLoad Actions tests", function() {
paste: true,
completeReplace: true,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
@ -481,7 +481,7 @@ describe("JSObjects OnLoad Actions tests", function() {
// `{{` +
// jsObjName +
// `.callCountry();
// showAlert('Your country is: ' + getCountry.data[0].country, 'info')}}`,
// Select1.selectedOptionValue? showAlert('Your country is: ' + getCountry.data[0].country, 'info'): null`,
// true,
// true,
// );

View File

@ -38,7 +38,7 @@ describe("[Bug] - 10784 - Passing params from JS to SQL query should not break",
paste: true,
completeReplace: false,
toRun: false,
shouldNavigate: true,
shouldCreateNewJSObj: true,
},
);
});

View File

@ -74,6 +74,7 @@
"evaluatedType": ".t--CodeEditor-evaluatedValue > div:first-of-type pre",
"evaluatedCurrentValue": "div:last-of-type .t--CodeEditor-evaluatedValue > div:last-of-type pre",
"entityExplorersearch": "#entity-explorer-search",
"searchEntityInExplorer": "#search-entity",
"entitySearchResult": ".t--entity-name:contains('",
"saveStatusContainer": ".t--save-status-container",
"saveStatusIsSaving": "t--save-status-is-saving",

View File

@ -134,17 +134,6 @@ export class AggregateHelper {
localStorage.setItem("inDeployedMode", "true");
}
public AddNewPage() {
cy.get(this.locator._newPage)
.first()
.click();
cy.wait("@createPage").should(
"have.nested.property",
"response.body.responseMeta.status",
201,
);
}
public ValidateToastMessage(text: string, length = 1) {
cy.get(this.locator._toastMsg)
.should("have.length", length)
@ -409,6 +398,17 @@ export class AggregateHelper {
.wait(500);
}
public GetNClickByContains(
selector: string,
containsText: string,
index = 0,
) {
cy.get(selector)
.contains(containsText)
.eq(index)
.click().wait(200);
}
public ToggleOnOrOff(propertyName: string, toggle: "On" | "Off") {
if (toggle == "On") {
cy.get(this.locator._propertyToggle(propertyName))
@ -605,7 +605,11 @@ export class AggregateHelper {
locator.should("not.exist");
}
public GetText(selector: string, textOrValue : 'text'| 'val' = 'text', index = 0) {
public GetText(
selector: string,
textOrValue: "text" | "val" = "text",
index = 0,
) {
let locator = selector.startsWith("//")
? cy.xpath(selector)
: cy.get(selector);

View File

@ -34,26 +34,28 @@ export class DataSources {
_reconnectModal = "div.reconnect-datasource-modal";
_activeDSListReconnectModal = (dbName: string) =>
"//div[contains(@class, 't--ds-list')]//span[text()='" + dbName + "']";
_apiQueryBtn = ".t--run-query";
_runQueryBtn = ".t--run-query";
_newDatabases = "#new-datasources";
_selectDatasourceDropdown = "[data-cy=t--datasource-dropdown]";
_datasourceDropdownOption = "[data-cy=t--datasource-dropdown-option]";
_selectTableDropdown = "[data-cy=t--table-dropdown]";
_tableDropdownOption = ".bp3-popover-content .t--dropdown-option";
_generatePageBtn = "[data-cy=t--generate-page-form-submit]";
public NavigateToDSAdd() {
public CreatePlugIn(pluginName: string) {
cy.get(this._createNewPlgin(pluginName)).trigger("click");
}
public NavigateToDSCreateNew() {
cy.get(this._addNewDataSource)
.last()
.scrollIntoView()
.should("be.visible")
.click({ force: true });
}
public CreatePlugIn(pluginName: string) {
cy.get(this._createNewPlgin(pluginName)).click();
}
public NavigateToDSCreateNew() {
this.NavigateToDSAdd();
cy.get(this._dsCreateNewTab)
.should("be.visible")
.click({ force: true });
cy.get(this.locator._loading).should("not.exist");
// cy.get(this._dsCreateNewTab)
// .should("be.visible")
// .click({ force: true });
cy.get(this._newDatabases).should("be.visible");
}
public FillPostgresDSForm(shouldAddTrailingSpaces = false) {
@ -73,6 +75,18 @@ export class DataSources {
cy.get(this._password).type(datasourceFormData["postgres-password"]);
}
public FillMongoDSForm(shouldAddTrailingSpaces = false) {
const hostAddress = shouldAddTrailingSpaces
? datasourceFormData["mongo-host"] + " "
: datasourceFormData["mongo-host"];
cy.get(this._host).type(hostAddress);
cy.get(this._port).type(datasourceFormData["mongo-port"].toString());
cy.get(this._sectionAuthentication).click();
cy.get(this._databaseName)
.clear()
.type(datasourceFormData["mongo-databaseName"]);
}
public TestSaveDatasource(expectedRes = true) {
this.TestDatasource(expectedRes);
this.SaveDatasource();
@ -94,7 +108,7 @@ export class DataSources {
}
public NavigateToActiveDSQueryPane(datasourceName: string) {
this.NavigateToDSAdd();
this.NavigateToDSCreateNew();
this.agHelper.GetNClick(this.locator._activeTab);
cy.get(this._datasourceCard)
.contains(datasourceName)
@ -145,7 +159,7 @@ export class DataSources {
}
RunQuery() {
cy.get(this._apiQueryBtn).click({ force: true });
cy.get(this._runQueryBtn).click({ force: true });
this.agHelper.ValidateNetworkExecutionSuccess("@postExecute");
}
}

View File

@ -1,69 +1,88 @@
import { ObjectsRegistry } from "../Objects/Registry"
import { ObjectsRegistry } from "../Objects/Registry";
export class EntityExplorer {
public agHelper = ObjectsRegistry.AggregateHelper
public agHelper = ObjectsRegistry.AggregateHelper;
public locator = ObjectsRegistry.CommonLocators;
public SelectEntityByName(entityNameinLeftSidebar: string, section: 'WIDGETS' | 'QUERIES/JS' | 'DATASOURCES' | '' = '') {
if (section)
this.expandCollapseEntity(section)//to expand respective section
public SelectEntityByName(
entityNameinLeftSidebar: string,
section: "WIDGETS" | "QUERIES/JS" | "DATASOURCES" | "" = "",
) {
if (section) this.expandCollapseEntity(section); //to expand respective section
cy.xpath(this.locator._entityNameInExplorer(entityNameinLeftSidebar))
.last()
.click({ multiple: true })
this.agHelper.Sleep()
.click({ multiple: true });
this.agHelper.Sleep();
}
public NavigateToSwitcher(navigationTab: 'explorer' | 'widgets') {
cy.get(this.locator._openNavigationTab(navigationTab)).click()
public AddNewPage() {
cy.get(this.locator._newPage)
.first()
.click();
this.agHelper.ValidateNetworkStatus("@createPage", 201);
}
public NavigateToSwitcher(navigationTab: "explorer" | "widgets") {
cy.get(this.locator._openNavigationTab(navigationTab)).click();
}
public AssertEntityPresenceInExplorer(entityNameinLeftSidebar: string) {
cy.xpath(this.locator._entityNameInExplorer(entityNameinLeftSidebar))
.should("have.length", 1);
cy.xpath(
this.locator._entityNameInExplorer(entityNameinLeftSidebar),
).should("have.length", 1);
}
public AssertEntityAbsenceInExplorer(entityNameinLeftSidebar: string) {
cy.xpath(this.locator._entityNameInExplorer(entityNameinLeftSidebar)).should('not.exist');
cy.xpath(
this.locator._entityNameInExplorer(entityNameinLeftSidebar),
).should("not.exist");
}
public expandCollapseEntity(entityName: string, expand = true) {
cy.xpath(this.locator._expandCollapseArrow(entityName)).invoke('attr', 'name').then((arrow) => {
if (expand && arrow == 'arrow-right')
cy.xpath(this.locator._expandCollapseArrow(entityName)).trigger('click', { multiple: true }).wait(1000);
else if (!expand && arrow == 'arrow-down')
cy.xpath(this.locator._expandCollapseArrow(entityName)).trigger('click', { multiple: true }).wait(1000);
else
this.agHelper.Sleep(500)
})
cy.xpath(this.locator._expandCollapseArrow(entityName))
.invoke("attr", "name")
.then((arrow) => {
if (expand && arrow == "arrow-right")
cy.xpath(this.locator._expandCollapseArrow(entityName))
.trigger("click", { multiple: true })
.wait(1000);
else if (!expand && arrow == "arrow-down")
cy.xpath(this.locator._expandCollapseArrow(entityName))
.trigger("click", { multiple: true })
.wait(1000);
else this.agHelper.Sleep(500);
});
}
public ActionContextMenuByEntityName(entityNameinLeftSidebar: string, action = "Delete", subAction = "") {
public ActionContextMenuByEntityName(
entityNameinLeftSidebar: string,
action = "Delete",
subAction = "",
) {
this.agHelper.Sleep();
cy.xpath(this.locator._contextMenu(entityNameinLeftSidebar))
.last()
.click({ force: true });
cy.xpath(this.locator._contextMenuItem(action))
.click({ force: true })
this.agHelper.Sleep(500)
cy.xpath(this.locator._contextMenuItem(action)).click({ force: true });
this.agHelper.Sleep(500);
if (subAction) {
cy.xpath(this.locator._contextMenuItem(subAction))
.click({ force: true })
this.agHelper.Sleep(500)
cy.xpath(this.locator._contextMenuItem(subAction)).click({ force: true });
this.agHelper.Sleep(500);
}
}
public DragDropWidgetNVerify(widgetType: string, x: number, y: number) {
this.NavigateToSwitcher('widgets')
this.agHelper.Sleep()
cy.get(this.locator._widgetPageIcon(widgetType)).first()
this.NavigateToSwitcher("widgets");
this.agHelper.Sleep();
cy.get(this.locator._widgetPageIcon(widgetType))
.first()
.trigger("dragstart", { force: true })
.trigger("mousemove", x, y, { force: true });
cy.get(this.locator._dropHere)
.trigger("mousemove", x, y, { eventConstructor: "MouseEvent" })
.trigger("mousemove", x, y, { eventConstructor: "MouseEvent" })
.trigger("mouseup", x, y, { eventConstructor: "MouseEvent" });
this.agHelper.AssertAutoSave()//settling time for widget on canvas!
cy.get(this.locator._widgetInCanvas(widgetType)).should('exist')
this.agHelper.AssertAutoSave(); //settling time for widget on canvas!
cy.get(this.locator._widgetInCanvas(widgetType)).should("exist");
}
}

View File

@ -29,7 +29,7 @@ export class HomePage {
private _editAppName = "bp3-editable-text-editing"
private _appMenu = ".t--editor-appname-menu-portal .bp3-menu-item"
private _buildFromScratchActionCard = ".t--BuildFromScratch"
private _buildFromDataTableActionCard = ".t--GenerateCRUDPage"
_buildFromDataTableActionCard = ".t--GenerateCRUDPage"
private _selectRole = "//span[text()='Select a role']/ancestor::div"
private _searchInput = "input[type='text']"
_appHoverIcon = (action: string) => ".t--application-" + action + "-link"

View File

@ -4,13 +4,13 @@ export interface ICreateJSObjectOptions {
paste: boolean;
completeReplace: boolean;
toRun: boolean;
shouldNavigate: boolean;
shouldCreateNewJSObj: boolean;
}
const DEFAULT_CREATE_JS_OBJECT_OPTIONS = {
paste: true,
completeReplace: false,
toRun: true,
shouldNavigate: true,
shouldCreateNewJSObj: true,
};
export class JSEditor {
@ -82,7 +82,7 @@ export class JSEditor {
//#endregion
//#region Page functions
public NavigateToJSEditor() {
public NavigateToNewJSEditor() {
cy.get(this.locator._createNew)
.last()
.click({ force: true });
@ -107,9 +107,9 @@ export class JSEditor {
JSCode: string,
options: ICreateJSObjectOptions = DEFAULT_CREATE_JS_OBJECT_OPTIONS,
) {
const { completeReplace, paste, shouldNavigate, toRun } = options;
const { completeReplace, paste, shouldCreateNewJSObj, toRun } = options;
shouldNavigate && this.NavigateToJSEditor();
shouldCreateNewJSObj && this.NavigateToNewJSEditor();
if (!completeReplace) {
cy.get(this.locator._codeMirrorTextArea)
.first()

View File

@ -313,7 +313,7 @@ Cypress.Commands.add("SearchApp", (appname) => {
});
Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => {
cy.get(commonlocators.entityExplorersearch)
cy.get(commonlocators.searchEntityInExplorer)
.clear({ force: true })
.type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
@ -329,7 +329,7 @@ Cypress.Commands.add("SearchEntity", (apiname1, apiname2) => {
Cypress.Commands.add("GlobalSearchEntity", (apiname1, dontAssertVisibility) => {
// entity explorer search will be hidden
cy.get(commonlocators.entityExplorersearch)
cy.get(commonlocators.searchEntityInExplorer)
.clear({ force: true })
.type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
@ -413,7 +413,7 @@ Cypress.Commands.add("CheckAndUnfoldWidgets", () => {
});
Cypress.Commands.add("SearchEntityandOpen", (apiname1) => {
cy.get(commonlocators.entityExplorersearch)
cy.get(commonlocators.searchEntityInExplorer)
.clear({ force: true })
.type(apiname1, { force: true });
cy.CheckAndUnfoldWidgets();
@ -432,7 +432,7 @@ Cypress.Commands.add("SearchEntityandOpen", (apiname1) => {
});
Cypress.Commands.add("SearchEntityAndUnfold", (apiname1) => {
cy.get(commonlocators.entityExplorersearch)
cy.get(commonlocators.searchEntityInExplorer)
.clear({ force: true })
.type(apiname1, { force: true });
// eslint-disable-next-line cypress/no-unnecessary-waiting
@ -451,7 +451,7 @@ Cypress.Commands.add("SearchEntityAndUnfold", (apiname1) => {
Cypress.Commands.add("OpenBindings", (apiname1) => {
cy.wait(500);
cy.get(commonlocators.entityExplorersearch)
cy.get(commonlocators.searchEntityInExplorer)
.clear({ force: true })
.type(apiname1, { force: true });
cy.CheckAndUnfoldWidgets();

View File

@ -7,22 +7,17 @@ import React, {
useMemo,
} from "react";
import classNames from "classnames";
import history from "utils/history";
import * as Sentry from "@sentry/react";
import { PanelStack } from "@blueprintjs/core";
import { useDispatch, useSelector } from "react-redux";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { AppState } from "reducers";
import {
getFirstTimeUserOnboardingComplete,
getIsFirstTimeUserOnboardingEnabled,
} from "selectors/onboardingSelectors";
import Explorer from "pages/Editor/Explorer";
import Switcher from "components/ads/Switcher";
import { trimQueryString } from "utils/helpers";
import AppComments from "comments/AppComments/AppComments";
import { setExplorerActiveAction } from "actions/explorerActions";
import {
@ -33,14 +28,10 @@ import { tailwindLayers } from "constants/Layers";
import TooltipComponent from "components/ads/Tooltip";
import { previewModeSelector } from "selectors/editorSelectors";
import useHorizontalResize from "utils/hooks/useHorizontalResize";
import { forceOpenWidgetPanel } from "actions/widgetSidebarActions";
import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions";
import OnboardingStatusbar from "pages/Editor/FirstTimeUserOnboarding/Statusbar";
import Pages from "pages/Editor/Explorer/Pages";
import { Colors } from "constants/Colors";
import { EntityProperties } from "pages/Editor/Explorer/Entity/EntityProperties";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { builderURL } from "RouteBuilder";
type Props = {
width: number;
@ -58,38 +49,12 @@ export const EntityExplorerSidebar = memo((props: Props) => {
const enableFirstTimeUserOnboarding = useSelector(
getIsFirstTimeUserOnboardingEnabled,
);
const isFirstTimeUserOnboardingEnabled = useSelector(
getIsFirstTimeUserOnboardingEnabled,
);
const resizer = useHorizontalResize(
sidebarRef,
props.onWidthChange,
props.onDragEnd,
);
const switches = [
{
id: "explorer",
text: "Explorer",
action: () => dispatch(forceOpenWidgetPanel(false)),
},
{
id: "widgets",
text: "Widgets",
action: () => {
!(trimQueryString(builderURL()) === window.location.pathname) &&
history.push(builderURL());
setTimeout(() => dispatch(forceOpenWidgetPanel(true)), 0);
if (isFirstTimeUserOnboardingEnabled) {
dispatch(toggleInOnboardingWidgetSelection(true));
}
},
},
];
const [activeSwitch, setActiveSwitch] = useState(switches[0]);
const [tooltipIsOpen, setTooltipIsOpen] = useState(false);
const isForceOpenWidgetPanel = useSelector(
(state: AppState) => state.ui.onBoarding.forceOpenWidgetPanel,
);
const isFirstTimeUserOnboardingComplete = useSelector(
getFirstTimeUserOnboardingComplete,
);
@ -98,14 +63,6 @@ export const EntityExplorerSidebar = memo((props: Props) => {
PerformanceTracker.stopTracking();
});
useEffect(() => {
if (isForceOpenWidgetPanel) {
setActiveSwitch(switches[1]);
} else {
setActiveSwitch(switches[0]);
}
}, [isForceOpenWidgetPanel]);
// registering event listeners
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
@ -191,7 +148,7 @@ export const EntityExplorerSidebar = memo((props: Props) => {
<div
className={classNames({
[`js-entity-explorer t--entity-explorer transform transition-all flex h-full duration-400 border-r border-gray-200 ${tailwindLayers.entityExplorer}`]: true,
"relative ": pinned && !isPreviewMode,
relative: pinned && !isPreviewMode,
"-translate-x-full": (!pinned && !active) || isPreviewMode,
"shadow-xl": !pinned,
fixed: !pinned || isPreviewMode,
@ -199,7 +156,7 @@ export const EntityExplorerSidebar = memo((props: Props) => {
>
{/* SIDEBAR */}
<div
className="flex flex-col p-0 overflow-y-auto bg-white t--sidebar min-w-48 max-w-96 group"
className="flex flex-col p-0 bg-white t--sidebar min-w-48 max-w-96 group"
ref={sidebarRef}
style={{ width: props.width }}
>
@ -209,19 +166,8 @@ export const EntityExplorerSidebar = memo((props: Props) => {
<Pages />
{/* Popover that contains the bindings info */}
<EntityProperties />
{/* SWITCHER */}
<div
className={`px-3 mt-1 py-2 border-t border-b border-[${Colors.Gallery}]`}
>
<Switcher activeObj={activeSwitch} switches={switches} />
</div>
<PanelStack
className="flex-grow"
initialPanel={{
component: Explorer,
}}
showPanelHeader={false}
/>
{/* Contains entity explorer & widgets library along with a switcher*/}
<Explorer />
<AppComments />
</div>
{/* RESIZER */}

View File

@ -1,2 +1,3 @@
export const ENTITY_EXPLORER_SEARCH_ID = "entity-explorer-search";
export const WIDGETS_SEARCH_ID = "#widgets-search";
export const SEARCH_ENTITY = "search-entity";

View File

@ -220,11 +220,11 @@ export const Entity = forwardRef(
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
if (props.isDefaultExpanded) {
if (props.isDefaultExpanded || props.searchKeyword) {
open(true);
props.onToggle && props.onToggle(true);
}
}, [props.isDefaultExpanded]);
}, [props.isDefaultExpanded, props.searchKeyword]);
useEffect(() => {
if (!props.searchKeyword && !props.isDefaultExpanded) {
open(false);

View File

@ -8,9 +8,7 @@ import React, {
import styled from "styled-components";
import Divider from "components/editorComponents/Divider";
import Search from "./ExplorerSearch";
import { NonIdealState, Classes, IPanelProps } from "@blueprintjs/core";
import WidgetSidebar from "../WidgetSidebar";
import history from "utils/history";
import { NonIdealState, Classes } from "@blueprintjs/core";
import JSDependencies from "./JSDependencies";
import PerformanceTracker, {
PerformanceTransactionName,
@ -29,6 +27,8 @@ import Datasources from "./Datasources";
import Files from "./Files";
import ExplorerWidgetGroup from "./Widgets/WidgetGroup";
import { builderURL } from "RouteBuilder";
import history from "utils/history";
import { SEARCH_ENTITY } from "constants/Explorer";
const Wrapper = styled.div`
height: 100%;
@ -71,7 +71,7 @@ const StyledDivider = styled(Divider)`
border-bottom-color: #f0f0f0;
`;
function EntityExplorer(props: IPanelProps) {
function EntityExplorer({ isActive }: { isActive: boolean }) {
const dispatch = useDispatch();
const [searchKeyword, setSearchKeyword] = useState("");
const searchInputRef: MutableRefObject<HTMLInputElement | null> = useRef(
@ -86,15 +86,13 @@ function EntityExplorer(props: IPanelProps) {
getIsFirstTimeUserOnboardingEnabled,
);
const noResults = false;
const { openPanel } = props;
const showWidgetsSidebar = useCallback(() => {
history.push(builderURL());
openPanel({ component: WidgetSidebar });
dispatch(forceOpenWidgetPanel(true));
if (isFirstTimeUserOnboardingEnabled) {
dispatch(toggleInOnboardingWidgetSelection(true));
}
}, [openPanel, isFirstTimeUserOnboardingEnabled]);
}, [isFirstTimeUserOnboardingEnabled]);
/**
* filter entitites
@ -112,10 +110,14 @@ function EntityExplorer(props: IPanelProps) {
};
return (
<Wrapper className={"relative"} ref={explorerRef}>
<Wrapper
className={`relative overflow-y-auto ${isActive ? "" : "hidden"}`}
ref={explorerRef}
>
{/* SEARCH */}
<Search
clear={clearSearchInput}
id={SEARCH_ENTITY}
isHidden
onChange={search}
ref={searchInputRef}

View File

@ -15,6 +15,7 @@ export const ExplorerSearch = forwardRef(
autoFocus?: boolean;
isHidden?: boolean;
onChange?: (e: any) => void;
id?: string;
},
ref: Ref<HTMLInputElement>,
) => {
@ -62,7 +63,7 @@ export const ExplorerSearch = forwardRef(
autoComplete="off"
autoFocus
className="flex-grow py-2 text-gray-800 bg-transparent placeholder-trueGray-500"
id={ENTITY_EXPLORER_SEARCH_ID}
id={props.id || ENTITY_EXPLORER_SEARCH_ID}
onBlur={() => setFocussed(false)}
onChange={onChange}
onFocus={() => setFocussed(true)}
@ -78,8 +79,7 @@ export const ExplorerSearch = forwardRef(
</div>
<div
className={classNames({
"border-b border-primary-500 transition-all duration-400 absolute bottom-0": true,
"w-0": !focussed,
"border-b border-primary-500 absolute bottom-0": true,
"w-full": focussed,
})}
/>

View File

@ -1,23 +1,73 @@
import { IPanelProps } from "@blueprintjs/core";
import React from "react";
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions";
import { forceOpenWidgetPanel } from "actions/widgetSidebarActions";
import { Switcher } from "components/ads";
import { Colors } from "constants/Colors";
import { tailwindLayers } from "constants/Layers";
import React, { useEffect, useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "reducers";
import { builderURL } from "RouteBuilder";
import { getIsFirstTimeUserOnboardingEnabled } from "selectors/onboardingSelectors";
import { trimQueryString } from "utils/helpers";
import history from "utils/history";
import WidgetSidebar from "../WidgetSidebar";
import EntityExplorer from "./EntityExplorer";
const isForceOpenWidgetPanelSelector = (state: AppState) =>
const selectForceOpenWidgetPanel = (state: AppState) =>
state.ui.onBoarding.forceOpenWidgetPanel;
function ExplorerContent(props: IPanelProps) {
const isForceOpenWidgetPanel = useSelector(isForceOpenWidgetPanelSelector);
useEffect(() => {
if (isForceOpenWidgetPanel) {
props.openPanel({ component: WidgetSidebar });
function ExplorerContent() {
const dispatch = useDispatch();
const isFirstTimeUserOnboardingEnabled = useSelector(
getIsFirstTimeUserOnboardingEnabled,
);
const switches = useMemo(
() => [
{
id: "explorer",
text: "Explorer",
action: () => dispatch(forceOpenWidgetPanel(false)),
},
{
id: "widgets",
text: "Widgets",
action: () => {
!(trimQueryString(builderURL()) === window.location.pathname) &&
history.push(builderURL());
dispatch(forceOpenWidgetPanel(true));
if (isFirstTimeUserOnboardingEnabled) {
dispatch(toggleInOnboardingWidgetSelection(true));
}
}, [isForceOpenWidgetPanel]);
},
},
],
[
dispatch,
forceOpenWidgetPanel,
isFirstTimeUserOnboardingEnabled,
toggleInOnboardingWidgetSelection,
],
);
const [activeSwitch, setActiveSwitch] = useState(switches[0]);
const openWidgetPanel = useSelector(selectForceOpenWidgetPanel);
return <EntityExplorer {...props} />;
useEffect(() => {
setActiveSwitch(switches[openWidgetPanel ? 1 : 0]);
}, [openWidgetPanel]);
return (
<div
className={`flex-1 flex flex-col overflow-hidden ${tailwindLayers.entityExplorer}`}
>
<div
className={`flex-shrink-0 px-3 mt-1 py-2 border-t border-b border-[${Colors.Gallery}]`}
>
<Switcher activeObj={activeSwitch} switches={switches} />
</div>
<WidgetSidebar isActive={activeSwitch.id === "widgets"} />
<EntityExplorer isActive={activeSwitch.id === "explorer"} />
</div>
);
}
export default ExplorerContent;

View File

@ -33,7 +33,6 @@ import {
ONBOARDING_STATUS_STEPS_THIRD_ALT,
} from "@appsmith/constants/messages";
import { getTypographyByKey } from "constants/DefaultTheme";
import { Colors } from "constants/Colors";
import { onboardingCheckListUrl } from "RouteBuilder";

View File

@ -1,22 +1,16 @@
import React, { useRef, useEffect, useState } from "react";
import React, { useRef, useState } from "react";
import { useSelector } from "react-redux";
import WidgetCard from "./WidgetCard";
import { getWidgetCards } from "selectors/editorSelectors";
import { IPanelProps } from "@blueprintjs/core";
import ExplorerSearch from "./Explorer/ExplorerSearch";
import { debounce } from "lodash";
import produce from "immer";
import { useLocation } from "react-router";
import {
createMessage,
WIDGET_SIDEBAR_CAPTION,
} from "@appsmith/constants/messages";
import { matchBuilderPath } from "constants/routes";
import { AppState } from "reducers";
function WidgetSidebar(props: IPanelProps) {
const location = useLocation();
function WidgetSidebar({ isActive }: { isActive: boolean }) {
const cards = useSelector(getWidgetCards);
const [filteredCards, setFilteredCards] = useState(cards);
const searchInputRef = useRef<HTMLInputElement | null>(null);
@ -33,17 +27,6 @@ function WidgetSidebar(props: IPanelProps) {
}
setFilteredCards(filteredCards);
};
const isForceOpenWidgetPanel = useSelector(
(state: AppState) => state.ui.onBoarding.forceOpenWidgetPanel,
);
const onCanvas = matchBuilderPath(window.location.pathname);
useEffect(() => {
if (!onCanvas || isForceOpenWidgetPanel === false) {
props.closePanel();
}
}, [onCanvas, location, isForceOpenWidgetPanel]);
/**
* filter widgets
@ -64,7 +47,9 @@ function WidgetSidebar(props: IPanelProps) {
};
return (
<div className="flex flex-col overflow-hidden">
<div
className={`flex flex-col overflow-hidden ${isActive ? "" : "hidden"}`}
>
<ExplorerSearch
autoFocus
clear={clearSearchInput}

View File

@ -25,6 +25,7 @@ import {
} from "./StyledComponents";
import { getCurrentUser as refreshCurrentUser } from "actions/authActions";
import { getAppsmithConfigs } from "@appsmith/configs";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
const { disableLoginForm } = getAppsmithConfigs();
const ForgotPassword = styled.a`
@ -73,6 +74,8 @@ function General() {
dispatch(refreshCurrentUser());
}, []);
if (user?.email === ANONYMOUS_USERNAME) return null;
return (
<Wrapper>
<FieldWrapper>

View File

@ -128,7 +128,12 @@ import {
collisionCheckPostReflow,
getBottomRowAfterReflow,
} from "utils/reflowHookUtils";
import { PrevReflowState, ReflowDirection, SpaceMap } from "reflow/reflowTypes";
import {
GridProps,
PrevReflowState,
ReflowDirection,
SpaceMap,
} from "reflow/reflowTypes";
import { WidgetSpace } from "constants/CanvasEditorConstants";
import { reflow } from "reflow";
import { getBottomMostRow } from "reflow/reflowUtils";
@ -845,7 +850,6 @@ export function calculateNewWidgetPosition(
* @param copiedTotalWidth total width of the copied widgets
* @param copiedTopMostRow top row of the top most copied widget
* @param copiedLeftMostColumn left column of the left most copied widget
* @param shouldGroup boolean to indicate if the user is grouping instead of pasting
* @returns
*/
const getNewPositions = function*(
@ -854,11 +858,7 @@ const getNewPositions = function*(
copiedTotalWidth: number,
copiedTopMostRow: number,
copiedLeftMostColumn: number,
shouldGroup: boolean,
) {
// if it is grouping instead of pasting then skip the pasting logic
if (shouldGroup) return {};
const selectedWidgetIDs: string[] = yield select(getSelectedWidgets);
const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
const {
@ -1187,16 +1187,18 @@ function* pasteWidgetSaga(
let widgets: CanvasWidgetsReduxState = canvasWidgets;
const selectedWidget: FlattenedWidgetProps<undefined> = yield getSelectedWidgetWhenPasting();
let reflowedMovementMap,
bottomMostRow: number | undefined,
gridProps: GridProps | undefined,
newPastingPositionMap: SpaceMap | undefined,
canvasId;
let pastingIntoWidgetId: string = yield getParentWidgetIdForPasting(
canvasWidgets,
selectedWidget,
);
let isThereACollision: boolean = yield isSelectedWidgetsColliding(
widgets,
copiedWidgetGroups,
pastingIntoWidgetId,
);
let isThereACollision = false;
// if this is true, selected widgets will be grouped in container
if (shouldGroup) {
@ -1204,7 +1206,6 @@ function* pasteWidgetSaga(
pastingIntoWidgetId = yield getParentWidgetIdForGrouping(
widgets,
copiedWidgetGroups,
pastingIntoWidgetId,
);
widgets = yield filterOutSelectedWidgets(
copiedWidgetGroups[0].parentId,
@ -1216,10 +1217,20 @@ function* pasteWidgetSaga(
pastingIntoWidgetId,
);
copiedWidgetGroups = yield groupWidgetsIntoContainer(
//while grouping, the container around the selected widgets will increase by 2 rows,
//hence if there are any widgets in that path then we reflow those widgets
// If there are already widgets inside the selection box even before grouping
//then we will have to move it down to the bottom most row
({
bottomMostRow,
copiedWidgetGroups,
gridProps,
reflowedMovementMap,
} = yield groupWidgetsIntoContainer(
copiedWidgetGroups,
pastingIntoWidgetId,
);
isThereACollision,
));
} else if (isCopiedModalWidget(copiedWidgetGroups, widgets)) {
pastingIntoWidgetId = MAIN_CONTAINER_WIDGET_ID;
}
@ -1242,25 +1253,27 @@ function* pasteWidgetSaga(
widgets,
);
// skip new position calculation if grouping
if (!shouldGroup) {
// new pasting positions, the variables are undefined if the positions cannot be calculated,
// then it pastes the regular way at the bottom of the canvas
const {
({
bottomMostRow,
canvasId,
gridProps,
newPastingPositionMap,
reflowedMovementMap,
}: NewPastePositionVariables = yield call(
} = yield call(
getNewPositions,
copiedWidgetGroups,
action.payload.mouseLocation,
copiedTotalWidth,
topMostWidget.topRow,
leftMostWidget.leftColumn,
shouldGroup,
);
));
if (canvasId) pastingIntoWidgetId = canvasId;
}
yield all(
copiedWidgetGroups.map((copiedWidgets) =>

View File

@ -33,15 +33,24 @@ import {
import { getNextEntityName } from "utils/AppsmithUtils";
import WidgetFactory from "utils/WidgetFactory";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import { OccupiedSpace } from "constants/CanvasEditorConstants";
import { OccupiedSpace, WidgetSpace } from "constants/CanvasEditorConstants";
import { areIntersecting } from "utils/WidgetPropsUtils";
import { GridProps, ReflowedSpaceMap, SpaceMap } from "reflow/reflowTypes";
import {
GridProps,
PrevReflowState,
ReflowDirection,
ReflowedSpaceMap,
SpaceMap,
} from "reflow/reflowTypes";
import {
getBaseWidgetClassName,
getSlidingCanvasName,
getStickyCanvasName,
POSITIONED_WIDGET,
} from "constants/componentClassNameConstants";
import { getWidgetSpacesSelectorForContainer } from "selectors/editorSelectors";
import { reflow } from "reflow";
import { getBottomRowAfterReflow } from "utils/reflowHookUtils";
export interface CopiedWidgetGroup {
widgetId: string;
@ -992,6 +1001,7 @@ export function isDropTarget(type: WidgetType, includeCanvasWidget = false) {
export const groupWidgetsIntoContainer = function*(
copiedWidgetGroups: CopiedWidgetGroup[],
pastingIntoWidgetId: string,
isThereACollision: boolean,
) {
const containerWidgetId = generateReactKey();
const evalTree: DataTree = yield select(getDataTree);
@ -1006,6 +1016,7 @@ export const groupWidgetsIntoContainer = function*(
"CANVAS_WIDGET",
evalTree,
);
let reflowedMovementMap, bottomMostRow, gridProps;
const {
bottomMostWidget,
leftMostWidget,
@ -1018,8 +1029,12 @@ export const groupWidgetsIntoContainer = function*(
(w) => w.widgetId === copiedWidgetGroup.widgetId,
),
);
//calculating parentColumnSpace because the values stored inside widget DSL are not entirely reliable
const parentColumnSpace =
copiedWidgetGroups[0].list[0].parentColumnSpace || 1;
getParentColumnSpace(canvasWidgets, pastingIntoWidgetId) ||
copiedWidgetGroups[0].list[0].parentColumnSpace ||
1;
const boundary = {
top: _.minBy(copiedWidgets, (copiedWidget) => copiedWidget?.topRow),
@ -1124,13 +1139,73 @@ export const groupWidgetsIntoContainer = function*(
const flatList = _.flattenDeep(list);
return [
// if there are no collision already then reflow the below widgets by 2 rows.
if (!isThereACollision) {
const widgetSpacesSelector = getWidgetSpacesSelectorForContainer(
pastingIntoWidgetId,
);
const widgetSpaces: WidgetSpace[] = yield select(widgetSpacesSelector) ||
[];
const copiedWidgetIds = copiedWidgets
.map((widget) => widget?.widgetId)
.filter((id) => !!id);
// filter out copiedWidgets from occupied spaces
const widgetOccupiedSpaces = widgetSpaces.filter(
(widgetSpace) => copiedWidgetIds.indexOf(widgetSpace.id) === -1,
);
// create the object of the new container in the form of OccupiedSpace
const containerSpace = {
id: "1",
left: newContainerWidget.leftColumn,
top: newContainerWidget.topRow,
right: newContainerWidget.rightColumn,
bottom: newContainerWidget.bottomRow,
};
gridProps = {
parentColumnSpace,
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
maxGridColumns: GridDefaults.DEFAULT_GRID_COLUMNS,
};
//get movement map of reflowed widgets
const { movementMap } = reflow(
[containerSpace],
[containerSpace],
widgetOccupiedSpaces,
ReflowDirection.BOTTOM,
gridProps,
true,
false,
{ prevSpacesMap: {} } as PrevReflowState,
);
reflowedMovementMap = movementMap;
//get the new calculated bottom row
bottomMostRow = getBottomRowAfterReflow(
reflowedMovementMap,
containerSpace.bottom,
widgetOccupiedSpaces,
gridProps,
);
}
return {
reflowedMovementMap,
bottomMostRow,
gridProps,
copiedWidgetGroups: [
{
list: [newContainerWidget, newCanvasWidget, ...flatList],
widgetId: newContainerWidget.widgetId,
parentId: pastingIntoWidgetId,
},
];
],
};
};
/**
@ -1225,27 +1300,20 @@ export const isSelectedWidgetsColliding = function*(
widget.parentId === pastingIntoWidgetId && widget.type !== "MODAL_WIDGET",
);
let isColliding = false;
for (let i = 0; i < widgetsArray.length; i++) {
const widget = widgetsArray[i];
if (
widget.bottomRow + 2 < topMostWidget.topRow ||
widget.topRow > bottomMostWidget.bottomRow
) {
isColliding = false;
} else if (
widget.rightColumn < leftMostWidget.leftColumn ||
widget.leftColumn > rightMostWidget.rightColumn
) {
isColliding = false;
} else {
!(
widget.leftColumn >= rightMostWidget.rightColumn ||
widget.rightColumn <= leftMostWidget.leftColumn ||
widget.topRow >= bottomMostWidget.bottomRow ||
widget.bottomRow <= topMostWidget.topRow
)
)
return true;
}
}
return isColliding;
return false;
};
/**
@ -1351,8 +1419,8 @@ export const getParentBottomRowAfterAddingWidget = (
export function* getParentWidgetIdForGrouping(
widgets: CanvasWidgetsReduxState,
copiedWidgetGroups: CopiedWidgetGroup[],
pastingIntoWidgetId: string,
) {
const pastingIntoWidgetId = copiedWidgetGroups[0]?.parentId;
const widgetIds = copiedWidgetGroups.map(
(widgetGroup) => widgetGroup.widgetId,
);
@ -1445,6 +1513,33 @@ export function purgeOrphanedDynamicPaths(widget: WidgetProps) {
return widget;
}
/**
*
* @param canvasWidgets
* @param pastingIntoWidgetId
* @returns
*/
export function getParentColumnSpace(
canvasWidgets: CanvasWidgetsReduxState,
pastingIntoWidgetId: string,
) {
const containerId = getContainerIdForCanvas(pastingIntoWidgetId);
const containerWidget = canvasWidgets[containerId];
const canvasDOM = document.querySelector(
`#${getSlidingCanvasName(pastingIntoWidgetId)}`,
);
if (!canvasDOM || !containerWidget) return;
const rect = canvasDOM.getBoundingClientRect();
// get Grid values such as snapRowSpace and snapColumnSpace
const { snapGrid } = getSnappedGrid(containerWidget, rect.width);
return snapGrid?.snapColumnSpace;
}
/*
* Function to extend the lodash's get function to check
* paths which have dots in it's key

View File

@ -62,7 +62,8 @@ describe("#parse", () => {
);
});
it("returns unmodified schema when existing field's value in data source changes to null/undefined", () => {
it("returns unmodified schema when existing field's value in data source changes to null and back", () => {
// Get the initial schema
const initialSchema = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: {},
@ -71,45 +72,390 @@ describe("#parse", () => {
expect(initialSchema).toEqual(testData.initialDataset.schemaOutput);
// With null field
const nulledDataSource = klona(testData.initialDataset.dataSource);
set(nulledDataSource, "dob", null);
// Set all keys to null
const nulledSourceData = klona(testData.initialDataset.dataSource);
set(nulledSourceData, "name", null);
set(nulledSourceData, "age", null);
set(nulledSourceData, "dob", null);
set(nulledSourceData, "boolean", null);
set(nulledSourceData, "hobbies", null);
set(nulledSourceData, "%%", null);
set(nulledSourceData, "हिन्दि", null);
set(nulledSourceData, "education", null);
set(nulledSourceData, "address", null);
const expectedNulledSchema = klona(initialSchema);
set(expectedNulledSchema, "__root_schema__.children.dob.sourceData", null);
set(expectedNulledSchema, "__root_schema__.sourceData.dob", null);
// Set the sourceData entry in each SchemaItem to null (only property that changes)
const expectedSchema = klona(initialSchema);
set(expectedSchema, "__root_schema__.children.name.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.name", null);
set(expectedSchema, "__root_schema__.children.age.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.age", null);
set(expectedSchema, "__root_schema__.children.dob.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.dob", null);
set(expectedSchema, "__root_schema__.children.boolean.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.boolean", null);
set(expectedSchema, "__root_schema__.children.hobbies.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.hobbies", null);
set(expectedSchema, "__root_schema__.children.education.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.education", null);
set(expectedSchema, "__root_schema__.children.__.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData['%%']", null);
set(
expectedSchema,
"__root_schema__.children.xn__j2bd4cyac6f.sourceData",
null,
);
set(expectedSchema, "__root_schema__.sourceData.हिन्दि", null);
set(expectedSchema, "__root_schema__.children.address.sourceData", null);
set(expectedSchema, "__root_schema__.sourceData.address", null);
const schemaWithNulledField = SchemaParser.parse(widgetName, {
currSourceData: nulledDataSource,
// Parse with the nulled sourceData
const schemaWithNullKeys = SchemaParser.parse(widgetName, {
currSourceData: nulledSourceData,
schema: initialSchema,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithNulledField).toEqual(expectedNulledSchema);
expect(schemaWithNullKeys).toEqual(expectedSchema);
// With undefined field
const undefinedDataSource = klona(nulledDataSource);
set(undefinedDataSource, "boolean", undefined);
const expectedUndefinedSchema = klona(expectedNulledSchema);
set(
expectedUndefinedSchema,
"__root_schema__.children.boolean.sourceData",
undefined,
);
set(
expectedUndefinedSchema,
"__root_schema__.sourceData.boolean",
undefined,
);
const schemaWithUndefinedField = SchemaParser.parse(widgetName, {
currSourceData: undefinedDataSource,
schema: schemaWithNulledField,
/**
* Parse with initial sourceData to check if previous schema with null sourceData
* can still retain the schema structure
*/
const schemaWithRevertedData = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: schemaWithNullKeys,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithUndefinedField).toEqual(expectedUndefinedSchema);
expect(schemaWithRevertedData).toEqual(
testData.initialDataset.schemaOutput,
);
});
it("returns unmodified schema when existing fields value in data source changes to undefined and back", () => {
// Get the initial schema
const initialSchema = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: {},
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(initialSchema).toEqual(testData.initialDataset.schemaOutput);
// Set all keys to undefined
const undefinedDataSource = klona(testData.initialDataset.dataSource);
set(undefinedDataSource, "name", undefined);
set(undefinedDataSource, "age", undefined);
set(undefinedDataSource, "dob", undefined);
set(undefinedDataSource, "boolean", undefined);
set(undefinedDataSource, "hobbies", undefined);
set(undefinedDataSource, "%%", undefined);
set(undefinedDataSource, "हिन्दि", undefined);
set(undefinedDataSource, "education", undefined);
set(undefinedDataSource, "address", undefined);
// Set the sourceData entry in each SchemaItem to undefined (only property that changes)
const expectedSchema = klona(initialSchema);
set(expectedSchema, "__root_schema__.children.name.sourceData", undefined);
set(expectedSchema, "__root_schema__.sourceData.name", undefined);
set(expectedSchema, "__root_schema__.children.age.sourceData", undefined);
set(expectedSchema, "__root_schema__.sourceData.age", undefined);
set(expectedSchema, "__root_schema__.children.dob.sourceData", undefined);
set(expectedSchema, "__root_schema__.sourceData.dob", undefined);
set(
expectedSchema,
"__root_schema__.children.boolean.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.boolean", undefined);
set(
expectedSchema,
"__root_schema__.children.hobbies.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.hobbies", undefined);
set(
expectedSchema,
"__root_schema__.children.education.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.education", undefined);
set(expectedSchema, "__root_schema__.children.__.sourceData", undefined);
set(expectedSchema, "__root_schema__.sourceData['%%']", undefined);
set(
expectedSchema,
"__root_schema__.children.xn__j2bd4cyac6f.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.हिन्दि", undefined);
set(
expectedSchema,
"__root_schema__.children.address.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.address", undefined);
// Parse with the undefined sourceData keys
const schemaWithUndefinedKeys = SchemaParser.parse(widgetName, {
currSourceData: undefinedDataSource,
schema: initialSchema,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithUndefinedKeys).toEqual(expectedSchema);
/**
* Parse with initial sourceData to check if previous schema with null sourceData
* can still retain the schema structure
*/
const schemaWithRevertedData = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: schemaWithUndefinedKeys,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithRevertedData).toEqual(
testData.initialDataset.schemaOutput,
);
});
it("returns unmodified schema when existing inner field's value in data source changes to null and back", () => {
// Get the initial schema
const initialSchema = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: {},
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(initialSchema).toEqual(testData.initialDataset.schemaOutput);
// Set all keys to null
const nulledSourceData = klona(testData.initialDataset.dataSource);
set(nulledSourceData, "address.Line1", null);
set(nulledSourceData, "address.city", null);
set(nulledSourceData, "education[0].college", null);
set(nulledSourceData, "education[0].number", null);
set(nulledSourceData, "education[0].graduationDate", null);
set(nulledSourceData, "education[0].boolean", null);
// Set the sourceData entry in each SchemaItem to null (only property that changes)
const expectedSchema = klona(initialSchema);
set(
expectedSchema,
"__root_schema__.children.address.children.Line1.sourceData",
null,
);
set(expectedSchema, "__root_schema__.sourceData.address.Line1", null);
set(
expectedSchema,
"__root_schema__.children.address.children.city.sourceData",
null,
);
set(expectedSchema, "__root_schema__.sourceData.address.city", null);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.college.sourceData",
null,
);
set(expectedSchema, "__root_schema__.children.address.sourceData", {
Line1: null,
city: null,
});
set(
expectedSchema,
"__root_schema__.sourceData.education[0].college",
null,
);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.number.sourceData",
null,
);
set(expectedSchema, "__root_schema__.sourceData.education[0].number", null);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.graduationDate.sourceData",
null,
);
set(
expectedSchema,
"__root_schema__.sourceData.education[0].graduationDate",
null,
);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.boolean.sourceData",
null,
);
set(
expectedSchema,
"__root_schema__.sourceData.education[0].boolean",
null,
);
set(expectedSchema, "__root_schema__.children.education.sourceData", [
{
college: null,
number: null,
graduationDate: null,
boolean: null,
},
]);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.sourceData",
{
college: null,
number: null,
graduationDate: null,
boolean: null,
},
);
// Parse with the nulled sourceData
const schemaWithNullKeys = SchemaParser.parse(widgetName, {
currSourceData: nulledSourceData,
schema: initialSchema,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithNullKeys).toEqual(expectedSchema);
/**
* Parse with initial sourceData to check if previous schema with null sourceData
* can still retain the schema structure
*/
const schemaWithRevertedData = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: schemaWithNullKeys,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithRevertedData).toEqual(
testData.initialDataset.schemaOutput,
);
});
it("returns unmodified schema when existing inner field's value in data source changes to undefined and back", () => {
// Get the initial schema
const initialSchema = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: {},
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(initialSchema).toEqual(testData.initialDataset.schemaOutput);
// Set all keys to undefined
const undefinedSourceData = klona(testData.initialDataset.dataSource);
set(undefinedSourceData, "address.Line1", undefined);
set(undefinedSourceData, "address.city", undefined);
set(undefinedSourceData, "education[0].college", undefined);
set(undefinedSourceData, "education[0].number", undefined);
set(undefinedSourceData, "education[0].graduationDate", undefined);
set(undefinedSourceData, "education[0].boolean", undefined);
// Set the sourceData entry in each SchemaItem to undefined (only property that changes)
const expectedSchema = klona(initialSchema);
set(
expectedSchema,
"__root_schema__.children.address.children.Line1.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.address.Line1", undefined);
set(
expectedSchema,
"__root_schema__.children.address.children.city.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.sourceData.address.city", undefined);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.college.sourceData",
undefined,
);
set(expectedSchema, "__root_schema__.children.address.sourceData", {
Line1: undefined,
city: undefined,
});
set(
expectedSchema,
"__root_schema__.sourceData.education[0].college",
undefined,
);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.number.sourceData",
undefined,
);
set(
expectedSchema,
"__root_schema__.sourceData.education[0].number",
undefined,
);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.graduationDate.sourceData",
undefined,
);
set(
expectedSchema,
"__root_schema__.sourceData.education[0].graduationDate",
undefined,
);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.children.boolean.sourceData",
undefined,
);
set(
expectedSchema,
"__root_schema__.sourceData.education[0].boolean",
undefined,
);
set(expectedSchema, "__root_schema__.children.education.sourceData", [
{
college: undefined,
number: undefined,
graduationDate: undefined,
boolean: undefined,
},
]);
set(
expectedSchema,
"__root_schema__.children.education.children.__array_item__.sourceData",
{
college: undefined,
number: undefined,
graduationDate: undefined,
boolean: undefined,
},
);
// Parse with the undefined sourceData
const schemaWithUndefinedKeys = SchemaParser.parse(widgetName, {
currSourceData: undefinedSourceData,
schema: initialSchema,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithUndefinedKeys).toEqual(expectedSchema);
/**
* Parse with initial sourceData to check if previous schema with undefined sourceData
* can still retain the schema structure
*/
const schemaWithRevertedData = SchemaParser.parse(widgetName, {
currSourceData: testData.initialDataset.dataSource,
schema: schemaWithUndefinedKeys,
fieldThemeStylesheets: testData.fieldThemeStylesheets,
});
expect(schemaWithRevertedData).toEqual(
testData.initialDataset.schemaOutput,
);
});
});

View File

@ -27,11 +27,10 @@ import {
} from "./constants";
import { getFieldStylesheet } from "./helper";
type Obj = Record<string, any>;
type JSON = Obj | Obj[];
type Obj = Record<string, unknown>;
type ParserOptions = {
currSourceData?: JSON | string;
currSourceData?: unknown;
fieldThemeStylesheets?: FieldThemeStylesheet;
fieldType?: FieldType;
isCustomField?: boolean;
@ -60,11 +59,15 @@ type GetKeysFromSchemaOptions = {
};
type ParseOptions = {
currSourceData?: JSON;
currSourceData?: unknown;
schema?: Schema;
fieldThemeStylesheets?: FieldThemeStylesheet;
};
function isObject(val: unknown): val is Obj {
return typeof val === "object" && !Array.isArray(val) && val !== null;
}
/**
*
* This method takes in array of object and squishes every object in the
@ -604,7 +607,7 @@ class SchemaParser {
// This method deals with the conversion of array data to a schema
static convertArrayToSchema = ({
currSourceData = [],
currSourceData,
fieldThemeStylesheets,
prevSchema = {},
sourceDataPath,
@ -612,7 +615,12 @@ class SchemaParser {
...rest
}: Omit<ParserOptions, "identifier">): Schema => {
const schema = klona(prevSchema);
const currData = normalizeArrayValue(currSourceData as any[]);
if (!Array.isArray(currSourceData)) {
return schema;
}
const currData = normalizeArrayValue(currSourceData);
const prevDataType = schema[ARRAY_ITEM_KEY]?.dataType;
const currDataType = dataTypeFor(currData);
@ -644,7 +652,7 @@ class SchemaParser {
// This method deals with the conversion of object data to a schema
static convertObjectToSchema = ({
currSourceData = {},
currSourceData,
prevSchema = {},
sourceDataPath,
widgetName,
@ -654,8 +662,10 @@ class SchemaParser {
const origIdentifierToIdentifierMap = mapOriginalIdentifierToSanitizedIdentifier(
schema,
);
const currObj = currSourceData as Obj;
if (!isObject(currSourceData)) {
return schema;
}
const customFieldAccessors = getKeysFromSchema(prevSchema, ["accessor"], {
onlyCustomFieldKeys: true,
});
@ -683,7 +693,7 @@ class SchemaParser {
modifiedKeys.forEach((modifiedKey) => {
const identifier = origIdentifierToIdentifierMap[modifiedKey];
const prevSchemaItem = klona(schema[identifier]);
const currData = currObj[modifiedKey];
const currData = currSourceData[modifiedKey];
const prevData = prevSchemaItem.sourceData;
const currDataType = dataTypeFor(currData);
const prevDataType = schema[identifier].dataType;
@ -743,7 +753,7 @@ class SchemaParser {
newKeys.forEach((newKey) => {
const schemaItem = SchemaParser.getSchemaItemFor(newKey, {
...rest,
currSourceData: currObj[newKey],
currSourceData: currSourceData[newKey],
sourceDataPath: getSourcePath(newKey, sourceDataPath),
identifier: sanitizeSchemaItemKey(newKey, schema),
widgetName,

View File

@ -106,7 +106,7 @@ class PhoneInputWidget extends BaseInputWidget<
propertyName: "defaultText",
label: "Default Text",
controlType: "INPUT_TEXT",
placeholderText: "John Doe",
placeholderText: "(000) 000-0000",
isBindProperty: true,
isTriggerProperty: false,
validation: {
@ -115,7 +115,7 @@ class PhoneInputWidget extends BaseInputWidget<
fn: defaultValueValidation,
expected: {
type: "string",
example: `000 0000`,
example: `(000) 000-0000`,
autocompleteDataType: AutocompleteDataType.STRING,
},
},

View File

@ -89,13 +89,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
public Mono<ResponseDTO<Boolean>> publish(@PathVariable String defaultApplicationId,
@RequestHeader(name = FieldName.BRANCH_NAME, required = false) String branchName) {
return applicationPageService.publish(defaultApplicationId, branchName, true)
.flatMap(application ->
// This event should parallel a similar event sent from the client, so we want it to be sent by the
// controller and not the service method.
applicationPageService.sendApplicationPublishedEvent(application)
// This will only be called when the publishing was successful, so we can always return `true` here.
.thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null))
);
.thenReturn(new ResponseDTO<>(HttpStatus.OK.value(), true, null));
}
@PutMapping("/{defaultApplicationId}/page/{defaultPageId}/makeDefault")

View File

@ -51,8 +51,6 @@ public interface ApplicationPageServiceCE {
void generateAndSetPagePolicies(Application application, PageDTO page);
Mono<Void> sendApplicationPublishedEvent(Application application);
Mono<ApplicationPagesDTO> reorderPage(String applicationId, String pageId, Integer order, String branchName);
Mono<Application> deleteApplicationByResource(Application application);

View File

@ -892,7 +892,7 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
application -> themeService.publishTheme(application.getId())
);
Flux<NewPage> publishApplicationAndPages = applicationMono
Mono<List<NewPage>> publishApplicationAndPages = applicationMono
//Return all the pages in the Application
.flatMap(application -> {
List<ApplicationPage> pages = application.getPages();
@ -952,10 +952,11 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
page.setPublishedPage(page.getUnpublishedPage());
return page;
}))
.flatMap(newPageService::save)
.collectList()
.flatMapMany(newPageService::saveAll);
.cache(); // caching as we'll need this to send analytics attributes after publishing the app
Flux<NewAction> publishedActionsFlux = newActionService
Mono<List<NewAction>> publishedActionsListMono = newActionService
.findAllByApplicationIdAndViewMode(applicationId, false, MANAGE_ACTIONS, null)
.flatMap(newAction -> {
// If the action was deleted in edit mode, now this document can be safely archived
@ -967,10 +968,11 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
newAction.setPublishedAction(newAction.getUnpublishedAction());
return Mono.just(newAction);
})
.flatMap(newActionService::save)
.collectList()
.flatMapMany(newActionService::saveAll);
.cache(); // caching as we'll need this to send analytics attributes after publishing the app
Flux<ActionCollection> publishedCollectionsFlux = actionCollectionService
Mono<List<ActionCollection>> publishedActionCollectionsListMono = actionCollectionService
.findAllByApplicationIdAndViewMode(applicationId, false, MANAGE_ACTIONS, null)
.flatMap(collection -> {
// If the collection was deleted in edit mode, now this can be safely deleted from the repository
@ -982,16 +984,42 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
collection.setPublishedCollection(collection.getUnpublishedCollection());
return Mono.just(collection);
})
.collectList()
.flatMapMany(actionCollectionService::saveAll);
.flatMap(actionCollectionService::save)
.collectList();
return Mono.when(
publishApplicationAndPages.collectList(),
publishedActionsFlux.collectList(),
publishedCollectionsFlux,
publishApplicationAndPages,
publishedActionsListMono,
publishedActionCollectionsListMono,
publishThemeMono
)
.then(applicationMono);
.then(sendApplicationPublishedEvent(publishApplicationAndPages, publishedActionsListMono, publishedActionCollectionsListMono, applicationId));
}
private Mono<Application> sendApplicationPublishedEvent(Mono<List<NewPage>> publishApplicationAndPages,
Mono<List<NewAction>> publishedActionsFlux,
Mono<List<ActionCollection>> publishedActionsCollectionFlux,
String applicationId) {
return Mono.zip(
publishApplicationAndPages,
publishedActionsFlux,
publishedActionsCollectionFlux,
// not using existing applicationMono because we need the latest Application after published
applicationService.findById(applicationId, MANAGE_APPLICATIONS)
)
.flatMap(objects -> {
Application application = objects.getT4();
Map<String, Object> extraProperties = new HashMap<>();
extraProperties.put("pageCount", objects.getT1().size());
extraProperties.put("queryCount", objects.getT2().size());
extraProperties.put("actionCollectionCount", objects.getT3().size());
extraProperties.put("appId", defaultIfNull(application.getId(), ""));
extraProperties.put("appName", defaultIfNull(application.getName(), ""));
extraProperties.put("orgId", defaultIfNull(application.getOrganizationId(), ""));
extraProperties.put("publishedAt", defaultIfNull(application.getLastDeployedAt(), ""));
return analyticsService.sendObjectEvent(AnalyticsEvents.PUBLISH_APPLICATION, application, extraProperties);
});
}
@Override
@ -1001,34 +1029,6 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
.map(responseUtils::updateApplicationWithDefaultResources);
}
@Override
public Mono<Void> sendApplicationPublishedEvent(Application application) {
if (!analyticsService.isActive()) {
return Mono.empty();
}
return sessionUserService.getCurrentUser()
.flatMap(user -> {
int publishedPageCount = 0;
if(application.getPublishedPages() != null) {
publishedPageCount = application.getPublishedPages().size();
}
analyticsService.sendEvent(
AnalyticsEvents.PUBLISH_APPLICATION.getEventName(),
user.getUsername(),
Map.of(
"appId", defaultIfNull(application.getId(), ""),
"appName", defaultIfNull(application.getName(), ""),
"orgId", defaultIfNull(application.getOrganizationId(), ""),
"pageCount", publishedPageCount + "",
"publishedAt", defaultIfNull(application.getLastDeployedAt(), "")
)
);
return Mono.empty();
});
}
/** This function walks through all the pages and reorders them and updates the order as per the user preference.
* A page can be moved up or down from the current position and accordingly the order of the remaining page changes.
* @param defaultAppId The id of the Application