diff --git a/src/hook/index.ts b/src/hook/index.ts index 1981a3c..94df74d 100644 --- a/src/hook/index.ts +++ b/src/hook/index.ts @@ -2,6 +2,7 @@ import { HOOK_NAME } from "../shared/constants"; import { getBricks, getBrickByUid, getBrickInfo } from "./traverse"; import { inspectElement, dismissInspections } from "./inspector"; import { emit } from "./emit"; +import { overrideProps } from "./overrideProps"; function injectHook(): void { if (Object.prototype.hasOwnProperty.call(window, HOOK_NAME)) { @@ -15,6 +16,8 @@ function injectHook(): void { inspectBrick: (uid: number) => inspectElement(getBrickByUid(uid)), dismissInspections, emit, + overrideProps: (uid: number, propertyName: string, value: any) => + overrideProps(getBrickByUid(uid), propertyName, value), }; Object.defineProperty(hook, "pageHasBricks", { diff --git a/src/hook/overrideProps.spec.ts b/src/hook/overrideProps.spec.ts new file mode 100644 index 0000000..c8562a3 --- /dev/null +++ b/src/hook/overrideProps.spec.ts @@ -0,0 +1,17 @@ +import { overrideProps } from "./overrideProps"; +import { BrickElement } from "../shared/interfaces"; + +describe("overrideProps", () => { + it("should work", () => { + overrideProps(undefined, "showCard", false); + const element = + { + tagName: "YOUR.awesome-brick", + } as BrickElement; + overrideProps(element, "showCard", false); + expect(element).toEqual({ + tagName: "YOUR.awesome-brick", + showCard: false, + }); + }); +}); diff --git a/src/hook/overrideProps.ts b/src/hook/overrideProps.ts new file mode 100644 index 0000000..2725bc7 --- /dev/null +++ b/src/hook/overrideProps.ts @@ -0,0 +1,11 @@ +import { BrickElement } from "../shared/interfaces"; + +export function overrideProps( + element: BrickElement, + propertyName: string, + value: any +): void { + if (element) { + element[propertyName] = value; + } +} diff --git a/src/panel/components/PropList.spec.tsx b/src/panel/components/PropList.spec.tsx index 2511bb5..129b08a 100644 --- a/src/panel/components/PropList.spec.tsx +++ b/src/panel/components/PropList.spec.tsx @@ -1,6 +1,6 @@ import React from "react"; import { shallow } from "enzyme"; -import { Icon } from "@blueprintjs/core"; +import { Icon, TextArea } from "@blueprintjs/core"; import { PropList, PropItem } from "./PropList"; import { PROP_DEHYDRATED } from "../../shared/constants"; @@ -76,10 +76,14 @@ describe("PropItem", () => { const wrapper = shallow(); expect(wrapper.hasClass("expanded")).toBe(false); expect(wrapper.find(Icon).prop("icon")).toBe("blank"); - wrapper.find(".prop-item-label").invoke("onClick")(null); + wrapper.find('span[data-testid="prop-name-wrapper"]').invoke("onClick")( + null + ); expect(wrapper.hasClass("expanded")).toBe(true); expect(wrapper.find(Icon).prop("icon")).toBe("blank"); - wrapper.find(".prop-item-label").invoke("onClick")(null); + wrapper.find('span[data-testid="prop-name-wrapper"]').invoke("onClick")( + null + ); expect(wrapper.hasClass("expanded")).toBe(false); expect(wrapper.find(Icon).prop("icon")).toBe("blank"); }); @@ -88,7 +92,9 @@ describe("PropItem", () => { const wrapper = shallow(); expect(wrapper.find(PropList).length).toBe(0); expect(wrapper.find(Icon).prop("icon")).toBe("caret-right"); - wrapper.find(".prop-item-label").invoke("onClick")(null); + wrapper.find('span[data-testid="prop-name-wrapper"]').invoke("onClick")( + null + ); expect(wrapper.find(PropList).prop("list")).toEqual([1]); expect(wrapper.find(Icon).prop("icon")).toBe("caret-down"); }); @@ -102,8 +108,51 @@ describe("PropItem", () => { it("should work for standalone with complex property value", () => { const wrapper = shallow(); expect(wrapper.find(Icon).prop("icon")).toBe("caret-right"); - wrapper.find(".prop-item-label").invoke("onClick")(null); + wrapper.find('span[data-testid="prop-name-wrapper"]').invoke("onClick")( + null + ); expect(wrapper.find(PropList).prop("list")).toEqual([1]); expect(wrapper.find(Icon).prop("icon")).toBe("caret-down"); + wrapper.find(".prop-value").invoke("onDoubleClick")({} as any); + expect(wrapper.find(TextArea).length).toBe(0); + }); + + it("should work when editable", () => { + const wrapper = shallow( + + ); + expect(wrapper.find(TextArea).length).toBe(0); + wrapper.find(".prop-value").invoke("onDoubleClick")({} as any); + expect(wrapper.find(TextArea).length).toBe(1); + wrapper.find(TextArea).invoke("onChange")({ + target: { + value: '"bad"', + }, + } as any); + expect(wrapper.find(TextArea).prop("value")).toBe('"bad"'); + wrapper.find(TextArea).invoke("onBlur")({} as any); + expect(wrapper.find(".bp3-intent-danger").length).toBe(0); + expect(wrapper.find(TextArea).length).toBe(0); + + wrapper.find(".prop-value").invoke("onDoubleClick")({} as any); + wrapper.find(TextArea).invoke("onChange")({ + target: { + value: "{a:errorJsonStringValue", + }, + } as any); + expect(wrapper.find(TextArea).prop("value")).toBe( + "{a:errorJsonStringValue" + ); + wrapper.find(TextArea).invoke("onBlur")({} as any); + expect(wrapper.find(".bp3-intent-danger").length).toBe(1); + expect(wrapper.find(TextArea).length).toBe(1); + wrapper.find(TextArea).invoke("onChange")({ + target: { + value: undefined, + }, + } as any); + expect(wrapper.find(TextArea).prop("value")).toBe(undefined); + wrapper.find(TextArea).invoke("onBlur")({} as any); + expect(wrapper.find(".bp3-intent-danger").length).toBe(0); }); }); diff --git a/src/panel/components/PropList.tsx b/src/panel/components/PropList.tsx index c0dae83..b6bff5b 100644 --- a/src/panel/components/PropList.tsx +++ b/src/panel/components/PropList.tsx @@ -1,15 +1,21 @@ import React from "react"; import classNames from "classnames"; -import { Icon } from "@blueprintjs/core"; +import { Icon, TextArea } from "@blueprintjs/core"; import { PROP_DEHYDRATED } from "../../shared/constants"; import { VariableDisplay } from "./VariableDisplay"; import { isDehydrated, isObject } from "../libs/utils"; interface PropListProps { list: any[] | Record; + editable?: boolean; + overrideProps?: (propName: string, propValue: string) => void; } -export function PropList({ list }: PropListProps): React.ReactElement { +export function PropList({ + list, + editable, + overrideProps, +}: PropListProps): React.ReactElement { const value = isDehydrated(list) ? list[PROP_DEHYDRATED].children || {} : list; @@ -17,10 +23,22 @@ export function PropList({ list }: PropListProps): React.ReactElement {
    {Array.isArray(value) ? value.map((item, index) => ( - + )) : Object.entries(value).map((entry) => ( - + ))}
); @@ -30,19 +48,54 @@ interface PropItemProps { propValue: any; propName?: string; standalone?: boolean; + editable?: boolean; + overrideProps?: (propName: string, propValue: string) => void; } export function PropItem({ propValue, propName, standalone, + editable, + overrideProps, }: PropItemProps): React.ReactElement { const [expanded, setExpanded] = React.useState(false); + const [editing, setEditing] = React.useState(false); + const [changeValue, setChangeValue] = React.useState( + JSON.stringify(propValue, null, 2) + ); + const [error, setError] = React.useState(false); const handleClick = React.useCallback(() => { setExpanded(!expanded); }, [expanded]); + const handleDoubleClick = React.useCallback(() => { + if (editable) { + setEditing(true); + } + }, [editable]); + + const handleBlur = () => { + try { + let result; + if (changeValue === "" || changeValue === undefined) { + result = undefined; + } else { + result = JSON.parse(changeValue); + } + overrideProps?.(propName, changeValue); + setEditing(false); + setError(false); + } catch (error) { + setError(true); + } + }; + + const handleChange = (v) => { + setChangeValue(v.target?.value); + }; + const hasChildren = isObject(propValue); return React.createElement( @@ -51,29 +104,47 @@ export function PropItem({ className: classNames("prop-item", { expanded }), }, <> -
- {(!standalone || hasChildren) && ( - + + {(!standalone || hasChildren) && ( + + )} + {!standalone && ( + <> + {propName} + :{" "} + + )} + + {editing && editable ? ( +