diff --git a/README.md b/README.md index 67d8487..135fd57 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ npm install npm start ``` -Follow [Official Tutorial](https://developer.chrome.com/extensions/getstarted), when loading unpacked extension, choose the `brick-next-devtools/extension` directory. +Follow [official tutorial](https://developer.chrome.com/extensions/getstarted), when loading unpacked extension, choose the `brick-next-devtools/extension` directory. ## Testing @@ -32,7 +32,7 @@ npm test src/some-file.spec.ts -- --watch To test a specified file and collect coverage from related files only: ``` -npm test src/some-file.spec.ts -- no-collect-coverage-from +npm test src/some-file.spec.ts -- --no-collect-coverage-from ``` ## Publish diff --git a/extension/manifest.json b/extension/manifest.json index 9b3f9c7..dbeb96c 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -30,7 +30,9 @@ "build/devtools.js", "build/hook.js", "build/panel.html", + "build/panel.css", "build/panel.js", + "build/panel.js.map", "build/icons-16.eot", "build/icons-16.ttf", "build/icons-16.woff", diff --git a/package-lock.json b/package-lock.json index 7de534e..d8eb9a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2345,12 +2345,6 @@ "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", "dev": true }, - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, "ansi-escapes": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", @@ -4179,64 +4173,6 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true }, - "copy-webpack-plugin": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.1.tgz", - "integrity": "sha512-P15M5ZC8dyCjQHWwd4Ia/dm0SgVvZJMYeykVIVYXbGyqO4dWB5oyPHp9i7wjwo5LhtlhKbiBCdS2NvM07Wlybg==", - "dev": true, - "requires": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - } - } - }, "core-js-compat": { "version": "3.6.4", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.4.tgz", @@ -4726,32 +4662,6 @@ "randombytes": "^2.0.0" } }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - }, - "dependencies": { - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, "discontinuous-range": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", @@ -6256,40 +6166,6 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } - } - }, "graceful-fs": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", @@ -7053,6 +6929,12 @@ "path-is-inside": "^1.0.2" } }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -9352,6 +9234,18 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + } + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -9648,6 +9542,18 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha1-LMDWazHqIwNkWENuNiDYWVTGbDw=", + "dev": true, + "requires": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + } + }, "normalize.css": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", @@ -10339,6 +10245,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, "prettier": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.3.tgz", @@ -10526,6 +10438,16 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", + "dev": true, + "requires": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -11630,6 +11552,15 @@ } } }, + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -11844,6 +11775,12 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -12014,28 +11951,6 @@ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", "dev": true }, - "style-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.1.3.tgz", - "integrity": "sha512-rlkH7X/22yuwFYK357fMN/BxYOorfnfq0eD7+vqlemSK4wEcejFF1dg4zxP0euBW8NrYx2WZzZ8PPFevr7D+Kw==", - "dev": true, - "requires": { - "loader-utils": "^1.2.3", - "schema-utils": "^2.6.4" - }, - "dependencies": { - "schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", - "dev": true, - "requires": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - } - } - } - }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -13098,16 +13013,6 @@ } } }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, "webpack-sources": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", diff --git a/package.json b/package.json index 7d4e5eb..c7b29fb 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "babel-jest": "^25.2.6", "babel-loader": "^8.1.0", "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^5.1.1", "coveralls": "^3.0.11", "cross-env": "^7.0.2", "css-loader": "^3.4.2", @@ -50,8 +49,8 @@ "husky": "^4.2.3", "jest": "^25.2.7", "lint-staged": "^10.1.2", + "mini-css-extract-plugin": "^0.9.0", "prettier": "^2.0.3", - "style-loader": "^1.1.3", "typescript": "^3.8.3", "webpack": "^4.42.1", "webpack-cli": "^3.3.11" diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx deleted file mode 100644 index a8d6479..0000000 --- a/src/components/Layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -import classNames from "classnames"; -import { TreeWrapper } from "./TreeWrapper"; -import { SelectedBrickWrapper } from "./SelectedBrickWrapper"; -import { BrickTreeContext } from "../libs/BrickTreeContext"; -import { BrickData, BricksByMountPoint } from "../libs/interfaces"; -import { SelectedBrickContext } from "../libs/SelectedBrickContext"; -import { BrowserThemeContext } from "../libs/BrowserThemeContext"; -import { CollapsedBrickIdsContext } from "../libs/CollapsedBrickIdsContext"; -import { ShowFullNameContext } from "../libs/ShowFullNameContext"; - -export function Layout(): React.ReactElement { - const [tree, setTree] = React.useState(); - const [collapsedBrickIds, setCollapsedBrickIds] = React.useState( - [] - ); - const [showFullName, setShowFullName] = React.useState(false); - const [selectedBrick, setSelectedBrick] = React.useState(); - const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : "light"; - - return ( -
- - - - - - - - - - - - -
- ); -} diff --git a/src/components/PropTree.tsx b/src/components/PropTree.tsx deleted file mode 100644 index 3033d36..0000000 --- a/src/components/PropTree.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from "react"; -import classNames from "classnames"; -import { Icon } from "@blueprintjs/core"; - -function isObject(value: any): value is Record { - return typeof value === "object" && value; -} - -interface PropTreeProps { - properties: any[] | Record; -} - -export function PropTree({ properties }: PropTreeProps): React.ReactElement { - return ( -
    - {Array.isArray(properties) - ? properties.map((item, index) => ( - - )) - : Object.entries(properties).map((entry) => ( - - ))} -
- ); -} - -interface PropItemProps { - propName: string; - propValue: any; -} - -export function PropItem({ - propName, - propValue, -}: PropItemProps): React.ReactElement { - const [expanded, setExpanded] = React.useState(false); - - const handleClick = React.useCallback(() => { - setExpanded(!expanded); - }, [expanded]); - - const hasChildren = isObject(propValue); - - return ( -
  • -
    - - {propName} - :{" "} - - - -
    - {isObject(propValue) && expanded && } -
  • - ); -} - -interface ValueStringifyProps { - value: any; - expanded?: boolean; -} - -export function ValueStringify({ - value, - expanded, -}: ValueStringifyProps): React.ReactElement { - if (Array.isArray(value)) { - if (expanded) { - return Array({value.length}); - } - - return ( - - {` (${value.length}) `} - [ - {value.map((item, index, array) => ( - - - {index < array.length - 1 && ", "} - - ))} - ] - - ); - } - - if (isObject(value)) { - if (expanded) { - return null; - } - - return ( - - {"{"} - {Object.entries(value).map((entry, index, array) => ( - - - {index < array.length - 1 && ", "} - - ))} - {"}"} - - ); - } - - if (typeof value === "string") { - return ( - <> - {'"'} - {value} - {'"'} - - ); - } - - if (typeof value === "function") { - return ƒ; - } - - return ( - - {String(value)} - - ); -} - -interface ValueItemStringifyProps { - item: any; -} - -export function ValueItemStringify({ - item, -}: ValueItemStringifyProps): React.ReactElement { - if (Array.isArray(item)) { - return {`Array(${item.length})`}; - } - - if (isObject(item)) { - return {`{…}`}; - } - - if (typeof item === "string") { - return {`"${item}"`}; - } - - if (typeof item === "function") { - return ƒ; - } - - return ( - - {String(item)} - - ); -} - -interface ObjectPropStringifyProps { - propName: string; - propValue: any; -} - -export function ObjectPropStringify({ - propName, - propValue, -}: ObjectPropStringifyProps): React.ReactElement { - return ( - - {propName} - :{" "} - - - - - ); -} diff --git a/src/content.ts b/src/content.ts index 0e1db79..343bd99 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,4 +1,4 @@ -import { MESSAGE_SOURCE_HOOK } from "./shared"; +import { MESSAGE_SOURCE_HOOK } from "./shared/constants"; function injectScript(file: string): void { const script = document.createElement("script"); diff --git a/src/devtools.ts b/src/devtools.ts index 0fe6674..a654fa2 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -2,7 +2,7 @@ import { HOOK_NAME, MESSAGE_SOURCE_DEVTOOLS, MESSAGE_SOURCE_HOOK, -} from "./shared"; +} from "./shared/constants"; let panelCreated = false; diff --git a/src/hook.ts b/src/hook.ts deleted file mode 100644 index 8108665..0000000 --- a/src/hook.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { HOOK_NAME, MESSAGE_SOURCE_HOOK } from "./shared"; -import { - RichBrickData, - BrickNode, - BrickElement, - BrickElementConstructor, - MountPointElement, - BricksByMountPoint, - BrickInfo, -} from "./libs/interfaces"; - -function injectHook(): void { - if (Object.prototype.hasOwnProperty.call(window, HOOK_NAME)) { - return; - } - - let uniqueIdCounter = 0; - function uniqueId(): number { - return (uniqueIdCounter += 1); - } - let uidToBrick = new Map(); - // let brickToUid = new WeakMap(); - - function getBrickByUid(uid: number): BrickElement { - return uidToBrick.get(uid); - } - - function walk(node: BrickNode): RichBrickData { - const element = node.$$brick?.element; - if (!element) { - return; - } - - const uid = uniqueId(); - uidToBrick.set(uid, element); - // brickToUid.set(element, uid); - - return { - uid, - tagName: element.tagName.toLowerCase(), - children: (node.children?.map(walk) ?? []).filter(Boolean), - }; - } - - function getBricks(): BricksByMountPoint { - uniqueIdCounter = 0; - uidToBrick = new Map(); - // brickToUid = new WeakMap(); - return { - main: getBricksByMountPoint("#main-mount-point"), - portal: getBricksByMountPoint("#portal-mount-point"), - bg: getBricksByMountPoint("#bg-mount-point"), - }; - } - - function getBricksByMountPoint(mountPoint: string): RichBrickData[] { - const element = document.querySelector(mountPoint) as MountPointElement; - return (element?.$$rootBricks?.map(walk) ?? []).filter(Boolean); - } - - function getBrickInfo(uid: number): BrickInfo { - let properties: Record = {}; - let events: string[]; - const element = uidToBrick.get(uid); - if (["brick", "provider", "custom-element"].includes(element?.$$typeof)) { - const props: string[] = - (element.constructor as BrickElementConstructor) - ._dev_only_definedProperties || []; - properties = Object.fromEntries( - props - .sort() - .map((propName) => [ - propName, - element[propName as keyof BrickElement], - ]) - ); - events = Array.isArray(element.$$eventListeners) - ? element.$$eventListeners.map((item) => item[0]) - : []; - } - return { properties, events }; - } - - const inspector: Record = {}; - let inspectBoxRemoved = false; - - function showInspectBox(uid: number): void { - hideInspectBox(); - const element = uidToBrick.get(uid); - if (!inspector.node) { - inspector.node = document.createElement("div"); - inspector.border = document.createElement("div"); - inspector.padding = document.createElement("div"); - inspector.content = document.createElement("div"); - - Object.assign(inspector.node.style, { - position: "absolute", - zIndex: "1000000", - pointerEvents: "none", - borderColor: "rgba(255, 155, 0, 0.3)", - }); - - inspector.border.style.borderColor = "rgba(255, 200, 50, 0.3)"; - inspector.padding.style.borderColor = "rgba(77, 200, 0, 0.3)"; - inspector.content.style.backgroundColor = "rgba(120, 170, 210, 0.7)"; - - inspector.node.appendChild(inspector.border); - inspector.border.appendChild(inspector.padding); - inspector.padding.appendChild(inspector.content); - } - const box = element.getBoundingClientRect(); - const dims = getElementDimensions(element); - - boxWrap(inspector.node, dims, "margin"); - boxWrap(inspector.border, dims, "border"); - boxWrap(inspector.padding, dims, "padding"); - - Object.assign(inspector.content.style, { - width: `${ - box.width - - dims.borderLeft - - dims.borderRight - - dims.paddingLeft - - dims.paddingRight - }px`, - height: `${ - box.height - - dims.borderTop - - dims.borderBottom - - dims.paddingTop - - dims.paddingBottom - }px`, - }); - - Object.assign(inspector.node.style, { - top: `${box.top - dims.marginTop + window.scrollY}px`, - left: `${box.left - dims.marginLeft + window.scrollX}px`, - }); - - document.body.appendChild(inspector.node); - document.body.addEventListener("mouseenter", hideInspectBox); - inspectBoxRemoved = false; - } - - function hideInspectBox(): void { - if (inspector.node && !inspectBoxRemoved) { - inspector.node.remove(); - document.body.removeEventListener("mouseenter", hideInspectBox); - inspectBoxRemoved = true; - } - } - - function getElementDimensions( - domElement: HTMLElement - ): Record { - const calculatedStyle = window.getComputedStyle(domElement); - return { - borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10), - borderRight: parseInt(calculatedStyle.borderRightWidth, 10), - borderTop: parseInt(calculatedStyle.borderTopWidth, 10), - borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10), - marginLeft: parseInt(calculatedStyle.marginLeft, 10), - marginRight: parseInt(calculatedStyle.marginRight, 10), - marginTop: parseInt(calculatedStyle.marginTop, 10), - marginBottom: parseInt(calculatedStyle.marginBottom, 10), - paddingLeft: parseInt(calculatedStyle.paddingLeft, 10), - paddingRight: parseInt(calculatedStyle.paddingRight, 10), - paddingTop: parseInt(calculatedStyle.paddingTop, 10), - paddingBottom: parseInt(calculatedStyle.paddingBottom, 10), - }; - } - - function boxWrap( - element: HTMLElement, - dims: Record, - what: string - ): void { - Object.assign(element.style, { - borderTopWidth: dims[what + "Top"] + "px", - borderLeftWidth: dims[what + "Left"] + "px", - borderRightWidth: dims[what + "Right"] + "px", - borderBottomWidth: dims[what + "Bottom"] + "px", - borderStyle: "solid", - }); - } - - function emit(payload: any): void { - window.postMessage( - { - source: MESSAGE_SOURCE_HOOK, - payload, - }, - "*" - ); - } - - const hook = { - getBrickByUid, - getBricks, - getBrickInfo, - showInspectBox, - hideInspectBox, - emit, - }; - - Object.defineProperty(hook, "pageHasBricks", { - get: function () { - return !!(window as any).BRICK_NEXT_VERSIONS; - }, - }); - - Object.defineProperty(window, HOOK_NAME, { - get: function () { - return hook; - }, - }); -} - -injectHook(); diff --git a/src/hook/dehydrate.spec.ts b/src/hook/dehydrate.spec.ts new file mode 100644 index 0000000..072b197 --- /dev/null +++ b/src/hook/dehydrate.spec.ts @@ -0,0 +1,209 @@ +import { dehydrate } from "./dehydrate"; +import { PROP_DEHYDRATED } from "../shared/constants"; + +describe("dehydrate", () => { + const circularObject: Record = {}; + circularObject.refOthers = { + refSelf: circularObject, + refSelfAgain: [circularObject], + }; + + const circularArray: any[] = []; + circularArray[0] = circularArray; + + it.each<[any, any, any[]]>([ + // Returns original value if it's serializable. + ["hello", "hello", []], + [1, 1, []], + [true, true, []], + [null, null, []], + [ + { + quality: "good", + }, + { + quality: "good", + }, + [], + ], + [["quality", "good"], ["quality", "good"], []], + + // Returns a dehydrated wrapper if it's not serializable. + [ + undefined, + { + [PROP_DEHYDRATED]: { + type: "undefined", + }, + }, + [], + ], + [ + NaN, + { + [PROP_DEHYDRATED]: { + type: "NaN", + }, + }, + [], + ], + [ + Infinity, + { + [PROP_DEHYDRATED]: { + type: "Infinity", + }, + }, + [], + ], + [ + -Infinity, + { + [PROP_DEHYDRATED]: { + type: "-Infinity", + }, + }, + [], + ], + [ + function noop() { + /* noop */ + }, + { + [PROP_DEHYDRATED]: { + type: "function", + }, + }, + [], + ], + [ + new Event("click"), + { + [PROP_DEHYDRATED]: { + type: "object", + constructorName: "Event", + children: { + type: "click", + }, + }, + }, + [], + ], + [ + new CustomEvent("click", { detail: "good" }), + { + [PROP_DEHYDRATED]: { + type: "object", + constructorName: "CustomEvent", + children: { + type: "click", + detail: "good", + }, + }, + }, + [], + ], + [ + new RegExp("pattern"), + { + [PROP_DEHYDRATED]: { + type: "object", + constructorName: "RegExp", + }, + }, + [], + ], + [ + Symbol("good"), + { + [PROP_DEHYDRATED]: { + type: "symbol", + }, + }, + [], + ], + [ + { + fromNoWhere: Object.create(null), + }, + { + fromNoWhere: { + [PROP_DEHYDRATED]: { + type: "object", + constructorName: "null", + }, + }, + }, + [], + ], + [ + circularObject, + { + refOthers: { + refSelf: { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + refSelfAgain: [ + { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + ], + }, + }, + [ + { + refOthers: { + refSelf: { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + refSelfAgain: [ + { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + ], + }, + }, + ], + ], + [ + circularArray, + [ + { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + ], + [ + [ + { + [PROP_DEHYDRATED]: { + type: "ref", + ref: 0, + }, + }, + ], + ], + ], + ])( + "`%j` should be dehydrated as `{ value: %j, repo: %j }`", + (value, result, expectedRepo) => { + const repo: any[] = []; + expect(dehydrate(value, repo)).toEqual(result); + expect(repo).toEqual(expectedRepo); + } + ); +}); diff --git a/src/hook/dehydrate.ts b/src/hook/dehydrate.ts new file mode 100644 index 0000000..da3a9ab --- /dev/null +++ b/src/hook/dehydrate.ts @@ -0,0 +1,109 @@ +import { DehydratedWrapper, Dehydrated } from "../shared/interfaces"; +import { PROP_DEHYDRATED } from "../shared/constants"; + +/** + * Make the `value` serializable for `postMessage`. + * E.g. transform the non-stringify-able data, and handle circular references. + * + * @param value data to transfer. + * @param repo repository to collect circular references. + * @returns original value if it's serializable, and a dehydrated wrapper if not. + */ +export function dehydrate(value: any, repo: any[], memo = new WeakMap()): any { + if (memo.has(value)) { + const processed = memo.get(value); + let repoIndex = repo.indexOf(processed); + if (repoIndex < 0) { + repoIndex = repo.length; + repo.push(processed); + } + return wrapDehydrated({ + type: "ref", + ref: repoIndex, + }); + } + + if (Array.isArray(value)) { + const processed: any[] = []; + memo.set(value, processed); + value.forEach((item) => { + processed.push(dehydrate(item, repo, memo)); + }); + return processed; + } + + if (typeof value === "object" && value) { + if (value.constructor === Object) { + const processed: Record = {}; + memo.set(value, processed); + Object.entries(value).forEach((entry) => { + processed[entry[0]] = dehydrate(entry[1], repo, memo); + }); + return processed; + } + + const processed = wrapDehydrated({ + type: "object", + constructorName: value.constructor?.name || "null", + }); + memo.set(value, processed); + + if (value instanceof Event) { + appendDehydratedChildren( + processed, + dehydrate( + { + type: value.type, + ...(value instanceof CustomEvent && { + detail: value.detail, + }), + }, + repo, + memo + ) + ); + } + + return processed; + } + + if (typeof value === "function") { + const processed = wrapDehydrated({ + type: "function", + }); + memo.set(value, processed); + return processed; + } + + if (typeof value === "symbol") { + return wrapDehydrated({ + type: "symbol", + }); + } + + if ( + Number.isNaN(value) || + value === Infinity || + value === -Infinity || + value === undefined + ) { + return wrapDehydrated({ + type: String(value) as "NaN" | "Infinity" | "-Infinity" | "undefined", + }); + } + + return value; +} + +function wrapDehydrated(data: Dehydrated): DehydratedWrapper { + return { + [PROP_DEHYDRATED]: data, + }; +} + +function appendDehydratedChildren( + dehydrated: DehydratedWrapper, + children: any +): void { + dehydrated[PROP_DEHYDRATED].children = children; +} diff --git a/src/hook/emit.spec.ts b/src/hook/emit.spec.ts new file mode 100644 index 0000000..ae2b402 --- /dev/null +++ b/src/hook/emit.spec.ts @@ -0,0 +1,35 @@ +import { emit } from "./emit"; +import { MESSAGE_SOURCE_HOOK } from "../shared/constants"; + +const postMessage = jest.fn(); +window.postMessage = postMessage; +const warn = jest.spyOn(console, "warn").mockImplementation(() => void 0); + +describe("emit", () => { + it("should work", () => { + emit({ + type: "evaluation", + payload: { + quality: "good", + }, + }); + expect(postMessage).toBeCalledWith( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: "evaluation", + payload: { + quality: "good", + }, + repo: [], + }, + }, + "*" + ); + }); + + it("should warn if data is nil", () => { + emit(null); + expect(warn).toBeCalled(); + }); +}); diff --git a/src/hook/emit.ts b/src/hook/emit.ts new file mode 100644 index 0000000..e3381e5 --- /dev/null +++ b/src/hook/emit.ts @@ -0,0 +1,23 @@ +import { dehydrate } from "./dehydrate"; +import { EmitData } from "../shared/interfaces"; +import { MESSAGE_SOURCE_HOOK } from "../shared/constants"; + +export function emit(data: EmitData): void { + try { + const repo: any[] = []; + const payload = dehydrate(data.payload, repo); + window.postMessage( + { + source: MESSAGE_SOURCE_HOOK, + payload: { + type: data.type, + payload, + repo, + }, + }, + "*" + ); + } catch (error) { + console.warn("brick-next-devtools emit failed:", error); + } +} diff --git a/src/hook/index.ts b/src/hook/index.ts new file mode 100644 index 0000000..1981a3c --- /dev/null +++ b/src/hook/index.ts @@ -0,0 +1,33 @@ +import { HOOK_NAME } from "../shared/constants"; +import { getBricks, getBrickByUid, getBrickInfo } from "./traverse"; +import { inspectElement, dismissInspections } from "./inspector"; +import { emit } from "./emit"; + +function injectHook(): void { + if (Object.prototype.hasOwnProperty.call(window, HOOK_NAME)) { + return; + } + + const hook = { + getBricks, + getBrickByUid, + getBrickInfo, + inspectBrick: (uid: number) => inspectElement(getBrickByUid(uid)), + dismissInspections, + emit, + }; + + Object.defineProperty(hook, "pageHasBricks", { + get: function () { + return !!(window as any).BRICK_NEXT_VERSIONS; + }, + }); + + Object.defineProperty(window, HOOK_NAME, { + get: function () { + return hook; + }, + }); +} + +injectHook(); diff --git a/src/hook/inspector.spec.ts b/src/hook/inspector.spec.ts new file mode 100644 index 0000000..c518021 --- /dev/null +++ b/src/hook/inspector.spec.ts @@ -0,0 +1,75 @@ +import { inspectElement, dismissInspections } from "./inspector"; + +jest.spyOn(window, "getComputedStyle").mockImplementation( + () => + ({ + borderLeftWidth: "1px", + borderRightWidth: "2px", + borderTopWidth: "3px", + borderBottomWidth: "4px", + marginLeft: "5px", + marginRight: "6px", + marginTop: "7px", + marginBottom: "8px", + paddingLeft: "9px", + paddingRight: "10px", + paddingTop: "11px", + paddingBottom: "12px", + } as any) +); + +describe("inspector", () => { + it("should work", () => { + const span = + { + getBoundingClientRect: () => ({ + top: 50, + left: 60, + width: 200, + height: 100, + }), + } as any; + + inspectElement(span); + expect(document.body.children.length).toBe(1); + + const node = document.body.firstChild as HTMLElement; + expect(node.style).toMatchObject({ + position: "absolute", + top: "43px", + left: "55px", + borderLeftWidth: "5px", + borderRightWidth: "6px", + borderTopWidth: "7px", + borderBottomWidth: "8px", + }); + + const border = node.firstChild as HTMLElement; + expect(border.style).toMatchObject({ + borderLeftWidth: "1px", + borderRightWidth: "2px", + borderTopWidth: "3px", + borderBottomWidth: "4px", + }); + + const padding = border.firstChild as HTMLElement; + expect(padding.style).toMatchObject({ + borderLeftWidth: "9px", + borderRightWidth: "10px", + borderTopWidth: "11px", + borderBottomWidth: "12px", + }); + + const content = padding.firstChild as HTMLElement; + expect(content.style).toMatchObject({ + width: "178px", + height: "70px", + }); + + inspectElement(span); + expect(document.body.children.length).toBe(1); + + dismissInspections(); + expect(document.body.firstChild as HTMLElement).toBe(null); + }); +}); diff --git a/src/hook/inspector.ts b/src/hook/inspector.ts new file mode 100644 index 0000000..167d9d3 --- /dev/null +++ b/src/hook/inspector.ts @@ -0,0 +1,99 @@ +const inspector: Record = {}; +let inspectBoxRemoved = false; + +export function inspectElement(element: HTMLElement): void { + dismissInspections(); + if (!inspector.node) { + inspector.node = document.createElement("div"); + inspector.border = document.createElement("div"); + inspector.padding = document.createElement("div"); + inspector.content = document.createElement("div"); + + Object.assign(inspector.node.style, { + position: "absolute", + zIndex: "1000000", + pointerEvents: "none", + borderColor: "rgba(255, 155, 0, 0.3)", + }); + + inspector.border.style.borderColor = "rgba(255, 200, 50, 0.3)"; + inspector.padding.style.borderColor = "rgba(77, 200, 0, 0.3)"; + inspector.content.style.backgroundColor = "rgba(120, 170, 210, 0.7)"; + + inspector.node.appendChild(inspector.border); + inspector.border.appendChild(inspector.padding); + inspector.padding.appendChild(inspector.content); + } + const box = element.getBoundingClientRect(); + const dims = getElementDimensions(element); + + boxWrap(inspector.node, dims, "margin"); + boxWrap(inspector.border, dims, "border"); + boxWrap(inspector.padding, dims, "padding"); + + Object.assign(inspector.content.style, { + width: `${ + box.width - + dims.borderLeft - + dims.borderRight - + dims.paddingLeft - + dims.paddingRight + }px`, + height: `${ + box.height - + dims.borderTop - + dims.borderBottom - + dims.paddingTop - + dims.paddingBottom + }px`, + }); + + Object.assign(inspector.node.style, { + top: `${box.top - dims.marginTop + window.scrollY}px`, + left: `${box.left - dims.marginLeft + window.scrollX}px`, + }); + + document.body.appendChild(inspector.node); + document.body.addEventListener("mouseenter", dismissInspections); + inspectBoxRemoved = false; +} + +export function dismissInspections(): void { + if (inspector.node && !inspectBoxRemoved) { + inspector.node.remove(); + document.body.removeEventListener("mouseenter", dismissInspections); + inspectBoxRemoved = true; + } +} + +function getElementDimensions(domElement: HTMLElement): Record { + const calculatedStyle = window.getComputedStyle(domElement); + return { + borderLeft: parseInt(calculatedStyle.borderLeftWidth, 10), + borderRight: parseInt(calculatedStyle.borderRightWidth, 10), + borderTop: parseInt(calculatedStyle.borderTopWidth, 10), + borderBottom: parseInt(calculatedStyle.borderBottomWidth, 10), + marginLeft: parseInt(calculatedStyle.marginLeft, 10), + marginRight: parseInt(calculatedStyle.marginRight, 10), + marginTop: parseInt(calculatedStyle.marginTop, 10), + marginBottom: parseInt(calculatedStyle.marginBottom, 10), + paddingLeft: parseInt(calculatedStyle.paddingLeft, 10), + paddingRight: parseInt(calculatedStyle.paddingRight, 10), + paddingTop: parseInt(calculatedStyle.paddingTop, 10), + paddingBottom: parseInt(calculatedStyle.paddingBottom, 10), + }; +} + +function boxWrap( + element: HTMLElement, + dims: Record, + what: string +): void { + Object.assign(element.style, { + borderTopWidth: dims[what + "Top"] + "px", + borderLeftWidth: dims[what + "Left"] + "px", + borderRightWidth: dims[what + "Right"] + "px", + borderBottomWidth: dims[what + "Bottom"] + "px", + borderStyle: "solid", + }); +} diff --git a/src/hook/traverse.spec.ts b/src/hook/traverse.spec.ts new file mode 100644 index 0000000..c31580c --- /dev/null +++ b/src/hook/traverse.spec.ts @@ -0,0 +1,164 @@ +import { getBricks, getBrickByUid, getBrickInfo } from "./traverse"; +import { MountPointElement } from "../shared/interfaces"; + +describe("traverse", () => { + beforeEach(() => { + const main = document.createElement("div"); + const portal = document.createElement("div"); + const bg = document.createElement("div"); + main.id = "main-mount-point"; + portal.id = "portal-mount-point"; + bg.id = "bg-mount-point"; + document.body.appendChild(main); + document.body.appendChild(portal); + document.body.appendChild(bg); + + const BrickProto = { + constructor: { + // eslint-disable-next-line @typescript-eslint/camelcase + _dev_only_definedProperties: ["propA", "propB"], + }, + }; + + (main as MountPointElement).$$rootBricks = [ + { + $$brick: { + element: Object.assign(Object.create(BrickProto), { + tagName: "YOUR.AWESOME-TPL", + $$typeof: "custom-template", + propB: "b in tpl", + propA: "a in tpl", + $$eventListeners: [], + }), + }, + children: [ + { + $$brick: { + element: Object.assign(Object.create(BrickProto), { + tagName: "YOUR.INNER-BRICK", + $$typeof: "brick", + propA: "a in brick", + propB: "b in brick", + $$eventListeners: [ + [ + "click", + function () { + /* noop */ + }, + { action: "console.log" }, + ], + ], + }), + }, + children: [], + }, + { + $$brick: { + element: Object.assign( + {}, + { + tagName: "DIV", + propA: "a in brick", + propB: "b in brick", + } + ), + }, + children: [], + }, + ], + }, + ]; + + (bg as MountPointElement).$$rootBricks = [ + { + $$brick: { + element: Object.assign(Object.create(BrickProto), { + tagName: "YOUR.AWESOME-PROVIDER", + $$typeof: "provider", + propA: "a in provider", + propB: "b in provider", + }), + }, + children: null, + }, + ]; + }); + + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("should work", () => { + expect(getBricks()).toEqual({ + main: [ + { + uid: 1, + tagName: "your.awesome-tpl", + children: [ + { + uid: 2, + tagName: "your.inner-brick", + children: [], + }, + { + uid: 3, + tagName: "div", + children: [], + }, + ], + }, + ], + portal: [], + bg: [ + { + uid: 4, + tagName: "your.awesome-provider", + children: [], + }, + ], + }); + + expect(getBrickByUid(1).tagName).toBe("YOUR.AWESOME-TPL"); + + expect(getBrickInfo(1)).toEqual({ + info: { + properties: { + propA: "a in tpl", + propB: "b in tpl", + }, + events: [], + }, + repo: [], + }); + + expect(getBrickInfo(2)).toEqual({ + info: { + properties: { + propA: "a in brick", + propB: "b in brick", + }, + events: [["click", { action: "console.log" }]], + }, + repo: [], + }); + + expect(getBrickInfo(3)).toEqual({ + info: { + properties: {}, + events: [], + }, + repo: [], + }); + + expect(getBrickInfo(4)).toEqual({ + info: { + properties: { + propA: "a in provider", + propB: "b in provider", + }, + events: [], + }, + repo: [], + }); + }); +}); diff --git a/src/hook/traverse.ts b/src/hook/traverse.ts new file mode 100644 index 0000000..30ca583 --- /dev/null +++ b/src/hook/traverse.ts @@ -0,0 +1,76 @@ +import { + BrickElement, + BrickElementConstructor, + DehydratedBrickInfo, + BricksByMountPoint, + RichBrickData, + MountPointElement, + BrickNode, +} from "../shared/interfaces"; +import { dehydrate } from "./dehydrate"; + +let uniqueIdCounter = 0; +function uniqueId(): number { + return (uniqueIdCounter += 1); +} +let uidToBrick = new Map(); +// let brickToUid = new WeakMap(); + +export function getBrickByUid(uid: number): BrickElement { + return uidToBrick.get(uid); +} + +export function getBricks(): BricksByMountPoint { + uniqueIdCounter = 0; + uidToBrick = new Map(); + // brickToUid = new WeakMap(); + return { + main: getBricksByMountPoint("#main-mount-point"), + portal: getBricksByMountPoint("#portal-mount-point"), + bg: getBricksByMountPoint("#bg-mount-point"), + }; +} + +export function getBrickInfo(uid: number): DehydratedBrickInfo { + let properties: Record = {}; + let events: [string, any][] = []; + const element = uidToBrick.get(uid); + if (["brick", "provider", "custom-template"].includes(element?.$$typeof)) { + const props: string[] = + (element.constructor as BrickElementConstructor) + ?._dev_only_definedProperties || []; + properties = Object.fromEntries( + props + .sort() + .map((propName) => [propName, element[propName as keyof BrickElement]]) + ); + events = Array.isArray(element.$$eventListeners) + ? element.$$eventListeners.map((item) => [item[0], item[2]]) + : []; + } + const repo: any[] = []; + const info = dehydrate({ properties, events }, repo); + return { info, repo }; +} + +function getBricksByMountPoint(mountPoint: string): RichBrickData[] { + const element = document.querySelector(mountPoint) as MountPointElement; + return (element?.$$rootBricks?.map(walk) ?? []).filter(Boolean); +} + +function walk(node: BrickNode): RichBrickData { + const element = node.$$brick?.element; + if (!element) { + return; + } + + const uid = uniqueId(); + uidToBrick.set(uid, element); + // brickToUid.set(element, uid); + + return { + uid, + tagName: element.tagName.toLowerCase(), + children: (node.children?.map(walk) ?? []).filter(Boolean), + }; +} diff --git a/src/libs/interfaces.ts b/src/libs/interfaces.ts deleted file mode 100644 index 0a007df..0000000 --- a/src/libs/interfaces.ts +++ /dev/null @@ -1,43 +0,0 @@ -export interface BrickData { - uid: number; - tagName: string; -} - -export interface RichBrickData extends BrickData { - children: RichBrickData[]; -} - -export interface BricksByMountPoint { - main?: RichBrickData[]; - portal?: RichBrickData[]; - bg?: RichBrickData[]; -} - -export interface BrickNode { - $$brick: RuntimeBrick; - children: BrickNode[]; -} - -export interface RuntimeBrick { - element?: BrickElement; -} - -export interface BrickElement extends HTMLElement { - $$typeof?: "brick" | "custom-template"; - $$eventListeners: [string, Function][]; -} - -export interface BrickElementConstructor extends Function { - _dev_only_definedProperties: string[]; -} - -export interface MountPointElement extends HTMLElement { - $$rootBricks?: BrickNode[]; -} - -export interface BrickInfo { - properties?: Record; - events?: string[]; -} - -export type BrowserTheme = "dark" | "light"; diff --git a/src/components/BrickLabel.spec.tsx b/src/panel/components/BrickLabel.spec.tsx similarity index 100% rename from src/components/BrickLabel.spec.tsx rename to src/panel/components/BrickLabel.spec.tsx diff --git a/src/components/BrickLabel.tsx b/src/panel/components/BrickLabel.tsx similarity index 100% rename from src/components/BrickLabel.tsx rename to src/panel/components/BrickLabel.tsx diff --git a/src/components/BrickTree.spec.tsx b/src/panel/components/BrickTree.spec.tsx similarity index 86% rename from src/components/BrickTree.spec.tsx rename to src/panel/components/BrickTree.spec.tsx index 9aa82b0..39849e8 100644 --- a/src/components/BrickTree.spec.tsx +++ b/src/panel/components/BrickTree.spec.tsx @@ -8,7 +8,7 @@ import { useCollapsedBrickIdsContext, ContextOfCollapsedBrickIds, } from "../libs/CollapsedBrickIdsContext"; -import { BricksByMountPoint, RichBrickData } from "../libs/interfaces"; +import { BricksByMountPoint, RichBrickData } from "../../shared/interfaces"; jest.mock("../libs/BrickTreeContext"); jest.mock("../libs/SelectedBrickContext"); @@ -125,19 +125,21 @@ describe("BrickTree", () => { (wrapper.find(Tree).invoke("onNodeCollapse") as any)({ id: 1, }); - expect( - collapsedBrickIdsContext.setCollapsedBrickIds - ).toHaveBeenNthCalledWith(1, [100, 1]); // Todo(steve): context not updated + expect(collapsedBrickIdsContext.setCollapsedBrickIds).toBeCalled(); + // expect( + // collapsedBrickIdsContext.setCollapsedBrickIds + // ).toHaveBeenNthCalledWith(1, [100, 1]); // expect(wrapper.find(Tree).prop("contents")[0].isExpanded).toBe(false); (wrapper.find(Tree).invoke("onNodeExpand") as any)({ id: 1, }); - expect( - collapsedBrickIdsContext.setCollapsedBrickIds - ).toHaveBeenNthCalledWith(2, [100]); // Todo(steve): context not updated + expect(collapsedBrickIdsContext.setCollapsedBrickIds).toBeCalled(); + // expect( + // collapsedBrickIdsContext.setCollapsedBrickIds + // ).toHaveBeenNthCalledWith(2, [100]); // expect(wrapper.find(Tree).prop("contents")[0].isExpanded).toBe(true); (wrapper.find(Tree).invoke("onNodeMouseEnter") as any)({ @@ -145,7 +147,7 @@ describe("BrickTree", () => { }); expect(mockEval).toHaveBeenNthCalledWith( 1, - "inspect(window.__BRICK_NEXT_DEVTOOLS_HOOK__.showInspectBox(1));" + "inspect(window.__BRICK_NEXT_DEVTOOLS_HOOK__.inspectBrick(1));" ); (wrapper.find(Tree).invoke("onNodeMouseLeave") as any)({ @@ -153,7 +155,7 @@ describe("BrickTree", () => { }); expect(mockEval).toHaveBeenNthCalledWith( 2, - "inspect(window.__BRICK_NEXT_DEVTOOLS_HOOK__.hideInspectBox(1));" + "inspect(window.__BRICK_NEXT_DEVTOOLS_HOOK__.dismissInspections(1));" ); (useBrickTreeContext as jest.Mock).mockReset(); diff --git a/src/components/BrickTree.tsx b/src/panel/components/BrickTree.tsx similarity index 86% rename from src/components/BrickTree.tsx rename to src/panel/components/BrickTree.tsx index 8416ac2..a64c081 100644 --- a/src/components/BrickTree.tsx +++ b/src/panel/components/BrickTree.tsx @@ -1,9 +1,9 @@ import React from "react"; import { ITreeNode, Tree, Tag } from "@blueprintjs/core"; -import { HOOK_NAME } from "../shared"; +import { HOOK_NAME } from "../../shared/constants"; import { useBrickTreeContext } from "../libs/BrickTreeContext"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; -import { RichBrickData, BrickData } from "../libs/interfaces"; +import { RichBrickData, BrickData } from "../../shared/interfaces"; import { useCollapsedBrickIdsContext } from "../libs/CollapsedBrickIdsContext"; import { BrickLabel } from "./BrickLabel"; @@ -30,22 +30,22 @@ export function BrickTree(): React.ReactElement { const handleNodeCollapse = React.useCallback( (node: ITreeNode) => { - setCollapsedBrickIds(collapsedBrickIds.concat(node.id as number)); + setCollapsedBrickIds((prev) => prev.concat(node.id as number)); }, - [collapsedBrickIds, setCollapsedBrickIds] + [setCollapsedBrickIds] ); const handleNodeExpand = React.useCallback( (node: ITreeNode) => { - setCollapsedBrickIds(collapsedBrickIds.filter((id) => id !== node.id)); + setCollapsedBrickIds((prev) => prev.filter((id) => id !== node.id)); }, - [collapsedBrickIds, setCollapsedBrickIds] + [setCollapsedBrickIds] ); const handleNodeMouseEnter = React.useCallback( (node: ITreeNode) => { chrome.devtools.inspectedWindow.eval( - `inspect(window.${HOOK_NAME}.showInspectBox(${node.id}));` + `inspect(window.${HOOK_NAME}.inspectBrick(${node.id}));` ); }, [] @@ -54,7 +54,7 @@ export function BrickTree(): React.ReactElement { const handleNodeMouseLeave = React.useCallback( (node: ITreeNode) => { chrome.devtools.inspectedWindow.eval( - `inspect(window.${HOOK_NAME}.hideInspectBox(${node.id}));` + `inspect(window.${HOOK_NAME}.dismissInspections(${node.id}));` ); }, [] diff --git a/src/panel/components/BricksPanel.spec.tsx b/src/panel/components/BricksPanel.spec.tsx new file mode 100644 index 0000000..ae1fa77 --- /dev/null +++ b/src/panel/components/BricksPanel.spec.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { BricksPanel } from "./BricksPanel"; + +describe("BricksPanel", () => { + it("should work", () => { + const wrapper = shallow(); + expect(wrapper.hasClass("bricks-panel")).toBe(true); + }); +}); diff --git a/src/panel/components/BricksPanel.tsx b/src/panel/components/BricksPanel.tsx new file mode 100644 index 0000000..ca1bba4 --- /dev/null +++ b/src/panel/components/BricksPanel.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { TreeWrapper } from "./TreeWrapper"; +import { SelectedBrickWrapper } from "./SelectedBrickWrapper"; +import { BrickTreeContext } from "../libs/BrickTreeContext"; +import { BrickData, BricksByMountPoint } from "../../shared/interfaces"; +import { SelectedBrickContext } from "../libs/SelectedBrickContext"; +import { CollapsedBrickIdsContext } from "../libs/CollapsedBrickIdsContext"; +import { ShowFullNameContext } from "../libs/ShowFullNameContext"; + +export function BricksPanel(): React.ReactElement { + const [tree, setTree] = React.useState(); + const [collapsedBrickIds, setCollapsedBrickIds] = React.useState( + [] + ); + const [showFullName, setShowFullName] = React.useState(false); + const [selectedBrick, setSelectedBrick] = React.useState(); + + return ( +
    + + + + + + + + + + +
    + ); +} diff --git a/src/panel/components/EvaluationsPanel.spec.tsx b/src/panel/components/EvaluationsPanel.spec.tsx new file mode 100644 index 0000000..efa801d --- /dev/null +++ b/src/panel/components/EvaluationsPanel.spec.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { Button } from "@blueprintjs/core"; +import { EvaluationsPanel } from "./EvaluationsPanel"; +import { useEvaluationsContext } from "../libs/EvaluationsContext"; + +jest.mock("../libs/EvaluationsContext"); +const setEvaluations = jest.fn(); +(useEvaluationsContext as jest.Mock).mockReturnValue({ + evaluations: [ + { + raw: "<% EVENT.detail %>", + result: "good", + context: { + EVENT: { + detail: "good", + }, + }, + }, + ], + setEvaluations, +}); + +describe("EvaluationsPanel", () => { + afterEach(() => { + setEvaluations.mockClear(); + }); + + it("should work", () => { + const wrapper = shallow(); + expect(wrapper.find("tbody").find("tr").length).toBe(1); + }); + + it("should handle clear", () => { + const wrapper = shallow(); + wrapper.find(Button).invoke("onClick")(null); + expect(setEvaluations).toBeCalled(); + }); +}); diff --git a/src/panel/components/EvaluationsPanel.tsx b/src/panel/components/EvaluationsPanel.tsx new file mode 100644 index 0000000..ddd97a8 --- /dev/null +++ b/src/panel/components/EvaluationsPanel.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { Button, Tooltip, ButtonGroup } from "@blueprintjs/core"; +import { PanelSelector } from "./PanelSelector"; +import { useEvaluationsContext } from "../libs/EvaluationsContext"; +import { PropList, PropItem } from "./PropList"; + +export function EvaluationsPanel(): React.ReactElement { + const { evaluations, setEvaluations } = useEvaluationsContext(); + + const handleClear = React.useCallback(() => { + setEvaluations([]); + }, [setEvaluations]); + + return ( +
    +
    + + + +
    +
    +
    + + + + + + + + + + {evaluations.map((item, key) => ( + + + + + + ))} + +
    ExpressionResultContext
    + + + + + +
    +
    +
    +
    + ); +} diff --git a/src/components/Layout.spec.tsx b/src/panel/components/Layout.spec.tsx similarity index 100% rename from src/components/Layout.spec.tsx rename to src/panel/components/Layout.spec.tsx diff --git a/src/panel/components/Layout.tsx b/src/panel/components/Layout.tsx new file mode 100644 index 0000000..01d0c8a --- /dev/null +++ b/src/panel/components/Layout.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import classNames from "classnames"; +import { BrowserThemeContext } from "../libs/BrowserThemeContext"; +import { BricksPanel } from "./BricksPanel"; +import { SelectedPanelContext } from "../libs/SelectedPanelContext"; +import { EvaluationsPanel } from "./EvaluationsPanel"; +import { TransformationsPanel } from "./TransformationsPanel"; +import { + Evaluation, + Transformation, + DehydratedPayload, +} from "../../shared/interfaces"; +import { EvaluationsContext } from "../libs/EvaluationsContext"; +import { MESSAGE_SOURCE_HOOK } from "../../shared/constants"; +import { TransformationsContext } from "../libs/TransformationsContext"; +import { Storage } from "../libs/Storage"; +import { hydrate } from "../libs/hydrate"; + +export function Layout(): React.ReactElement { + const [selectedPanel, setSelectedPanel] = React.useState( + Storage.getItem("selectedPanel") ?? "Bricks" + ); + const [evaluations, setEvaluations] = React.useState([]); + const [transformations, setTransformations] = React.useState< + Transformation[] + >([]); + + React.useEffect(() => { + function onMessage(event: MessageEvent): void { + let data: DehydratedPayload; + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + ((data = event.data.payload), data?.type === "evaluation") + ) { + setEvaluations((prev) => prev.concat(hydrate(data.payload, data.repo))); + } + } + window.addEventListener("message", onMessage); + return (): void => window.removeEventListener("message", onMessage); + }, []); + + React.useEffect(() => { + function onMessage(event: MessageEvent): void { + let data: DehydratedPayload; + if ( + event.data?.source === MESSAGE_SOURCE_HOOK && + ((data = event.data.payload), data?.type === "transformation") + ) { + setTransformations((prev) => + prev.concat(hydrate(data.payload, data.repo)) + ); + } + } + window.addEventListener("message", onMessage); + return (): void => window.removeEventListener("message", onMessage); + }, []); + + React.useEffect(() => { + Storage.setItem("selectedPanel", selectedPanel); + }, [selectedPanel]); + + const theme = chrome.devtools.panels.themeName === "dark" ? "dark" : "light"; + + return ( +
    + + + {selectedPanel === "Evaluations" ? ( + + + + ) : selectedPanel === "Transformations" ? ( + + + + ) : ( + + )} + + +
    + ); +} diff --git a/src/panel/components/PanelSelector.spec.tsx b/src/panel/components/PanelSelector.spec.tsx new file mode 100644 index 0000000..a36113b --- /dev/null +++ b/src/panel/components/PanelSelector.spec.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { HTMLSelect } from "@blueprintjs/core"; +import { PanelSelector } from "./PanelSelector"; +import { useSelectedPanelContext } from "../libs/SelectedPanelContext"; + +jest.mock("../libs/SelectedPanelContext"); +const setSelectedPanel = jest.fn(); +(useSelectedPanelContext as jest.Mock).mockReturnValue({ + selectedPanel: "Bricks", + setSelectedPanel, +}); + +describe("PanelSelector", () => { + afterEach(() => { + setSelectedPanel.mockClear(); + }); + + it("should work", () => { + const wrapper = shallow(); + expect(wrapper.find(HTMLSelect).prop("value")).toBe("Bricks"); + }); + + it("should handle change", () => { + const wrapper = shallow(); + wrapper.find(HTMLSelect).invoke("onChange")({ + target: { + value: "Evaluations", + }, + } as any); + expect(setSelectedPanel).toBeCalledWith("Evaluations"); + }); +}); diff --git a/src/panel/components/PanelSelector.tsx b/src/panel/components/PanelSelector.tsx new file mode 100644 index 0000000..3eacdef --- /dev/null +++ b/src/panel/components/PanelSelector.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { HTMLSelect } from "@blueprintjs/core"; +import { useSelectedPanelContext } from "../libs/SelectedPanelContext"; + +export function PanelSelector(): React.ReactElement { + const { selectedPanel, setSelectedPanel } = useSelectedPanelContext(); + + const handleChange = (event: React.ChangeEvent): void => { + setSelectedPanel(event.target.value); + }; + + return ( +
    + { + + } +
    + ); +} diff --git a/src/components/PropTree.spec.tsx b/src/panel/components/PropList.spec.tsx similarity index 74% rename from src/components/PropTree.spec.tsx rename to src/panel/components/PropList.spec.tsx index fe7f0a4..cf60314 100644 --- a/src/components/PropTree.spec.tsx +++ b/src/panel/components/PropList.spec.tsx @@ -1,16 +1,12 @@ import React from "react"; -import { shallow, mount } from "enzyme"; +import { shallow } from "enzyme"; import { Icon } from "@blueprintjs/core"; -import { - PropTree, - PropItem, - ValueStringify, - ValueItemStringify, -} from "./PropTree"; - -describe("PropTree", () => { +import { PropList, PropItem } from "./PropList"; +import { PROP_DEHYDRATED } from "../../shared/constants"; + +describe("PropList", () => { it("should work for array", () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find(PropItem).length).toBe(2); expect(wrapper.find(PropItem).at(0).props()).toMatchObject({ propName: "0", @@ -24,7 +20,7 @@ describe("PropTree", () => { it("should work for object", () => { const wrapper = shallow( - + ); expect(wrapper.find(PropItem).length).toBe(2); expect(wrapper.find(PropItem).at(0).props()).toMatchObject({ @@ -36,12 +32,44 @@ describe("PropTree", () => { propValue: "world", }); }); + + it("should work for dehydrated with no children", () => { + const wrapper = shallow( + + ); + expect(wrapper.find("ul").text()).toBe(""); + }); + + it("should work for dehydrated with children", () => { + const wrapper = shallow( + + ); + expect(wrapper.find(PropItem).props()).toEqual({ + propName: "type", + propValue: "click", + }); + }); }); describe("PropItem", () => { it("should work for primitive property value", () => { const wrapper = shallow(); - expect(wrapper.find(PropTree).length).toBe(0); + expect(wrapper.find(PropList).length).toBe(0); }); it("should handle toggle", () => { @@ -58,15 +86,29 @@ describe("PropItem", () => { it("should work for complex property value", () => { const wrapper = shallow(); - expect(wrapper.find(PropTree).length).toBe(0); + expect(wrapper.find(PropList).length).toBe(0); expect(wrapper.find(Icon).prop("icon")).toBe("caret-right"); wrapper.find(".prop-item-label").invoke("onClick")(null); - expect(wrapper.find(PropTree).prop("properties")).toEqual([1]); + expect(wrapper.find(PropList).prop("list")).toEqual([1]); + expect(wrapper.find(Icon).prop("icon")).toBe("caret-down"); + }); + + it("should work for standalone with primitive property value", () => { + const wrapper = shallow(); + expect(wrapper.find(Icon).length).toBe(0); + expect(wrapper.find(PropList).length).toBe(0); + }); + + 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); + expect(wrapper.find(PropList).prop("list")).toEqual([1]); expect(wrapper.find(Icon).prop("icon")).toBe("caret-down"); }); }); -describe("ValueStringify", () => { +/* describe("ValueStringify", () => { it("should work for array value when not expanded", () => { const wrapper = mount(); expect(wrapper.text()).toBe(" (2) [1, 2]"); @@ -160,4 +202,4 @@ describe("ValueItemStringify", () => { expect(wrapper.hasClass("prop-value-item-nil")).toBe(false); expect(wrapper.text()).toBe("0"); }); -}); +}); */ diff --git a/src/panel/components/PropList.tsx b/src/panel/components/PropList.tsx new file mode 100644 index 0000000..cb0cb3f --- /dev/null +++ b/src/panel/components/PropList.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import classNames from "classnames"; +import { Icon } from "@blueprintjs/core"; +import { PROP_DEHYDRATED } from "../../shared/constants"; +import { VariableDisplay } from "./VariableDisplay"; +import { isDehydrated, checkHasChildren } from "../libs/utils"; + +interface PropListProps { + list: any[] | Record; +} + +export function PropList({ list }: PropListProps): React.ReactElement { + const value = isDehydrated(list) + ? list[PROP_DEHYDRATED].children || {} + : list; + return ( +
      + {Array.isArray(value) + ? value.map((item, index) => ( + + )) + : Object.entries(value).map((entry) => ( + + ))} +
    + ); +} + +interface PropItemProps { + propValue: any; + propName?: string; + standalone?: boolean; +} + +export function PropItem({ + propValue, + propName, + standalone, +}: PropItemProps): React.ReactElement { + const [expanded, setExpanded] = React.useState(false); + + const handleClick = React.useCallback(() => { + setExpanded(!expanded); + }, [expanded]); + + const hasChildren = checkHasChildren(propValue); + + return React.createElement( + standalone ? "div" : "li", + { + className: classNames("prop-item", { expanded }), + }, + <> +
    + {(!standalone || hasChildren) && ( + + )} + {!standalone && ( + <> + {propName} + :{" "} + + )} + + + +
    + {hasChildren && expanded && } + + ); +} diff --git a/src/components/PropView.spec.tsx b/src/panel/components/PropView.spec.tsx similarity index 76% rename from src/components/PropView.spec.tsx rename to src/panel/components/PropView.spec.tsx index 99ad07f..04f47c1 100644 --- a/src/components/PropView.spec.tsx +++ b/src/panel/components/PropView.spec.tsx @@ -2,7 +2,7 @@ import React from "react"; import { mount } from "enzyme"; import { PropView } from "./PropView"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; -import { PropTree } from "./PropTree"; +import { PropList } from "./PropList"; jest.mock("../libs/SelectedBrickContext"); @@ -10,10 +10,13 @@ jest.mock("../libs/SelectedBrickContext"); const mockEval = jest.fn((string: string, fn: Function): void => { fn({ - properties: { - quality: "good", + info: { + properties: { + quality: "good", + }, + events: [["click", { action: "console.log" }]], }, - events: ["click"], + repo: [], }); }); @@ -46,12 +49,12 @@ describe("PropView", () => { expect(mockEval.mock.calls[0][0]).toBe( "window.__BRICK_NEXT_DEVTOOLS_HOOK__.getBrickInfo(1)" ); - expect(wrapper.find(PropTree).at(0).prop("properties")).toEqual({ + expect(wrapper.find(PropList).at(0).prop("list")).toEqual({ quality: "good", }); - expect( - (wrapper.find(PropTree).at(1).prop("properties") as string[])[0][0] - ).toBe("click"); + expect(wrapper.find(PropList).at(1).prop("list")).toEqual([ + ["click", { action: "console.log" }], + ]); (useSelectedBrickContext as jest.Mock).mockReset(); }); diff --git a/src/components/PropView.tsx b/src/panel/components/PropView.tsx similarity index 62% rename from src/components/PropView.tsx rename to src/panel/components/PropView.tsx index 81088ba..1d5bf6f 100644 --- a/src/components/PropView.tsx +++ b/src/panel/components/PropView.tsx @@ -1,16 +1,10 @@ import React from "react"; import { Tag } from "@blueprintjs/core"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; -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 -} +import { HOOK_NAME } from "../../shared/constants"; +import { BrickInfo, DehydratedBrickInfo } from "../../shared/interfaces"; +import { hydrate } from "../libs/hydrate"; +import { PropList } from "./PropList"; export function PropView(): React.ReactElement { const { selectedBrick } = useSelectedBrickContext(); @@ -20,13 +14,13 @@ export function PropView(): React.ReactElement { if (selectedBrick) { chrome.devtools.inspectedWindow.eval( `window.${HOOK_NAME}.getBrickInfo(${selectedBrick.uid})`, - function (result: BrickInfo, error) { + function (result: DehydratedBrickInfo, error) { // istanbul ignore if if (error) { - console.error("getBrickInfo()", error); + console.error("getBrickInfo()", error, result); } - setBrickInfo(result); + setBrickInfo(hydrate(result.info, result.repo)); } ); } @@ -43,15 +37,13 @@ export function PropView(): React.ReactElement { properties
    - +
    events
    - [item, noop])} - /> +
    diff --git a/src/components/SelectedBrickToolbar.spec.tsx b/src/panel/components/SelectedBrickToolbar.spec.tsx similarity index 100% rename from src/components/SelectedBrickToolbar.spec.tsx rename to src/panel/components/SelectedBrickToolbar.spec.tsx diff --git a/src/components/SelectedBrickToolbar.tsx b/src/panel/components/SelectedBrickToolbar.tsx similarity index 96% rename from src/components/SelectedBrickToolbar.tsx rename to src/panel/components/SelectedBrickToolbar.tsx index 04afd29..c5f00fc 100644 --- a/src/components/SelectedBrickToolbar.tsx +++ b/src/panel/components/SelectedBrickToolbar.tsx @@ -1,7 +1,7 @@ import React from "react"; import classNames from "classnames"; import { Button, ButtonGroup, Classes, Tooltip } from "@blueprintjs/core"; -import { HOOK_NAME } from "../shared"; +import { HOOK_NAME } from "../../shared/constants"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; export function SelectedBrickToolbar(): React.ReactElement { diff --git a/src/components/SelectedBrickWrapper.spec.tsx b/src/panel/components/SelectedBrickWrapper.spec.tsx similarity index 100% rename from src/components/SelectedBrickWrapper.spec.tsx rename to src/panel/components/SelectedBrickWrapper.spec.tsx diff --git a/src/components/SelectedBrickWrapper.tsx b/src/panel/components/SelectedBrickWrapper.tsx similarity index 100% rename from src/components/SelectedBrickWrapper.tsx rename to src/panel/components/SelectedBrickWrapper.tsx diff --git a/src/panel/components/TransformationsPanel.spec.tsx b/src/panel/components/TransformationsPanel.spec.tsx new file mode 100644 index 0000000..eae2fee --- /dev/null +++ b/src/panel/components/TransformationsPanel.spec.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { shallow } from "enzyme"; +import { Button } from "@blueprintjs/core"; +import { TransformationsPanel } from "./TransformationsPanel"; +import { useTransformationsContext } from "../libs/TransformationsContext"; + +jest.mock("../libs/TransformationsContext"); +const setTransformations = jest.fn(); +(useTransformationsContext as jest.Mock).mockReturnValue({ + transformations: [ + { + transform: "quality", + result: { + quality: "good", + }, + data: "good", + options: {}, + }, + ], + setTransformations, +}); + +describe("TransformationsPanel", () => { + afterEach(() => { + setTransformations.mockClear(); + }); + + it("should work", () => { + const wrapper = shallow(); + expect(wrapper.find("tbody").find("tr").length).toBe(1); + }); + + it("should handle clear", () => { + const wrapper = shallow(); + wrapper.find(Button).invoke("onClick")(null); + expect(setTransformations).toBeCalled(); + }); +}); diff --git a/src/panel/components/TransformationsPanel.tsx b/src/panel/components/TransformationsPanel.tsx new file mode 100644 index 0000000..d0195ab --- /dev/null +++ b/src/panel/components/TransformationsPanel.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { Button, Tooltip, ButtonGroup } from "@blueprintjs/core"; +import { PanelSelector } from "./PanelSelector"; +import { useTransformationsContext } from "../libs/TransformationsContext"; +import { PropItem } from "./PropList"; + +export function TransformationsPanel(): React.ReactElement { + const { transformations, setTransformations } = useTransformationsContext(); + + const handleClear = React.useCallback(() => { + setTransformations([]); + }, [setTransformations]); + + return ( +
    +
    + + + +
    +
    +
    + + + + + + + + + + + {transformations.map((item, key) => ( + + + + + + + ))} + +
    TransformResultDataOptions
    + + + + + + + +
    +
    +
    +
    + ); +} diff --git a/src/components/TreeToolbar.spec.tsx b/src/panel/components/TreeToolbar.spec.tsx similarity index 97% rename from src/components/TreeToolbar.spec.tsx rename to src/panel/components/TreeToolbar.spec.tsx index f4a93ff..f714f85 100644 --- a/src/components/TreeToolbar.spec.tsx +++ b/src/panel/components/TreeToolbar.spec.tsx @@ -6,7 +6,7 @@ import { useBrickTreeContext } from "../libs/BrickTreeContext"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; import { useCollapsedBrickIdsContext } from "../libs/CollapsedBrickIdsContext"; import { useShowFullNameContext } from "../libs/ShowFullNameContext"; -import { MESSAGE_SOURCE_HOOK } from "../shared"; +import { MESSAGE_SOURCE_HOOK } from "../../shared/constants"; jest.mock("../libs/BrickTreeContext"); jest.mock("../libs/SelectedBrickContext"); diff --git a/src/components/TreeToolbar.tsx b/src/panel/components/TreeToolbar.tsx similarity index 91% rename from src/components/TreeToolbar.tsx rename to src/panel/components/TreeToolbar.tsx index d9ba500..6d0056b 100644 --- a/src/components/TreeToolbar.tsx +++ b/src/panel/components/TreeToolbar.tsx @@ -1,11 +1,12 @@ import React from "react"; import { Button, ButtonGroup, Tooltip, Switch } from "@blueprintjs/core"; -import { HOOK_NAME, MESSAGE_SOURCE_HOOK } from "../shared"; +import { HOOK_NAME, MESSAGE_SOURCE_HOOK } from "../../shared/constants"; import { useBrickTreeContext } from "../libs/BrickTreeContext"; import { useSelectedBrickContext } from "../libs/SelectedBrickContext"; -import { BricksByMountPoint } from "../libs/interfaces"; +import { BricksByMountPoint } from "../../shared/interfaces"; import { useCollapsedBrickIdsContext } from "../libs/CollapsedBrickIdsContext"; import { useShowFullNameContext } from "../libs/ShowFullNameContext"; +import { PanelSelector } from "./PanelSelector"; export function TreeToolbar(): React.ReactElement { const { setTree } = useBrickTreeContext(); @@ -55,6 +56,7 @@ export function TreeToolbar(): React.ReactElement { return (
    +