Skip to content

Commit

Permalink
[DevTools] Support for adding props | Improved state/props value edit…
Browse files Browse the repository at this point in the history
…ing (#16700)

* Extracted sanitizeForParse

* Added canAddEntries flag to InspectedElementTree

* Added EditableKey component.

* Added support to add an additional entry.

* 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.

* Fixed flow error.

* Removed unneeded fragment.

* Renamed EditableKey -> EditableName

* Removed unneeded dependency

* Removed problematic props to state hook.

* Prettified changes.

* Removed unused import.

* Fixed shouldStringify check.

* Removed testing props from EditableProps.

* Made some inline tweaks
  • Loading branch information
hristo-kanchev authored and Brian Vaughn committed Sep 10, 2019
1 parent 4ef6387 commit 709baf1
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 118 deletions.
43 changes: 43 additions & 0 deletions packages/react-devtools-shared/src/devtools/utils.js
Expand Up @@ -82,3 +82,46 @@ 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.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);
}
@@ -0,0 +1,9 @@
.Input {
flex: 0 1 auto;
padding: 1px;
box-shadow: 0px 1px 3px transparent;
}
.Input:focus {
color: var(--color-text);
box-shadow: 0px 1px 3px var(--color-shadow);
}
@@ -0,0 +1,78 @@
/**
* 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, {useCallback, useState} from 'react';
import AutoSizeInput from './NativeStyleEditor/AutoSizeInput';
import styles from './EditableName.css';

type OverrideNameFn = (path: Array<string | number>, 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 handleChange = useCallback(
({target}) => {
const value = target.value.trim();

if (value) {
setIsValid(true);
} else {
setIsValid(false);
}

setEditableName(value);
},
[overrideNameFn],
);

const handleKeyDown = useCallback(
event => {
// Prevent keydown events from e.g. change selected element in the tree
event.stopPropagation();

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 (
<AutoSizeInput
autoFocus={autoFocus}
className={styles.Input}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="new prop"
type="text"
value={editableName}
/>
);
}
Expand Up @@ -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;
}
Expand Down
Expand Up @@ -7,143 +7,89 @@
* @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 {useEditableValue} from '../hooks';

type OverrideValueFn = (path: Array<string | number>, value: any) => void;

type EditableValueProps = {|
dataType: string,
initialValue: any,
overrideValueFn: OverrideValueFn,
path: Array<string | number>,
value: any,
|};

export default function EditableValue({
dataType,
initialValue,
overrideValueFn,
path,
value,
}: EditableValueProps) {
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [editableValue, setEditableValue] = useState(value);
const inputRef = useRef<HTMLInputElement | null>(null);

if (hasPendingChanges && editableValue === value) {
setHasPendingChanges(false);
}

const handleChange = useCallback(
({target}) => {
if (dataType === 'boolean') {
setEditableValue(target.checked);
overrideValueFn(path, target.checked);
} else {
setEditableValue(target.value);
}
setHasPendingChanges(true);
},
[dataType, overrideValueFn, path],
);

const handleReset = useCallback(
() => {
setEditableValue(value);
setHasPendingChanges(false);

if (inputRef.current !== null) {
inputRef.current.focus();
}
},
[value],
);
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') {
if (dataType === 'number') {
const parsedValue = parseFloat(editableValue);
if (!Number.isNaN(parsedValue)) {
switch (event.key) {
case 'Enter':
if (isValid && hasPendingChanges) {
overrideValueFn(path, parsedValue);
}
} else {
overrideValueFn(path, editableValue);
}

// 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);
setHasPendingChanges(false);
break;
case 'Escape':
reset();
break;
default:
break;
}
},
[editableValue, dataType, overrideValueFn, path, value],
[hasPendingChanges, isValid, overrideValueFn, parsedValue, reset],
);

// 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;
if (hasPendingChanges) {
inputValue = editableValue == null ? '' : editableValue;
}

let placeholder = '';
if (value === null) {
placeholder = '(null)';
} else if (value === undefined) {
if (editableValue === undefined) {
placeholder = '(undefined)';
} else if (dataType === 'string') {
placeholder = '(string)';
} else {
placeholder = 'Enter valid JSON';
}

return (
<Fragment>
{dataType === 'boolean' && (
<label className={styles.CheckboxLabel}>
<input
checked={inputValue}
className={styles.Checkbox}
onChange={handleChange}
onKeyDown={handleKeyDown}
ref={inputRef}
type={type}
/>
</label>
)}
{dataType !== 'boolean' && (
<input
className={styles.Input}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
ref={inputRef}
type={type}
value={inputValue}
/>
<input
autoComplete="new-password"
className={isValid ? styles.Input : styles.Invalid}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
ref={inputRef}
type="text"
value={editableValue}
/>
{hasPendingChanges && (
<Button
className={styles.ResetButton}
onClick={reset}
title="Reset value">
<ButtonIcon type="undo" />
</Button>
)}
{hasPendingChanges &&
dataType !== 'boolean' && (
<Button
className={styles.ResetButton}
onClick={handleReset}
title="Reset value">
<ButtonIcon type="undo" />
</Button>
)}
</Fragment>
);
}
Expand Up @@ -46,3 +46,10 @@
font-style: italic;
padding-left: 0.75rem;
}

.AddEntry {
padding-left: 1rem;
white-space: nowrap;
display: flex;
align-items: center;
}

0 comments on commit 709baf1

Please sign in to comment.