chore: Function Render of State Inspector (#38893)
This commit is contained in:
parent
632443c013
commit
0f8e41fc7c
|
|
@ -1,38 +0,0 @@
|
||||||
import type { MouseEventHandler } from "react";
|
|
||||||
import AnalyticsUtil from "ee/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");
|
|
||||||
};
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import type { MutableRefObject } from "react";
|
import React, {
|
||||||
import { useState } from "react";
|
type MutableRefObject,
|
||||||
import React, { useEffect, useRef } from "react";
|
useCallback,
|
||||||
import ReactJson from "react-json-view";
|
useMemo,
|
||||||
import { JsonWrapper, reactJsonProps } from "./JsonWrapper";
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { useEventCallback } from "usehooks-ts";
|
||||||
import { componentWillAppendToBody } from "react-append-to-body";
|
import { componentWillAppendToBody } from "react-append-to-body";
|
||||||
import _, { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { zIndexLayers } from "constants/CanvasEditorConstants";
|
import { zIndexLayers } from "constants/CanvasEditorConstants";
|
||||||
import { objectCollapseAnalytics, textSelectAnalytics } from "./Analytics";
|
|
||||||
import { Divider } from "@appsmith/ads";
|
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { getConfigTree, getDataTree } from "selectors/dataTreeSelectors";
|
import { getConfigTree, getDataTree } from "selectors/dataTreeSelectors";
|
||||||
import { filterInternalProperties } from "utils/FilterInternalProperties";
|
import { filterInternalProperties } from "utils/FilterInternalProperties";
|
||||||
import { getJSCollections } from "ee/selectors/entitiesSelector";
|
import { getJSCollections } from "ee/selectors/entitiesSelector";
|
||||||
|
import * as Styled from "./styles";
|
||||||
|
import { CONTAINER_MAX_HEIGHT_PX, PEEK_OVERLAY_DELAY } from "./constants";
|
||||||
|
import { getDataTypeHeader, getPropertyData } from "./utils";
|
||||||
|
import { JSONViewer, Size } from "../../JSONViewer";
|
||||||
|
|
||||||
export interface PeekOverlayStateProps {
|
export interface PeekOverlayStateProps {
|
||||||
objectName: string;
|
objectName: string;
|
||||||
|
|
@ -31,155 +35,83 @@ export const PeekOverlayPopUp = componentWillAppendToBody(
|
||||||
PeekOverlayPopUpContent,
|
PeekOverlayPopUpContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
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(
|
export function PeekOverlayPopUpContent(
|
||||||
props: PeekOverlayStateProps & {
|
props: PeekOverlayStateProps & {
|
||||||
hidePeekOverlay: () => void;
|
hidePeekOverlay: () => void;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const CONTAINER_MAX_HEIGHT_PX = 252;
|
const { hidePeekOverlay, objectName, position, propertyPath } = props;
|
||||||
const dataWrapperRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
const dataWrapperRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
|
||||||
const dataTree = useSelector(getDataTree);
|
const dataTree = useSelector(getDataTree);
|
||||||
const configTree = useSelector(getConfigTree);
|
const configTree = useSelector(getConfigTree);
|
||||||
const jsActions = useSelector(getJSCollections);
|
const jsActions = useSelector(getJSCollections);
|
||||||
|
|
||||||
const filteredData = filterInternalProperties(
|
const filteredData = filterInternalProperties(
|
||||||
props.objectName,
|
objectName,
|
||||||
dataTree[props.objectName],
|
dataTree[objectName],
|
||||||
jsActions,
|
jsActions,
|
||||||
dataTree,
|
dataTree,
|
||||||
configTree,
|
configTree,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Because getPropertyData can return a function
|
const [jsData, dataType] = useMemo(
|
||||||
// And we don't want to execute it.
|
// Because getPropertyData can return a function
|
||||||
const [jsData] = useState(() =>
|
// And we don't want to execute it.
|
||||||
getPropertyData(filteredData, props.propertyPath),
|
() => {
|
||||||
|
const jsData = getPropertyData(filteredData, propertyPath);
|
||||||
|
const dataType = getDataTypeHeader(jsData);
|
||||||
|
|
||||||
|
return [jsData, dataType];
|
||||||
|
},
|
||||||
|
[filteredData, propertyPath],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [dataType] = useState(getDataTypeHeader(jsData));
|
const debouncedHide = debounce(hidePeekOverlay, PEEK_OVERLAY_DELAY);
|
||||||
|
|
||||||
useEffect(() => {
|
const getPositionValues = useCallback(() => {
|
||||||
const wheelCallback = () => {
|
const positionValues: { $left: string; $bottom?: string; $top?: string } = {
|
||||||
props.hidePeekOverlay();
|
// Always have a minimum of 8px from the left
|
||||||
|
$left: Math.max(position.right - 300, 8) + "px",
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("wheel", wheelCallback);
|
// if the peek overlay is going to be more than the container height, then show it from the bottom
|
||||||
|
if (position.top >= CONTAINER_MAX_HEIGHT_PX) {
|
||||||
|
positionValues.$bottom = `calc(100vh - ${position.top}px)`;
|
||||||
|
} else {
|
||||||
|
positionValues.$top = `${position.bottom}px`;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return positionValues;
|
||||||
window.removeEventListener("wheel", wheelCallback);
|
}, [position]);
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const onWheel = useEventCallback((ev: React.WheelEvent) => {
|
||||||
if (!dataWrapperRef.current) return;
|
ev.stopPropagation();
|
||||||
|
hidePeekOverlay();
|
||||||
dataWrapperRef.current.addEventListener("copy", textSelectAnalytics);
|
});
|
||||||
|
|
||||||
return () =>
|
|
||||||
dataWrapperRef.current?.removeEventListener("copy", textSelectAnalytics);
|
|
||||||
}, [dataWrapperRef, dataWrapperRef.current]);
|
|
||||||
|
|
||||||
const debouncedHide = debounce(
|
|
||||||
() => props.hidePeekOverlay(),
|
|
||||||
PEEK_OVERLAY_DELAY,
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLeftPosition = (position: DOMRect) => {
|
|
||||||
let left = position.right - 300;
|
|
||||||
|
|
||||||
if (left < 0) left = 8;
|
|
||||||
|
|
||||||
return left;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Styled.PeekOverlayContainer
|
||||||
className={`absolute ${zIndexLayers.PEEK_OVERLAY}`}
|
className={`absolute ${zIndexLayers.PEEK_OVERLAY}`}
|
||||||
id="t--peek-overlay-container"
|
id="t--peek-overlay-container"
|
||||||
onMouseEnter={() => debouncedHide.cancel()}
|
onMouseEnter={debouncedHide.cancel}
|
||||||
onMouseLeave={() => debouncedHide()}
|
onMouseLeave={debouncedHide}
|
||||||
onWheel={(ev) => ev.stopPropagation()}
|
onWheel={onWheel}
|
||||||
style={{
|
{...getPositionValues()}
|
||||||
minHeight: "46px",
|
|
||||||
maxHeight: `${CONTAINER_MAX_HEIGHT_PX}px`,
|
|
||||||
width: "300px",
|
|
||||||
backgroundColor: "var(--ads-v2-color-bg)",
|
|
||||||
boxShadow: "0px 0px 10px #0000001A", // color used from designs
|
|
||||||
borderRadius: "var(--ads-v2-border-radius)",
|
|
||||||
left: `${getLeftPosition(props.position)}px`,
|
|
||||||
...(props.position.top >= CONTAINER_MAX_HEIGHT_PX
|
|
||||||
? {
|
|
||||||
bottom: `calc(100vh - ${props.position.top}px)`,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
top: `${props.position.bottom}px`,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<Styled.DataType className="first-letter:uppercase">
|
||||||
className="first-letter:uppercase"
|
|
||||||
style={{
|
|
||||||
height: "24px",
|
|
||||||
color: "var(--appsmith-color-black-700)",
|
|
||||||
padding: "4px 0px 4px 12px",
|
|
||||||
fontSize: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{dataType}
|
{dataType}
|
||||||
</div>
|
</Styled.DataType>
|
||||||
<Divider style={{ display: "block" }} />
|
<Styled.BlockDivider />
|
||||||
<div
|
<Styled.PeekOverlayData id="t--peek-overlay-data" ref={dataWrapperRef}>
|
||||||
id="t--peek-overlay-data"
|
|
||||||
ref={dataWrapperRef}
|
|
||||||
style={{
|
|
||||||
minHeight: "20px",
|
|
||||||
padding: "2px 0px 2px 12px",
|
|
||||||
fontSize: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(dataType === "object" || dataType === "array") && jsData !== null && (
|
{(dataType === "object" || dataType === "array") && jsData !== null && (
|
||||||
<JsonWrapper
|
<Styled.JsonWrapper className="as-mask">
|
||||||
className="as-mask"
|
<JSONViewer size={Size.SMALL} src={jsData} />
|
||||||
onClick={objectCollapseAnalytics}
|
</Styled.JsonWrapper>
|
||||||
style={{
|
|
||||||
minHeight: "20px",
|
|
||||||
maxHeight: "225px",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ReactJson src={jsData} {...reactJsonProps} />
|
|
||||||
</JsonWrapper>
|
|
||||||
)}
|
)}
|
||||||
{/* TODO: Fix this the next time the file is edited */}
|
{dataType === "function" && <div>{jsData.toString()}</div>}
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
{dataType === "boolean" && <div>{jsData.toString()}</div>}
|
||||||
{dataType === "function" && <div>{(jsData as any).toString()}</div>}
|
{dataType === "string" && <div>{jsData.toString()}</div>}
|
||||||
{/* TODO: Fix this the next time the file is edited */}
|
{dataType === "number" && <div>{jsData.toString()}</div>}
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{dataType === "boolean" && <div>{(jsData as any).toString()}</div>}
|
|
||||||
{/* TODO: Fix this the next time the file is edited */}
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{dataType === "string" && <div>{(jsData as any).toString()}</div>}
|
|
||||||
{/* TODO: Fix this the next time the file is edited */}
|
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{dataType === "number" && <div>{(jsData as any).toString()}</div>}
|
|
||||||
{((dataType !== "object" &&
|
{((dataType !== "object" &&
|
||||||
dataType !== "function" &&
|
dataType !== "function" &&
|
||||||
dataType !== "boolean" &&
|
dataType !== "boolean" &&
|
||||||
|
|
@ -188,14 +120,12 @@ export function PeekOverlayPopUpContent(
|
||||||
dataType !== "number") ||
|
dataType !== "number") ||
|
||||||
jsData === null) && (
|
jsData === null) && (
|
||||||
<div>
|
<div>
|
||||||
{/* TODO: Fix this the next time the file is edited */}
|
{jsData?.toString() ?? jsData ?? jsData === undefined
|
||||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
|
||||||
{(jsData as any)?.toString() ?? jsData ?? jsData === undefined
|
|
||||||
? "undefined"
|
? "undefined"
|
||||||
: "null"}
|
: "null"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Styled.PeekOverlayData>
|
||||||
</div>
|
</Styled.PeekOverlayContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const CONTAINER_MAX_HEIGHT_PX = 252;
|
||||||
|
|
||||||
|
export const PEEK_OVERLAY_DELAY = 200;
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { Divider } from "@appsmith/ads";
|
||||||
|
|
||||||
|
export const PeekOverlayContainer = styled.div<{
|
||||||
|
$left: string;
|
||||||
|
$top?: string;
|
||||||
|
$bottom?: string;
|
||||||
|
}>`
|
||||||
|
min-height: 46px;
|
||||||
|
max-height: 252px;
|
||||||
|
width: 300px;
|
||||||
|
background-color: var(--ads-v2-color-bg);
|
||||||
|
box-shadow: 0 0 10px #0000001a; // color used from designs
|
||||||
|
border-radius: var(--ads-v2-border-radius);
|
||||||
|
left: ${({ $left }) => $left};
|
||||||
|
top: ${({ $top }) => $top};
|
||||||
|
bottom: ${({ $bottom }) => $bottom};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DataType = styled.div`
|
||||||
|
height: 24px;
|
||||||
|
color: var(--appsmith-color-black-700);
|
||||||
|
padding: var(--ads-v2-spaces-2) 0 var(--ads-v2-spaces-2)
|
||||||
|
var(--ads-v2-spaces-4);
|
||||||
|
font-size: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BlockDivider = styled(Divider)`
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const PeekOverlayData = styled.div`
|
||||||
|
min-height: 20px;
|
||||||
|
padding: var(--ads-v2-spaces-1) 0 var(--ads-v2-spaces-1)
|
||||||
|
var(--ads-v2-spaces-4);
|
||||||
|
font-size: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const JsonWrapper = styled.div`
|
||||||
|
min-height: 20px;
|
||||||
|
max-height: 225px;
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { get } from "lodash";
|
||||||
|
|
||||||
|
export const getPropertyData = (src: unknown, propertyPath: string[]) => {
|
||||||
|
return propertyPath.length > 0 ? get(src, propertyPath) : src;
|
||||||
|
};
|
||||||
|
export const getDataTypeHeader = (data: unknown) => {
|
||||||
|
const dataType = typeof data;
|
||||||
|
|
||||||
|
if (dataType === "object") {
|
||||||
|
if (Array.isArray(data)) return "array";
|
||||||
|
|
||||||
|
if (data === null) return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataType;
|
||||||
|
};
|
||||||
|
|
@ -128,10 +128,7 @@ import { getEntitiesForNavigation } from "selectors/navigationSelectors";
|
||||||
import history, { NavigationMethod } from "utils/history";
|
import history, { NavigationMethod } from "utils/history";
|
||||||
import { CursorPositionOrigin } from "ee/reducers/uiReducers/editorContextReducer";
|
import { CursorPositionOrigin } from "ee/reducers/uiReducers/editorContextReducer";
|
||||||
import type { PeekOverlayStateProps } from "./PeekOverlayPopup/PeekOverlayPopup";
|
import type { PeekOverlayStateProps } from "./PeekOverlayPopup/PeekOverlayPopup";
|
||||||
import {
|
import { PeekOverlayPopUp } from "./PeekOverlayPopup/PeekOverlayPopup";
|
||||||
PeekOverlayPopUp,
|
|
||||||
PEEK_OVERLAY_DELAY,
|
|
||||||
} from "./PeekOverlayPopup/PeekOverlayPopup";
|
|
||||||
import ConfigTreeActions from "utils/configTree";
|
import ConfigTreeActions from "utils/configTree";
|
||||||
import {
|
import {
|
||||||
getSaveAndAutoIndentKey,
|
getSaveAndAutoIndentKey,
|
||||||
|
|
@ -164,6 +161,7 @@ import CodeMirrorTernService from "utils/autocomplete/CodemirrorTernService";
|
||||||
import { getEachEntityInformation } from "ee/utils/autocomplete/EntityDefinitions";
|
import { getEachEntityInformation } from "ee/utils/autocomplete/EntityDefinitions";
|
||||||
import { getCurrentPageId } from "selectors/editorSelectors";
|
import { getCurrentPageId } from "selectors/editorSelectors";
|
||||||
import { executeCommandAction } from "actions/pluginActionActions";
|
import { executeCommandAction } from "actions/pluginActionActions";
|
||||||
|
import { PEEK_OVERLAY_DELAY } from "./PeekOverlayPopup/constants";
|
||||||
|
|
||||||
type ReduxStateProps = ReturnType<typeof mapStateToProps>;
|
type ReduxStateProps = ReturnType<typeof mapStateToProps>;
|
||||||
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
|
type ReduxDispatchProps = ReturnType<typeof mapDispatchToProps>;
|
||||||
|
|
@ -202,6 +200,7 @@ export interface EditorStyleProps {
|
||||||
popperZIndex?: Indices;
|
popperZIndex?: Indices;
|
||||||
blockCompletions?: Array<BlockCompletion>;
|
blockCompletions?: Array<BlockCompletion>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* line => Line to which the gutter is added
|
* line => Line to which the gutter is added
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import "@testing-library/jest-dom";
|
||||||
import { StateInspector } from "./StateInspector";
|
import { StateInspector } from "./StateInspector";
|
||||||
import { useStateInspectorItems } from "./hooks";
|
import { useStateInspectorItems } from "./hooks";
|
||||||
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
||||||
|
import { lightTheme } from "selectors/themeSelectors";
|
||||||
|
import { ThemeProvider } from "styled-components";
|
||||||
|
|
||||||
jest.mock("./hooks");
|
jest.mock("./hooks");
|
||||||
jest.mock("IDE/utils");
|
jest.mock("IDE/utils");
|
||||||
|
|
@ -31,7 +33,11 @@ describe("StateInspector", () => {
|
||||||
],
|
],
|
||||||
{ key: "value1" },
|
{ key: "value1" },
|
||||||
]);
|
]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
const searchInput = screen.getByPlaceholderText("Search entities");
|
const searchInput = screen.getByPlaceholderText("Search entities");
|
||||||
|
|
||||||
fireEvent.change(searchInput, { target: { value: "Group 1" } });
|
fireEvent.change(searchInput, { target: { value: "Group 1" } });
|
||||||
|
|
@ -53,7 +59,11 @@ describe("StateInspector", () => {
|
||||||
],
|
],
|
||||||
{ key: "value1" },
|
{ key: "value1" },
|
||||||
]);
|
]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
fireEvent.click(screen.getByText("Item 2"));
|
fireEvent.click(screen.getByText("Item 2"));
|
||||||
|
|
||||||
expect(mockOnClick).toHaveBeenCalled();
|
expect(mockOnClick).toHaveBeenCalled();
|
||||||
|
|
@ -71,7 +81,11 @@ describe("StateInspector", () => {
|
||||||
],
|
],
|
||||||
{ key: "Value1" },
|
{ key: "Value1" },
|
||||||
]);
|
]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId("t--selected-entity-details").textContent,
|
screen.getByTestId("t--selected-entity-details").textContent,
|
||||||
|
|
@ -84,7 +98,11 @@ describe("StateInspector", () => {
|
||||||
|
|
||||||
it("does not render selected item details when no item is selected", () => {
|
it("does not render selected item details when no item is selected", () => {
|
||||||
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
|
expect(screen.queryByText("Item 1")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,12 +119,20 @@ describe("StateInspector", () => {
|
||||||
{ key: "value1" },
|
{ key: "value1" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
expect(screen.getByText("Group 1")).toBeInTheDocument();
|
expect(screen.getByText("Group 1")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Group 2")).toBeInTheDocument();
|
expect(screen.getByText("Group 2")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it("renders no items when search term does not match any group", () => {
|
it("renders no items when search term does not match any group", () => {
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
const searchInput = screen.getByPlaceholderText("Search entities");
|
const searchInput = screen.getByPlaceholderText("Search entities");
|
||||||
|
|
||||||
fireEvent.change(searchInput, { target: { value: "Nonexistent Group" } });
|
fireEvent.change(searchInput, { target: { value: "Nonexistent Group" } });
|
||||||
|
|
@ -116,7 +142,11 @@ describe("StateInspector", () => {
|
||||||
|
|
||||||
it("renders no items when items list is empty", () => {
|
it("renders no items when items list is empty", () => {
|
||||||
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
mockedUseStateInspectorItems.mockReturnValue([null, [], null]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
expect(screen.queryByText("Group 1")).not.toBeInTheDocument();
|
expect(screen.queryByText("Group 1")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Group 2")).not.toBeInTheDocument();
|
expect(screen.queryByText("Group 2")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
@ -130,7 +160,11 @@ describe("StateInspector", () => {
|
||||||
],
|
],
|
||||||
{},
|
{},
|
||||||
]);
|
]);
|
||||||
render(<StateInspector />);
|
render(
|
||||||
|
<ThemeProvider theme={lightTheme}>
|
||||||
|
<StateInspector />
|
||||||
|
</ThemeProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByTestId("t--selected-entity-details").textContent,
|
screen.getByTestId("t--selected-entity-details").textContent,
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,21 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ReactJson from "react-json-view";
|
|
||||||
import {
|
import {
|
||||||
EntityGroupsList,
|
EntityGroupsList,
|
||||||
Flex,
|
Flex,
|
||||||
|
type FlexProps,
|
||||||
type ListItemProps,
|
type ListItemProps,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
Text,
|
Text,
|
||||||
} from "@appsmith/ads";
|
} from "@appsmith/ads";
|
||||||
|
import { JSONViewer, Size } from "components/editorComponents/JSONViewer";
|
||||||
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
import { filterEntityGroupsBySearchTerm } from "IDE/utils";
|
||||||
import { useStateInspectorItems } from "./hooks";
|
import { useStateInspectorItems } from "./hooks";
|
||||||
import * as Styled from "./styles";
|
import * as Styled from "./styles";
|
||||||
|
|
||||||
export const reactJsonProps = {
|
const GroupListPadding = {
|
||||||
name: null,
|
pl: "spaces-3",
|
||||||
enableClipboard: false,
|
pr: "spaces-3",
|
||||||
displayDataTypes: false,
|
} as FlexProps;
|
||||||
displayArrayKey: true,
|
|
||||||
quotesOnKeys: false,
|
|
||||||
style: {
|
|
||||||
fontSize: "12px",
|
|
||||||
},
|
|
||||||
collapsed: 1,
|
|
||||||
indentWidth: 2,
|
|
||||||
collapseStringsAfterLength: 30,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const StateInspector = () => {
|
export const StateInspector = () => {
|
||||||
const [selectedItem, items, selectedItemCode] = useStateInspectorItems();
|
const [selectedItem, items, selectedItemCode] = useStateInspectorItems();
|
||||||
|
|
@ -51,10 +43,7 @@ export const StateInspector = () => {
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<EntityGroupsList
|
<EntityGroupsList
|
||||||
flexProps={{
|
flexProps={GroupListPadding}
|
||||||
pl: "spaces-3",
|
|
||||||
pr: "spaces-3",
|
|
||||||
}}
|
|
||||||
groups={filteredItemGroups.map((item) => {
|
groups={filteredItemGroups.map((item) => {
|
||||||
return {
|
return {
|
||||||
groupTitle: item.group,
|
groupTitle: item.group,
|
||||||
|
|
@ -81,8 +70,8 @@ export const StateInspector = () => {
|
||||||
{selectedItem.icon}
|
{selectedItem.icon}
|
||||||
<Text kind="body-m">{selectedItem.title}</Text>
|
<Text kind="body-m">{selectedItem.title}</Text>
|
||||||
</Styled.SelectedItem>
|
</Styled.SelectedItem>
|
||||||
<Flex overflowY="auto" px="spaces-3">
|
<Flex className="as-mask" overflowY="auto" px="spaces-3">
|
||||||
<ReactJson src={selectedItemCode} {...reactJsonProps} />
|
<JSONViewer size={Size.MEDIUM} src={selectedItemCode} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
import type { JSONViewerProps } from "./types";
|
||||||
|
import ReactJson from "react-json-view";
|
||||||
|
import * as Styled from "./styles";
|
||||||
|
import { FontSize, IconSize, reactJsonProps } from "./constants";
|
||||||
|
|
||||||
|
export function JSONViewer(props: JSONViewerProps) {
|
||||||
|
const fontSize = FontSize[props.size];
|
||||||
|
const iconSize = IconSize[props.size];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Styled.Container $fontSize={fontSize} $iconSize={iconSize}>
|
||||||
|
<ReactJson src={props.src} {...reactJsonProps} />
|
||||||
|
</Styled.Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Size } from "./types";
|
||||||
|
|
||||||
|
export const FontSize = {
|
||||||
|
[Size.SMALL]: "10px",
|
||||||
|
[Size.MEDIUM]: "12px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconSize = {
|
||||||
|
[Size.SMALL]: "8px",
|
||||||
|
[Size.MEDIUM]: "10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reactJsonProps = {
|
||||||
|
name: null,
|
||||||
|
enableClipboard: false,
|
||||||
|
displayDataTypes: false,
|
||||||
|
displayArrayKey: true,
|
||||||
|
quotesOnKeys: false,
|
||||||
|
collapsed: 1,
|
||||||
|
indentWidth: 2,
|
||||||
|
collapseStringsAfterLength: 30,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { JSONViewer } from "./JSONViewer";
|
||||||
|
export { Size } from "./types";
|
||||||
|
export type { JSONViewerProps } from "./types";
|
||||||
|
|
@ -1,33 +1,24 @@
|
||||||
import styled from "styled-components";
|
import styled, { css } from "styled-components";
|
||||||
|
|
||||||
export const reactJsonProps = {
|
const ReactJSONViewerOverrider = css<{ $fontSize: string; $iconSize: string }>`
|
||||||
name: null,
|
font-size: ${({ $fontSize }) => $fontSize} !important;
|
||||||
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
|
// all ellipsis font size
|
||||||
|
|
||||||
.node-ellipsis,
|
.node-ellipsis,
|
||||||
.function-collapsed span:nth-child(2),
|
.function-collapsed span:nth-child(2),
|
||||||
.string-value span {
|
.string-value span {
|
||||||
font-size: 10px !important;
|
font-size: ${({ $fontSize }) => $fontSize} !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable and hide first object collapser
|
// disable and hide first object collapse icon
|
||||||
|
|
||||||
.pretty-json-container
|
.pretty-json-container
|
||||||
> .object-content:first-of-type
|
> .object-content:first-of-type
|
||||||
> .object-key-val:first-of-type
|
> .object-key-val:first-of-type
|
||||||
> span {
|
> span {
|
||||||
pointer-events: none !important;
|
pointer-events: none !important;
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
@ -38,28 +29,42 @@ export const JsonWrapper = styled.div`
|
||||||
}
|
}
|
||||||
|
|
||||||
// collapse icon color change and alignment
|
// collapse icon color change and alignment
|
||||||
|
|
||||||
.icon-container {
|
.icon-container {
|
||||||
width: 10px !important;
|
width: ${({ $iconSize }) => $iconSize} !important;
|
||||||
height: 8px !important;
|
height: ${({ $iconSize }) => $iconSize} !important;
|
||||||
|
|
||||||
|
.expanded-icon {
|
||||||
|
svg {
|
||||||
|
vertical-align: middle !important;
|
||||||
|
padding-left: 0px !important;
|
||||||
|
width: 0.8em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--appsmith-color-black-600) !important;
|
color: var(--appsmith-color-black-600) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// font-sizes and alignments
|
// font-sizes and alignments
|
||||||
|
|
||||||
.pushed-content.object-container {
|
.pushed-content.object-container {
|
||||||
.object-content {
|
.object-content {
|
||||||
padding-left: 4px !important;
|
padding-left: 4px !important;
|
||||||
|
|
||||||
.variable-row {
|
.variable-row {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
border-left: 0 !important;
|
border-left: 0 !important;
|
||||||
|
|
||||||
.variable-value div {
|
.variable-value div {
|
||||||
font-size: 10px !important;
|
font-size: ${({ $fontSize }) => $fontSize} !important;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.object-key-val {
|
.object-key-val {
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
|
|
@ -70,25 +75,35 @@ export const JsonWrapper = styled.div`
|
||||||
}
|
}
|
||||||
|
|
||||||
// disabling function collapse and neutral styling
|
// disabling function collapse and neutral styling
|
||||||
|
|
||||||
.rjv-function-container {
|
.rjv-function-container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
|
|
||||||
> span:first-child:before {
|
> span:first-child:before {
|
||||||
// In prod build, for some reason react-json-viewer
|
// In prod build, for some reason react-json-viewer
|
||||||
// misses adding this opening braces for function
|
// misses adding this opening braces for function
|
||||||
content: "(";
|
content: "(";
|
||||||
}
|
}
|
||||||
|
|
||||||
.function-collapsed {
|
.function-collapsed {
|
||||||
font-weight: normal !important;
|
font-weight: normal !important;
|
||||||
|
|
||||||
span:nth-child(1) {
|
span:nth-child(1) {
|
||||||
display: none; // hiding extra braces
|
display: none; // hiding extra braces
|
||||||
}
|
}
|
||||||
|
|
||||||
span:nth-child(2) {
|
span:nth-child(2) {
|
||||||
color: #393939 !important;
|
color: #393939 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div:has(.rjv-function-container) {
|
div:has(.rjv-function-container) {
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Container = styled.div<{ $fontSize: string; $iconSize: string }>`
|
||||||
|
${ReactJSONViewerOverrider}
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export enum Size {
|
||||||
|
SMALL = "small",
|
||||||
|
MEDIUM = "medium",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JSONViewerProps {
|
||||||
|
src: unknown;
|
||||||
|
size: Size;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user