Skip to content

Commit

Permalink
feat: peek overlay (#20053)
Browse files Browse the repository at this point in the history
## Description

Hover over appsmith properties in code to peek data.
<img width="380" alt="image"
src="https://user-images.githubusercontent.com/66776129/217707810-164924c0-36e8-4450-b087-18af333c7547.png">

This right now covers:
- Queries/JsObjects/Apis/Widgets and their properties.
- Note: For query or Api, this'll work only upto `Api.data`. (Not
`Api.data.users[0].id`)
- This is because of the way codemirror renders code and we'll need more
time to see how this is best handled.


Misc:
- added `react-append-to-body` to work with variable height for peek
overlay
- we needed a container that doesn't apply `position: absolute` to
itself
- Because, when a container's `height` is zero with `position: absolute`
(like in bp3-portal), child elements cannot be positioned using just the
`bottom` property
- with `react-append-to-body`, the container won't have `position:
absolute`, instead it is applied to the child element `<div>` directly,
hence we can position using `bottom` property.


Fixes #17507


Media
https://www.loom.com/share/0f17918fcd604805b023c215d57fce43


## Type of change
- New feature (non-breaking change which adds functionality)


## How Has This Been Tested?

- Manual

### Test Plan
https://github.com/appsmithorg/TestSmith/issues/2173
https://github.com/appsmithorg/TestSmith/issues/2178

### Issues raised during DP testing

#20053 (comment)

#20053 (comment)

## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [x] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [x] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test
  • Loading branch information
eco-monk authored Feb 17, 2023
1 parent a958033 commit 73ba3a3
Show file tree
Hide file tree
Showing 24 changed files with 1,117 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("1. CommandClickNavigation", function() {

cy.get(`[${NAVIGATION_ATTRIBUTE}="Graphql_Query"]`).click({
ctrlKey: true,
force: true,
});

cy.url().should("contain", "/api/");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import * as _ from "../../../../support/Objects/ObjectsCore";

describe("peek overlay", () => {
it("main test", () => {
_.ee.DragDropWidgetNVerify("tablewidgetv2", 500, 100);
_.apiPage.CreateAndFillApi(_.agHelper.mockApiUrl);
_.apiPage.RunAPI();
_.apiPage.CreateAndFillApi(_.agHelper.mockApiUrl);
_.jsEditor.CreateJSObject(
`export default {
numArray: [1, 2, 3],
objectArray: [ {x: 123}, { y: "123"} ],
objectData: { x: 123, y: "123" },
nullData: null,
numberData: 1,
myFun1: () => {
// TODO: handle this keyword failure on CI tests
JSObject1.numArray; JSObject1.objectData; JSObject1.nullData; JSObject1.numberData;
Api1.run(); Api1.isLoading; Api2.data;
appsmith.mode; appsmith.store.abc;
Table1.pageNo; Table1.tableData;
},
myFun2: async () => {
storeValue("abc", 123)
return Api1.run()
}
}`,
{
paste: true,
completeReplace: true,
toRun: false,
shouldCreateNewJSObj: true,
lineNumber: 0,
prettify: true,
},
);
_.jsEditor.SelectFunctionDropdown("myFun2");
_.jsEditor.RunJSObj();
_.agHelper.Sleep();

// check number array
_.peekOverlay.HoverCode("JSObject1.numArray");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("array");
_.peekOverlay.CheckPrimitveArrayInOverlay([1, 2, 3]);
_.peekOverlay.ResetHover();

// check basic object
_.peekOverlay.HoverCode("JSObject1.objectData");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("object");
_.peekOverlay.CheckBasicObjectInOverlay({ x: 123, y: "123" });
_.peekOverlay.ResetHover();

// check null - with this keyword
_.peekOverlay.HoverCode("JSObject1.nullData");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("null");
_.peekOverlay.CheckPrimitiveValue("null");
_.peekOverlay.ResetHover();

// check number
_.peekOverlay.HoverCode("JSObject1.numberData");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("number");
_.peekOverlay.CheckPrimitiveValue("1");
_.peekOverlay.ResetHover();

// check undefined
_.peekOverlay.HoverCode("Api2.data");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("undefined");
_.peekOverlay.CheckPrimitiveValue("undefined");
_.peekOverlay.ResetHover();

// check boolean
_.peekOverlay.HoverCode("Api1.isLoading");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("boolean");
_.peekOverlay.CheckPrimitiveValue("false");
_.peekOverlay.ResetHover();

// TODO: handle this function failure on CI tests -> "function(){}"
// check function
// _.peekOverlay.HoverCode("Api1.run");
// _.peekOverlay.IsOverlayOpen();
// _.peekOverlay.VerifyDataType("function");
// _.peekOverlay.CheckPrimitiveValue("function () {}");
// _.peekOverlay.ResetHover();

// check string
_.peekOverlay.HoverCode("appsmith.mode");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("string");
_.peekOverlay.CheckPrimitiveValue("EDIT");
_.peekOverlay.ResetHover();

// check if overlay closes
_.peekOverlay.HoverCode("appsmith.store");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.ResetHover();
_.peekOverlay.IsOverlayOpen(false);

// widget object
_.peekOverlay.HoverCode("Table1");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("object");
_.peekOverlay.ResetHover();

// widget property
_.peekOverlay.HoverCode("Table1.pageNo");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("number");
_.peekOverlay.CheckPrimitiveValue("1");
_.peekOverlay.ResetHover();

// widget property
_.peekOverlay.HoverCode("Table1.tableData");
_.peekOverlay.IsOverlayOpen();
_.peekOverlay.VerifyDataType("array");
_.peekOverlay.CheckObjectArrayInOverlay([{}, {}, {}]);
_.peekOverlay.ResetHover();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ describe("File picker widget v2", () => {
cy.get(widgetsPage.explorerSwitchId).click();
ee.ExpandCollapseEntity("Queries/JS");
cy.get(".t--entity-item:contains(Api1)").click();
cy.get("[class*='t--actionConfiguration']")
.eq(0)
.click();
cy.focusCodeInput("[class*='t--actionConfiguration']");
cy.wait(1000);
cy.validateEvaluatedValue("[]");
});
Expand Down
1 change: 1 addition & 0 deletions app/client/cypress/support/Objects/CommonLocators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,5 @@ export class CommonLocators {
_consoleString = ".cm-string";
_commentString = ".cm-comment";
_modalWrapper = "[data-cy='modal-wrapper']";
_editorBackButton = ".t--close-editor";
}
1 change: 1 addition & 0 deletions app/client/cypress/support/Objects/ObjectsCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ export const inviteModal = ObjectsRegistry.InviteModal;
export const table = ObjectsRegistry.TableV2;
export const debuggerHelper = ObjectsRegistry.DebuggerHelper;
export const templates = ObjectsRegistry.Templates;
export const peekOverlay = ObjectsRegistry.PeekOverlay;
9 changes: 9 additions & 0 deletions app/client/cypress/support/Objects/Registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GitSync } from "../Pages/GitSync";
import { FakerHelper } from "../Pages/FakerHelper";
import { DebuggerHelper } from "../Pages/DebuggerHelper";
import { LibraryInstaller } from "../Pages/LibraryInstaller";
import { PeekOverlay } from "../Pages/PeekOverlay";
import { InviteModal } from "../Pages/InviteModal";
import { AppSettings } from "../Pages/AppSettings/AppSettings";
import { GeneralSettings } from "../Pages/AppSettings/GeneralSettings";
Expand Down Expand Up @@ -182,6 +183,14 @@ export class ObjectsRegistry {
return ObjectsRegistry.LibraryInstaller__;
}

private static peekOverlay__: PeekOverlay;
static get PeekOverlay(): PeekOverlay {
if (ObjectsRegistry.peekOverlay__ === undefined) {
ObjectsRegistry.peekOverlay__ = new PeekOverlay();
}
return ObjectsRegistry.peekOverlay__;
}

private static inviteModal__: InviteModal;
static get InviteModal(): InviteModal {
if (ObjectsRegistry.inviteModal__ === undefined) {
Expand Down
107 changes: 107 additions & 0 deletions app/client/cypress/support/Pages/PeekOverlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { ObjectsRegistry } from "../Objects/Registry";

export class PeekOverlay {
private readonly PEEKABLE_ATTRIBUTE = "peek-data";
private readonly locators = {
_overlayContainer: "#t--peek-overlay-container",
_dataContainer: "#t--peek-overlay-data",
_peekableCode: (peekableAttr: string) =>
`[${this.PEEKABLE_ATTRIBUTE}="${peekableAttr}"]`,

// react json viewer selectors
_rjv_variableValue: ".variable-value",
_rjv_topLevelArrayData:
".pushed-content.object-container .object-content .object-key-val",
_rjv_firstLevelBraces:
".pretty-json-container > .object-content:first-of-type > .object-key-val:first-of-type > span",
};
private readonly agHelper = ObjectsRegistry.AggregateHelper;

HoverCode(peekableAttribute: string, visibleText?: string) {
(visibleText
? this.agHelper.GetNAssertContains(
this.locators._peekableCode(peekableAttribute),
visibleText,
)
: this.agHelper.GetElement(this.locators._peekableCode(peekableAttribute))
).realHover();
this.agHelper.Sleep();
}

IsOverlayOpen(checkIsOpen = true) {
checkIsOpen
? this.agHelper.AssertElementExist(this.locators._overlayContainer)
: this.agHelper.AssertElementAbsence(this.locators._overlayContainer);
}

ResetHover() {
this.agHelper.GetElement("body").realHover({ position: "bottomLeft" });
this.agHelper.Sleep();
}

CheckPrimitiveValue(data: string) {
this.agHelper
.GetElement(this.locators._dataContainer)
.children("div")
.should("have.text", data);
}

CheckPrimitveArrayInOverlay(array: Array<string | number>) {
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_variableValue)
.should("have.length", array.length);
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(0)
.contains("[");
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(1)
.contains("]");
}

CheckObjectArrayInOverlay(array: Array<Record<string, any>>) {
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_topLevelArrayData)
.should("have.length", array.length);
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(0)
.contains("[");
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(1)
.contains("]");
}

CheckBasicObjectInOverlay(object: Record<string, string | number>) {
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_variableValue)
.should("have.length", Object.entries(object).length);
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(0)
.contains("{");
this.agHelper
.GetElement(this.locators._dataContainer)
.find(this.locators._rjv_firstLevelBraces)
.eq(1)
.contains("}");
}

VerifyDataType(type: string) {
this.agHelper
.GetElement(this.locators._overlayContainer)
.children("div")
.eq(0)
.should("have.text", type);
}
}
1 change: 1 addition & 0 deletions app/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
"rc-tree-select": "^5.4.0",
"re-reselect": "^3.4.0",
"react": "^17.0.2",
"react-append-to-body": "^2.0.26",
"react-beautiful-dnd": "^12.2.0",
"react-custom-scrollbars": "^4.2.1",
"react-device-detect": "^2.2.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MouseEventHandler } from "react";
import AnalyticsUtil from "utils/AnalyticsUtil";

export const objectCollapseAnalytics: MouseEventHandler = (ev) => {
/*
* Analytics events to be logged whenever user clicks on
* react json viewer's controls to expand or collapse object/array
*/
const targetNode = ev.target as HTMLElement;

if (
// collapse/expand icon click, object key click
targetNode.parentElement?.parentElement?.parentElement?.firstElementChild?.classList.contains(
"icon-container",
) ||
// : click
targetNode.parentElement?.parentElement?.firstElementChild?.classList.contains(
"icon-container",
) ||
// { click
targetNode.parentElement?.firstElementChild?.classList.contains(
"icon-container",
) ||
// ellipsis click
targetNode.classList.contains("node-ellipsis") ||
// collapse/expand icon - svg path click
targetNode.parentElement?.parentElement?.classList.contains(
"collapsed-icon",
) ||
targetNode.parentElement?.parentElement?.classList.contains("expanded-icon")
) {
AnalyticsUtil.logEvent("PEEK_OVERLAY_COLLAPSE_EXPAND_CLICK");
}
};

export const textSelectAnalytics = () => {
AnalyticsUtil.logEvent("PEEK_OVERLAY_VALUE_COPIED");
};
Loading

0 comments on commit 73ba3a3

Please sign in to comment.