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 (
);
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"