diff --git a/package.json b/package.json index 0d912b612b..1786de1407 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "chrome-launch": "^1.1.4", "classnames": "2.2.1", "cli-spinners": "^1.0.0", - "clipboard-js": "^0.3.3", + "clipboard-js": "^0.3.6", "css-loader": "^1.0.1", "error-stack-parser": "^2.0.2", "es6-symbol": "3.0.2", diff --git a/src/devtools/views/ButtonIcon.js b/src/devtools/views/ButtonIcon.js index 49935bb24c..031905d49e 100644 --- a/src/devtools/views/ButtonIcon.js +++ b/src/devtools/views/ButtonIcon.js @@ -7,6 +7,7 @@ type Props = {| type: | 'back' | 'close' + | 'copy' | 'down' | 'more' | 'search' @@ -25,6 +26,9 @@ export default function ButtonIcon({ type }: Props) { case 'close': pathData = PATH_CLOSE; break; + case 'copy': + pathData = PATH_COPY; + break; case 'down': pathData = PATH_DOWN; break; @@ -74,6 +78,11 @@ const PATH_BACK = ` const PATH_CLOSE = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'; +const PATH_COPY = ` + M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3a2 2 0 0 0 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 + 2v10a2 2 0 0 0 2 2h10c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z +`; + const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'; const PATH_MORE = ` diff --git a/src/devtools/views/HooksTree.css b/src/devtools/views/HooksTree.css index 255e3fe0ad..ad07918e4a 100644 --- a/src/devtools/views/HooksTree.css +++ b/src/devtools/views/HooksTree.css @@ -7,7 +7,13 @@ padding-left: 0.75rem; } +.HeaderRow { + display: flex; + align-items: center; +} + .Header { + flex: 1 1; font-family: var(--font-family-sans); } diff --git a/src/devtools/views/HooksTree.js b/src/devtools/views/HooksTree.js index 4c9434b55d..bd97344067 100644 --- a/src/devtools/views/HooksTree.js +++ b/src/devtools/views/HooksTree.js @@ -1,9 +1,13 @@ // @flow -import React, { useContext } from 'react'; +import { copy } from 'clipboard-js'; +import React, { useCallback, useContext } from 'react'; import { BridgeContext, StoreContext } from './context'; +import Button from './Button'; +import ButtonIcon from './ButtonIcon'; import EditableValue from './EditableValue'; import KeyValue from './KeyValue'; +import { serializeHooksForCopy } from './utils'; import styles from './HooksTree.css'; import type { HooksNode, HooksTree } from 'src/backend/types'; @@ -15,12 +19,21 @@ type HooksTreeViewProps = {| |}; export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) { + const handleCopy = useCallback(() => copy(serializeHooksForCopy(hooks)), [ + hooks, + ]); + if (hooks === null) { return null; } else { return (
-
hooks
+
+
hooks
+ +
); diff --git a/src/devtools/views/InspectedElementTree.css b/src/devtools/views/InspectedElementTree.css index 6b8278df4b..49b6186659 100644 --- a/src/devtools/views/InspectedElementTree.css +++ b/src/devtools/views/InspectedElementTree.css @@ -6,7 +6,13 @@ border-top: none; } +.HeaderRow { + display: flex; + align-items: center; +} + .Header { + flex: 1 1; font-family: var(--font-family-sans); } diff --git a/src/devtools/views/InspectedElementTree.js b/src/devtools/views/InspectedElementTree.js index 012d78875b..5b14f48fb6 100644 --- a/src/devtools/views/InspectedElementTree.js +++ b/src/devtools/views/InspectedElementTree.js @@ -1,7 +1,11 @@ // @flow -import React from 'react'; +import { copy } from 'clipboard-js'; +import React, { useCallback } from 'react'; +import Button from './Button'; +import ButtonIcon from './ButtonIcon'; import KeyValue from './KeyValue'; +import { serializeDataForCopy } from './utils'; import styles from './InspectedElementTree.css'; type OverrideValueFn = (path: Array, value: any) => void; @@ -21,13 +25,24 @@ export default function InspectedElementTree({ }: Props) { const isEmpty = data === null || Object.keys(data).length === 0; + const handleCopy = useCallback(() => copy(serializeDataForCopy(data)), [ + data, + ]); + if (isEmpty && !showWhenEmpty) { return null; } else { // TODO Add click and key handlers for toggling element open/close state. return (
-
{label}
+
+
{label}
+ {!isEmpty && ( + + )} +
{isEmpty &&
None
} {!isEmpty && Object.keys((data: any)).map(name => ( diff --git a/src/devtools/views/utils.js b/src/devtools/views/utils.js index cbd08456c6..8d18ece1ab 100644 --- a/src/devtools/views/utils.js +++ b/src/devtools/views/utils.js @@ -3,6 +3,8 @@ import escapeStringRegExp from 'escape-string-regexp'; import { meta } from '../../hydration'; +import type { HooksTree } from 'src/backend/types'; + export function createRegExp(string: string): RegExp { return new RegExp(escapeStringRegExp(string), 'i'); } @@ -29,3 +31,58 @@ export function getMetaValueLabel(data: Object): string | null { return null; } } + +function sanitize(data: Object): void { + for (const key in data) { + const value = data[key]; + + if (value && value[meta.type]) { + data[key] = getMetaValueLabel(value); + } else if (value != null) { + if (Array.isArray(value)) { + sanitize(value); + } else if (typeof value === 'object') { + sanitize(value); + } + } + } +} + +export function serializeDataForCopy(props: Object): string { + const cloned = Object.assign({}, props); + + sanitize(cloned); + + try { + return JSON.stringify(cloned, null, 2); + } catch (error) { + return ''; + } +} + +export function serializeHooksForCopy(hooks: HooksTree | null): string { + // $FlowFixMe "HooksTree is not an object" + const cloned = Object.assign([], hooks); + + const queue = [...cloned]; + + while (queue.length > 0) { + const current = queue.pop(); + + // These aren't meaningful + delete current.isEditable; + delete current.index; + + if (current.subHooks.length > 0) { + queue.push(...current.subHooks); + } + } + + sanitize(cloned); + + try { + return JSON.stringify(cloned, null, 2); + } catch (error) { + return ''; + } +} diff --git a/yarn.lock b/yarn.lock index e1d0713f31..4dc51cfd6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2636,9 +2636,10 @@ cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" -clipboard-js@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/clipboard-js/-/clipboard-js-0.3.3.tgz#62264c17c80ebf84f4022286f032501e890b8922" +clipboard-js@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/clipboard-js/-/clipboard-js-0.3.6.tgz#6260add69b5318fde40b80f9d3c8c863efdaf339" + integrity sha512-hyrmvbrYCeRBHdiR3KrEz0tmrUTXXEU8HLeGW0Y0icUSwYmAsmc+d6wfE4EDb/TxZmAVJG0eTfMlulCIT+ecfw== cliui@^2.1.0: version "2.1.0"