From 9dd015a1e6c2f8b61c787353549112fbd18ddc43 Mon Sep 17 00:00:00 2001 From: Anand Srinivasan <66776129+eco-monk@users.noreply.github.com> Date: Fri, 26 May 2023 17:12:10 +0530 Subject: [PATCH] feat: peek overlay nested properties + perf improvements (#23414) Fixes #23057 Fixes #23054 ## Description TL;DR Added support for peeking on nested properties. e.g. `Api1.data[0].id`. This won't work when: - local variables are involved in the expression. e.g. `Api1.data[x].id` won't support peeking at the variable `[x]` or anything after that. - library code is involved e.g. `moment`, `_` etc... - when functions are called. e.g. Api1.data[0].id.toFixed() Because these cases requires evaluation. image #### Media (old vs new) https://www.loom.com/share/dedcf113439c4ee2a19028acca54045e ## Performance improvements: - Use AST to identify expressions instead marking text manually. - This reduces the number of markers we process (~ half). - Before ![image](https://github.com/appsmithorg/appsmith/assets/66776129/bb16ac6b-46dd-4e39-8524-e4f4fa2c3243) - After ![image](https://github.com/appsmithorg/appsmith/assets/66776129/28f0f209-5437-4718-a74a-f025c576afda) - AST logs https://www.loom.com/share/ddde93233cc8470ea04309d8a8332240 #### Type of change - Bug fix (non-breaking change which fixes an issue) - New feature (non-breaking change which adds functionality) ## Testing > #### How Has This Been Tested? - [x] Manual - [x] Jest - [x] Cypress > > #### Test Plan https://github.com/appsmithorg/TestSmith/issues/2402 #### Issues raised during DP testing https://github.com/appsmithorg/appsmith/pull/23414#issuecomment-1553164908 ## 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] [Speedbreak features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change) have been covered - [x] Test plan covers all impacted features and [areas of interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest) - [ ] Test plan has been peer reviewed by project stakeholders and other QA members - [x] Manually tested functionality on DP - [ ] We had an implementation alignment call with stakeholders post QA Round 2 - [x] Cypress test cases have been added and approved by SDET/manual QA - [x] Added `Test Plan Approved` label after Cypress tests were reviewed - [ ] Added `Test Plan Approved` label after JUnit tests were reviewed --- .../PeekOverlay/PeekOverlay_Spec.ts | 101 +++++---- .../cypress/support/Pages/PeekOverlay.ts | 33 +-- app/client/package.json | 4 +- app/client/packages/ast/index.ts | 6 + app/client/packages/ast/src/constants/ast.ts | 4 +- app/client/packages/ast/src/index.ts | 34 +++ .../ast/src/peekOverlay/index.test.ts | 192 ++++++++++++++++ .../packages/ast/src/peekOverlay/index.ts | 83 +++++++ .../packages/ast/src/peekOverlay/utils.ts | 184 ++++++++++++++++ .../CodeEditor/MarkHelpers/entityMarker.ts | 37 +--- .../PeekOverlayPopup/PeekOverlayPopup.tsx | 89 +++++--- .../editorComponents/CodeEditor/index.tsx | 208 +++++++++++++----- .../editorComponents/CodeEditor/modes.ts | 133 ++++++----- .../CodeEditor/styledComponents.ts | 10 +- app/client/src/pages/Editor/JSEditor/Form.tsx | 1 + .../src/selectors/navigationSelectors.ts | 34 +-- .../utils/FilterInternalProperties/Action.ts | 26 +++ .../FilterInternalProperties/Appsmith.ts | 14 ++ .../utils/FilterInternalProperties/Common.ts | 40 ++++ .../FilterInternalProperties/JsAction.ts | 29 +++ .../utils/FilterInternalProperties/Widget.ts | 34 +++ .../utils/FilterInternalProperties/index.ts | 42 ++++ .../NavigationSelector/ActionChildren.ts | 49 ----- .../NavigationSelector/AppsmithNavData.ts | 30 --- .../utils/NavigationSelector/JsChildren.ts | 29 +-- .../NavigationSelector/WidgetChildren.ts | 69 +----- .../src/utils/NavigationSelector/common.ts | 75 +------ app/client/yarn.lock | 22 +- 28 files changed, 1089 insertions(+), 523 deletions(-) create mode 100644 app/client/packages/ast/src/peekOverlay/index.test.ts create mode 100644 app/client/packages/ast/src/peekOverlay/index.ts create mode 100644 app/client/packages/ast/src/peekOverlay/utils.ts create mode 100644 app/client/src/utils/FilterInternalProperties/Action.ts create mode 100644 app/client/src/utils/FilterInternalProperties/Appsmith.ts create mode 100644 app/client/src/utils/FilterInternalProperties/Common.ts create mode 100644 app/client/src/utils/FilterInternalProperties/JsAction.ts create mode 100644 app/client/src/utils/FilterInternalProperties/Widget.ts create mode 100644 app/client/src/utils/FilterInternalProperties/index.ts delete mode 100644 app/client/src/utils/NavigationSelector/ActionChildren.ts delete mode 100644 app/client/src/utils/NavigationSelector/AppsmithNavData.ts diff --git a/app/client/cypress/e2e/Regression/ClientSide/PeekOverlay/PeekOverlay_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/PeekOverlay/PeekOverlay_Spec.ts index 708e26c0bc..cdffed6887 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/PeekOverlay/PeekOverlay_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/PeekOverlay/PeekOverlay_Spec.ts @@ -8,75 +8,49 @@ describe("Peek overlay", () => { _.apiPage.CreateAndFillApi(datasourceFormData["mockApiUrl"]); _.apiPage.RunAPI(); _.apiPage.CreateAndFillApi(datasourceFormData["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.CreateJSObject(JsObjectContent, { + paste: true, + completeReplace: true, + toRun: false, + shouldCreateNewJSObj: true, + lineNumber: 0, + prettify: true, + }); _.jsEditor.SelectFunctionDropdown("myFun2"); _.jsEditor.RunJSObj(); _.agHelper.Sleep(); + _.debuggerHelper.CloseBottomBar(); // check number array - _.peekOverlay.HoverCode("JSObject1.numArray"); + _.peekOverlay.HoverCode(8, 3, "numArray"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("array"); _.peekOverlay.CheckPrimitveArrayInOverlay([1, 2, 3]); _.peekOverlay.ResetHover(); // check basic object - _.peekOverlay.HoverCode("JSObject1.objectData"); + _.peekOverlay.HoverCode(9, 3, "objectData"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("object"); _.peekOverlay.CheckBasicObjectInOverlay({ x: 123, y: "123" }); _.peekOverlay.ResetHover(); // check null - with this keyword - _.peekOverlay.HoverCode("JSObject1.nullData"); + _.peekOverlay.HoverCode(10, 3, "nullData"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("null"); _.peekOverlay.CheckPrimitiveValue("null"); _.peekOverlay.ResetHover(); // check number - _.peekOverlay.HoverCode("JSObject1.numberData"); + _.peekOverlay.HoverCode(11, 3, "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.HoverCode(12, 3, "isLoading"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("boolean"); _.peekOverlay.CheckPrimitiveValue("false"); @@ -84,40 +58,47 @@ describe("Peek overlay", () => { // TODO: handle this function failure on CI tests -> "function(){}" // check function - // _.peekOverlay.HoverCode("Api1.run"); + // _.peekOverlay.HoverCode(13, 3, "run"); // _.peekOverlay.IsOverlayOpen(); // _.peekOverlay.VerifyDataType("function"); // _.peekOverlay.CheckPrimitiveValue("function () {}"); // _.peekOverlay.ResetHover(); + // check undefined + _.peekOverlay.HoverCode(14, 3, "data"); + _.peekOverlay.IsOverlayOpen(); + _.peekOverlay.VerifyDataType("undefined"); + _.peekOverlay.CheckPrimitiveValue("undefined"); + _.peekOverlay.ResetHover(); + // check string - _.peekOverlay.HoverCode("appsmith.mode"); + _.peekOverlay.HoverCode(15, 3, "mode"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("string"); _.peekOverlay.CheckPrimitiveValue("EDIT"); _.peekOverlay.ResetHover(); // check if overlay closes - _.peekOverlay.HoverCode("appsmith.store"); + _.peekOverlay.HoverCode(16, 3, "store"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.ResetHover(); _.peekOverlay.IsOverlayOpen(false); // widget object - _.peekOverlay.HoverCode("Table1"); + _.peekOverlay.HoverCode(17, 1, "Table1"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("object"); _.peekOverlay.ResetHover(); // widget property - _.peekOverlay.HoverCode("Table1.pageNo"); + _.peekOverlay.HoverCode(18, 3, "pageNo"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("number"); _.peekOverlay.CheckPrimitiveValue("1"); _.peekOverlay.ResetHover(); // widget property - _.peekOverlay.HoverCode("Table1.tableData"); + _.peekOverlay.HoverCode(19, 3, "tableData"); _.peekOverlay.IsOverlayOpen(); _.peekOverlay.VerifyDataType("array"); _.peekOverlay.CheckObjectArrayInOverlay([{}, {}, {}]); @@ -125,3 +106,31 @@ describe("Peek overlay", () => { }); }); }); + +const JsObjectContent = `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.isLoading; + Api1.run(); + Api2.data; + appsmith.mode; + appsmith.store.abc; + Table1; + Table1.pageNo; + Table1.tableData; + Api1.data.users[0].id; + }, + myFun2: async () => { + storeValue("abc", 123) + return Api1.run() + } +}`; diff --git a/app/client/cypress/support/Pages/PeekOverlay.ts b/app/client/cypress/support/Pages/PeekOverlay.ts index 9c40a0c477..b369bac114 100644 --- a/app/client/cypress/support/Pages/PeekOverlay.ts +++ b/app/client/cypress/support/Pages/PeekOverlay.ts @@ -1,12 +1,9 @@ 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", @@ -19,15 +16,18 @@ export class PeekOverlay { }; 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(); + HoverCode(lineNumber: number, tokenNumber: number, verifyText: string) { + this.agHelper + .GetElement(".CodeMirror-line") + .eq(lineNumber) + .children() + .children() + .eq(tokenNumber) + .should("have.text", verifyText) + .then(($el) => { + const pos = $el[0].getBoundingClientRect(); + this.HoverByPosition({ x: pos.left, y: pos.top }); + }); } IsOverlayOpen(checkIsOpen = true) { @@ -36,8 +36,15 @@ export class PeekOverlay { : this.agHelper.AssertElementAbsence(this.locators._overlayContainer); } + HoverByPosition(position: { x: number; y: number }) { + this.agHelper.GetElement("body").realHover({ position }); + this.agHelper.Sleep(); + } + ResetHover() { - this.agHelper.GetElement("body").realHover({ position: "bottomLeft" }); + this.agHelper + .GetElement(".CodeMirror-code") + .realHover({ position: "bottomLeft" }); this.agHelper.Sleep(); } diff --git a/app/client/package.json b/app/client/package.json index fa247e04c8..c3d0679b9b 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -282,10 +282,10 @@ "cypress-multi-reporters": "^1.2.4", "cypress-network-idle": "^1.14.2", "cypress-plugin-tab": "^1.0.5", - "cypress-real-events": "^1.7.1", + "cypress-real-events": "^1.8.1", "cypress-tags": "^1.1.2", "cypress-wait-until": "^1.7.2", - "cypress-xpath": "^1.4.0", + "cypress-xpath": "^1.6.0", "diff": "^5.0.0", "dotenv": "^8.1.0", "eslint": "^8.36.0", diff --git a/app/client/packages/ast/index.ts b/app/client/packages/ast/index.ts index c05f083000..36ad64746c 100644 --- a/app/client/packages/ast/index.ts +++ b/app/client/packages/ast/index.ts @@ -60,6 +60,10 @@ import { checkIfArgumentExistAtPosition, } from "./src/actionCreator"; +// peekOverlay +import type { PeekOverlayExpressionIdentifierOptions } from "./src/peekOverlay"; +import { PeekOverlayExpressionIdentifier } from "./src/peekOverlay"; + // types or interfaces should be exported with type keyword, while enums can be exported like normal functions export type { ObjectExpression, @@ -68,6 +72,7 @@ export type { IdentifierInfo, TParsedJSProperty, JSPropertyPosition, + PeekOverlayExpressionIdentifierOptions, }; export { @@ -118,4 +123,5 @@ export { checkIfArgumentExistAtPosition, isJSFunctionProperty, isFunctionPresent, + PeekOverlayExpressionIdentifier, }; diff --git a/app/client/packages/ast/src/constants/ast.ts b/app/client/packages/ast/src/constants/ast.ts index c8f2f6c86a..2acc2c6a49 100644 --- a/app/client/packages/ast/src/constants/ast.ts +++ b/app/client/packages/ast/src/constants/ast.ts @@ -1,6 +1,6 @@ export const ECMA_VERSION = 11; -/* Indicates the mode the code should be parsed in. +/* Indicates the mode the code should be parsed in. This influences global strict mode and parsing of import and export declarations. */ export enum SourceType { @@ -31,4 +31,6 @@ export enum NodeTypes { BinaryExpression = "BinaryExpression", ExpressionStatement = "ExpressionStatement", BlockStatement = "BlockStatement", + ConditionalExpression = "ConditionalExpression", + AwaitExpression = "AwaitExpression", } diff --git a/app/client/packages/ast/src/index.ts b/app/client/packages/ast/src/index.ts index 0fa870e45c..d390b11988 100644 --- a/app/client/packages/ast/src/index.ts +++ b/app/client/packages/ast/src/index.ts @@ -118,6 +118,25 @@ export interface CallExpressionNode extends Node { arguments: ArgumentTypes[]; } +// https://github.com/estree/estree/blob/master/es5.md#thisexpression +export interface ThisExpressionNode extends Expression { + type: "ThisExpression"; +} + +// https://github.com/estree/estree/blob/master/es5.md#conditionalexpression +export interface ConditionalExpressionNode extends Expression { + type: "ConditionalExpression"; + test: Expression; + alternate: Expression; + consequent: Expression; +} + +// https://github.com/estree/estree/blob/master/es2017.md#awaitexpression +export interface AwaitExpressionNode extends Expression { + type: "AwaitExpression"; + argument: Expression; +} + export interface BlockStatementNode extends Node { type: "BlockStatement"; body: [Node]; @@ -182,6 +201,21 @@ export const isMemberExpressionNode = ( return node.type === NodeTypes.MemberExpression; }; +export const isThisExpressionNode = ( + node: Node, +): node is ThisExpressionNode => { + return node.type === NodeTypes.ThisExpression; +}; + +export const isConditionalExpressionNode = ( + node: Node, +): node is ConditionalExpressionNode => + node.type === NodeTypes.ConditionalExpression; + +export const isAwaitExpressionNode = ( + node: Node, +): node is AwaitExpressionNode => node.type === NodeTypes.AwaitExpression; + export const isBinaryExpressionNode = ( node: Node, ): node is BinaryExpressionNode => { diff --git a/app/client/packages/ast/src/peekOverlay/index.test.ts b/app/client/packages/ast/src/peekOverlay/index.test.ts new file mode 100644 index 0000000000..bcfb1bbcb9 --- /dev/null +++ b/app/client/packages/ast/src/peekOverlay/index.test.ts @@ -0,0 +1,192 @@ +import { SourceType } from "../constants/ast"; +import { PeekOverlayExpressionIdentifier } from "./index"; + +describe("extractExpressionAtPositionWholeDoc", () => { + const scriptIdentifier = new PeekOverlayExpressionIdentifier({ + sourceType: SourceType.script, + }); + + const jsObjectIdentifier = new PeekOverlayExpressionIdentifier({ + sourceType: SourceType.module, + thisExpressionReplacement: "JsObject", + }); + + const checkExpressionAtScript = async ( + pos: number, + resultString?: string, + ) => { + let result; + try { + result = await scriptIdentifier.extractExpressionAtPosition(pos); + } catch (e) { + expect(e).toBe( + "PeekOverlayExpressionIdentifier - No expression found at position", + ); + } + expect(result).toBe(resultString); + }; + + const checkExpressionAtJsObject = async ( + pos: number, + resultString?: string, + ) => { + let result; + try { + result = await jsObjectIdentifier.extractExpressionAtPosition(pos); + } catch (e) { + expect(e).toBe( + "PeekOverlayExpressionIdentifier - No expression found at position", + ); + } + expect(result).toBe(resultString); + }; + + it("handles MemberExpressions", async () => { + // nested properties + scriptIdentifier.updateScript("Api1.data[0].id"); + // at position 'A' + // 'A'pi1.data[0].id + checkExpressionAtScript(0, "Api1"); + // Ap'i'1.data[0].id + checkExpressionAtScript(2, "Api1"); + // Api1.'d'ata[0].id + checkExpressionAtScript(6, "Api1.data"); + // Api1.data['0'].id + checkExpressionAtScript(11, "Api1.data[0]"); + // Api1.data[0].i'd' + checkExpressionAtScript(14, "Api1.data[0].id"); + + // function call + // argument hover - Api1 + scriptIdentifier.updateScript(`storeValue("abc", Api1.run)`); + checkExpressionAtScript(18, "Api1"); + scriptIdentifier.updateScript("Api1.check.run()"); + // Ap'i'1.check.run() + checkExpressionAtScript(2, "Api1"); + // Api1.check.'r'un() + checkExpressionAtScript(12, "Api1.check.run"); + + // local varibles are filtered + scriptIdentifier.updateScript("Api1.check.data[x].id"); + // Api1 + checkExpressionAtScript(2, "Api1"); + // check + checkExpressionAtScript(7, "Api1.check"); + // data + checkExpressionAtScript(12, "Api1.check.data"); + // x + checkExpressionAtScript(16); + // id + checkExpressionAtScript(19); + }); + + it("handles ExpressionStatements", async () => { + // simple statement + scriptIdentifier.updateScript("Api1"); + // Ap'i'1 + checkExpressionAtScript(2, "Api1"); + + // function call + scriptIdentifier.updateScript(`storeValue("abc", 123)`); + // storeValue("a'b'c", 123) + checkExpressionAtScript(13); + // st'o'reValue("abc", 123) - functionality not supported now + checkExpressionAtScript(2); + + // consequent function calls + scriptIdentifier.updateScript(`Api1.data[0].id.toFixed().toString()`); + // toFixed + checkExpressionAtScript(16, "Api1.data[0].id.toFixed"); + // toString + checkExpressionAtScript(26); + + // function call argument hover + scriptIdentifier.updateScript(`storeValue("abc", Api1)`); + checkExpressionAtScript(18, "Api1"); + }); + + it("handles BinaryExpressions", async () => { + // binary expression + scriptIdentifier.updateScript( + `Api1.data.users[0].id === "myData test" ? "Yes" : "No"`, + ); + + // id + checkExpressionAtScript(19, "Api1.data.users[0].id"); + // myData + checkExpressionAtScript(27); + // ? + checkExpressionAtScript(40); + // Yes + checkExpressionAtScript(43); + // : + checkExpressionAtScript(48); + // No + checkExpressionAtScript(51); + + // hardcoded LHS + scriptIdentifier.updateScript(`"sample" === "myData test" ? "Yes" : "No"`); + // sample + checkExpressionAtScript(1); + + // nested expressions + scriptIdentifier.updateScript( + `"sample" === "myData test" ? "nested" === "nested check" ? "Yes" : "No" : "No"`, + ); + // nested + checkExpressionAtScript(31); + // nested check + checkExpressionAtScript(44); + // Yes + checkExpressionAtScript(61); + // No + checkExpressionAtScript(69); + }); + + it("handles JsObject cases", async () => { + jsObjectIdentifier.updateScript(JsObjectWithThisKeyword); + + // this keyword cases + // this + checkExpressionAtJsObject(140, "JsObject"); + // numArray + checkExpressionAtJsObject(159, "JsObject.numArray"); + // objectArray + checkExpressionAtJsObject(180, "JsObject.objectArray"); + // [0] + checkExpressionAtJsObject(183, "JsObject.objectArray[0]"); + // x + checkExpressionAtJsObject(186, "JsObject.objectArray[0].x"); + // 'x' + checkExpressionAtJsObject(208, "JsObject.objectData['x']"); + // 'a' + checkExpressionAtJsObject(238, "JsObject.objectData['x']['a']"); + // b + checkExpressionAtJsObject(243, "JsObject.objectData['x']['a'].b"); + + // await keyword cases + // resetWidget + checkExpressionAtJsObject(255); + // "Switch1" + checkExpressionAtJsObject(266); + // Api1 + checkExpressionAtJsObject(287, "Api1"); + // run + checkExpressionAtJsObject(292, "Api1.run"); + }); +}); + +const JsObjectWithThisKeyword = `export default { + numArray: [1, 2, 3], + objectArray: [ {x: 123}, { y: "123"} ], + objectData: { x: 123, y: "123" }, + myFun1: async () => { + this; + this.numArray; + this.objectArray[0].x; + this.objectData["x"]; + this.objectData["x"]["a"].b; + await resetWidget("Switch1"); + await Api1.run(); + }, +}`; diff --git a/app/client/packages/ast/src/peekOverlay/index.ts b/app/client/packages/ast/src/peekOverlay/index.ts new file mode 100644 index 0000000000..d9a4473816 --- /dev/null +++ b/app/client/packages/ast/src/peekOverlay/index.ts @@ -0,0 +1,83 @@ +import type { Node } from "acorn"; +import { parse } from "acorn"; +import { simple } from "acorn-walk"; +import type { SourceType } from "../constants/ast"; +import { ECMA_VERSION } from "../constants/ast"; +import { getExpressionStringAtPos, isPositionWithinNode } from "./utils"; + +export class PeekOverlayExpressionIdentifier { + private parsedScript?: Node; + private options: PeekOverlayExpressionIdentifierOptions; + + constructor( + options: PeekOverlayExpressionIdentifierOptions, + script?: string, + ) { + this.options = options; + if (script) this.updateScript(script); + } + + hasParsedScript() { + return !!this.parsedScript; + } + + updateScript(script: string) { + try { + this.parsedScript = parse(script, { + ecmaVersion: ECMA_VERSION, + sourceType: this.options.sourceType, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + } + + clearScript() { + this.parsedScript = undefined; + } + + extractExpressionAtPosition(pos: number): Promise { + return new Promise((resolve, reject) => { + if (!this.parsedScript) { + throw "PeekOverlayExpressionIdentifier - No valid script found"; + } + + let nodeFound: Node | undefined; + + simple(this.parsedScript, { + MemberExpression(node: Node) { + if (!nodeFound && isPositionWithinNode(node, pos)) { + nodeFound = node; + } + }, + ExpressionStatement(node: Node) { + if (!nodeFound && isPositionWithinNode(node, pos)) { + nodeFound = node; + } + }, + }); + + if (nodeFound) { + const expressionFound = getExpressionStringAtPos( + nodeFound, + pos, + this.options, + ); + if (expressionFound) { + resolve(expressionFound); + } else { + reject( + "PeekOverlayExpressionIdentifier - No expression found at position", + ); + } + } + reject("PeekOverlayExpressionIdentifier - No node found"); + }); + } +} + +export type PeekOverlayExpressionIdentifierOptions = { + sourceType: SourceType; + thisExpressionReplacement?: string; +}; diff --git a/app/client/packages/ast/src/peekOverlay/utils.ts b/app/client/packages/ast/src/peekOverlay/utils.ts new file mode 100644 index 0000000000..28f401f68e --- /dev/null +++ b/app/client/packages/ast/src/peekOverlay/utils.ts @@ -0,0 +1,184 @@ +import type { Node } from "acorn"; +import type { + BinaryExpressionNode, + CallExpressionNode, + ConditionalExpressionNode, + ExpressionStatement, + IdentifierNode, + MemberExpressionNode, +} from "../index"; +import { isAwaitExpressionNode } from "../index"; +import { isBinaryExpressionNode } from "../index"; +import { isConditionalExpressionNode } from "../index"; +import { + isCallExpressionNode, + isExpressionStatementNode, + isIdentifierNode, + isMemberExpressionNode, + isThisExpressionNode, +} from "../index"; +import * as escodegen from "escodegen"; +import { NodeTypes } from "../constants/ast"; +import type { PeekOverlayExpressionIdentifierOptions } from "./index"; + +export const isPositionWithinNode = (node: Node, pos: number) => + pos >= node.start && pos <= node.end; + +export const getExpressionStringAtPos = ( + node: Node, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, + replaceThisExpression = true, +): string | undefined => { + if (!isPositionWithinNode(node, pos)) return; + if (isMemberExpressionNode(node)) { + return getExpressionAtPosFromMemberExpression( + node, + pos, + options, + replaceThisExpression, + ); + } else if (isExpressionStatementNode(node)) { + return getExpressionAtPosFromExpressionStatement(node, pos, options); + } else if (isCallExpressionNode(node)) { + return getExpressionAtPosFromCallExpression(node, pos, options); + } else if (isBinaryExpressionNode(node)) { + return getExpressionAtPosFromBinaryExpression(node, pos, options); + } else if (isAwaitExpressionNode(node)) { + return getExpressionStringAtPos(node.argument, pos, options); + } else if (isConditionalExpressionNode(node)) { + return getExpressionAtPosFromConditionalExpression(node, pos, options); + } else if (isIdentifierNode(node)) { + return removeSemiColon(escodegen.generate(node)); + } +}; + +const getExpressionAtPosFromMemberExpression = ( + node: MemberExpressionNode, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, + replaceThisExpression = true, +): string | undefined => { + const objectNode = node.object; + if (isLocalVariableNode(node) || isLocalVariableNode(objectNode)) return; + if (replaceThisExpression && options?.thisExpressionReplacement) { + node = replaceThisinMemberExpression(node, options); + } + // stop if objectNode is a function call -> needs evaluation + if (isCallExpressionNode(objectNode)) return; + // position is within the object node + if (pos <= objectNode.end) { + return getExpressionStringAtPos(objectNode, pos, options, false); + } + // position is within the property node + else { + const propertyNode = node.property; + if (isMemberExpressionNode(propertyNode)) { + return getExpressionAtPosFromMemberExpression( + propertyNode, + pos, + options, + false, + ); + } + // generate string for the whole path + return escodegen.generate(node); + } +}; + +const getExpressionAtPosFromExpressionStatement = ( + node: ExpressionStatement, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, +): string | undefined => { + if ( + isThisExpressionNode(node.expression) && + options?.thisExpressionReplacement + ) { + node.expression = thisReplacementNode(node.expression, options); + } + return getExpressionStringAtPos(node.expression, pos, options); +}; + +const getExpressionAtPosFromCallExpression = ( + node: CallExpressionNode, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, +): string | undefined => { + let selectedNode: Node | undefined; + // function call -> needs evaluation + // if (isPositionWithinNode(node.callee, pos)) { + // selectedNode = node.callee; + // } + if (node.arguments.length > 0) { + const argumentNode = node.arguments.find((node) => + isPositionWithinNode(node, pos), + ); + if (argumentNode) { + selectedNode = argumentNode; + } + } + return selectedNode && getExpressionStringAtPos(selectedNode, pos, options); +}; + +const getExpressionAtPosFromConditionalExpression = ( + node: ConditionalExpressionNode, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, +): string | undefined => { + let selectedNode: Node | undefined; + if (isPositionWithinNode(node.test, pos)) { + selectedNode = node.test; + } else if (isPositionWithinNode(node.consequent, pos)) { + selectedNode = node.consequent; + } else if (isPositionWithinNode(node.alternate, pos)) { + selectedNode = node.alternate; + } + return selectedNode && getExpressionStringAtPos(selectedNode, pos, options); +}; + +const getExpressionAtPosFromBinaryExpression = ( + node: BinaryExpressionNode, + pos: number, + options?: PeekOverlayExpressionIdentifierOptions, +): string | undefined => { + let selectedNode: Node | undefined; + if (isPositionWithinNode(node.left, pos)) { + selectedNode = node.left; + } else if (isPositionWithinNode(node.right, pos)) { + selectedNode = node.right; + } + return selectedNode && getExpressionStringAtPos(selectedNode, pos, options); +}; + +export const replaceThisinMemberExpression = ( + node: MemberExpressionNode, + options: PeekOverlayExpressionIdentifierOptions, +): MemberExpressionNode => { + if (isMemberExpressionNode(node.object)) { + node.object = replaceThisinMemberExpression(node.object, options); + } else if (isThisExpressionNode(node.object)) { + node.object = thisReplacementNode(node.object, options); + } + return node; +}; + +// replace "this" node with the provided replacement +const thisReplacementNode = ( + node: Node, + options: PeekOverlayExpressionIdentifierOptions, +) => { + return { + ...node, + type: NodeTypes.Identifier, + name: options.thisExpressionReplacement, + } as IdentifierNode; +}; + +const removeSemiColon = (value: string) => + value.slice(-1) === ";" ? value.slice(0, value.length - 1) : value; + +const isLocalVariableNode = (node: Node) => + isMemberExpressionNode(node) && + node.computed && + isIdentifierNode(node.property); diff --git a/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts index d89989b2c2..b4f04ee1ea 100644 --- a/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts +++ b/app/client/src/components/editorComponents/CodeEditor/MarkHelpers/entityMarker.ts @@ -12,13 +12,6 @@ const hasReference = (token: CodeMirror.Token) => { return token.type === "variable" || tokenString === "this"; }; -export const PEEKABLE_CLASSNAME = "peekable-entity-highlight"; -export const PEEKABLE_ATTRIBUTE = "peek-data"; -export const PEEKABLE_LINE = "peek-line"; -export const PEEKABLE_CH_START = "peek-ch-start"; -export const PEEKABLE_CH_END = "peek-ch-end"; -export const PEEK_STYLE_PERSIST_CLASS = "peek-style-persist"; - export const entityMarker: MarkHelper = ( editor: CodeMirror.Editor, entityNavigationData, @@ -66,11 +59,11 @@ const addMarksForLine = ( const tokenString = token.string; if (hasReference(token) && tokenString in entityNavigationData) { const data = entityNavigationData[tokenString]; - if (data.navigable || data.peekable) { + if (data.navigable) { editor.markText( { ch: token.start, line: lineNo }, { ch: token.end, line: lineNo }, - getMarkOptions(data, token, lineNo), + getMarkOptions(data), ); } addMarksForChildren( @@ -100,11 +93,11 @@ const addMarksForChildren = ( ); if (token.string in childNodes) { const childLink = childNodes[token.string]; - if (childLink.navigable || childLink.peekable) { + if (childLink.navigable) { editor.markText( { ch: token.start, line: lineNo }, { ch: token.end, line: lineNo }, - getMarkOptions(childLink, token, lineNo), + getMarkOptions(childLink), ); } addMarksForChildren(childNodes[token.string], lineNo, token.end, editor); @@ -112,27 +105,15 @@ const addMarksForChildren = ( } }; -const getMarkOptions = ( - data: NavigationData, - token: CodeMirror.Token, - lineNo: number, -): CodeMirror.TextMarkerOptions => { +const getMarkOptions = (data: NavigationData): CodeMirror.TextMarkerOptions => { return { - className: `${data.navigable ? NAVIGATION_CLASSNAME : ""} ${ - data.peekable ? PEEKABLE_CLASSNAME : "" - }`, + className: `${data.navigable ? NAVIGATION_CLASSNAME : ""}`, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore attributes: { ...(data.navigable && { [NAVIGATE_TO_ATTRIBUTE]: `${data.name}`, }), - ...(data.peekable && { - [PEEKABLE_ATTRIBUTE]: data.name, - [PEEKABLE_CH_START]: token.start, - [PEEKABLE_CH_END]: token.end, - [PEEKABLE_LINE]: lineNo, - }), }, atomic: false, title: data.name, @@ -141,10 +122,6 @@ const getMarkOptions = ( const clearMarkers = (markers: CodeMirror.TextMarker[]) => { markers.forEach((marker) => { - if ( - marker.className?.includes(NAVIGATION_CLASSNAME) || - marker.className?.includes(PEEKABLE_CLASSNAME) - ) - marker.clear(); + if (marker.className?.includes(NAVIGATION_CLASSNAME)) marker.clear(); }); }; diff --git a/app/client/src/components/editorComponents/CodeEditor/PeekOverlayPopup/PeekOverlayPopup.tsx b/app/client/src/components/editorComponents/CodeEditor/PeekOverlayPopup/PeekOverlayPopup.tsx index c0bff505cd..d3e042afd2 100644 --- a/app/client/src/components/editorComponents/CodeEditor/PeekOverlayPopup/PeekOverlayPopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/PeekOverlayPopup/PeekOverlayPopup.tsx @@ -1,19 +1,23 @@ import type { MutableRefObject } from "react"; +import { useState } from "react"; import React, { useEffect, useRef } from "react"; import ReactJson from "react-json-view"; import { JsonWrapper, reactJsonProps } from "./JsonWrapper"; import { componentWillAppendToBody } from "react-append-to-body"; -import { debounce } from "lodash"; +import _, { debounce } from "lodash"; import { zIndexLayers } from "constants/CanvasEditorConstants"; import { objectCollapseAnalytics, textSelectAnalytics } from "./Analytics"; import { Divider } from "design-system"; +import { useSelector } from "react-redux"; +import { getDataTree } from "selectors/dataTreeSelectors"; +import { filterInternalProperties } from "utils/FilterInternalProperties"; +import { getJSCollections } from "selectors/entitiesSelector"; export type PeekOverlayStateProps = { - name: string; + objectName: string; + propertyPath: string[]; position: DOMRect; textWidth: number; - data: unknown; - dataType: string; }; /* @@ -29,6 +33,19 @@ export const PeekOverlayPopUp = componentWillAppendToBody( export const PEEK_OVERLAY_DELAY = 200; +const getPropertyData = (src: unknown, propertyPath: string[]) => { + return propertyPath.length > 0 ? _.get(src, propertyPath) : src; +}; + +const getDataTypeHeader = (data: unknown) => { + const dataType = typeof data; + if (dataType === "object") { + if (Array.isArray(data)) return "array"; + if (data === null) return "null"; + } + return dataType; +}; + export function PeekOverlayPopUpContent( props: PeekOverlayStateProps & { hidePeekOverlay: () => void; @@ -36,6 +53,23 @@ export function PeekOverlayPopUpContent( ) { const CONTAINER_MAX_HEIGHT_PX = 252; const dataWrapperRef: MutableRefObject = useRef(null); + const dataTree = useSelector(getDataTree); + const jsActions = useSelector(getJSCollections); + + const filteredData = filterInternalProperties( + props.objectName, + dataTree[props.objectName], + jsActions, + dataTree, + ); + + // Because getPropertyData can return a function + // And we don't want to execute it. + const [jsData] = useState(() => + getPropertyData(filteredData, props.propertyPath), + ); + + const [dataType] = useState(getDataTypeHeader(jsData)); useEffect(() => { const wheelCallback = () => { @@ -60,14 +94,6 @@ export function PeekOverlayPopUpContent( PEEK_OVERLAY_DELAY, ); - const getDataTypeHeader = (dataType: string) => { - if (props.dataType === "object") { - if (Array.isArray(props.data)) return "array"; - if (props.data === null) return "null"; - } - return dataType; - }; - return (
- {getDataTypeHeader(props.dataType)} + {dataType}
- {props.dataType === "object" && props.data !== null && ( + {(dataType === "object" || dataType === "array") && jsData !== null && ( - + )} - {props.dataType === "function" && ( -
{(props.data as any).toString()}
- )} - {props.dataType === "boolean" && ( -
{(props.data as any).toString()}
- )} - {props.dataType === "string" && ( -
{(props.data as any).toString()}
- )} - {props.dataType === "number" && ( -
{(props.data as any).toString()}
- )} - {((props.dataType !== "object" && - props.dataType !== "function" && - props.dataType !== "boolean" && - props.dataType !== "string" && - props.dataType !== "number") || - props.data === null) && ( + {dataType === "function" &&
{(jsData as any).toString()}
} + {dataType === "boolean" &&
{(jsData as any).toString()}
} + {dataType === "string" &&
{(jsData as any).toString()}
} + {dataType === "number" &&
{(jsData as any).toString()}
} + {((dataType !== "object" && + dataType !== "function" && + dataType !== "boolean" && + dataType !== "string" && + dataType !== "array" && + dataType !== "number") || + jsData === null) && (
- {(props.data as any)?.toString() ?? - props.data ?? - props.data === undefined + {(jsData as any)?.toString() ?? jsData ?? jsData === undefined ? "undefined" : "null"}
diff --git a/app/client/src/components/editorComponents/CodeEditor/index.tsx b/app/client/src/components/editorComponents/CodeEditor/index.tsx index 8107351506..5f9cd01dfa 100644 --- a/app/client/src/components/editorComponents/CodeEditor/index.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/index.tsx @@ -60,16 +60,12 @@ import { DynamicAutocompleteInputWrapper, EditorWrapper, IconContainer, + PEEK_STYLE_PERSIST_CLASS, } from "components/editorComponents/CodeEditor/styledComponents"; import { bindingMarker } from "components/editorComponents/CodeEditor/MarkHelpers/bindingMarker"; import { entityMarker, NAVIGATE_TO_ATTRIBUTE, - PEEKABLE_ATTRIBUTE, - PEEKABLE_CH_END, - PEEKABLE_CH_START, - PEEKABLE_LINE, - PEEK_STYLE_PERSIST_CLASS, } from "components/editorComponents/CodeEditor/MarkHelpers/entityMarker"; import { bindingHint, @@ -153,7 +149,14 @@ import { APPSMITH_AI, askAIEnabled, } from "@appsmith/components/editorComponents/GPT/trigger"; -import { getAllDatasourceTableKeys } from "selectors/entitiesSelector"; +import { + getAllDatasourceTableKeys, + selectInstalledLibraries, +} from "selectors/entitiesSelector"; +import { debug } from "loglevel"; +import { PeekOverlayExpressionIdentifier, SourceType } from "@shared/ast"; +import type { MultiplexingModeConfig } from "components/editorComponents/CodeEditor/modes"; +import { MULTIPLEXING_MODE_CONFIGS } from "components/editorComponents/CodeEditor/modes"; type ReduxStateProps = ReturnType; type ReduxDispatchProps = ReturnType; @@ -229,6 +232,7 @@ export type EditorProps = EditorStyleProps & isReadOnly?: boolean; isRawView?: boolean; isJSObject?: boolean; + jsObjectName?: string; containerHeight?: number; // Custom gutter customGutter?: CodeEditorGutter; @@ -253,7 +257,7 @@ type State = { ctrlPressed: boolean; peekOverlayProps: | (PeekOverlayStateProps & { - marker?: CodeMirror.TextMarker; + tokenElement: Element; }) | undefined; isDynamic: boolean; @@ -278,9 +282,11 @@ class CodeEditor extends Component { hinters: Hinter[] = []; annotations: Annotation[] = []; updateLintingCallback: UpdateLintingCallback | undefined; + private peekOverlayExpressionIdentifier: PeekOverlayExpressionIdentifier; private editorWrapperRef = React.createRef(); currentLineNumber: number | null = null; AIEnabled = false; + private multiplexConfig?: MultiplexingModeConfig; constructor(props: Props) { super(props); @@ -296,6 +302,18 @@ class CodeEditor extends Component { showAIWindow: false, }; this.updatePropertyValue = this.updatePropertyValue.bind(this); + this.peekOverlayExpressionIdentifier = new PeekOverlayExpressionIdentifier( + props.isJSObject + ? { + sourceType: SourceType.module, + thisExpressionReplacement: props.jsObjectName, + } + : { + sourceType: SourceType.script, + }, + props.input.value, + ); + this.multiplexConfig = MULTIPLEXING_MODE_CONFIGS[this.props.mode]; } componentDidMount(): void { @@ -577,48 +595,40 @@ class CodeEditor extends Component { }; showPeekOverlay = ( - peekableAttribute: string, + expression: string, + paths: string[], tokenElement: Element, - tokenElementPosition: DOMRect, - dataToShow: unknown, ) => { - const line = tokenElement.getAttribute(PEEKABLE_LINE), - chStart = tokenElement.getAttribute(PEEKABLE_CH_START), - chEnd = tokenElement.getAttribute(PEEKABLE_CH_END); - - this.state.peekOverlayProps?.marker?.clear(); - let marker: CodeMirror.TextMarker | undefined; - if (line && chStart && chEnd) { - marker = this.editor.markText( - { ch: Number(chStart), line: Number(line) }, - { ch: Number(chEnd), line: Number(line) }, - { - className: PEEK_STYLE_PERSIST_CLASS, - }, - ); + const tokenElementPosition = tokenElement.getBoundingClientRect(); + if (this.state.peekOverlayProps) { + if (tokenElement === this.state.peekOverlayProps.tokenElement) return; + this.hidePeekOverlay(); } - + tokenElement.classList.add(PEEK_STYLE_PERSIST_CLASS); this.setState({ peekOverlayProps: { - name: peekableAttribute, + objectName: paths[0], + propertyPath: paths.slice(1), position: tokenElementPosition, + tokenElement, textWidth: tokenElementPosition.width, - marker, - data: dataToShow, - dataType: typeof dataToShow, }, }); AnalyticsUtil.logEvent("PEEK_OVERLAY_OPENED", { - property: peekableAttribute, + property: expression, }); }; hidePeekOverlay = () => { - this.state.peekOverlayProps?.marker?.clear(); - this.setState({ - peekOverlayProps: undefined, - }); + if (this.state.peekOverlayProps) { + this.state.peekOverlayProps.tokenElement.classList.remove( + PEEK_STYLE_PERSIST_CLASS, + ); + this.setState({ + peekOverlayProps: undefined, + }); + } }; debounceHandleMouseOver = debounce( @@ -648,36 +658,109 @@ class CodeEditor extends Component { setTimeout(delayedWork, 0); }; - handleMouseOver = (event: MouseEvent) => { + isPeekableElement = (element: Element) => { if ( - event.target instanceof Element && - event.target.hasAttribute(PEEKABLE_ATTRIBUTE) + !element.classList.contains("cm-m-javascript") || + element.classList.contains("binding-brackets") + ) + return false; + if ( + // global variables and functions + // JsObject1, storeValue() + element.classList.contains("cm-variable") || + // properties and function calls + // JsObject.myFun(), Api1.data + element.classList.contains("cm-property") || + // array indices - [0] + element.classList.contains("cm-number") || + // string accessor - ["x"] + element.classList.contains("cm-string") ) { - const tokenElement = event.target; - const tokenElementPosition = tokenElement.getBoundingClientRect(); - const peekableAttribute = tokenElement.getAttribute(PEEKABLE_ATTRIBUTE); - if (peekableAttribute) { - // don't retrigger if hovering over the same token - if ( - this.state.peekOverlayProps?.name === peekableAttribute && - this.state.peekOverlayProps?.position.top === - tokenElementPosition.top && - this.state.peekOverlayProps?.position.left === - tokenElementPosition.left - ) { - return; - } - const paths = peekableAttribute.split("."); - if (paths.length) { - paths.splice(1, 0, "peekData"); - this.showPeekOverlay( - peekableAttribute, - tokenElement, - tokenElementPosition, - _.get(this.props.entitiesForNavigation, paths), - ); - } + return true; + } else if (element.classList.contains("cm-keyword")) { + // this keyword for jsObjects + if (this.props.isJSObject && element.innerHTML === "this") { + return true; } + } + }; + + getBindingSnippetAtPos = ( + multiPlexConfig: MultiplexingModeConfig, + pos: number, + ) => { + return multiPlexConfig.innerModes.map((innerMode) => { + const doc = this.editor.getValue(); + const openPos = + doc.lastIndexOf(innerMode.open, pos) + innerMode.open.length; + const closePos = doc.indexOf(innerMode.close, pos); + return { + value: doc.slice(openPos, closePos), + offset: openPos, + }; + }); + }; + + updateScriptForPeekOverlay = (chIndex: number) => { + if ( + !this.peekOverlayExpressionIdentifier.hasParsedScript() || + this.multiplexConfig + ) { + if (this.multiplexConfig) { + const bindingSnippetsByInnerMode = this.getBindingSnippetAtPos( + this.multiplexConfig, + chIndex, + ); + for (const snippet of bindingSnippetsByInnerMode) { + if (snippet.value) { + this.peekOverlayExpressionIdentifier.updateScript(snippet.value); + chIndex -= snippet.offset; + break; + } + } + } else { + this.peekOverlayExpressionIdentifier.updateScript( + this.editor.getValue(), + ); + } + } + return chIndex; + }; + + isPathLibrary = (paths: string[]) => { + return !!this.props.installedLibraries.find((installedLib) => + installedLib.accessor.find((accessor) => accessor === paths[0]), + ); + }; + + handleMouseOver = (event: MouseEvent) => { + const tokenElement = event.target; + if ( + tokenElement instanceof Element && + this.isPeekableElement(tokenElement) + ) { + const tokenPos = this.editor.coordsChar({ + left: event.clientX, + top: event.clientY, + }); + const chIndex = this.updateScriptForPeekOverlay( + this.editor.indexFromPos(tokenPos), + ); + + this.peekOverlayExpressionIdentifier + .extractExpressionAtPosition(chIndex) + .then((lineExpression: string) => { + const paths = _.toPath(lineExpression); + if (!this.isPathLibrary(paths)) { + this.showPeekOverlay(lineExpression, paths, tokenElement); + } else { + this.hidePeekOverlay(); + } + }) + .catch((e) => { + this.hidePeekOverlay(); + debug(e); + }); } else { this.hidePeekOverlay(); } @@ -1086,6 +1169,8 @@ class CodeEditor extends Component { changeObj.to, ); } + + this.peekOverlayExpressionIdentifier.clearScript(); }; handleDebouncedChange = _.debounce(this.handleChange, 600); @@ -1568,6 +1653,7 @@ const mapStateToProps = (state: AppState, props: EditorProps) => ({ ), featureFlags: selectFeatureFlags(state), datasourceTableKeys: getAllDatasourceTableKeys(state, props.dataTreePath), + installedLibraries: selectInstalledLibraries(state), }); const mapDispatchToProps = (dispatch: any) => ({ diff --git a/app/client/src/components/editorComponents/CodeEditor/modes.ts b/app/client/src/components/editorComponents/CodeEditor/modes.ts index 5a5a5cf00c..476e60a8ca 100644 --- a/app/client/src/components/editorComponents/CodeEditor/modes.ts +++ b/app/client/src/components/editorComponents/CodeEditor/modes.ts @@ -1,73 +1,96 @@ import CodeMirror from "codemirror"; +import type { TEditorModes } from "components/editorComponents/CodeEditor/EditorConfig"; import { EditorModes } from "components/editorComponents/CodeEditor/EditorConfig"; import "codemirror/addon/mode/multiplex"; import "codemirror/mode/javascript/javascript"; import "codemirror/mode/sql/sql"; import "codemirror/addon/hint/sql-hint"; +import type { TEditorSqlModes } from "./sql/config"; import { sqlModesConfig } from "./sql/config"; -CodeMirror.defineMode(EditorModes.TEXT_WITH_BINDING, function (config) { - // @ts-expect-error: Types are not available - return CodeMirror.multiplexingMode( - CodeMirror.getMode(config, EditorModes.TEXT), - { - open: "{{", - close: "}}", - mode: CodeMirror.getMode(config, { - name: "javascript", - }), - }, - ); -}); +export const BINDING_OPEN = "{{", + BINDING_CLOSE = "}}"; -CodeMirror.defineMode(EditorModes.JSON_WITH_BINDING, function (config) { - // @ts-expect-error: Types are not available - return CodeMirror.multiplexingMode( - CodeMirror.getMode(config, { name: "javascript", json: true }), - { - open: "{{", - close: "}}", - mode: CodeMirror.getMode(config, { - name: "javascript", - }), - }, - ); -}); +export interface MultiplexingModeConfig { + outerMode: string | { name: string; json?: boolean }; + innerModes: { + open: string; + close: string; + }[]; +} -CodeMirror.defineMode(EditorModes.GRAPHQL_WITH_BINDING, function (config) { - // @ts-expect-error: Types are not available - return CodeMirror.multiplexingMode( - CodeMirror.getMode(config, EditorModes.GRAPHQL), - { - open: "{{", - close: "}}", - mode: CodeMirror.getMode(config, { - name: "javascript", - }), - }, - { - open: '"{{', - close: '}}"', - mode: CodeMirror.getMode(config, { - name: "javascript", - }), - }, - ); -}); +export type MultiplexingModeConfigs = Record< + TEditorModes, + MultiplexingModeConfig | undefined +>; -for (const sqlModeConfig of Object.values(sqlModesConfig)) { - if (!sqlModeConfig.isMultiplex) continue; - CodeMirror.defineMode(sqlModeConfig.mode, function (config) { +export const MULTIPLEXING_MODE_CONFIGS: MultiplexingModeConfigs = { + [EditorModes.TEXT_WITH_BINDING]: { + outerMode: EditorModes.TEXT, + innerModes: [ + { + open: BINDING_OPEN, + close: BINDING_CLOSE, + }, + ], + }, + [EditorModes.JSON_WITH_BINDING]: { + outerMode: { name: "javascript", json: true }, + innerModes: [ + { + open: BINDING_OPEN, + close: BINDING_CLOSE, + }, + ], + }, + [EditorModes.GRAPHQL_WITH_BINDING]: { + outerMode: EditorModes.GRAPHQL, + innerModes: [ + { + open: BINDING_OPEN, + close: BINDING_CLOSE, + }, + { + // https://github.com/appsmithorg/appsmith/issues/16702 + open: '"{{', + close: '}}"', + }, + ], + }, + ...Object.values(sqlModesConfig) + .filter((config) => config.isMultiplex) + .reduce((prev, current) => { + prev[current.mode] = { + outerMode: current.mime, + innerModes: [ + { + open: BINDING_OPEN, + close: BINDING_CLOSE, + }, + ], + }; + return prev; + }, {} as Record), + "text/plain": undefined, + "application/json": undefined, + javascript: undefined, + graphql: undefined, +}; + +Object.keys(MULTIPLEXING_MODE_CONFIGS).forEach((key) => { + const multiplexConfig = MULTIPLEXING_MODE_CONFIGS[key as TEditorModes]; + if (!multiplexConfig) return; + CodeMirror.defineMode(key, function (config) { // @ts-expect-error: Types are not available return CodeMirror.multiplexingMode( - CodeMirror.getMode(config, sqlModeConfig.mime), - { - open: "{{", - close: "}}", + CodeMirror.getMode(config, multiplexConfig.outerMode), + ...multiplexConfig.innerModes.map((innerMode) => ({ + open: innerMode.open, + close: innerMode.close, mode: CodeMirror.getMode(config, { name: "javascript", }), - }, + })), ); }); -} +}); diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts index 9d37d2dd52..0c6f856375 100644 --- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts +++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts @@ -7,11 +7,9 @@ import { import type { Theme } from "constants/DefaultTheme"; import { Skin } from "constants/DefaultTheme"; import { Colors } from "constants/Colors"; -import { - NAVIGATION_CLASSNAME, - PEEKABLE_CLASSNAME, - PEEK_STYLE_PERSIST_CLASS, -} from "./MarkHelpers/entityMarker"; +import { NAVIGATION_CLASSNAME } from "./MarkHelpers/entityMarker"; + +export const PEEK_STYLE_PERSIST_CLASS = "peek-style-persist"; const getBorderStyle = ( props: { theme: Theme } & { @@ -263,7 +261,7 @@ export const EditorWrapper = styled.div<{ font-weight: 700; } - .${PEEKABLE_CLASSNAME}:hover, .${PEEK_STYLE_PERSIST_CLASS} { + .${PEEK_STYLE_PERSIST_CLASS} { border-color: var(--ads-v2-color-border-emphasis); background-color: #ededed; } diff --git a/app/client/src/pages/Editor/JSEditor/Form.tsx b/app/client/src/pages/Editor/JSEditor/Form.tsx index 9076d769e8..5b10d496c2 100644 --- a/app/client/src/pages/Editor/JSEditor/Form.tsx +++ b/app/client/src/pages/Editor/JSEditor/Form.tsx @@ -404,6 +404,7 @@ function JSEditorForm({ jsCollection: currentJSCollection }: Props) { onChange: handleEditorChange, }} isJSObject + jsObjectName={currentJSCollection.name} mode={EditorModes.JAVASCRIPT} placeholder="Let's write some code!" showLightningMenu={false} diff --git a/app/client/src/selectors/navigationSelectors.ts b/app/client/src/selectors/navigationSelectors.ts index fb9d8ec155..9492e370b2 100644 --- a/app/client/src/selectors/navigationSelectors.ts +++ b/app/client/src/selectors/navigationSelectors.ts @@ -1,7 +1,4 @@ -import type { - DataTree, - AppsmithEntity, -} from "entities/DataTree/dataTreeFactory"; +import type { DataTree } from "entities/DataTree/dataTreeFactory"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; import { createSelector } from "reselect"; import { @@ -15,11 +12,9 @@ import { getCurrentPageId } from "selectors/editorSelectors"; import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers"; import { jsCollectionIdURL, widgetURL } from "RouteBuilder"; import { getDataTree } from "selectors/dataTreeSelectors"; -import { getActionChildrenNavData } from "utils/NavigationSelector/ActionChildren"; import { createNavData } from "utils/NavigationSelector/common"; import { getWidgetChildrenNavData } from "utils/NavigationSelector/WidgetChildren"; import { getJsChildrenNavData } from "utils/NavigationSelector/JsChildren"; -import { getAppsmithNavData } from "utils/NavigationSelector/AppsmithNavData"; import { getEntityNameAndPropertyPath, isJSAction, @@ -36,8 +31,6 @@ export type NavigationData = { url: string | undefined; navigable: boolean; children: EntityNavigationData; - peekable: boolean; - peekData?: unknown; key?: string; pluginName?: string; isMock?: boolean; @@ -79,8 +72,6 @@ export const getEntitiesForNavigation = createSelector( (datasource) => datasource.id === datasourceId, ); const config = getActionConfig(action.config.pluginType); - // dataTree used to get entityDefinitions and peekData - const result = getActionChildrenNavData(action, dataTree); if (!config) return; navigationData[action.config.name] = createNavData({ id: action.config.id, @@ -92,9 +83,7 @@ export const getEntitiesForNavigation = createSelector( action.config.pluginType, plugin, ), - peekable: true, - peekData: result?.peekData, - children: result?.childNavData || {}, + children: {}, // Adding below data as it is required for analytical events pluginName: plugin?.name, datasourceId: datasource?.id, @@ -105,36 +94,33 @@ export const getEntitiesForNavigation = createSelector( }); jsActions.forEach((jsAction) => { - // dataTree for null check and peekData + // dataTree for null check const result = getJsChildrenNavData(jsAction, pageId, dataTree); navigationData[jsAction.config.name] = createNavData({ id: jsAction.config.id, name: jsAction.config.name, type: ENTITY_TYPE.JSACTION, url: jsCollectionIdURL({ pageId, collectionId: jsAction.config.id }), - peekable: true, - peekData: result?.peekData, children: result?.childNavData || {}, }); }); Object.values(widgets).forEach((widget) => { - // dataTree to get entityDefinitions, for url (can use getWidgetByName?) and peekData - const result = getWidgetChildrenNavData(widget, dataTree, pageId); + // dataTree to get entityDefinitions, for url (can use getWidgetByName?) + const result = getWidgetChildrenNavData( + widget.widgetName, + widget.type, + dataTree, + pageId, + ); navigationData[widget.widgetName] = createNavData({ id: widget.widgetId, name: widget.widgetName, type: ENTITY_TYPE.WIDGET, url: widgetURL({ pageId, selectedWidgets: [widget.widgetId] }), - peekable: true, - peekData: result?.peekData, children: result?.childNavData || {}, }); }); - // dataTree to get entity definitions and peekData - navigationData["appsmith"] = getAppsmithNavData( - dataTree.appsmith as AppsmithEntity, - ); if ( entityName && isJSAction(dataTree[entityName]) && diff --git a/app/client/src/utils/FilterInternalProperties/Action.ts b/app/client/src/utils/FilterInternalProperties/Action.ts new file mode 100644 index 0000000000..2339fdcce3 --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/Action.ts @@ -0,0 +1,26 @@ +import { entityDefinitions } from "@appsmith/utils/autocomplete/EntityDefinitions"; +import type { DataTree } from "entities/DataTree/dataTreeFactory"; +import type { ActionEntity } from "entities/DataTree/types"; + +export const getActionChildrenPeekData = ( + actionName: string, + dataTree: DataTree, +) => { + const dataTreeAction = dataTree[actionName] as ActionEntity; + if (dataTreeAction) { + const definitions = entityDefinitions.ACTION(dataTreeAction, {}); + const peekData: Record = {}; + Object.keys(definitions).forEach((key) => { + if (key.indexOf("!") === -1) { + if (key === "data" || key === "isLoading" || key === "responseMeta") { + peekData[key] = dataTreeAction[key]; + } else if (key === "run" || key === "clear") { + // eslint-disable-next-line @typescript-eslint/no-empty-function + peekData[key] = function () {}; // tern inference required here + } + } + }); + + return { peekData }; + } +}; diff --git a/app/client/src/utils/FilterInternalProperties/Appsmith.ts b/app/client/src/utils/FilterInternalProperties/Appsmith.ts new file mode 100644 index 0000000000..25c5ebbc55 --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/Appsmith.ts @@ -0,0 +1,14 @@ +import { entityDefinitions } from "@appsmith/utils/autocomplete/EntityDefinitions"; +import type { + AppsmithEntity, + DataTree, +} from "entities/DataTree/dataTreeFactory"; +import { createObjectPeekData } from "./Common"; + +export const getAppsmithPeekData = (dataTree: DataTree) => { + const defs: any = entityDefinitions.APPSMITH( + dataTree.appsmith as AppsmithEntity, + {}, + ); + return createObjectPeekData(defs, dataTree.appsmith, {}, "appsmith"); +}; diff --git a/app/client/src/utils/FilterInternalProperties/Common.ts b/app/client/src/utils/FilterInternalProperties/Common.ts new file mode 100644 index 0000000000..e57a878d99 --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/Common.ts @@ -0,0 +1,40 @@ +import _ from "lodash"; + +export const isTernFunctionDef = (data: any) => + typeof data === "string" && /^fn\((?:[\w,: \(\)->])*\) -> [\w]*$/.test(data); + +export const createObjectPeekData = ( + defs: any, + data: any, + peekData: any, + parentKey: string, +) => { + Object.keys(defs).forEach((key: string) => { + if (key.indexOf("!") === -1) { + const childKeyPathArray = [parentKey, key]; + if (isObject(defs[key])) { + if (Object.keys(defs[key]).length > 0) { + peekData[key] = {}; + const result = createObjectPeekData( + defs[key], + data[key], + peekData[key], + key, + ); + _.set(peekData, childKeyPathArray, result.peekData); + } else { + peekData[key] = data[key]; + } + } else { + peekData[key] = isTernFunctionDef(defs[key]) + ? // eslint-disable-next-line @typescript-eslint/no-empty-function + function () {} // tern inference required here + : data[key]; + } + } + }); + return { peekData }; +}; + +const isObject = (data: any) => + typeof data === "object" && !Array.isArray(data) && data !== null; diff --git a/app/client/src/utils/FilterInternalProperties/JsAction.ts b/app/client/src/utils/FilterInternalProperties/JsAction.ts new file mode 100644 index 0000000000..8b59ca3cdd --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/JsAction.ts @@ -0,0 +1,29 @@ +import type { DataTree } from "entities/DataTree/dataTreeFactory"; +import type { JSCollectionData } from "reducers/entityReducers/jsActionsReducer"; +import type { JSActionEntity } from "entities/DataTree/types"; + +export const getJsActionPeekData = ( + jsAction: JSCollectionData, + dataTree: DataTree, +) => { + const peekData: Record = {}; + + const dataTreeAction = dataTree[jsAction.config.name] as JSActionEntity; + + if (dataTreeAction) { + jsAction.config.actions.forEach((jsChild) => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + peekData[jsChild.name] = function () {}; + + if (jsAction.data?.[jsChild.id] && jsChild.executeOnLoad) { + (peekData[jsChild.name] as any).data = jsAction.data[jsChild.id]; + } + }); + + jsAction.config.variables.forEach((jsChild) => { + if (dataTreeAction) peekData[jsChild.name] = dataTreeAction[jsChild.name]; + }); + + return { peekData }; + } +}; diff --git a/app/client/src/utils/FilterInternalProperties/Widget.ts b/app/client/src/utils/FilterInternalProperties/Widget.ts new file mode 100644 index 0000000000..f08fb159fc --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/Widget.ts @@ -0,0 +1,34 @@ +import type { EntityDefinitionsOptions } from "ce/utils/autocomplete/EntityDefinitions"; +import type { DataTree, WidgetEntity } from "entities/DataTree/dataTreeFactory"; +import { isFunction } from "lodash"; +import WidgetFactory from "utils/WidgetFactory"; + +export const getWidgetChildrenPeekData = ( + widgetName: string, + widgetType: string, + dataTree: DataTree, +) => { + const peekData: Record = {}; + const dataTreeWidget: WidgetEntity = dataTree[widgetName] as WidgetEntity; + if (widgetType !== "FORM_WIDGET" && dataTreeWidget) { + const type: Exclude< + EntityDefinitionsOptions, + | "CANVAS_WIDGET" + | "ICON_WIDGET" + | "SKELETON_WIDGET" + | "TABS_MIGRATOR_WIDGET" + > = dataTreeWidget.type as any; + let config: any = WidgetFactory.getAutocompleteDefinitions(type); + if (config) { + if (isFunction(config)) config = config(dataTreeWidget); + const widgetProps = Object.keys(config).filter( + (k) => k.indexOf("!") === -1, + ); + widgetProps.forEach((prop) => { + const data = dataTreeWidget[prop]; + peekData[prop] = data; + }); + } + } + return { peekData }; +}; diff --git a/app/client/src/utils/FilterInternalProperties/index.ts b/app/client/src/utils/FilterInternalProperties/index.ts new file mode 100644 index 0000000000..1b9f9adbd2 --- /dev/null +++ b/app/client/src/utils/FilterInternalProperties/index.ts @@ -0,0 +1,42 @@ +import { getActionChildrenPeekData } from "./Action"; +import type { + DataTree, + DataTreeEntity, +} from "entities/DataTree/dataTreeFactory"; +import type { JSCollectionDataState } from "reducers/entityReducers/jsActionsReducer"; +import { getWidgetChildrenPeekData } from "./Widget"; +import { getJsActionPeekData } from "./JsAction"; +import { getAppsmithPeekData } from "./Appsmith"; +import { + isActionEntity, + isWidgetEntity, +} from "components/editorComponents/CodeEditor/codeEditorUtils"; +import { + isAppsmithEntity, + isJSAction, +} from "ce/workers/Evaluation/evaluationUtils"; + +export const filterInternalProperties = ( + objectName: string, + dataTreeEntity: DataTreeEntity, + jsActions: JSCollectionDataState, + dataTree: DataTree, +) => { + if (!dataTreeEntity) return; + if (isActionEntity(dataTreeEntity)) { + return getActionChildrenPeekData(objectName, dataTree)?.peekData; + } else if (isAppsmithEntity(dataTreeEntity)) { + return getAppsmithPeekData(dataTree).peekData; + } else if (isJSAction(dataTreeEntity)) { + const jsAction = jsActions.find( + (jsAction) => jsAction.config.id === dataTreeEntity.actionId, + ); + return jsAction + ? getJsActionPeekData(jsAction, dataTree)?.peekData + : dataTreeEntity; + } else if (isWidgetEntity(dataTreeEntity)) { + return getWidgetChildrenPeekData(objectName, dataTreeEntity.type, dataTree) + ?.peekData; + } + return dataTreeEntity; +}; diff --git a/app/client/src/utils/NavigationSelector/ActionChildren.ts b/app/client/src/utils/NavigationSelector/ActionChildren.ts deleted file mode 100644 index 3dde65b44f..0000000000 --- a/app/client/src/utils/NavigationSelector/ActionChildren.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { entityDefinitions } from "@appsmith/utils/autocomplete/EntityDefinitions"; -import type { DataTree } from "entities/DataTree/dataTreeFactory"; -import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import type { ActionData } from "reducers/entityReducers/actionsReducer"; -import type { EntityNavigationData } from "selectors/navigationSelectors"; -import { createNavData } from "./common"; -import type { ActionEntity } from "entities/DataTree/types"; - -export const getActionChildrenNavData = ( - action: ActionData, - dataTree: DataTree, -) => { - const dataTreeAction = dataTree[action.config.name] as ActionEntity; - if (dataTreeAction) { - const definitions = entityDefinitions.ACTION(dataTreeAction, {}); - const peekData: Record = {}; - const childNavData: EntityNavigationData = {}; - Object.keys(definitions).forEach((key) => { - if (key.indexOf("!") === -1) { - if (key === "data" || key === "isLoading" || key === "responseMeta") { - peekData[key] = dataTreeAction[key]; - childNavData[key] = createNavData({ - id: `${action.config.name}.${key}`, - name: `${action.config.name}.${key}`, - type: ENTITY_TYPE.ACTION, - url: undefined, - peekable: true, - peekData: undefined, - children: {}, - }); - } else if (key === "run" || key === "clear") { - // eslint-disable-next-line @typescript-eslint/no-empty-function - peekData[key] = function () {}; // tern inference required here - childNavData[key] = createNavData({ - id: `${action.config.name}.${key}`, - name: `${action.config.name}.${key}`, - type: ENTITY_TYPE.ACTION, - url: undefined, - peekable: true, - peekData: undefined, - children: {}, - }); - } - } - }); - - return { peekData, childNavData }; - } -}; diff --git a/app/client/src/utils/NavigationSelector/AppsmithNavData.ts b/app/client/src/utils/NavigationSelector/AppsmithNavData.ts deleted file mode 100644 index ee284c1509..0000000000 --- a/app/client/src/utils/NavigationSelector/AppsmithNavData.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { entityDefinitions } from "@appsmith/utils/autocomplete/EntityDefinitions"; -import type { AppsmithEntity } from "entities/DataTree/dataTreeFactory"; -import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import { createNavData, createObjectNavData } from "./common"; - -export const getAppsmithNavData = (dataTree: AppsmithEntity) => { - const defs: any = entityDefinitions.APPSMITH(dataTree, {}); - - const result = createObjectNavData( - defs, - dataTree, - "appsmith", - {}, - { - // restricting peek after appsmith.store because it can contain user data - // which if large will slow down nav data generation - "appsmith.store": true, - }, - ); - - return createNavData({ - id: "appsmith", - name: "appsmith", - type: ENTITY_TYPE.APPSMITH, - url: undefined, - peekable: true, - peekData: result.peekData, - children: result.entityNavigationData, - }); -}; diff --git a/app/client/src/utils/NavigationSelector/JsChildren.ts b/app/client/src/utils/NavigationSelector/JsChildren.ts index 8d95161a25..550c547948 100644 --- a/app/client/src/utils/NavigationSelector/JsChildren.ts +++ b/app/client/src/utils/NavigationSelector/JsChildren.ts @@ -15,31 +15,12 @@ export const getJsChildrenNavData = ( pageId: string, dataTree: DataTree, ) => { - const peekData: Record = {}; let childNavData: EntityNavigationData = {}; const dataTreeAction = dataTree[jsAction.config.name] as JSActionEntity; if (dataTreeAction) { let children: NavigationData[] = jsAction.config.actions.map((jsChild) => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - peekData[jsChild.name] = function () {}; // can use new Function to parse string - - const children: EntityNavigationData = {}; - if (jsAction.data?.[jsChild.id] && jsChild.executeOnLoad) { - (peekData[jsChild.name] as any).data = jsAction.data[jsChild.id]; - children.data = createNavData({ - id: `${jsAction.config.name}.${jsChild.name}.data`, - name: `${jsAction.config.name}.${jsChild.name}.data`, - type: ENTITY_TYPE.JSACTION, - url: undefined, - peekable: true, - peekData: undefined, - children: {}, - key: jsChild.name + ".data", - }); - } - return createNavData({ id: `${jsAction.config.name}.${jsChild.name}`, name: `${jsAction.config.name}.${jsChild.name}`, @@ -49,17 +30,13 @@ export const getJsChildrenNavData = ( collectionId: jsAction.config.id, functionName: jsChild.name, }), - peekable: true, - peekData: undefined, - children, + children: {}, key: jsChild.name, }); }); const variableChildren: NavigationData[] = jsAction.config.variables.map( (jsChild) => { - if (dataTreeAction) - peekData[jsChild.name] = dataTreeAction[jsChild.name]; return createNavData({ id: `${jsAction.config.name}.${jsChild.name}`, name: `${jsAction.config.name}.${jsChild.name}`, @@ -69,8 +46,6 @@ export const getJsChildrenNavData = ( collectionId: jsAction.config.id, functionName: jsChild.name, }), - peekable: true, - peekData: undefined, children: {}, key: jsChild.name, }); @@ -84,6 +59,6 @@ export const getJsChildrenNavData = ( NavigationData >; - return { childNavData, peekData }; + return { childNavData }; } }; diff --git a/app/client/src/utils/NavigationSelector/WidgetChildren.ts b/app/client/src/utils/NavigationSelector/WidgetChildren.ts index 0040da5949..aa4fdbd40a 100644 --- a/app/client/src/utils/NavigationSelector/WidgetChildren.ts +++ b/app/client/src/utils/NavigationSelector/WidgetChildren.ts @@ -1,80 +1,33 @@ -import type { EntityDefinitionsOptions } from "@appsmith/utils/autocomplete/EntityDefinitions"; import type { DataTree, WidgetEntity } from "entities/DataTree/dataTreeFactory"; import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory"; -import { isFunction } from "lodash"; -import type { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer"; import { builderURL } from "RouteBuilder"; import type { EntityNavigationData } from "selectors/navigationSelectors"; import { createNavData } from "./common"; -import WidgetFactory from "utils/WidgetFactory"; export const getWidgetChildrenNavData = ( - widget: FlattenedWidgetProps, + widgetName: string, + widgetType: string, dataTree: DataTree, pageId: string, ) => { - const peekData: Record = {}; - const childNavData: EntityNavigationData = {}; - const dataTreeWidget: WidgetEntity = dataTree[ - widget.widgetName - ] as WidgetEntity; - if (widget.type === "FORM_WIDGET") { + const dataTreeWidget: WidgetEntity = dataTree[widgetName] as WidgetEntity; + if (widgetType === "FORM_WIDGET") { const children: EntityNavigationData = {}; const formChildren: EntityNavigationData = {}; if (dataTreeWidget) { - Object.keys(dataTreeWidget.data || {}).forEach((widgetName) => { - const childWidgetId = (dataTree[widgetName] as WidgetEntity).widgetId; - formChildren[widgetName] = createNavData({ - id: `${widget.widgetName}.data.${widgetName}`, - name: widgetName, + Object.keys(dataTreeWidget.data || {}).forEach((childWidgetName) => { + const childWidgetId = (dataTree[childWidgetName] as WidgetEntity) + .widgetId; + formChildren[childWidgetName] = createNavData({ + id: `${widgetName}.data.${childWidgetName}`, + name: childWidgetName, type: ENTITY_TYPE.WIDGET, url: builderURL({ pageId, hash: childWidgetId }), - peekable: false, - peekData: undefined, children: {}, }); }); } - children.data = createNavData({ - id: `${widget.widgetName}.data`, - name: "data", - type: ENTITY_TYPE.WIDGET, - url: undefined, - peekable: false, - peekData: undefined, - children: formChildren, - }); - return { childNavData: children, peekData }; - } - if (dataTreeWidget) { - const type: Exclude< - EntityDefinitionsOptions, - | "CANVAS_WIDGET" - | "ICON_WIDGET" - | "SKELETON_WIDGET" - | "TABS_MIGRATOR_WIDGET" - > = dataTreeWidget.type as any; - let config: any = WidgetFactory.getAutocompleteDefinitions(type); - if (config) { - if (isFunction(config)) config = config(dataTreeWidget); - const widgetProps = Object.keys(config).filter( - (k) => k.indexOf("!") === -1, - ); - widgetProps.forEach((prop) => { - const data = dataTreeWidget[prop]; - peekData[prop] = data; - childNavData[prop] = createNavData({ - id: `${widget.widgetName}.${prop}`, - name: `${widget.widgetName}.${prop}`, - type: ENTITY_TYPE.WIDGET, - url: undefined, - peekable: true, - peekData: undefined, - children: {}, - }); - }); - } - return { childNavData, peekData }; + return { childNavData: children }; } }; diff --git a/app/client/src/utils/NavigationSelector/common.ts b/app/client/src/utils/NavigationSelector/common.ts index 986e7b134c..9b26d349ea 100644 --- a/app/client/src/utils/NavigationSelector/common.ts +++ b/app/client/src/utils/NavigationSelector/common.ts @@ -1,4 +1,4 @@ -import { ENTITY_TYPE } from "entities/DataTree/types"; +import type { ENTITY_TYPE } from "entities/DataTree/types"; import type { EntityNavigationData, NavigationData, @@ -11,8 +11,6 @@ export const createNavData = (general: { children: EntityNavigationData; key?: string; url: string | undefined; - peekable: boolean; - peekData: unknown; pluginName?: string; datasourceId?: string; isMock?: boolean; @@ -26,80 +24,9 @@ export const createNavData = (general: { key: general.key, url: general.url, navigable: !!general.url, - peekable: general.peekable, - peekData: general.peekData, pluginName: general.pluginName, datasourceId: general.datasourceId, isMock: general.isMock, actionType: general.actionType, }; }; - -export const isTernFunctionDef = (data: any) => - typeof data === "string" && /^fn\((?:[\w,: \(\)->])*\) -> [\w]*$/.test(data); - -export const createObjectNavData = ( - defs: any, - data: any, - parentKey: string, - peekData: any, - restrictKeysFrom: Record, -) => { - const entityNavigationData: EntityNavigationData = {}; - Object.keys(defs).forEach((key: string) => { - if (key.indexOf("!") === -1) { - const childKey = parentKey + "." + key; - if (isObject(defs[key])) { - if (Object.keys(defs[key]).length > 0 && !restrictKeysFrom[childKey]) { - peekData[key] = {}; - const result = createObjectNavData( - defs[key], - data[key], - childKey, - peekData[key], - restrictKeysFrom, - ); - peekData[key] = result.peekData; - entityNavigationData[key] = createNavData({ - id: childKey, - name: childKey, - type: ENTITY_TYPE.APPSMITH, - children: result.entityNavigationData, - url: undefined, - peekable: true, - peekData: undefined, - }); - } else { - peekData[key] = data[key]; - entityNavigationData[key] = createNavData({ - id: childKey, - name: childKey, - type: ENTITY_TYPE.APPSMITH, - children: {}, - url: undefined, - peekable: true, - peekData: undefined, - }); - } - } else { - peekData[key] = isTernFunctionDef(defs[key]) - ? // eslint-disable-next-line @typescript-eslint/no-empty-function - function () {} // tern inference required here - : data[key]; - entityNavigationData[key] = createNavData({ - id: childKey, - name: childKey, - type: ENTITY_TYPE.APPSMITH, - children: {}, - url: undefined, - peekable: true, - peekData: undefined, - }); - } - } - }); - return { peekData, entityNavigationData }; -}; - -const isObject = (data: any) => - typeof data === "object" && !Array.isArray(data) && data !== null; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index f2b14c1095..eb7c2e008b 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -9576,10 +9576,10 @@ __metadata: cypress-multi-reporters: ^1.2.4 cypress-network-idle: ^1.14.2 cypress-plugin-tab: ^1.0.5 - cypress-real-events: ^1.7.1 + cypress-real-events: ^1.8.1 cypress-tags: ^1.1.2 cypress-wait-until: ^1.7.2 - cypress-xpath: ^1.4.0 + cypress-xpath: ^1.6.0 dayjs: ^1.10.6 deep-diff: ^1.0.2 design-system: "npm:@appsmithorg/design-system@2.1.9" @@ -13203,12 +13203,12 @@ __metadata: languageName: node linkType: hard -"cypress-real-events@npm:^1.7.1": - version: 1.7.1 - resolution: "cypress-real-events@npm:1.7.1" +"cypress-real-events@npm:^1.8.1": + version: 1.8.1 + resolution: "cypress-real-events@npm:1.8.1" peerDependencies: - cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x - checksum: b31c2facfa03e01e298926cd0925260b12474770fc1a3ce8998da21818db7e6d9fc2f9eb60d1771aa4ce3c29aca63d04da21e1a63e3bb117b1506a72ab0c3eb1 + cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x + checksum: 48f229335f26f8c225428563e94ca091ab7a00e943a4c953e062135b1d37672edec9cb7667452d894b43528b52924cd1b6061df044ccb0665b9c0965a0fe214f languageName: node linkType: hard @@ -13232,10 +13232,10 @@ __metadata: languageName: node linkType: hard -"cypress-xpath@npm:^1.4.0": - version: 1.6.0 - resolution: "cypress-xpath@npm:1.6.0" - checksum: 67cc5613d70a090b1a162d31d0ef11831576592c87ca07d242e2c07a6df2fca413fc81ea7df148a0d0c888025bb7ec0dad34684f2a322eeaa2863b8f3b65dd32 +"cypress-xpath@npm:^1.6.0": + version: 1.8.0 + resolution: "cypress-xpath@npm:1.8.0" + checksum: 11792ec46b898f2e00ced81dedcdca8e30d5c232778780210423d1ec0b07cd8b2a29a475d38eadaf9fd7e962efec7f5cee17f92884c308636cfa506efe6017e6 languageName: node linkType: hard