From c963476acc31cb1c7a3ccfa7249ef44cd3e12375 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 17:22:12 +0200 Subject: [PATCH 01/15] Extracted sanitizeForParse --- .../react-devtools-shared/src/devtools/utils.js | 13 +++++++++++++ .../Components/NativeStyleEditor/StyleEditor.js | 14 +------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index aaffcad56e272..6cd23d4dbf3ce 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -82,3 +82,16 @@ export function printStore(store: Store, includeWeight: boolean = false) { return snapshotLines.join('\n'); } + +// We use JSON.parse to parse string values +// e.g. 'foo' is not valid JSON but it is a valid string +// so this method replaces e.g. 'foo' with "foo" +export function sanitizeForParse(value: any) { + if (typeof value === 'string') { + if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") { + return '"' + value.substr(1, value.length - 2) + '"'; + } + } + + return value; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js index 2a08e99cf4e71..8e0210d44b49c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/StyleEditor.js @@ -19,6 +19,7 @@ import ButtonIcon from '../../ButtonIcon'; import {serializeDataForCopy} from '../../utils'; import AutoSizeInput from './AutoSizeInput'; import styles from './StyleEditor.css'; +import {sanitizeForParse} from '../../../utils'; import type {Style} from './types'; @@ -290,16 +291,3 @@ function Field({ /> ); } - -// We use JSON.parse to parse string values -// e.g. 'foo' is not valid JSON but it is a valid string -// so this method replaces e.g. 'foo' with "foo" -function sanitizeForParse(value: any) { - if (typeof value === 'string') { - if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") { - return '"' + value.substr(1, value.length - 2) + '"'; - } - } - - return value; -} From 4470a5bd67ba9fc0ec9bcbf597095ef91bbe2b67 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 17:22:56 +0200 Subject: [PATCH 02/15] Added canAddEntries flag to InspectedElementTree --- .../src/devtools/views/Components/SelectedElement.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js index a553f494af1a9..ff848c7a943c2 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js @@ -357,6 +357,7 @@ function InspectedElementView({ inspectPath={inspectPropsPath} overrideValueFn={overridePropsFn} showWhenEmpty={true} + canAddEntries={true} /> {type === ElementTypeSuspense ? ( Date: Sat, 7 Sep 2019 17:24:50 +0200 Subject: [PATCH 03/15] Added EditableKey component. --- .../devtools/views/Components/EditableKey.css | 15 ++++ .../devtools/views/Components/EditableKey.js | 80 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css create mode 100644 packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css new file mode 100644 index 0000000000000..7bd2ca428aa43 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css @@ -0,0 +1,15 @@ +.Input { + width: 100px; + background: none; + border: 1px solid transparent; + color: var(--color-attribute-name); + border-radius: 0.125rem; + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); +} + +.Input:focus { + color: var(--color-attribute-editable-value); + background-color: var(--color-button-background-focus); + outline: none; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js new file mode 100644 index 0000000000000..a9cffd2c4001c --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import React, {Fragment, useRef, useCallback, useEffect, useState} from 'react'; +import styles from './EditableKey.css'; + +type OverrideKeyFn = (path: Array, value: any) => void; + +type EditableKeyProps = {| + key?: string, + overrideKeyFn: OverrideKeyFn, +|}; + +export default function EditableKey({ + key = '', + overrideKeyFn, +}: EditableKeyProps) { + const [editableKey, setEditableKey] = useState(key); + const [isValid, setIsValid] = useState(false); + const inputRef = useRef(null); + + useEffect( + () => { + if (inputRef.current !== null) { + inputRef.current.focus(); + } + }, + [inputRef], + ); + + const handleChange = useCallback( + ({target}) => { + const value = target.value.trim(); + + if (value) { + setIsValid(true); + } else { + setIsValid(false); + } + + setEditableKey(value); + }, + [overrideKeyFn], + ); + + const handleKeyDown = useCallback( + event => { + // Prevent keydown events from e.g. change selected element in the tree + event.stopPropagation(); + + const eventKey = event.key; + + if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) { + overrideKeyFn(editableKey); + } else if (eventKey === 'Escape') { + setEditableKey(key); + } + }, + [editableKey, setEditableKey, isValid, key, overrideKeyFn], + ); + + return ( + + + + ); +} From 33611461a2e2f421545abf9ec4f239177c142731 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 17:25:53 +0200 Subject: [PATCH 04/15] Added support to add an additional entry. --- .../views/Components/InspectedElementTree.css | 5 ++ .../views/Components/InspectedElementTree.js | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css index 620fbf412dd04..4bb01abfb76ad 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css @@ -46,3 +46,8 @@ font-style: italic; padding-left: 0.75rem; } + +.AddEntry { + display: flex; + padding-left: 0.9rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index 1acc27af5a82c..912d0eb84b407 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -8,10 +8,12 @@ */ import {copy} from 'clipboard-js'; -import React, {useCallback} from 'react'; +import React, {useEffect, useCallback, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import KeyValue from './KeyValue'; +import EditableKey from './EditableKey'; +import EditableValue from './EditableValue'; import {alphaSortEntries, serializeDataForCopy} from '../utils'; import styles from './InspectedElementTree.css'; @@ -25,6 +27,7 @@ type Props = {| label: string, overrideValueFn?: ?OverrideValueFn, showWhenEmpty?: boolean, + canAddEntries?: boolean, |}; export default function InspectedElementTree({ @@ -32,12 +35,22 @@ export default function InspectedElementTree({ inspectPath, label, overrideValueFn, + canAddEntries = false, showWhenEmpty = false, }: Props) { - const entries = data != null ? Object.entries(data) : null; - if (entries !== null) { - entries.sort(alphaSortEntries); - } + const [entries, setEntries] = useState(null); + const [entryToAdd, setEntryToAdd] = useState(null); + + useEffect( + () => { + if (data != null) { + setEntries(Object.entries(data).sort(alphaSortEntries)); + } else { + setEntries(null); + } + }, + [data], + ); const isEmpty = entries === null || entries.length === 0; @@ -46,6 +59,34 @@ export default function InspectedElementTree({ [data], ); + const handleEntryAdd = useCallback( + () => { + setEntryToAdd({ + key: null, + value: '', + }); + }, + [setEntryToAdd], + ); + + const handleEntryAddName = useCallback( + key => { + setEntryToAdd({ + ...entryToAdd, + key, + }); + }, + [entryToAdd, setEntryToAdd], + ); + + const handleEntryAddValue = useCallback( + (...args) => { + setEntryToAdd(null); + overrideValueFn(...args); + }, + [overrideValueFn, setEntryToAdd], + ); + if (isEmpty && !showWhenEmpty) { return null; } else { @@ -58,6 +99,11 @@ export default function InspectedElementTree({ )} + {canAddEntries && ( + + )} {isEmpty &&
None
} {!isEmpty && @@ -73,6 +119,17 @@ export default function InspectedElementTree({ value={value} /> ))} + {entryToAdd && ( +
+ : + +
+ )} ); } From 9661a48992767b0920f31ceffa6afba954738c1f Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 17:27:30 +0200 Subject: [PATCH 05/15] Added support to add more complex data structures in the EditableValue component. Added support to change the dataType of the value that is being changed. --- .../views/Components/EditableValue.css | 18 ++++- .../views/Components/EditableValue.js | 78 ++++++++++--------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css index 3087b852617e0..30fa8a43ee657 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css @@ -19,7 +19,23 @@ font-family: var(--font-family-monospace); font-size: var(--font-size-monospace-normal); } -.Input:focus { + +.Invalid { + flex: 1 1; + background: none; + border: 1px solid transparent; + color: var(--color-attribute-editable-value); + border-radius: 0.125rem; + font-family: var(--font-family-monospace); + font-size: var(--font-size-monospace-normal); + background-color: var(--color-background-invalid); + color: var(--color-text-invalid); + + --color-border: var(--color-text-invalid); +} + +.Input:focus, +.Invalid:focus { background-color: var(--color-button-background-focus); outline: none; } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index 46bc177763fe0..412edc4820722 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -7,10 +7,11 @@ * @flow */ -import React, {Fragment, useCallback, useRef, useState} from 'react'; +import React, {Fragment, useEffect, useCallback, useRef, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import styles from './EditableValue.css'; +import {sanitizeForParse} from '../../utils'; type OverrideValueFn = (path: Array, value: any) => void; @@ -27,20 +28,38 @@ export default function EditableValue({ path, value, }: EditableValueProps) { + const [isValid, setIsValid] = useState(true); const [hasPendingChanges, setHasPendingChanges] = useState(false); - const [editableValue, setEditableValue] = useState(value); + const [stringifiedValue, setStringifiedValue] = useState( + JSON.stringify(value), + ); + const [editableValue, setEditableValue] = useState(stringifiedValue); const inputRef = useRef(null); - if (hasPendingChanges && editableValue === value) { + useEffect( + () => { + setStringifiedValue(JSON.stringify(value)); + }, + [value], + ); + + if (hasPendingChanges && editableValue === stringifiedValue) { setHasPendingChanges(false); } const handleChange = useCallback( ({target}) => { if (dataType === 'boolean') { - setEditableValue(target.checked); + setEditableValue(JSON.stringify(target.checked)); overrideValueFn(path, target.checked); } else { + let isValidJSON = false; + try { + JSON.parse(sanitizeForParse(target.value)); + isValidJSON = true; + } catch (error) {} + + setIsValid(isValidJSON); setEditableValue(target.value); } setHasPendingChanges(true); @@ -50,14 +69,15 @@ export default function EditableValue({ const handleReset = useCallback( () => { - setEditableValue(value); + setEditableValue(stringifiedValue); setHasPendingChanges(false); + setIsValid(true); if (inputRef.current !== null) { inputRef.current.focus(); } }, - [value], + [stringifiedValue], ); const handleKeyDown = useCallback( @@ -67,47 +87,35 @@ export default function EditableValue({ const {key} = event; - if (key === 'Enter') { - if (dataType === 'number') { - const parsedValue = parseFloat(editableValue); - if (!Number.isNaN(parsedValue)) { - overrideValueFn(path, parsedValue); - } - } else { - overrideValueFn(path, editableValue); + if (key === 'Enter' && isValid) { + const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue)); + + if (value !== parsedEditableValue) { + overrideValueFn(path, parsedEditableValue); } // Don't reset the pending change flag here. // The inspected fiber won't be updated until after the next "inspectElement" message. // We'll reset that flag during a subsequent render. } else if (key === 'Escape') { - setEditableValue(value); + setEditableValue(stringifiedValue); setHasPendingChanges(false); + setIsValid(true); } }, - [editableValue, dataType, overrideValueFn, path, value], + [editableValue, isValid, dataType, overrideValueFn, path, value], ); - // Render different input types based on the dataType - let type = 'text'; - if (dataType === 'boolean') { - type = 'checkbox'; - } else if (dataType === 'number') { - type = 'number'; - } - - let inputValue = value == null ? '' : value; + let inputValue = value === undefined ? '' : stringifiedValue; if (hasPendingChanges) { - inputValue = editableValue == null ? '' : editableValue; + inputValue = editableValue; } let placeholder = ''; - if (value === null) { - placeholder = '(null)'; - } else if (value === undefined) { + if (value === undefined) { placeholder = '(undefined)'; - } else if (dataType === 'string') { - placeholder = '(string)'; + } else { + placeholder = 'Enter valid JSON'; } return ( @@ -115,23 +123,23 @@ export default function EditableValue({ {dataType === 'boolean' && ( )} {dataType !== 'boolean' && ( )} From c61c8f7880c3acdff5528cf88f11a3ff179d88b6 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 17:59:16 +0200 Subject: [PATCH 06/15] Fixed flow error. --- .../src/devtools/views/Components/InspectedElementTree.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index 912d0eb84b407..41a13499fa1af 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -82,7 +82,10 @@ export default function InspectedElementTree({ const handleEntryAddValue = useCallback( (...args) => { setEntryToAdd(null); - overrideValueFn(...args); + + if (typeof overrideValueFn === 'function') { + overrideValueFn(...args); + } }, [overrideValueFn, setEntryToAdd], ); From 36c4905f031844de468faae06f307e0658dbf163 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Sat, 7 Sep 2019 19:36:27 +0200 Subject: [PATCH 07/15] Removed unneeded fragment. --- .../devtools/views/Components/EditableKey.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js index a9cffd2c4001c..e1d10a244833c 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js @@ -7,7 +7,7 @@ * @flow */ -import React, {Fragment, useRef, useCallback, useEffect, useState} from 'react'; +import React, {useRef, useCallback, useEffect, useState} from 'react'; import styles from './EditableKey.css'; type OverrideKeyFn = (path: Array, value: any) => void; @@ -66,15 +66,13 @@ export default function EditableKey({ ); return ( - - - + ); } From a30cfa4bd1f66f9319a26ac74c63054406b7aa44 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 09:11:22 +0200 Subject: [PATCH 08/15] Renamed EditableKey -> EditableName --- .../{EditableKey.css => EditableName.css} | 0 .../{EditableKey.js => EditableName.js} | 30 +++++++++---------- .../views/Components/InspectedElementTree.js | 4 +-- .../src/app/EditableProps/index.js | 11 ++++++- 4 files changed, 27 insertions(+), 18 deletions(-) rename packages/react-devtools-shared/src/devtools/views/Components/{EditableKey.css => EditableName.css} (100%) rename packages/react-devtools-shared/src/devtools/views/Components/{EditableKey.js => EditableName.js} (69%) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css similarity index 100% rename from packages/react-devtools-shared/src/devtools/views/Components/EditableKey.css rename to packages/react-devtools-shared/src/devtools/views/Components/EditableName.css diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js similarity index 69% rename from packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js rename to packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index e1d10a244833c..3d2e6cb332dbb 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableKey.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -8,20 +8,20 @@ */ import React, {useRef, useCallback, useEffect, useState} from 'react'; -import styles from './EditableKey.css'; +import styles from './EditableName.css'; -type OverrideKeyFn = (path: Array, value: any) => void; +type OverrideNameFn = (path: Array, value: any) => void; -type EditableKeyProps = {| +type EditableNameProps = {| key?: string, - overrideKeyFn: OverrideKeyFn, + overrideNameFn: OverrideNameFn, |}; -export default function EditableKey({ - key = '', - overrideKeyFn, -}: EditableKeyProps) { - const [editableKey, setEditableKey] = useState(key); +export default function EditableName({ + name = '', + overrideNameFn, +}: EditableNameProps) { + const [editableName, setEditableName] = useState(name); const [isValid, setIsValid] = useState(false); const inputRef = useRef(null); @@ -44,9 +44,9 @@ export default function EditableKey({ setIsValid(false); } - setEditableKey(value); + setEditableName(value); }, - [overrideKeyFn], + [overrideNameFn], ); const handleKeyDown = useCallback( @@ -57,12 +57,12 @@ export default function EditableKey({ const eventKey = event.key; if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) { - overrideKeyFn(editableKey); + overrideNameFn(editableName); } else if (eventKey === 'Escape') { - setEditableKey(key); + setEditableName(name); } }, - [editableKey, setEditableKey, isValid, key, overrideKeyFn], + [editableName, setEditableName, isValid, name, overrideNameFn], ); return ( @@ -72,7 +72,7 @@ export default function EditableKey({ onKeyDown={handleKeyDown} ref={inputRef} type="text" - value={editableKey} + value={editableName} /> ); } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index 41a13499fa1af..55e0702125ee4 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -12,7 +12,7 @@ import React, {useEffect, useCallback, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import KeyValue from './KeyValue'; -import EditableKey from './EditableKey'; +import EditableName from './EditableName'; import EditableValue from './EditableValue'; import {alphaSortEntries, serializeDataForCopy} from '../utils'; import styles from './InspectedElementTree.css'; @@ -124,7 +124,7 @@ export default function InspectedElementTree({ ))} {entryToAdd && (
- : + :

Editable props

Class - + Function Memoized Class From 578947267fde93118e709f7d4cbd843e80b7957a Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 09:12:11 +0200 Subject: [PATCH 09/15] Removed unneeded dependency --- .../src/devtools/views/Components/EditableName.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index 3d2e6cb332dbb..d18c5694df0bf 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -31,7 +31,7 @@ export default function EditableName({ inputRef.current.focus(); } }, - [inputRef], + [], ); const handleChange = useCallback( From 20280fbe3ff64c36ca6c91c1eda08fb299350868 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 09:59:28 +0200 Subject: [PATCH 10/15] Removed problematic props to state hook. --- .../devtools/views/Components/EditableName.js | 6 +-- .../views/Components/EditableValue.js | 40 +++++++++---------- .../views/Components/InspectedElementTree.js | 2 +- .../src/devtools/views/Components/KeyValue.js | 3 +- 4 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index d18c5694df0bf..adfea2640629e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -13,15 +13,15 @@ import styles from './EditableName.css'; type OverrideNameFn = (path: Array, value: any) => void; type EditableNameProps = {| - key?: string, + initialValue?: string, overrideNameFn: OverrideNameFn, |}; export default function EditableName({ - name = '', + initialValue = '', overrideNameFn, }: EditableNameProps) { - const [editableName, setEditableName] = useState(name); + const [editableName, setEditableName] = useState(initialValue); const [isValid, setIsValid] = useState(false); const inputRef = useRef(null); diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index 412edc4820722..a6b2e6abe7d85 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -19,38 +19,34 @@ type EditableValueProps = {| dataType: string, overrideValueFn: OverrideValueFn, path: Array, - value: any, + initialValue: any, |}; +function useEditableValue(initialValue) { + const [editableValue, setEditableValue] = useState(JSON.stringify(initialValue)); + + return [editableValue, (value, {shouldStringify} = {}) => (shouldStringify ? setEditableValue(JSON.stringify(value)) : setEditableValue(value))]; +} + export default function EditableValue({ dataType, overrideValueFn, path, - value, + initialValue, }: EditableValueProps) { const [isValid, setIsValid] = useState(true); const [hasPendingChanges, setHasPendingChanges] = useState(false); - const [stringifiedValue, setStringifiedValue] = useState( - JSON.stringify(value), - ); - const [editableValue, setEditableValue] = useState(stringifiedValue); + const [editableValue, setEditableValue] = useEditableValue(initialValue); const inputRef = useRef(null); - useEffect( - () => { - setStringifiedValue(JSON.stringify(value)); - }, - [value], - ); - - if (hasPendingChanges && editableValue === stringifiedValue) { + if (hasPendingChanges && editableValue === JSON.stringify(initialValue)) { setHasPendingChanges(false); } const handleChange = useCallback( ({target}) => { if (dataType === 'boolean') { - setEditableValue(JSON.stringify(target.checked)); + setEditableValue(target.checked, {shouldStringify: true}); overrideValueFn(path, target.checked); } else { let isValidJSON = false; @@ -69,7 +65,7 @@ export default function EditableValue({ const handleReset = useCallback( () => { - setEditableValue(stringifiedValue); + setEditableValue(initialValue, {shouldStringify: true}); setHasPendingChanges(false); setIsValid(true); @@ -77,7 +73,7 @@ export default function EditableValue({ inputRef.current.focus(); } }, - [stringifiedValue], + [initialValue], ); const handleKeyDown = useCallback( @@ -90,7 +86,7 @@ export default function EditableValue({ if (key === 'Enter' && isValid) { const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue)); - if (value !== parsedEditableValue) { + if (initialValue !== parsedEditableValue) { overrideValueFn(path, parsedEditableValue); } @@ -98,21 +94,21 @@ export default function EditableValue({ // The inspected fiber won't be updated until after the next "inspectElement" message. // We'll reset that flag during a subsequent render. } else if (key === 'Escape') { - setEditableValue(stringifiedValue); + setEditableValue(initialValue, {shouldStringify: true}); setHasPendingChanges(false); setIsValid(true); } }, - [editableValue, isValid, dataType, overrideValueFn, path, value], + [editableValue, isValid, dataType, overrideValueFn, path, initialValue], ); - let inputValue = value === undefined ? '' : stringifiedValue; + let inputValue = initialValue === undefined ? '' : JSON.stringify(initialValue); if (hasPendingChanges) { inputValue = editableValue; } let placeholder = ''; - if (value === undefined) { + if (initialValue === undefined) { placeholder = '(undefined)'; } else { placeholder = 'Enter valid JSON'; diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index 55e0702125ee4..54f9aec97fb1e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -129,7 +129,7 @@ export default function InspectedElementTree({ dataType={typeof entryToAdd.value} overrideValueFn={handleEntryAddValue} path={[entryToAdd.key]} - value={entryToAdd.value} + initialValue={entryToAdd.value} />
)} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index 8844a13f6807d..5480916384421 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -16,6 +16,7 @@ import {meta} from '../../../hydration'; import styles from './KeyValue.css'; import type {InspectPath} from './SelectedElement'; +import EditableName from "./EditableName"; type OverrideValueFn = (path: Array, value: any) => void; @@ -105,7 +106,7 @@ export default function KeyValue({ dataType={dataType} overrideValueFn={((overrideValueFn: any): OverrideValueFn)} path={path} - value={value} + initialValue={value} /> ) : ( {displayValue} From c53c79a0158fe2ed77daf8e589e37f5046673971 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 10:09:55 +0200 Subject: [PATCH 11/15] Prettified changes. --- .../devtools/views/Components/EditableName.js | 17 +++++------ .../views/Components/EditableValue.js | 30 ++++++++++++++----- .../src/devtools/views/Components/KeyValue.js | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index adfea2640629e..d96653c603d61 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -25,14 +25,11 @@ export default function EditableName({ const [isValid, setIsValid] = useState(false); const inputRef = useRef(null); - useEffect( - () => { - if (inputRef.current !== null) { - inputRef.current.focus(); - } - }, - [], - ); + useEffect(() => { + if (inputRef.current !== null) { + inputRef.current.focus(); + } + }, []); const handleChange = useCallback( ({target}) => { @@ -59,10 +56,10 @@ export default function EditableName({ if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) { overrideNameFn(editableName); } else if (eventKey === 'Escape') { - setEditableName(name); + setEditableName(initialValue); } }, - [editableName, setEditableName, isValid, name, overrideNameFn], + [editableName, setEditableName, isValid, initialValue, overrideNameFn], ); return ( diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index a6b2e6abe7d85..58f3d8c8f6470 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -7,7 +7,7 @@ * @flow */ -import React, {Fragment, useEffect, useCallback, useRef, useState} from 'react'; +import React, {Fragment, useCallback, useRef, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import styles from './EditableValue.css'; @@ -22,12 +22,6 @@ type EditableValueProps = {| initialValue: any, |}; -function useEditableValue(initialValue) { - const [editableValue, setEditableValue] = useState(JSON.stringify(initialValue)); - - return [editableValue, (value, {shouldStringify} = {}) => (shouldStringify ? setEditableValue(JSON.stringify(value)) : setEditableValue(value))]; -} - export default function EditableValue({ dataType, overrideValueFn, @@ -102,7 +96,8 @@ export default function EditableValue({ [editableValue, isValid, dataType, overrideValueFn, path, initialValue], ); - let inputValue = initialValue === undefined ? '' : JSON.stringify(initialValue); + let inputValue = + initialValue === undefined ? '' : JSON.stringify(initialValue); if (hasPendingChanges) { inputValue = editableValue; } @@ -151,3 +146,22 @@ export default function EditableValue({ ); } + +function useEditableValue(initialValue: any): [any, Function] { + const [editableValue, setEditableValue] = useState( + JSON.stringify(initialValue), + ); + + function setEditableValueWithStringify( + value: any, + {shouldStringify}: Object = {}, + ) { + if (shouldStringify) { + setEditableValue(JSON.stringify(value)); + } + + setEditableValue(value); + } + + return [editableValue, setEditableValueWithStringify]; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index 5480916384421..662a5f0e91a3a 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -16,7 +16,7 @@ import {meta} from '../../../hydration'; import styles from './KeyValue.css'; import type {InspectPath} from './SelectedElement'; -import EditableName from "./EditableName"; +import EditableName from './EditableName'; type OverrideValueFn = (path: Array, value: any) => void; From a5cc62b2c5b849b51732508876ed38554ef15b90 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 10:16:16 +0200 Subject: [PATCH 12/15] Removed unused import. --- .../src/devtools/views/Components/KeyValue.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js index 662a5f0e91a3a..e05a35fc57a7f 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js @@ -16,7 +16,6 @@ import {meta} from '../../../hydration'; import styles from './KeyValue.css'; import type {InspectPath} from './SelectedElement'; -import EditableName from './EditableName'; type OverrideValueFn = (path: Array, value: any) => void; From d4c415adb4e73f026dfc84e07fb3078d6598cd82 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 11:35:18 +0200 Subject: [PATCH 13/15] Fixed shouldStringify check. --- .../src/devtools/views/Components/EditableValue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index 58f3d8c8f6470..fbe98fb348047 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -158,9 +158,9 @@ function useEditableValue(initialValue: any): [any, Function] { ) { if (shouldStringify) { setEditableValue(JSON.stringify(value)); + } else { + setEditableValue(value); } - - setEditableValue(value); } return [editableValue, setEditableValueWithStringify]; From 389010a27e9a0bb0f332dff844ff744b23e1a5e0 Mon Sep 17 00:00:00 2001 From: Hristo Kanchev Date: Tue, 10 Sep 2019 11:56:35 +0200 Subject: [PATCH 14/15] Removed testing props from EditableProps. --- .../src/app/EditableProps/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/react-devtools-shell/src/app/EditableProps/index.js b/packages/react-devtools-shell/src/app/EditableProps/index.js index 9efa1cd5f5070..220a58c17be32 100644 --- a/packages/react-devtools-shell/src/app/EditableProps/index.js +++ b/packages/react-devtools-shell/src/app/EditableProps/index.js @@ -126,16 +126,7 @@ export default function EditableProps() {

Editable props

Class - + Function Memoized Class From 0373472c988b87ba63af25556b770453a31084bd Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 10 Sep 2019 13:19:09 -0700 Subject: [PATCH 15/15] Made some inline tweaks --- .../src/devtools/utils.js | 34 +++- .../views/Components/EditableName.css | 18 +- .../devtools/views/Components/EditableName.js | 35 ++-- .../views/Components/EditableValue.js | 164 +++++------------- .../views/Components/InspectedElementTree.css | 4 +- .../views/Components/InspectedElementTree.js | 77 +++----- .../src/devtools/views/hooks.js | 66 ++++++- 7 files changed, 198 insertions(+), 200 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/utils.js b/packages/react-devtools-shared/src/devtools/utils.js index 6cd23d4dbf3ce..0eb75451f2b6c 100644 --- a/packages/react-devtools-shared/src/devtools/utils.js +++ b/packages/react-devtools-shared/src/devtools/utils.js @@ -88,10 +88,40 @@ export function printStore(store: Store, includeWeight: boolean = false) { // so this method replaces e.g. 'foo' with "foo" export function sanitizeForParse(value: any) { if (typeof value === 'string') { - if (value.charAt(0) === "'" && value.charAt(value.length - 1) === "'") { + if ( + value.length >= 2 && + value.charAt(0) === "'" && + value.charAt(value.length - 1) === "'" + ) { return '"' + value.substr(1, value.length - 2) + '"'; } } - return value; } + +export function smartParse(value: any) { + switch (value) { + case 'Infinity': + return Infinity; + case 'NaN': + return NaN; + case 'undefined': + return undefined; + default: + return JSON.parse(sanitizeForParse(value)); + } +} + +export function smartStringify(value: any) { + if (typeof value === 'number') { + if (Number.isNaN(value)) { + return 'NaN'; + } else if (!Number.isFinite(value)) { + return 'Infinity'; + } + } else if (value === undefined) { + return 'undefined'; + } + + return JSON.stringify(value); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css index 7bd2ca428aa43..38b1f1d92b4f8 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.css @@ -1,15 +1,9 @@ .Input { - width: 100px; - background: none; - border: 1px solid transparent; - color: var(--color-attribute-name); - border-radius: 0.125rem; - font-family: var(--font-family-monospace); - font-size: var(--font-size-monospace-normal); + flex: 0 1 auto; + padding: 1px; + box-shadow: 0px 1px 3px transparent; } - .Input:focus { - color: var(--color-attribute-editable-value); - background-color: var(--color-button-background-focus); - outline: none; -} + color: var(--color-text); + box-shadow: 0px 1px 3px var(--color-shadow); +} \ No newline at end of file diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js index d96653c603d61..73b85cddf8c42 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableName.js @@ -7,29 +7,25 @@ * @flow */ -import React, {useRef, useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useState} from 'react'; +import AutoSizeInput from './NativeStyleEditor/AutoSizeInput'; import styles from './EditableName.css'; type OverrideNameFn = (path: Array, value: any) => void; type EditableNameProps = {| + autoFocus?: boolean, initialValue?: string, overrideNameFn: OverrideNameFn, |}; export default function EditableName({ + autoFocus = false, initialValue = '', overrideNameFn, }: EditableNameProps) { const [editableName, setEditableName] = useState(initialValue); const [isValid, setIsValid] = useState(false); - const inputRef = useRef(null); - - useEffect(() => { - if (inputRef.current !== null) { - inputRef.current.focus(); - } - }, []); const handleChange = useCallback( ({target}) => { @@ -51,23 +47,30 @@ export default function EditableName({ // Prevent keydown events from e.g. change selected element in the tree event.stopPropagation(); - const eventKey = event.key; - - if ((eventKey === 'Enter' || eventKey === 'Tab') && isValid) { - overrideNameFn(editableName); - } else if (eventKey === 'Escape') { - setEditableName(initialValue); + switch (event.key) { + case 'Enter': + case 'Tab': + if (isValid) { + overrideNameFn(editableName); + } + break; + case 'Escape': + setEditableName(initialValue); + break; + default: + break; } }, [editableName, setEditableName, isValid, initialValue, overrideNameFn], ); return ( - diff --git a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js index fbe98fb348047..96f505c20f188 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js @@ -7,103 +7,64 @@ * @flow */ -import React, {Fragment, useCallback, useRef, useState} from 'react'; +import React, {Fragment, useCallback, useRef} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import styles from './EditableValue.css'; -import {sanitizeForParse} from '../../utils'; +import {useEditableValue} from '../hooks'; type OverrideValueFn = (path: Array, value: any) => void; type EditableValueProps = {| dataType: string, + initialValue: any, overrideValueFn: OverrideValueFn, path: Array, - initialValue: any, |}; export default function EditableValue({ dataType, + initialValue, overrideValueFn, path, - initialValue, }: EditableValueProps) { - const [isValid, setIsValid] = useState(true); - const [hasPendingChanges, setHasPendingChanges] = useState(false); - const [editableValue, setEditableValue] = useEditableValue(initialValue); const inputRef = useRef(null); - - if (hasPendingChanges && editableValue === JSON.stringify(initialValue)) { - setHasPendingChanges(false); - } - - const handleChange = useCallback( - ({target}) => { - if (dataType === 'boolean') { - setEditableValue(target.checked, {shouldStringify: true}); - overrideValueFn(path, target.checked); - } else { - let isValidJSON = false; - try { - JSON.parse(sanitizeForParse(target.value)); - isValidJSON = true; - } catch (error) {} - - setIsValid(isValidJSON); - setEditableValue(target.value); - } - setHasPendingChanges(true); - }, - [dataType, overrideValueFn, path], - ); - - const handleReset = useCallback( - () => { - setEditableValue(initialValue, {shouldStringify: true}); - setHasPendingChanges(false); - setIsValid(true); - - if (inputRef.current !== null) { - inputRef.current.focus(); - } - }, - [initialValue], - ); + const { + editableValue, + hasPendingChanges, + isValid, + parsedValue, + reset, + update, + } = useEditableValue(initialValue); + + const handleChange = useCallback(({target}) => update(target.value), [ + update, + ]); const handleKeyDown = useCallback( event => { // Prevent keydown events from e.g. change selected element in the tree event.stopPropagation(); - const {key} = event; - - if (key === 'Enter' && isValid) { - const parsedEditableValue = JSON.parse(sanitizeForParse(editableValue)); - - if (initialValue !== parsedEditableValue) { - overrideValueFn(path, parsedEditableValue); - } - - // Don't reset the pending change flag here. - // The inspected fiber won't be updated until after the next "inspectElement" message. - // We'll reset that flag during a subsequent render. - } else if (key === 'Escape') { - setEditableValue(initialValue, {shouldStringify: true}); - setHasPendingChanges(false); - setIsValid(true); + switch (event.key) { + case 'Enter': + if (isValid && hasPendingChanges) { + overrideValueFn(path, parsedValue); + } + break; + case 'Escape': + reset(); + break; + default: + break; } }, - [editableValue, isValid, dataType, overrideValueFn, path, initialValue], + [hasPendingChanges, isValid, overrideValueFn, parsedValue, reset], ); - let inputValue = - initialValue === undefined ? '' : JSON.stringify(initialValue); - if (hasPendingChanges) { - inputValue = editableValue; - } - let placeholder = ''; - if (initialValue === undefined) { + if (editableValue === undefined) { placeholder = '(undefined)'; } else { placeholder = 'Enter valid JSON'; @@ -111,57 +72,24 @@ export default function EditableValue({ return ( - {dataType === 'boolean' && ( - + + {hasPendingChanges && ( + )} - {dataType !== 'boolean' && ( - - )} - {hasPendingChanges && - dataType !== 'boolean' && ( - - )} ); } - -function useEditableValue(initialValue: any): [any, Function] { - const [editableValue, setEditableValue] = useState( - JSON.stringify(initialValue), - ); - - function setEditableValueWithStringify( - value: any, - {shouldStringify}: Object = {}, - ) { - if (shouldStringify) { - setEditableValue(JSON.stringify(value)); - } else { - setEditableValue(value); - } - } - - return [editableValue, setEditableValueWithStringify]; -} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css index 4bb01abfb76ad..76c1e40761019 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css @@ -48,6 +48,8 @@ } .AddEntry { + padding-left: 1rem; + white-space: nowrap; display: flex; - padding-left: 0.9rem; + align-items: center; } diff --git a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js index 54f9aec97fb1e..edfb5f2c5679e 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js @@ -8,7 +8,7 @@ */ import {copy} from 'clipboard-js'; -import React, {useEffect, useCallback, useState} from 'react'; +import React, {useCallback, useState} from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import KeyValue from './KeyValue'; @@ -38,19 +38,13 @@ export default function InspectedElementTree({ canAddEntries = false, showWhenEmpty = false, }: Props) { - const [entries, setEntries] = useState(null); - const [entryToAdd, setEntryToAdd] = useState(null); + const entries = data != null ? Object.entries(data) : null; + if (entries !== null) { + entries.sort(alphaSortEntries); + } - useEffect( - () => { - if (data != null) { - setEntries(Object.entries(data).sort(alphaSortEntries)); - } else { - setEntries(null); - } - }, - [data], - ); + const [newPropKey, setNewPropKey] = useState(0); + const [newPropName, setNewPropName] = useState(''); const isEmpty = entries === null || entries.length === 0; @@ -59,38 +53,23 @@ export default function InspectedElementTree({ [data], ); - const handleEntryAdd = useCallback( - () => { - setEntryToAdd({ - key: null, - value: '', - }); - }, - [setEntryToAdd], - ); - - const handleEntryAddName = useCallback( - key => { - setEntryToAdd({ - ...entryToAdd, - key, - }); - }, - [entryToAdd, setEntryToAdd], - ); + const handleNewEntryValue = useCallback( + (name, value) => { + if (!newPropName) { + return; + } - const handleEntryAddValue = useCallback( - (...args) => { - setEntryToAdd(null); + setNewPropName(''); + setNewPropKey(key => key + 1); if (typeof overrideValueFn === 'function') { - overrideValueFn(...args); + overrideValueFn(name, value); } }, - [overrideValueFn, setEntryToAdd], + [newPropName, overrideValueFn], ); - if (isEmpty && !showWhenEmpty) { + if (isEmpty && !showWhenEmpty && !canAddEntries) { return null; } else { return ( @@ -102,11 +81,6 @@ export default function InspectedElementTree({ )} - {canAddEntries && ( - - )} {isEmpty &&
None
} {!isEmpty && @@ -122,14 +96,17 @@ export default function InspectedElementTree({ value={value} /> ))} - {entryToAdd && ( -
- : + {canAddEntries && ( +
+ 0} + overrideNameFn={setNewPropName} + /> + : 
)} diff --git a/packages/react-devtools-shared/src/devtools/views/hooks.js b/packages/react-devtools-shared/src/devtools/views/hooks.js index b86fb93b0e01b..f027f751d399f 100644 --- a/packages/react-devtools-shared/src/devtools/views/hooks.js +++ b/packages/react-devtools-shared/src/devtools/views/hooks.js @@ -8,11 +8,75 @@ */ import throttle from 'lodash.throttle'; -import {useCallback, useEffect, useLayoutEffect, useState} from 'react'; +import { + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useState, +} from 'react'; +import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'; import { localStorageGetItem, localStorageSetItem, } from 'react-devtools-shared/src/storage'; +import {sanitizeForParse, smartParse, smartStringify} from '../utils'; + +type EditableValue = {| + editableValue: any, + hasPendingChanges: boolean, + isValid: boolean, + parsedValue: any, + reset: () => void, + update: (newValue: any) => void, +|}; + +// Convenience hook for working with an editable value that is validated via JSON.parse. +export function useEditableValue( + initialValue: any, + initialIsValid?: boolean = true, +): EditableValue { + const [editableValue, setEditableValue] = useState(() => + smartStringify(initialValue), + ); + const [parsedValue, setParsedValue] = useState(initialValue); + const [isValid, setIsValid] = useState(initialIsValid); + + const reset = useCallback(() => { + setEditableValue(smartStringify(initialValue)); + setParsedValue(initialValue); + setIsValid(initialIsValid); + }, []); + + const update = useCallback(newValue => { + let isNewValueValid = false; + let newParsedValue; + try { + newParsedValue = smartParse(newValue); + isNewValueValid = true; + } catch (error) {} + + batchedUpdates(() => { + setEditableValue(sanitizeForParse(newValue)); + if (isNewValueValid) { + setParsedValue(newParsedValue); + } + setIsValid(isNewValueValid); + }); + }, []); + + return useMemo( + () => ({ + editableValue, + hasPendingChanges: smartStringify(initialValue) !== editableValue, + isValid, + parsedValue, + reset, + update, + }), + [editableValue, initialValue, isValid, parsedValue], + ); +} export function useIsOverflowing( containerRef: {current: HTMLDivElement | null},