diff --git a/src/components/EvaluationsPanel.tsx b/src/components/EvaluationsPanel.tsx index 9f232c4..deed726 100644 --- a/src/components/EvaluationsPanel.tsx +++ b/src/components/EvaluationsPanel.tsx @@ -1,68 +1,62 @@ import React from "react"; +import { Button, Tooltip, ButtonGroup } from "@blueprintjs/core"; import { PanelSelector } from "./PanelSelector"; import { PropTree, StandaloneValue } from "./PropTree"; +import { useEvaluationsContext } from "../libs/EvaluationsContext"; export function EvaluationsPanel(): React.ReactElement { + const { evaluations, setEvaluations } = useEvaluationsContext(); + + const handleClear = React.useCallback(() => { + setEvaluations([]); + }, [setEvaluations]); + return (
+ + +
- - - - - - - - - - - - - - - - - - - - -
ExpressionContextResult
- - {/* {'"'} - {'<% EVENT.detail %>'} - {'"'} */} - - - - -
- - - - - -
+
+ + + + + + + + + + {evaluations.map((item, key) => ( + + + + + + ))} + +
ExpressionContextResult
+ + + + + +
+
); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 0e3e472..7c0415b 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -5,9 +5,52 @@ import { BricksPanel } from "./BricksPanel"; import { SelectedPanelContext } from "../libs/SelectedPanelContext"; import { EvaluationsPanel } from "./EvaluationsPanel"; import { TransformationsPanel } from "./TransformationsPanel"; +import { Evaluation, Transformation, RepoWrapped } from "../libs/interfaces"; +import { EvaluationsContext } from "../libs/EvaluationsContext"; +import { MESSAGE_SOURCE_HOOK } from "../shared"; +import { TransformationsContext } from "../libs/TransformationsContext"; +import { Storage } from "../libs/Storage"; export function Layout(): React.ReactElement { - const [selectedPanel, setSelectedPanel] = React.useState("Bricks"); + const [selectedPanel, setSelectedPanel] = React.useState( + Storage.getItem("selectedPanel") ?? "Bricks" + ); + const [evaluations, setEvaluations] = React.useState< + RepoWrapped[] + >([]); + const [transformations, setTransformations] = React.useState< + RepoWrapped[] + >([]); + + React.useEffect(() => { + function onMessage(event: MessageEvent): void { + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + event.data.payload?.type === "evaluation" + ) { + setEvaluations(evaluations.concat(event.data.payload)); + } + } + window.addEventListener("message", onMessage); + return (): void => window.removeEventListener("message", onMessage); + }, [evaluations]); + + React.useEffect(() => { + function onMessage(event: MessageEvent): void { + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + event.data.payload?.type === "transformation" + ) { + setTransformations(transformations.concat(event.data.payload)); + } + } + window.addEventListener("message", onMessage); + return (): void => window.removeEventListener("message", onMessage); + }, [transformations]); + + React.useEffect(() => { + Storage.setItem("selectedPanel", selectedPanel); + }, [selectedPanel]); const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : "light"; @@ -22,9 +65,17 @@ export function Layout(): React.ReactElement { value={{ selectedPanel, setSelectedPanel }} > {selectedPanel === "Evaluations" ? ( - + + + ) : selectedPanel === "Transformations" ? ( - + + + ) : ( )} diff --git a/src/components/PropTree.tsx b/src/components/PropTree.tsx index b083395..1974606 100644 --- a/src/components/PropTree.tsx +++ b/src/components/PropTree.tsx @@ -1,6 +1,13 @@ import React from "react"; import classNames from "classnames"; import { Icon } from "@blueprintjs/core"; +import { TransferSerializedWrapper } from "../libs/interfaces"; + +function isTransferSerializedWrapper( + value: any +): value is TransferSerializedWrapper { + return !!value?.$$brickNextDevtoolsSerialized; +} function isObject(value: any): value is Record { return typeof value === "object" && value; @@ -8,17 +15,31 @@ function isObject(value: any): value is Record { interface PropTreeProps { properties: any[] | Record; + repo?: any[]; } -export function PropTree({ properties }: PropTreeProps): React.ReactElement { +export function PropTree({ + properties, + repo, +}: PropTreeProps): React.ReactElement { return (
    {Array.isArray(properties) ? properties.map((item, index) => ( - + )) : Object.entries(properties).map((entry) => ( - + ))}
); @@ -27,11 +48,13 @@ export function PropTree({ properties }: PropTreeProps): React.ReactElement { interface PropItemProps { propName: string; propValue: any; + repo?: any[]; } export function PropItem({ propName, propValue, + repo, }: PropItemProps): React.ReactElement { const [expanded, setExpanded] = React.useState(false); @@ -39,7 +62,8 @@ export function PropItem({ setExpanded(!expanded); }, [expanded]); - const hasChildren = isObject(propValue); + const hasChildren = + isObject(propValue) && !isTransferSerializedWrapper(propValue); return (
  • @@ -56,22 +80,30 @@ export function PropItem({ {propName} :{" "} - + - {isObject(propValue) && expanded && } + {hasChildren && expanded && ( + + )}
  • ); } -export function StandaloneValue({ value }: { value: any }): React.ReactElement { +export function StandaloneValue({ + value, + repo, +}: { + value: any; + repo?: any[]; +}): React.ReactElement { const [expanded, setExpanded] = React.useState(false); const handleClick = React.useCallback(() => { setExpanded(!expanded); }, [expanded]); - const hasChildren = isObject(value); + const hasChildren = isObject(value) && !isTransferSerializedWrapper(value); return (
    @@ -83,10 +115,10 @@ export function StandaloneValue({ value }: { value: any }): React.ReactElement { )} - +
    - {isObject(value) && expanded && } + {hasChildren && expanded && } ); } @@ -94,12 +126,39 @@ export function StandaloneValue({ value }: { value: any }): React.ReactElement { interface ValueStringifyProps { value: any; expanded?: boolean; + repo?: any[]; } export function ValueStringify({ value, expanded, + repo, }: ValueStringifyProps): React.ReactElement { + if (isTransferSerializedWrapper(value)) { + const serialized = value.$$brickNextDevtoolsSerialized; + switch (serialized.type) { + case "object": + return ( + + {serialized.constructorName} + {" {}"} + + ); + case "function": + return ƒ; + case "ref": + return ( + + ); + default: + return Unexpected; + } + } + if (Array.isArray(value)) { if (expanded) { return Array({value.length}); @@ -111,7 +170,7 @@ export function ValueStringify({ [ {value.map((item, index, array) => ( - + {index < array.length - 1 && ", "} ))} @@ -122,7 +181,7 @@ export function ValueStringify({ if (isObject(value)) { if (expanded) { - return null; + return Object; } return ( @@ -130,7 +189,11 @@ export function ValueStringify({ {"{"} {Object.entries(value).map((entry, index, array) => ( - + {index < array.length - 1 && ", "} ))} @@ -143,7 +206,9 @@ export function ValueStringify({ return ( <> {'"'} - {value} + + {value} + {'"'} ); @@ -166,11 +231,32 @@ export function ValueStringify({ interface ValueItemStringifyProps { item: any; + repo?: any[]; } export function ValueItemStringify({ item, + repo, }: ValueItemStringifyProps): React.ReactElement { + if (isTransferSerializedWrapper(item)) { + const serialized = item.$$brickNextDevtoolsSerialized; + switch (serialized.type) { + case "object": + return ( + + {serialized.constructorName} + {" {}"} + + ); + case "function": + return ƒ; + case "ref": + return ; + default: + return Unexpected; + } + } + if (Array.isArray(item)) { return {`Array(${item.length})`}; } @@ -201,18 +287,20 @@ export function ValueItemStringify({ interface ObjectPropStringifyProps { propName: string; propValue: any; + repo?: any[]; } export function ObjectPropStringify({ propName, propValue, + repo, }: ObjectPropStringifyProps): React.ReactElement { return ( {propName} :{" "} - + ); diff --git a/src/components/PropView.tsx b/src/components/PropView.tsx index 81088ba..a90b2f9 100644 --- a/src/components/PropView.tsx +++ b/src/components/PropView.tsx @@ -5,13 +5,6 @@ import { HOOK_NAME } from "../shared"; import { PropTree } from "./PropTree"; import { BrickInfo } from "../libs/interfaces"; -// `Function`s can't be passed through `chrome.devtools.inspectedWindow.eval`. -// Use a noop function to mock the event listener. -// istanbul ignore next -function noop(): void { - // noop -} - export function PropView(): React.ReactElement { const { selectedBrick } = useSelectedBrickContext(); const [brickInfo, setBrickInfo] = React.useState({}); @@ -23,7 +16,7 @@ export function PropView(): React.ReactElement { function (result: BrickInfo, error) { // istanbul ignore if if (error) { - console.error("getBrickInfo()", error); + console.error("getBrickInfo()", error, result); } setBrickInfo(result); @@ -49,9 +42,7 @@ export function PropView(): React.ReactElement { events
    - [item, noop])} - /> +
    diff --git a/src/components/TransformationsPanel.tsx b/src/components/TransformationsPanel.tsx index 4dae1a7..a516c1f 100644 --- a/src/components/TransformationsPanel.tsx +++ b/src/components/TransformationsPanel.tsx @@ -1,13 +1,58 @@ import React from "react"; +import { Button, Tooltip, ButtonGroup } from "@blueprintjs/core"; import { PanelSelector } from "./PanelSelector"; +import { StandaloneValue } from "./PropTree"; +import { useTransformationsContext } from "../libs/TransformationsContext"; export function TransformationsPanel(): React.ReactElement { + const { transformations, setTransformations } = useTransformationsContext(); + + const handleClear = React.useCallback(() => { + setTransformations([]); + }, [setTransformations]); + return (
    + + +
    +
    +
    + + + + + + + + + + + {transformations.map((item, key) => ( + + + + + + + ))} + +
    TransformDataResultOptions
    + + + + + + + +
    +
    -
    TransformationsPanel
    ); } diff --git a/src/hook.ts b/src/hook.ts index 8108665..cf92f58 100644 --- a/src/hook.ts +++ b/src/hook.ts @@ -60,9 +60,9 @@ function injectHook(): void { function getBrickInfo(uid: number): BrickInfo { let properties: Record = {}; - let events: string[]; + let events: [string, any][]; const element = uidToBrick.get(uid); - if (["brick", "provider", "custom-element"].includes(element?.$$typeof)) { + if (["brick", "provider", "custom-template"].includes(element?.$$typeof)) { const props: string[] = (element.constructor as BrickElementConstructor) ._dev_only_definedProperties || []; @@ -75,7 +75,7 @@ function injectHook(): void { ]) ); events = Array.isArray(element.$$eventListeners) - ? element.$$eventListeners.map((item) => item[0]) + ? element.$$eventListeners.map((item) => [item[0], item[2]]) : []; } return { properties, events }; @@ -184,14 +184,83 @@ function injectHook(): void { }); } + function processPayload(payload: any, repo: any[], ref = new WeakMap()): any { + if (ref.has(payload)) { + const processed = ref.get(payload); + let repoIndex = repo.indexOf(processed); + if (repoIndex === -1) { + repoIndex = repo.length; + repo.push(processed); + } + return { + $$brickNextDevtoolsSerialized: { + type: "ref", + ref: repoIndex, + }, + }; + // repo[] = ref.get(payload); + // return ref.get(payload); + // return processed; + } + if (Array.isArray(payload)) { + const processed: any[] = []; + ref.set(payload, processed); + payload.forEach((item) => { + processed.push(processPayload(item, repo, ref)); + }); + return processed; + } + if (typeof payload === "object" && payload) { + if (payload.constructor === Object) { + const processed: Record = {}; + ref.set(payload, processed); + Object.entries(payload).forEach((entry) => { + processed[entry[0]] = processPayload(entry[1], repo, ref); + }); + return processed; + } + const processed = { + $$brickNextDevtoolsSerialized: { + type: "object", + constructorName: payload.constructor?.name || "UnknownObject", + cleanedValue: {}, + }, + }; + ref.set(payload, processed); + return processed; + } + if (typeof payload === "function") { + const processed = { + $$brickNextDevtoolsSerialized: { + type: "function", + constructorName: payload.constructor?.name || "UnknownObject", + cleanedValue: {}, + }, + }; + ref.set(payload, processed); + return processed; + } + return payload; + } + function emit(payload: any): void { - window.postMessage( - { - source: MESSAGE_SOURCE_HOOK, - payload, - }, - "*" - ); + try { + const repo: any[] = []; + const processed = processPayload(payload.payload, repo); + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + ...payload, + payload: processed, + repo, + }, + }, + "*" + ); + } catch (error) { + console.warn("brick-next-devtools emit failed:", error); + } } const hook = { diff --git a/src/libs/EvaluationsContext.ts b/src/libs/EvaluationsContext.ts new file mode 100644 index 0000000..b47f4d9 --- /dev/null +++ b/src/libs/EvaluationsContext.ts @@ -0,0 +1,13 @@ +import React from "react"; +import { Evaluation, RepoWrapped } from "./interfaces"; + +export interface ContextOfEvaluations { + evaluations?: RepoWrapped[]; + setEvaluations?: React.Dispatch[]>; +} + +export const EvaluationsContext = React.createContext({}); + +// istanbul ignore next +export const useEvaluationsContext = (): ContextOfEvaluations => + React.useContext(EvaluationsContext); diff --git a/src/libs/Storage.ts b/src/libs/Storage.ts new file mode 100644 index 0000000..b4d9531 --- /dev/null +++ b/src/libs/Storage.ts @@ -0,0 +1,25 @@ +function setItem(key: string, value: any): void { + sessionStorage.setItem(key, JSON.stringify(value)); +} + +function getItem(key: string): any { + const value = sessionStorage.getItem(key); + if (value === null) { + return null; + } + try { + return JSON.parse(value); + } catch (e) { + return null; + } +} + +function clear(): void { + sessionStorage.clear(); +} + +export const Storage = { + setItem, + getItem, + clear, +}; diff --git a/src/libs/TransformationsContext.ts b/src/libs/TransformationsContext.ts new file mode 100644 index 0000000..42f8374 --- /dev/null +++ b/src/libs/TransformationsContext.ts @@ -0,0 +1,15 @@ +import React from "react"; +import { Transformation, RepoWrapped } from "./interfaces"; + +export interface ContextOfTransformations { + transformations?: RepoWrapped[]; + setTransformations?: React.Dispatch[]>; +} + +export const TransformationsContext = React.createContext< + ContextOfTransformations +>({}); + +// istanbul ignore next +export const useTransformationsContext = (): ContextOfTransformations => + React.useContext(TransformationsContext); diff --git a/src/libs/interfaces.ts b/src/libs/interfaces.ts index 0a007df..800c13d 100644 --- a/src/libs/interfaces.ts +++ b/src/libs/interfaces.ts @@ -24,7 +24,7 @@ export interface RuntimeBrick { export interface BrickElement extends HTMLElement { $$typeof?: "brick" | "custom-template"; - $$eventListeners: [string, Function][]; + $$eventListeners: [string, Function, any][]; } export interface BrickElementConstructor extends Function { @@ -37,7 +37,39 @@ export interface MountPointElement extends HTMLElement { export interface BrickInfo { properties?: Record; - events?: string[]; + events?: [string, any][]; } export type BrowserTheme = "dark" | "light"; + +export interface RepoWrapped { + payload: T; + repo: any[]; +} + +export interface Evaluation { + raw: string; + context: Record; + result: any; +} + +export interface Transformation { + transform: any; + data: any; + options: { + from: string | string[]; + mapArray: boolean | "auto"; + }; + result: any; +} + +export interface TransferSerializedWrapper { + $$brickNextDevtoolsSerialized: TransferSerialized; +} + +export interface TransferSerialized { + type: "function" | "object" | "number" | "undefined" | "ref"; + constructorName?: string; + ref?: number; + cleanedValue?: any; +} diff --git a/src/style.css b/src/style.css index c81021b..091135e 100644 --- a/src/style.css +++ b/src/style.css @@ -131,6 +131,7 @@ body { } .prop-item-label > .bp3-icon { + /* margin-top: -1px; */ vertical-align: middle; color: var(--caret-color); }