feat: peek overlay (#20053)
## 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 https://github.com/appsmithorg/appsmith/pull/20053#issuecomment-1420545330 https://github.com/appsmithorg/appsmith/pull/20053#issuecomment-1424427913 ## 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
This commit is contained in:
parent
a9580339f2
commit
73ba3a39c5
|
|
@ -95,6 +95,7 @@ describe("1. CommandClickNavigation", function() {
|
|||
|
||||
cy.get(`[${NAVIGATION_ATTRIBUTE}="Graphql_Query"]`).click({
|
||||
ctrlKey: true,
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.url().should("contain", "/api/");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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("[]");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -174,4 +174,5 @@ export class CommonLocators {
|
|||
_consoleString = ".cm-string";
|
||||
_commentString = ".cm-comment";
|
||||
_modalWrapper = "[data-cy='modal-wrapper']";
|
||||
_editorBackButton = ".t--close-editor";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
107
app/client/cypress/support/Pages/PeekOverlay.ts
Normal file
107
app/client/cypress/support/Pages/PeekOverlay.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
};
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import styled from "styled-components";
|
||||
|
||||
export const reactJsonProps = {
|
||||
name: null,
|
||||
enableClipboard: false,
|
||||
displayDataTypes: false,
|
||||
displayArrayKey: true,
|
||||
quotesOnKeys: false,
|
||||
style: {
|
||||
fontSize: "10px",
|
||||
},
|
||||
collapsed: 1,
|
||||
indentWidth: 2,
|
||||
collapseStringsAfterLength: 30,
|
||||
};
|
||||
|
||||
export const JsonWrapper = styled.div`
|
||||
// all ellipsis font size
|
||||
.node-ellipsis,
|
||||
.function-collapsed span:nth-child(2),
|
||||
.string-value span {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
// disable and hide first object collapser
|
||||
.pretty-json-container
|
||||
> .object-content:first-of-type
|
||||
> .object-key-val:first-of-type
|
||||
> span {
|
||||
pointer-events: none !important;
|
||||
.icon-container {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// collapse icon color change and alignment
|
||||
.icon-container {
|
||||
width: 10px !important;
|
||||
height: 8px !important;
|
||||
svg {
|
||||
color: var(--appsmith-color-black-600) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// font-sizes and alignments
|
||||
.pushed-content.object-container {
|
||||
.object-content {
|
||||
padding-left: 4px !important;
|
||||
.variable-row {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
border-left: 0 !important;
|
||||
.variable-value div {
|
||||
text-transform: lowercase;
|
||||
font-size: 10px !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.object-key-val {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
border-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disabling function collapse and neutral styling
|
||||
.rjv-function-container {
|
||||
pointer-events: none;
|
||||
font-weight: normal !important;
|
||||
> span:first-child:before {
|
||||
// In prod build, for some reason react-json-viewer
|
||||
// misses adding this opening braces for function
|
||||
content: "(";
|
||||
}
|
||||
.function-collapsed {
|
||||
font-weight: normal !important;
|
||||
span:nth-child(1) {
|
||||
display: none; // hiding extra braces
|
||||
}
|
||||
span:nth-child(2) {
|
||||
color: #393939 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
div:has(.rjv-function-container) {
|
||||
cursor: default !important;
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import React, { MutableRefObject, useEffect, useRef } from "react";
|
||||
import ReactJson from "react-json-view";
|
||||
import { JsonWrapper, reactJsonProps } from "./JsonWrapper";
|
||||
import { componentWillAppendToBody } from "react-append-to-body";
|
||||
import { MenuDivider } from "design-system-old";
|
||||
import { debounce } from "lodash";
|
||||
import { zIndexLayers } from "constants/CanvasEditorConstants";
|
||||
import { objectCollapseAnalytics, textSelectAnalytics } from "./Analytics";
|
||||
|
||||
export type PeekOverlayStateProps = {
|
||||
name: string;
|
||||
position: DOMRect;
|
||||
textWidth: number;
|
||||
data: unknown;
|
||||
dataType: string;
|
||||
};
|
||||
|
||||
/*
|
||||
* using `componentWillAppendToBody` to work with variable height for peek overlay
|
||||
* we need a container that doesn't apply `position: absolute` to itself with zero height (bp3-portal does this)
|
||||
* Because then, child elements cannot be positioned using `bottom` property
|
||||
* with `react-append-to-body`, the container won't have `position: absolute`
|
||||
* instead we're applying it to the child element `<div>` directly, hence we can position using `bottom` property.
|
||||
*/
|
||||
export const PeekOverlayPopUp = componentWillAppendToBody(
|
||||
PeekOverlayPopUpContent,
|
||||
);
|
||||
|
||||
export const PEEK_OVERLAY_DELAY = 200;
|
||||
|
||||
export function PeekOverlayPopUpContent(
|
||||
props: PeekOverlayStateProps & {
|
||||
hidePeekOverlay: () => void;
|
||||
},
|
||||
) {
|
||||
const CONTAINER_MAX_HEIGHT_PX = 252;
|
||||
const dataWrapperRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const wheelCallback = () => {
|
||||
props.hidePeekOverlay();
|
||||
};
|
||||
|
||||
window.addEventListener("wheel", wheelCallback);
|
||||
return () => {
|
||||
window.removeEventListener("wheel", wheelCallback);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataWrapperRef.current) return;
|
||||
dataWrapperRef.current.addEventListener("copy", textSelectAnalytics);
|
||||
return () =>
|
||||
dataWrapperRef.current?.removeEventListener("copy", textSelectAnalytics);
|
||||
}, [dataWrapperRef, dataWrapperRef.current]);
|
||||
|
||||
const debouncedHide = debounce(
|
||||
() => props.hidePeekOverlay(),
|
||||
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 (
|
||||
<div
|
||||
className={`absolute ${zIndexLayers.PEEK_OVERLAY}`}
|
||||
id="t--peek-overlay-container"
|
||||
onMouseEnter={() => debouncedHide.cancel()}
|
||||
onMouseLeave={() => debouncedHide()}
|
||||
onWheel={(ev) => ev.stopPropagation()}
|
||||
style={{
|
||||
minHeight: "46px",
|
||||
maxHeight: `${CONTAINER_MAX_HEIGHT_PX}px`,
|
||||
width: "300px",
|
||||
backgroundColor: "var(--appsmith-color-black-0)",
|
||||
boxShadow: "0px 0px 10px #0000001A", // color used from designs
|
||||
left: `${props.position.left + props.position.width - 300}px`,
|
||||
...(props.position.top >= CONTAINER_MAX_HEIGHT_PX
|
||||
? {
|
||||
bottom: `calc(100vh - ${props.position.top}px)`,
|
||||
}
|
||||
: {
|
||||
top: `${props.position.bottom}px`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="first-letter:uppercase"
|
||||
style={{
|
||||
height: "24px",
|
||||
color: "var(--appsmith-color-black-700)",
|
||||
padding: "4px 0px 4px 12px",
|
||||
fontSize: "10px",
|
||||
}}
|
||||
>
|
||||
{getDataTypeHeader(props.dataType)}
|
||||
</div>
|
||||
<MenuDivider style={{ margin: 0 }} />
|
||||
<div
|
||||
id="t--peek-overlay-data"
|
||||
ref={dataWrapperRef}
|
||||
style={{
|
||||
minHeight: "20px",
|
||||
padding: "2px 0px 2px 12px",
|
||||
fontSize: "10px",
|
||||
}}
|
||||
>
|
||||
{props.dataType === "object" && props.data !== null && (
|
||||
<JsonWrapper
|
||||
onClick={objectCollapseAnalytics}
|
||||
style={{
|
||||
minHeight: "20px",
|
||||
maxHeight: "225px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
<ReactJson src={props.data} {...reactJsonProps} />
|
||||
</JsonWrapper>
|
||||
)}
|
||||
{props.dataType === "function" && (
|
||||
<div>{(props.data as any).toString()}</div>
|
||||
)}
|
||||
{props.dataType === "boolean" && (
|
||||
<div>{(props.data as any).toString()}</div>
|
||||
)}
|
||||
{props.dataType === "string" && (
|
||||
<div>{(props.data as any).toString()}</div>
|
||||
)}
|
||||
{props.dataType === "number" && (
|
||||
<div>{(props.data as any).toString()}</div>
|
||||
)}
|
||||
{((props.dataType !== "object" &&
|
||||
props.dataType !== "function" &&
|
||||
props.dataType !== "boolean" &&
|
||||
props.dataType !== "string" &&
|
||||
props.dataType !== "number") ||
|
||||
props.data === null) && (
|
||||
<div>
|
||||
{(props.data as any)?.toString() ??
|
||||
props.data ??
|
||||
props.data === undefined
|
||||
? "undefined"
|
||||
: "null"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ import "codemirror/addon/comment/comment";
|
|||
import { getDataTreeForAutocomplete } from "selectors/dataTreeSelectors";
|
||||
import EvaluatedValuePopup from "components/editorComponents/CodeEditor/EvaluatedValuePopup";
|
||||
import { WrappedFieldInputProps } from "redux-form";
|
||||
import _, { isEqual } from "lodash";
|
||||
import _, { debounce, isEqual } from "lodash";
|
||||
|
||||
import {
|
||||
DataTree,
|
||||
|
|
@ -57,6 +57,11 @@ import {
|
|||
bindingMarker,
|
||||
entityMarker,
|
||||
NAVIGATE_TO_ATTRIBUTE,
|
||||
PEEKABLE_ATTRIBUTE,
|
||||
PEEKABLE_CH_END,
|
||||
PEEKABLE_CH_START,
|
||||
PEEKABLE_LINE,
|
||||
PEEK_STYLE_PERSIST_CLASS,
|
||||
} from "components/editorComponents/CodeEditor/markHelpers";
|
||||
import { bindingHint } from "components/editorComponents/CodeEditor/hintHelpers";
|
||||
import BindingPrompt from "./BindingPrompt";
|
||||
|
|
@ -125,6 +130,11 @@ import history, { NavigationMethod } from "utils/history";
|
|||
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
|
||||
import { CursorPositionOrigin } from "reducers/uiReducers/editorContextReducer";
|
||||
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
|
||||
import {
|
||||
PeekOverlayPopUp,
|
||||
PeekOverlayStateProps,
|
||||
PEEK_OVERLAY_DELAY,
|
||||
} from "./PeekOverlayPopup/PeekOverlayPopup";
|
||||
|
||||
type ReduxStateProps = ReturnType<typeof mapStateToProps>;
|
||||
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
|
||||
|
|
@ -219,6 +229,11 @@ type State = {
|
|||
// Flag for determining whether the entity change has been started or not so that even if the initial and final value remains the same, the status should be changed to not loading
|
||||
changeStarted: boolean;
|
||||
ctrlPressed: boolean;
|
||||
peekOverlayProps:
|
||||
| (PeekOverlayStateProps & {
|
||||
marker?: CodeMirror.TextMarker;
|
||||
})
|
||||
| undefined;
|
||||
isDynamic: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -251,6 +266,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
hinterOpen: false,
|
||||
changeStarted: false,
|
||||
ctrlPressed: false,
|
||||
peekOverlayProps: undefined,
|
||||
};
|
||||
this.updatePropertyValue = this.updatePropertyValue.bind(this);
|
||||
}
|
||||
|
|
@ -365,6 +381,11 @@ class CodeEditor extends Component<Props, State> {
|
|||
editor.on("blur", this.handleEditorBlur);
|
||||
editor.on("postPick", () => this.handleAutocompleteVisibility(editor));
|
||||
editor.on("mousedown", this.handleClick);
|
||||
CodeMirror.on(
|
||||
editor.getWrapperElement(),
|
||||
"mousemove",
|
||||
this.debounceHandleMouseOver,
|
||||
);
|
||||
|
||||
if (this.props.height) {
|
||||
editor.setSize("100%", this.props.height);
|
||||
|
|
@ -503,6 +524,91 @@ class CodeEditor extends Component<Props, State> {
|
|||
this.editor.clearHistory();
|
||||
}
|
||||
|
||||
showPeekOverlay = (
|
||||
peekableAttribute: 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
peekOverlayProps: {
|
||||
name: peekableAttribute,
|
||||
position: tokenElementPosition,
|
||||
textWidth: tokenElementPosition.width,
|
||||
marker,
|
||||
data: dataToShow,
|
||||
dataType: typeof dataToShow,
|
||||
},
|
||||
});
|
||||
|
||||
AnalyticsUtil.logEvent("PEEK_OVERLAY_OPENED", {
|
||||
property: peekableAttribute,
|
||||
});
|
||||
};
|
||||
|
||||
hidePeekOverlay = () => {
|
||||
this.state.peekOverlayProps?.marker?.clear();
|
||||
this.setState({
|
||||
peekOverlayProps: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
debounceHandleMouseOver = debounce(
|
||||
(ev) => this.handleMouseOver(ev),
|
||||
PEEK_OVERLAY_DELAY,
|
||||
);
|
||||
|
||||
handleMouseOver = (event: MouseEvent) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.hasAttribute(PEEKABLE_ATTRIBUTE)
|
||||
) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.hidePeekOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseMove = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
this.handleCustomGutter(this.editor.lineAtHeight(e.clientY, "window"));
|
||||
// this code only runs when we want custom tool tip for any highlighted text inside codemirror instance
|
||||
|
|
@ -557,6 +663,11 @@ class CodeEditor extends Component<Props, State> {
|
|||
this.editor.off("postPick", () =>
|
||||
this.handleAutocompleteVisibility(this.editor),
|
||||
);
|
||||
CodeMirror.off(
|
||||
this.editor.getWrapperElement(),
|
||||
"mousemove",
|
||||
this.debounceHandleMouseOver,
|
||||
);
|
||||
// @ts-expect-error: Types are not available
|
||||
this.editor.closeHint();
|
||||
}
|
||||
|
|
@ -661,6 +772,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
if (navigationData.type === ENTITY_TYPE.WIDGET) {
|
||||
this.props.selectWidget(navigationData.id);
|
||||
}
|
||||
this.hidePeekOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -882,6 +994,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
});
|
||||
this.props.startingEntityUpdate();
|
||||
}
|
||||
this.hidePeekOverlay();
|
||||
this.handleDebouncedChange(instance, changeObj);
|
||||
};
|
||||
|
||||
|
|
@ -1155,7 +1268,7 @@ class CodeEditor extends Component<Props, State> {
|
|||
expected={expected}
|
||||
hasError={isInvalid}
|
||||
hideEvaluatedValue={hideEvaluatedValue}
|
||||
isOpen={showEvaluatedValue}
|
||||
isOpen={showEvaluatedValue && !this.state.peekOverlayProps}
|
||||
popperPlacement={this.props.popperPlacement}
|
||||
popperZIndex={this.props.popperZIndex}
|
||||
theme={theme || EditorTheme.LIGHT}
|
||||
|
|
@ -1184,6 +1297,12 @@ class CodeEditor extends Component<Props, State> {
|
|||
ref={this.editorWrapperRef}
|
||||
size={size}
|
||||
>
|
||||
{this.state.peekOverlayProps && (
|
||||
<PeekOverlayPopUp
|
||||
hidePeekOverlay={() => this.hidePeekOverlay()}
|
||||
{...this.state.peekOverlayProps}
|
||||
/>
|
||||
)}
|
||||
{this.props.leftIcon && (
|
||||
<IconContainer>{this.props.leftIcon}</IconContainer>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -49,13 +49,24 @@ const hasReference = (token: CodeMirror.Token) => {
|
|||
return token.type === "variable" || tokenString === "this";
|
||||
};
|
||||
|
||||
export const PEEKABLE_CLASSNAME = "peekaboo";
|
||||
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,
|
||||
) => {
|
||||
editor
|
||||
.getAllMarks()
|
||||
.filter((marker) => marker.className === NAVIGATION_CLASSNAME)
|
||||
.filter(
|
||||
(marker) =>
|
||||
marker.className === NAVIGATION_CLASSNAME ||
|
||||
marker.className === PEEKABLE_CLASSNAME,
|
||||
)
|
||||
.forEach((marker) => marker.clear());
|
||||
|
||||
editor.eachLine((line: CodeMirror.LineHandle) => {
|
||||
|
|
@ -79,6 +90,25 @@ export const entityMarker: MarkHelper = (
|
|||
title: data.name,
|
||||
},
|
||||
);
|
||||
if (data.peekable) {
|
||||
editor.markText(
|
||||
{ ch: token.start, line: lineNo },
|
||||
{ ch: token.end, line: lineNo },
|
||||
{
|
||||
className: PEEKABLE_CLASSNAME,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
attributes: {
|
||||
[PEEKABLE_ATTRIBUTE]: data.name,
|
||||
[PEEKABLE_CH_START]: token.start,
|
||||
[PEEKABLE_CH_END]: token.end,
|
||||
[PEEKABLE_LINE]: lineNo,
|
||||
},
|
||||
atomic: false,
|
||||
title: data.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
addMarksForChildren(
|
||||
entityNavigationData[tokenString],
|
||||
lineNo,
|
||||
|
|
@ -123,6 +153,25 @@ const addMarksForChildren = (
|
|||
},
|
||||
);
|
||||
}
|
||||
if (childLink.peekable) {
|
||||
editor.markText(
|
||||
{ ch: token.start, line: lineNo },
|
||||
{ ch: token.end, line: lineNo },
|
||||
{
|
||||
className: PEEKABLE_CLASSNAME,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
attributes: {
|
||||
[PEEKABLE_ATTRIBUTE]: childLink.name,
|
||||
[PEEKABLE_CH_START]: token.start,
|
||||
[PEEKABLE_CH_END]: token.end,
|
||||
[PEEKABLE_LINE]: lineNo,
|
||||
},
|
||||
atomic: false,
|
||||
title: childLink.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
addMarksForChildren(childNodes[token.string], lineNo, token.end, editor);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import {
|
|||
} from "components/editorComponents/CodeEditor/EditorConfig";
|
||||
import { Skin, Theme } from "constants/DefaultTheme";
|
||||
import { Colors } from "constants/Colors";
|
||||
import {
|
||||
NAVIGATION_CLASSNAME,
|
||||
PEEKABLE_CLASSNAME,
|
||||
PEEK_STYLE_PERSIST_CLASS,
|
||||
} from "./markHelpers";
|
||||
|
||||
const getBorderStyle = (
|
||||
props: { theme: Theme } & {
|
||||
|
|
@ -163,12 +168,21 @@ export const EditorWrapper = styled.div<{
|
|||
: props.theme.colors.bindingText};
|
||||
font-weight: 700;
|
||||
}
|
||||
.navigable-entity-highlight {
|
||||
cursor: ${(props) => (props.ctrlPressed ? "pointer" : "selection")};
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.${PEEKABLE_CLASSNAME}:hover, .${PEEK_STYLE_PERSIST_CLASS} {
|
||||
background-color: #F4FFDE;
|
||||
}
|
||||
|
||||
.${NAVIGATION_CLASSNAME} {
|
||||
cursor: ${(props) => (props.ctrlPressed ? "pointer" : "selection")};
|
||||
${(props) =>
|
||||
props.ctrlPressed &&
|
||||
`&:hover {
|
||||
text-decoration: underline;
|
||||
background-color: #FFEFCF;
|
||||
}`}
|
||||
}
|
||||
|
||||
.CodeMirror-matchingbracket {
|
||||
text-decoration: none;
|
||||
color: #ffd600 !important;
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ export const zIndexLayers = {
|
|||
PROPERTY_PANE: "z-[3]",
|
||||
ENTITY_EXPLORER: "z-[3]",
|
||||
RESIZER: "z-[4]",
|
||||
PEEK_OVERLAY: "z-[10]", // to hover over the header
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
DataTree,
|
||||
DataTreeWidget,
|
||||
DataTreeAppsmith,
|
||||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { createSelector } from "reselect";
|
||||
|
|
@ -13,10 +13,12 @@ import { getWidgets } from "sagas/selectors";
|
|||
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";
|
||||
import { builderURL, jsCollectionIdURL } from "RouteBuilder";
|
||||
import { keyBy } from "lodash";
|
||||
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
|
||||
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
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";
|
||||
|
||||
export type NavigationData = {
|
||||
name: string;
|
||||
|
|
@ -24,7 +26,10 @@ export type NavigationData = {
|
|||
type: ENTITY_TYPE;
|
||||
url: string | undefined;
|
||||
navigable: boolean;
|
||||
children: Record<string, NavigationData>;
|
||||
children: EntityNavigationData;
|
||||
peekable: boolean;
|
||||
peekData?: unknown;
|
||||
key?: string;
|
||||
};
|
||||
export type EntityNavigationData = Record<string, NavigationData>;
|
||||
|
||||
|
|
@ -43,10 +48,11 @@ export const getEntitiesForNavigation = createSelector(
|
|||
(plugin) => plugin.id === action.config.pluginId,
|
||||
);
|
||||
const config = getActionConfig(action.config.pluginType);
|
||||
const result = getActionChildrenNavData(action, dataTree);
|
||||
if (!config) return;
|
||||
navigationData[action.config.name] = {
|
||||
name: action.config.name,
|
||||
navigationData[action.config.name] = createNavData({
|
||||
id: action.config.id,
|
||||
name: action.config.name,
|
||||
type: ENTITY_TYPE.ACTION,
|
||||
url: config.getURL(
|
||||
pageId,
|
||||
|
|
@ -54,91 +60,40 @@ export const getEntitiesForNavigation = createSelector(
|
|||
action.config.pluginType,
|
||||
plugin,
|
||||
),
|
||||
navigable: true,
|
||||
children: {},
|
||||
};
|
||||
peekable: true,
|
||||
peekData: result?.peekData,
|
||||
children: result?.childNavData || {},
|
||||
});
|
||||
});
|
||||
|
||||
jsActions.forEach((jsAction) => {
|
||||
navigationData[jsAction.config.name] = {
|
||||
name: jsAction.config.name,
|
||||
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 }),
|
||||
navigable: true,
|
||||
children: getJsObjectChildren(jsAction, pageId),
|
||||
};
|
||||
peekable: true,
|
||||
peekData: result?.peekData,
|
||||
children: result?.childNavData || {},
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(widgets).forEach((widget) => {
|
||||
navigationData[widget.widgetName] = {
|
||||
name: widget.widgetName,
|
||||
const result = getWidgetChildrenNavData(widget, dataTree, pageId);
|
||||
navigationData[widget.widgetName] = createNavData({
|
||||
id: widget.widgetId,
|
||||
name: widget.widgetName,
|
||||
type: ENTITY_TYPE.WIDGET,
|
||||
url: builderURL({ pageId, hash: widget.widgetId }),
|
||||
navigable: true,
|
||||
children: getWidgetChildren(widget, dataTree, pageId),
|
||||
};
|
||||
peekable: true,
|
||||
peekData: result?.peekData,
|
||||
children: result?.childNavData || {},
|
||||
});
|
||||
});
|
||||
navigationData["appsmith"] = getAppsmithNavData(
|
||||
dataTree.appsmith as DataTreeAppsmith,
|
||||
);
|
||||
return navigationData;
|
||||
},
|
||||
);
|
||||
|
||||
const getJsObjectChildren = (jsAction: JSCollectionData, pageId: string) => {
|
||||
const children = [
|
||||
...jsAction.config.actions,
|
||||
...jsAction.config.variables,
|
||||
].map((jsChild) => ({
|
||||
name: `${jsAction.config.name}.${jsChild.name}`,
|
||||
key: jsChild.name,
|
||||
id: `${jsAction.config.name}.${jsChild.name}`,
|
||||
type: ENTITY_TYPE.JSACTION,
|
||||
url: jsCollectionIdURL({
|
||||
pageId,
|
||||
collectionId: jsAction.config.id,
|
||||
functionName: jsChild.name,
|
||||
}),
|
||||
navigable: true,
|
||||
children: {},
|
||||
}));
|
||||
|
||||
return keyBy(children, (data) => data.key);
|
||||
};
|
||||
|
||||
const getWidgetChildren = (
|
||||
widget: FlattenedWidgetProps,
|
||||
dataTree: DataTree,
|
||||
pageId: string,
|
||||
) => {
|
||||
if (widget.type === "FORM_WIDGET") {
|
||||
const children: EntityNavigationData = {};
|
||||
const dataTreeWidget: DataTreeWidget = dataTree[
|
||||
widget.widgetName
|
||||
] as DataTreeWidget;
|
||||
const formChildren: EntityNavigationData = {};
|
||||
if (dataTreeWidget) {
|
||||
Object.keys(dataTreeWidget.data || {}).forEach((widgetName) => {
|
||||
const childWidgetId = (dataTree[widgetName] as DataTreeWidget).widgetId;
|
||||
formChildren[widgetName] = {
|
||||
name: widgetName,
|
||||
id: `${widget.widgetName}.data.${widgetName}`,
|
||||
type: ENTITY_TYPE.WIDGET,
|
||||
navigable: true,
|
||||
children: {},
|
||||
url: builderURL({ pageId, hash: childWidgetId }),
|
||||
};
|
||||
});
|
||||
}
|
||||
children.data = {
|
||||
name: "data",
|
||||
id: `${widget.widgetName}.data`,
|
||||
type: ENTITY_TYPE.WIDGET,
|
||||
navigable: false,
|
||||
children: formChildren,
|
||||
url: undefined,
|
||||
};
|
||||
|
||||
return children;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -279,6 +279,9 @@ export type EventName =
|
|||
| "BRANDING_SUBMIT_CLICK"
|
||||
| "Cmd+Click Navigation"
|
||||
| "WIDGET_PROPERTY_SEARCH"
|
||||
| "PEEK_OVERLAY_OPENED"
|
||||
| "PEEK_OVERLAY_COLLAPSE_EXPAND_CLICK"
|
||||
| "PEEK_OVERLAY_VALUE_COPIED"
|
||||
| LIBRARY_EVENTS;
|
||||
|
||||
export type LIBRARY_EVENTS =
|
||||
|
|
|
|||
51
app/client/src/utils/NavigationSelector/ActionChildren.ts
Normal file
51
app/client/src/utils/NavigationSelector/ActionChildren.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { entityDefinitions } from "ce/utils/autocomplete/EntityDefinitions";
|
||||
import {
|
||||
DataTree,
|
||||
DataTreeAction,
|
||||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { ActionData } from "reducers/entityReducers/actionsReducer";
|
||||
import { EntityNavigationData } from "selectors/navigationSelectors";
|
||||
import { createNavData } from "./common";
|
||||
|
||||
export const getActionChildrenNavData = (
|
||||
action: ActionData,
|
||||
dataTree: DataTree,
|
||||
) => {
|
||||
const dataTreeAction = dataTree[action.config.name] as DataTreeAction;
|
||||
if (dataTreeAction) {
|
||||
const definitions = entityDefinitions.ACTION(dataTreeAction, {});
|
||||
const peekData: Record<string, unknown> = {};
|
||||
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 };
|
||||
}
|
||||
};
|
||||
32
app/client/src/utils/NavigationSelector/AppsmithNavData.ts
Normal file
32
app/client/src/utils/NavigationSelector/AppsmithNavData.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { entityDefinitions } from "ce/utils/autocomplete/EntityDefinitions";
|
||||
import {
|
||||
DataTreeAppsmith,
|
||||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { createNavData, createObjectNavData } from "./common";
|
||||
|
||||
export const getAppsmithNavData = (dataTree: DataTreeAppsmith) => {
|
||||
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,
|
||||
});
|
||||
};
|
||||
91
app/client/src/utils/NavigationSelector/JsChildren.ts
Normal file
91
app/client/src/utils/NavigationSelector/JsChildren.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
DataTree,
|
||||
DataTreeJSAction,
|
||||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { keyBy } from "lodash";
|
||||
import { JSCollectionData } from "reducers/entityReducers/jsActionsReducer";
|
||||
import { jsCollectionIdURL } from "RouteBuilder";
|
||||
import {
|
||||
EntityNavigationData,
|
||||
NavigationData,
|
||||
} from "selectors/navigationSelectors";
|
||||
import { createNavData } from "./common";
|
||||
|
||||
export const getJsChildrenNavData = (
|
||||
jsAction: JSCollectionData,
|
||||
pageId: string,
|
||||
dataTree: DataTree,
|
||||
) => {
|
||||
const peekData: Record<string, unknown> = {};
|
||||
let childNavData: EntityNavigationData = {};
|
||||
|
||||
const dataTreeAction = dataTree[jsAction.config.name] as DataTreeJSAction;
|
||||
|
||||
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}`,
|
||||
type: ENTITY_TYPE.JSACTION,
|
||||
url: jsCollectionIdURL({
|
||||
pageId,
|
||||
collectionId: jsAction.config.id,
|
||||
functionName: jsChild.name,
|
||||
}),
|
||||
peekable: true,
|
||||
peekData: undefined,
|
||||
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}`,
|
||||
type: ENTITY_TYPE.JSACTION,
|
||||
url: jsCollectionIdURL({
|
||||
pageId,
|
||||
collectionId: jsAction.config.id,
|
||||
functionName: jsChild.name,
|
||||
}),
|
||||
peekable: true,
|
||||
peekData: undefined,
|
||||
children: {},
|
||||
key: jsChild.name,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
children = children.concat(variableChildren);
|
||||
|
||||
childNavData = keyBy(children, (data) => data.key) as Record<
|
||||
string,
|
||||
NavigationData
|
||||
>;
|
||||
|
||||
return { childNavData, peekData };
|
||||
}
|
||||
};
|
||||
85
app/client/src/utils/NavigationSelector/WidgetChildren.ts
Normal file
85
app/client/src/utils/NavigationSelector/WidgetChildren.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import {
|
||||
entityDefinitions,
|
||||
EntityDefinitionsOptions,
|
||||
} from "ce/utils/autocomplete/EntityDefinitions";
|
||||
import {
|
||||
DataTree,
|
||||
DataTreeWidget,
|
||||
ENTITY_TYPE,
|
||||
} from "entities/DataTree/dataTreeFactory";
|
||||
import { isFunction } from "lodash";
|
||||
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
|
||||
import { builderURL } from "RouteBuilder";
|
||||
import { EntityNavigationData } from "selectors/navigationSelectors";
|
||||
import { createNavData } from "./common";
|
||||
|
||||
export const getWidgetChildrenNavData = (
|
||||
widget: FlattenedWidgetProps,
|
||||
dataTree: DataTree,
|
||||
pageId: string,
|
||||
) => {
|
||||
const peekData: Record<string, unknown> = {};
|
||||
const childNavData: EntityNavigationData = {};
|
||||
const dataTreeWidget: DataTreeWidget = dataTree[
|
||||
widget.widgetName
|
||||
] as DataTreeWidget;
|
||||
if (widget.type === "FORM_WIDGET") {
|
||||
const children: EntityNavigationData = {};
|
||||
const formChildren: EntityNavigationData = {};
|
||||
if (dataTreeWidget) {
|
||||
Object.keys(dataTreeWidget.data || {}).forEach((widgetName) => {
|
||||
const childWidgetId = (dataTree[widgetName] as DataTreeWidget).widgetId;
|
||||
formChildren[widgetName] = createNavData({
|
||||
id: `${widget.widgetName}.data.${widgetName}`,
|
||||
name: widgetName,
|
||||
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 = entityDefinitions[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 };
|
||||
}
|
||||
};
|
||||
97
app/client/src/utils/NavigationSelector/common.ts
Normal file
97
app/client/src/utils/NavigationSelector/common.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { ENTITY_TYPE } from "entities/DataTree/types";
|
||||
import {
|
||||
EntityNavigationData,
|
||||
NavigationData,
|
||||
} from "selectors/navigationSelectors";
|
||||
|
||||
export const createNavData = (general: {
|
||||
name: string;
|
||||
id: string;
|
||||
type: ENTITY_TYPE;
|
||||
children: EntityNavigationData;
|
||||
key?: string;
|
||||
url: string | undefined;
|
||||
peekable: boolean;
|
||||
peekData: unknown;
|
||||
}): NavigationData => {
|
||||
return {
|
||||
name: general.name,
|
||||
id: general.id,
|
||||
type: general.type,
|
||||
children: general.children,
|
||||
key: general.key,
|
||||
url: general.url,
|
||||
navigable: !!general.url,
|
||||
peekable: general.peekable,
|
||||
peekData: general.peekData,
|
||||
};
|
||||
};
|
||||
|
||||
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<string, boolean>,
|
||||
) => {
|
||||
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;
|
||||
1
app/client/typings/react-append-to-body/index.d.ts
vendored
Normal file
1
app/client/typings/react-append-to-body/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
declare module "react-append-to-body";
|
||||
|
|
@ -12459,6 +12459,11 @@ react-app-polyfill@^3.0.0:
|
|||
regenerator-runtime "^0.13.9"
|
||||
whatwg-fetch "^3.6.2"
|
||||
|
||||
react-append-to-body@^2.0.26:
|
||||
version "2.0.26"
|
||||
resolved "https://registry.yarnpkg.com/react-append-to-body/-/react-append-to-body-2.0.26.tgz#8ada0cb404462417651bbcd172c99f8100d6470e"
|
||||
integrity sha512-ZFtJH8V1kVV4LR2wrrkFJhoT+pC/MxXCWPh2FAcT4yMMTCh+QXJwkVvr3WHnAi2KvG9LvyFmjLWsvWDlLi9eeA==
|
||||
|
||||
react-async-script@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user