From 02a83fc40fefcb857ed20891f371f0e2aab9e6e2 Mon Sep 17 00:00:00 2001 From: jiangzhefeng Date: Sun, 2 Aug 2020 18:43:44 +0800 Subject: [PATCH 1/2] feat(): devtools transformmationsPanel and evaluationsPanel support edit --- src/content.ts | 16 +++- src/devtools.ts | 16 +++- .../components/EvaluationsPanel.spec.tsx | 56 ++++++++++--- src/panel/components/EvaluationsPanel.tsx | 39 +++++++++- src/panel/components/Layout.spec.tsx | 78 +++++++++++++++++++ src/panel/components/Layout.tsx | 44 ++++++++++- src/panel/components/PropList.tsx | 6 +- src/panel/components/PropsEditText.spec.tsx | 30 +++++++ src/panel/components/PropsEditText.tsx | 28 +++++++ .../components/TransformationsPanel.spec.tsx | 47 +++++++++-- src/panel/components/TransformationsPanel.tsx | 77 ++++++++++++------ src/panel/index.tsx | 7 +- src/shared/constants.ts | 3 + src/shared/interfaces.ts | 13 +++- 14 files changed, 405 insertions(+), 55 deletions(-) create mode 100644 src/panel/components/PropsEditText.spec.tsx create mode 100644 src/panel/components/PropsEditText.tsx diff --git a/src/content.ts b/src/content.ts index 343bd99..48e69b7 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,4 +1,9 @@ -import { MESSAGE_SOURCE_HOOK } from "./shared/constants"; +import { + MESSAGE_SOURCE_HOOK, + EVALUATION_EDIT, + TRANSFORMATION_EDIT, + MESSAGE_SOURCE_PANEL, +} from "./shared/constants"; function injectScript(file: string): void { const script = document.createElement("script"); @@ -16,6 +21,15 @@ function initPort(): void { port.onDisconnect.addListener(() => { port = null; }); + + port.onMessage.addListener((message) => { + if ( + message.source === MESSAGE_SOURCE_PANEL && + [EVALUATION_EDIT, TRANSFORMATION_EDIT].includes(message.payload?.type) + ) { + window.postMessage(message, "*"); + } + }); } if (document.contentType === "text/html") { diff --git a/src/devtools.ts b/src/devtools.ts index a654fa2..c6535b6 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -1,11 +1,13 @@ import { + EVALUATION_EDIT, HOOK_NAME, MESSAGE_SOURCE_DEVTOOLS, MESSAGE_SOURCE_HOOK, + MESSAGE_SOURCE_PANEL, + TRANSFORMATION_EDIT, } from "./shared/constants"; let panelCreated = false; - // Check to see if BrickNext has loaded once per second in case BrickNext is added // after page load const loadCheckInterval = setInterval(function () { @@ -46,6 +48,17 @@ function createPanelForBricks(): void { } } + function onPanelMessage(event: MessageEvent): void { + if ( + event?.data.source === MESSAGE_SOURCE_PANEL && + [EVALUATION_EDIT, TRANSFORMATION_EDIT].includes( + event.data.payload?.type + ) + ) { + port?.postMessage(event.data); + } + } + port.onMessage.addListener(onPortMessage); chrome.devtools.panels.create( @@ -55,6 +68,7 @@ function createPanelForBricks(): void { function (panel) { panel.onShown.addListener((win) => { panelWindow = win; + panelWindow.addEventListener("message", onPanelMessage); }); } ); diff --git a/src/panel/components/EvaluationsPanel.spec.tsx b/src/panel/components/EvaluationsPanel.spec.tsx index 332abea..993a3b7 100644 --- a/src/panel/components/EvaluationsPanel.spec.tsx +++ b/src/panel/components/EvaluationsPanel.spec.tsx @@ -11,20 +11,28 @@ const savePreserveLogs = jest.fn(); (useEvaluationsContext as jest.Mock).mockReturnValue({ evaluations: [ { - raw: "<% EVENT.detail %>", - result: "good", - context: { - EVENT: { - detail: "good", + detail: { + raw: "<% EVENT.detail %>", + result: "good", + context: { + EVENT: { + detail: "good", + }, + DATA: { + name: "easyops", + }, }, }, + id: 0, }, { - raw: "<% DATA.quality %>", - result: "better", - context: { - EVENT: { - detail: "better", + detail: { + raw: "<% DATA.quality %>", + result: "better", + context: { + EVENT: { + detail: "better", + }, }, }, }, @@ -82,4 +90,32 @@ describe("EvaluationsPanel", () => { "<% EVENT.detail %>" ); }); + + it("should post edited text message", () => { + const wrapper = shallow(); + const postMessage = jest.spyOn(window, "postMessage"); + + wrapper.find(PropItem).at(0).invoke("overrideProps")( + "propName", + "propValue", + "<% DATA.name %>" + ); + + expect(postMessage.mock.calls[0][0]).toEqual({ + payload: { + context: { + data: { + name: "easyops", + }, + event: { + detail: "good", + }, + }, + id: 0, + raw: "<% DATA.name %>", + type: "devtools-evaluation-edit", + }, + source: "brick-next-devtools-panel", + }); + }); }); diff --git a/src/panel/components/EvaluationsPanel.tsx b/src/panel/components/EvaluationsPanel.tsx index 14d3d4a..aa38c68 100644 --- a/src/panel/components/EvaluationsPanel.tsx +++ b/src/panel/components/EvaluationsPanel.tsx @@ -10,6 +10,8 @@ import classNames from "classnames"; import { PanelSelector } from "./PanelSelector"; import { useEvaluationsContext } from "../libs/EvaluationsContext"; import { PropList, PropItem } from "./PropList"; +import { EVALUATION_EDIT, MESSAGE_SOURCE_PANEL } from "../../shared/constants"; +import { Evaluation } from "../../shared/interfaces"; export function EvaluationsPanel(): React.ReactElement { const { @@ -37,7 +39,7 @@ export function EvaluationsPanel(): React.ReactElement { return evaluations; } return evaluations.filter((item) => - item.raw.toLocaleLowerCase().includes(q.toLocaleLowerCase()) + item.detail?.raw.toLocaleLowerCase().includes(q.toLocaleLowerCase()) ); }, [evaluations, q]); @@ -55,6 +57,28 @@ export function EvaluationsPanel(): React.ReactElement { [savePreserveLogs] ); + const handleEvaluations = (item: Evaluation, value: string) => { + const { + context: { DATA, EVENT }, + } = item.detail; + + window.postMessage( + { + source: MESSAGE_SOURCE_PANEL, + payload: { + type: EVALUATION_EDIT, + context: { + data: DATA, + event: EVENT, + }, + id: item.id, + raw: value, + }, + }, + "*" + ); + }; + return (
( - + + handleEvaluations(item, value) + } + /> - + - + ))} diff --git a/src/panel/components/Layout.spec.tsx b/src/panel/components/Layout.spec.tsx index 1d724c5..7fd6411 100644 --- a/src/panel/components/Layout.spec.tsx +++ b/src/panel/components/Layout.spec.tsx @@ -116,6 +116,45 @@ describe("Layout", () => { wrapper.unmount(); }); + it("should work for edit evaluations", async () => { + storageGetItem.mockReturnValue("Evaluations"); + const wrapper = mount(); + await act(async () => { + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: "evaluation", + payload: { + id: 0, + result: "good", + }, + }, + }, + location.origin + ); + await new Promise((resolve) => setTimeout(resolve)); + }); + await act(async () => { + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: "re-evaluation", + payload: { + id: 0, + result: "new", + }, + }, + }, + location.origin + ); + await new Promise((resolve) => setTimeout(resolve)); + }); + expect(wrapper.text()).toBe("EvaluationsPanel (1)"); + wrapper.unmount(); + }); + it("should work for new transformations", async () => { storageGetItem.mockReturnValue("Transformations"); const wrapper = mount(); @@ -136,6 +175,45 @@ describe("Layout", () => { wrapper.unmount(); }); + it("should work for edit transformations", async () => { + storageGetItem.mockReturnValue("Transformations"); + const wrapper = mount(); + await act(async () => { + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: "transformation", + payload: { + result: "good", + id: 0, + }, + }, + }, + location.origin + ); + await new Promise((resolve) => setTimeout(resolve)); + }); + await act(async () => { + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: "re-transformation", + payload: { + id: 0, + result: "new", + }, + }, + }, + location.origin + ); + await new Promise((resolve) => setTimeout(resolve)); + }); + expect(wrapper.text()).toBe("TransformationsPanel (1)"); + wrapper.unmount(); + }); + it.each([ [true, 1], [false, 0], diff --git a/src/panel/components/Layout.tsx b/src/panel/components/Layout.tsx index 75a47b0..3ed01b1 100644 --- a/src/panel/components/Layout.tsx +++ b/src/panel/components/Layout.tsx @@ -16,6 +16,11 @@ import { TransformationsContext } from "../libs/TransformationsContext"; import { Storage } from "../libs/Storage"; import { hydrate } from "../libs/hydrate"; +let uniqueIdCounter = 0; +function getUniqueId(): number { + return (uniqueIdCounter += 1); +} + export function Layout(): React.ReactElement { const [selectedPanel, setSelectedPanel] = React.useState( Storage.getItem("selectedPanel") ?? "Bricks" @@ -33,7 +38,26 @@ export function Layout(): React.ReactElement { event.data?.source === MESSAGE_SOURCE_HOOK && ((data = event.data.payload), data?.type === "evaluation") ) { - setEvaluations((prev) => prev.concat(hydrate(data.payload, data.repo))); + setEvaluations((prev) => + prev.concat({ + detail: hydrate(data.payload, data.repo), + id: getUniqueId(), + }) + ); + } + + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + ((data = event.data.payload), data?.type === "re-evaluation") + ) { + const value = hydrate(data.payload, data.repo); + const { id, ...changeData } = value; + setEvaluations((prev) => { + const selected = prev.find((item) => item.id === id); + selected && Object.assign(selected.detail, changeData); + + return [...prev]; + }); } } window.addEventListener("message", onMessage); @@ -48,9 +72,25 @@ export function Layout(): React.ReactElement { ((data = event.data.payload), data?.type === "transformation") ) { setTransformations((prev) => - prev.concat(hydrate(data.payload, data.repo)) + prev.concat({ + detail: hydrate(data.payload, data.repo), + id: getUniqueId(), + }) ); } + + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + ((data = event.data.payload), data?.type === "re-transformation") + ) { + const value = hydrate(data.payload, data.repo); + const { id, ...changeData } = value; + setTransformations((prev) => { + const selected = prev.find((item) => item.id === id); + selected && Object.assign(selected.detail, changeData); + return [...prev]; + }); + } } window.addEventListener("message", onMessage); return (): void => window.removeEventListener("message", onMessage); diff --git a/src/panel/components/PropList.tsx b/src/panel/components/PropList.tsx index b6bff5b..e46ddbc 100644 --- a/src/panel/components/PropList.tsx +++ b/src/panel/components/PropList.tsx @@ -8,7 +8,7 @@ import { isDehydrated, isObject } from "../libs/utils"; interface PropListProps { list: any[] | Record; editable?: boolean; - overrideProps?: (propName: string, propValue: string) => void; + overrideProps?: (propName: string, propValue: string, result?: any) => void; } export function PropList({ @@ -49,7 +49,7 @@ interface PropItemProps { propName?: string; standalone?: boolean; editable?: boolean; - overrideProps?: (propName: string, propValue: string) => void; + overrideProps?: (propName: string, propValue: string, result?: any) => void; } export function PropItem({ @@ -84,7 +84,7 @@ export function PropItem({ } else { result = JSON.parse(changeValue); } - overrideProps?.(propName, changeValue); + overrideProps?.(propName, changeValue, result); setEditing(false); setError(false); } catch (error) { diff --git a/src/panel/components/PropsEditText.spec.tsx b/src/panel/components/PropsEditText.spec.tsx new file mode 100644 index 0000000..74c2c50 --- /dev/null +++ b/src/panel/components/PropsEditText.spec.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { shallow, mount } from "enzyme"; +import { PropsEditText } from "./PropsEditText"; + +describe("PropsEditText", () => { + it("should work", () => { + const props = { + value: "<% DATA.message %>", + onConfirm: jest.fn(), + }; + const wrapper = shallow(); + + wrapper.invoke("onChange")("DATA.age"); + expect(wrapper.prop("value")).toEqual("DATA.age"); + }); + + it("should update value", () => { + const props = { + value: "<% DATA.message %>", + onConfirm: jest.fn(), + }; + const wrapper = mount(); + + wrapper.setProps({ + value: "<% DATA.name %>", + }); + + expect(wrapper.prop("value")).toEqual("<% DATA.name %>"); + }); +}); diff --git a/src/panel/components/PropsEditText.tsx b/src/panel/components/PropsEditText.tsx new file mode 100644 index 0000000..8a740f2 --- /dev/null +++ b/src/panel/components/PropsEditText.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { EditableText, IEditableTextProps } from "@blueprintjs/core"; + +interface EditablePropsItemProps extends IEditableTextProps { + onConfirm: (value: string) => void; + value: string; +} + +export function PropsEditText(props: EditablePropsItemProps) { + const { value, onConfirm, ...textProps } = props; + const [text, setText] = React.useState(value); + + React.useEffect(() => { + setText(value); + }, [value]); + + return ( + setText(newValue)} + /> + ); +} diff --git a/src/panel/components/TransformationsPanel.spec.tsx b/src/panel/components/TransformationsPanel.spec.tsx index 070780c..63c1e3a 100644 --- a/src/panel/components/TransformationsPanel.spec.tsx +++ b/src/panel/components/TransformationsPanel.spec.tsx @@ -3,6 +3,7 @@ import { shallow } from "enzyme"; import { Button, Switch } from "@blueprintjs/core"; import { TransformationsPanel } from "./TransformationsPanel"; import { useTransformationsContext } from "../libs/TransformationsContext"; +import { PropItem } from "./PropList"; jest.mock("../libs/TransformationsContext"); const setTransformations = jest.fn(); @@ -10,15 +11,18 @@ const savePreserveLogs = jest.fn(); (useTransformationsContext as jest.Mock).mockReturnValue({ transformations: [ { - transform: "quality", - result: { - quality: "good", - }, - data: "good", - options: { - from: "list", - mapArray: undefined, + detail: { + transform: "quality", + result: { + quality: "good", + }, + data: "good", + options: { + from: "list", + mapArray: undefined, + }, }, + id: 1, }, ], setTransformations, @@ -61,4 +65,31 @@ describe("TransformationsPanel", () => { wrapper.find(Button).invoke("onClick")(null); expect(setTransformations).toBeCalled(); }); + + it("should post edited transformation message", () => { + const wrapper = shallow(); + const postMessage = jest.spyOn(window, "postMessage"); + + wrapper.find(PropItem).at(0).invoke("overrideProps")( + "propName", + "propValue", + { name: "<% DATA.name %>" } + ); + + expect(postMessage.mock.calls[0][0]).toEqual({ + payload: { + data: "good", + id: 1, + options: { + from: "list", + mapArray: undefined, + }, + transform: { + name: "<% DATA.name %>", + }, + type: "devtools-transformation-edit", + }, + source: "brick-next-devtools-panel", + }); + }); }); diff --git a/src/panel/components/TransformationsPanel.tsx b/src/panel/components/TransformationsPanel.tsx index c1df036..f3b8819 100644 --- a/src/panel/components/TransformationsPanel.tsx +++ b/src/panel/components/TransformationsPanel.tsx @@ -4,6 +4,11 @@ import classNames from "classnames"; import { PanelSelector } from "./PanelSelector"; import { useTransformationsContext } from "../libs/TransformationsContext"; import { PropItem } from "./PropList"; +import { + TRANSFORMATION_EDIT, + MESSAGE_SOURCE_PANEL, +} from "../../shared/constants"; +import { Transformation } from "../../shared/interfaces"; export function TransformationsPanel(): React.ReactElement { const { @@ -32,6 +37,23 @@ export function TransformationsPanel(): React.ReactElement { [savePreserveLogs] ); + const handleTransform = (item: Transformation, value: any) => { + const { options, data } = item.detail; + window.postMessage( + { + source: MESSAGE_SOURCE_PANEL, + payload: { + type: TRANSFORMATION_EDIT, + options, + data, + id: item.id, + transform: value, + }, + }, + "*" + ); + }; + return (
- {transformations.map((item, key) => ( - - - - - - - - - - - - entry[1] !== undefined - ) - )} - standalone - /> - - - ))} + {transformations.map((item, key) => { + return ( + + + + handleTransform(item, value) + } + propValue={item.detail?.transform} + standalone + editable + /> + + + + + + + + + entry[1] !== undefined + ) + )} + standalone + /> + + + ); + })}
diff --git a/src/panel/index.tsx b/src/panel/index.tsx index fe6211b..76ec4e6 100644 --- a/src/panel/index.tsx +++ b/src/panel/index.tsx @@ -1,7 +1,10 @@ import React from "react"; import ReactDOM from "react-dom"; import { Layout } from "./components/Layout"; -import { MESSAGE_SOURCE_DEVTOOLS } from "../shared/constants"; +import { + MESSAGE_SOURCE_DEVTOOLS, + MESSAGE_SOURCE_PANEL, +} from "../shared/constants"; import "normalize.css"; import "@blueprintjs/core/lib/css/blueprint.css"; @@ -13,7 +16,7 @@ root.id = "root"; document.body.appendChild(root); ReactDOM.render(, root); - +// istanbul ignore next window.addEventListener("message", function onMessage( event: MessageEvent ): void { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 39624f5..0673cd8 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,4 +1,7 @@ export const HOOK_NAME = "__BRICK_NEXT_DEVTOOLS_HOOK__"; export const MESSAGE_SOURCE_DEVTOOLS = "brick-next-devtools"; export const MESSAGE_SOURCE_HOOK = "brick-next-devtools-hook"; +export const MESSAGE_SOURCE_PANEL = "brick-next-devtools-panel"; export const PROP_DEHYDRATED = "$$brickNextDevtoolsDehydrated"; +export const EVALUATION_EDIT = "devtools-evaluation-edit"; +export const TRANSFORMATION_EDIT = "devtools-transformation-edit"; diff --git a/src/shared/interfaces.ts b/src/shared/interfaces.ts index f796862..734fae3 100644 --- a/src/shared/interfaces.ts +++ b/src/shared/interfaces.ts @@ -26,6 +26,7 @@ export interface RuntimeBrick { export interface BrickElement extends HTMLElement { $$typeof?: "brick" | "provider" | "custom-template" | "native" | "invalid"; + // eslint-disable-next-line @typescript-eslint/ban-types $$eventListeners?: [string, Function, any?][]; } @@ -50,13 +51,23 @@ export interface BrickInfo { export type BrowserTheme = "dark" | "light"; -export interface Evaluation { +export interface EvaluationDetail { raw: string; context: Record; result: any; } +export interface Evaluation { + detail: EvaluationDetail; + id?: number; +} + export interface Transformation { + detail: TransformationDetail; + id?: number; +} + +export interface TransformationDetail { transform: any; data: any; options: { From d4211e112b071d8ff5e018e1827f65ffbb35786d Mon Sep 17 00:00:00 2001 From: jiangzhefeng Date: Wed, 5 Aug 2020 15:46:41 +0800 Subject: [PATCH 2/2] fix(): remove unsed component --- src/panel/components/PropsEditText.spec.tsx | 30 --------------------- src/panel/components/PropsEditText.tsx | 28 ------------------- 2 files changed, 58 deletions(-) delete mode 100644 src/panel/components/PropsEditText.spec.tsx delete mode 100644 src/panel/components/PropsEditText.tsx diff --git a/src/panel/components/PropsEditText.spec.tsx b/src/panel/components/PropsEditText.spec.tsx deleted file mode 100644 index 74c2c50..0000000 --- a/src/panel/components/PropsEditText.spec.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { shallow, mount } from "enzyme"; -import { PropsEditText } from "./PropsEditText"; - -describe("PropsEditText", () => { - it("should work", () => { - const props = { - value: "<% DATA.message %>", - onConfirm: jest.fn(), - }; - const wrapper = shallow(); - - wrapper.invoke("onChange")("DATA.age"); - expect(wrapper.prop("value")).toEqual("DATA.age"); - }); - - it("should update value", () => { - const props = { - value: "<% DATA.message %>", - onConfirm: jest.fn(), - }; - const wrapper = mount(); - - wrapper.setProps({ - value: "<% DATA.name %>", - }); - - expect(wrapper.prop("value")).toEqual("<% DATA.name %>"); - }); -}); diff --git a/src/panel/components/PropsEditText.tsx b/src/panel/components/PropsEditText.tsx deleted file mode 100644 index 8a740f2..0000000 --- a/src/panel/components/PropsEditText.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { EditableText, IEditableTextProps } from "@blueprintjs/core"; - -interface EditablePropsItemProps extends IEditableTextProps { - onConfirm: (value: string) => void; - value: string; -} - -export function PropsEditText(props: EditablePropsItemProps) { - const { value, onConfirm, ...textProps } = props; - const [text, setText] = React.useState(value); - - React.useEffect(() => { - setText(value); - }, [value]); - - return ( - setText(newValue)} - /> - ); -}