Skip to content

Commit 709baf1

Browse files
hristo-kanchevBrian Vaughn
authored andcommitted
[DevTools] Support for adding props | Improved state/props value editing (#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
1 parent 4ef6387 commit 709baf1

File tree

11 files changed

+307
-118
lines changed

11 files changed

+307
-118
lines changed

packages/react-devtools-shared/src/devtools/utils.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,46 @@ export function printStore(store: Store, includeWeight: boolean = false) {
8282

8383
return snapshotLines.join('\n');
8484
}
85+
86+
// We use JSON.parse to parse string values
87+
// e.g. 'foo' is not valid JSON but it is a valid string
88+
// so this method replaces e.g. 'foo' with "foo"
89+
export function sanitizeForParse(value: any) {
90+
if (typeof value === 'string') {
91+
if (
92+
value.length >= 2 &&
93+
value.charAt(0) === "'" &&
94+
value.charAt(value.length - 1) === "'"
95+
) {
96+
return '"' + value.substr(1, value.length - 2) + '"';
97+
}
98+
}
99+
return value;
100+
}
101+
102+
export function smartParse(value: any) {
103+
switch (value) {
104+
case 'Infinity':
105+
return Infinity;
106+
case 'NaN':
107+
return NaN;
108+
case 'undefined':
109+
return undefined;
110+
default:
111+
return JSON.parse(sanitizeForParse(value));
112+
}
113+
}
114+
115+
export function smartStringify(value: any) {
116+
if (typeof value === 'number') {
117+
if (Number.isNaN(value)) {
118+
return 'NaN';
119+
} else if (!Number.isFinite(value)) {
120+
return 'Infinity';
121+
}
122+
} else if (value === undefined) {
123+
return 'undefined';
124+
}
125+
126+
return JSON.stringify(value);
127+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.Input {
2+
flex: 0 1 auto;
3+
padding: 1px;
4+
box-shadow: 0px 1px 3px transparent;
5+
}
6+
.Input:focus {
7+
color: var(--color-text);
8+
box-shadow: 0px 1px 3px var(--color-shadow);
9+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import React, {useCallback, useState} from 'react';
11+
import AutoSizeInput from './NativeStyleEditor/AutoSizeInput';
12+
import styles from './EditableName.css';
13+
14+
type OverrideNameFn = (path: Array<string | number>, value: any) => void;
15+
16+
type EditableNameProps = {|
17+
autoFocus?: boolean,
18+
initialValue?: string,
19+
overrideNameFn: OverrideNameFn,
20+
|};
21+
22+
export default function EditableName({
23+
autoFocus = false,
24+
initialValue = '',
25+
overrideNameFn,
26+
}: EditableNameProps) {
27+
const [editableName, setEditableName] = useState(initialValue);
28+
const [isValid, setIsValid] = useState(false);
29+
30+
const handleChange = useCallback(
31+
({target}) => {
32+
const value = target.value.trim();
33+
34+
if (value) {
35+
setIsValid(true);
36+
} else {
37+
setIsValid(false);
38+
}
39+
40+
setEditableName(value);
41+
},
42+
[overrideNameFn],
43+
);
44+
45+
const handleKeyDown = useCallback(
46+
event => {
47+
// Prevent keydown events from e.g. change selected element in the tree
48+
event.stopPropagation();
49+
50+
switch (event.key) {
51+
case 'Enter':
52+
case 'Tab':
53+
if (isValid) {
54+
overrideNameFn(editableName);
55+
}
56+
break;
57+
case 'Escape':
58+
setEditableName(initialValue);
59+
break;
60+
default:
61+
break;
62+
}
63+
},
64+
[editableName, setEditableName, isValid, initialValue, overrideNameFn],
65+
);
66+
67+
return (
68+
<AutoSizeInput
69+
autoFocus={autoFocus}
70+
className={styles.Input}
71+
onChange={handleChange}
72+
onKeyDown={handleKeyDown}
73+
placeholder="new prop"
74+
type="text"
75+
value={editableName}
76+
/>
77+
);
78+
}

packages/react-devtools-shared/src/devtools/views/Components/EditableValue.css

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,23 @@
1919
font-family: var(--font-family-monospace);
2020
font-size: var(--font-size-monospace-normal);
2121
}
22-
.Input:focus {
22+
23+
.Invalid {
24+
flex: 1 1;
25+
background: none;
26+
border: 1px solid transparent;
27+
color: var(--color-attribute-editable-value);
28+
border-radius: 0.125rem;
29+
font-family: var(--font-family-monospace);
30+
font-size: var(--font-size-monospace-normal);
31+
background-color: var(--color-background-invalid);
32+
color: var(--color-text-invalid);
33+
34+
--color-border: var(--color-text-invalid);
35+
}
36+
37+
.Input:focus,
38+
.Invalid:focus {
2339
background-color: var(--color-button-background-focus);
2440
outline: none;
2541
}

packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js

Lines changed: 46 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -7,143 +7,89 @@
77
* @flow
88
*/
99

10-
import React, {Fragment, useCallback, useRef, useState} from 'react';
10+
import React, {Fragment, useCallback, useRef} from 'react';
1111
import Button from '../Button';
1212
import ButtonIcon from '../ButtonIcon';
1313
import styles from './EditableValue.css';
14+
import {useEditableValue} from '../hooks';
1415

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

1718
type EditableValueProps = {|
1819
dataType: string,
20+
initialValue: any,
1921
overrideValueFn: OverrideValueFn,
2022
path: Array<string | number>,
21-
value: any,
2223
|};
2324

2425
export default function EditableValue({
2526
dataType,
27+
initialValue,
2628
overrideValueFn,
2729
path,
28-
value,
2930
}: EditableValueProps) {
30-
const [hasPendingChanges, setHasPendingChanges] = useState(false);
31-
const [editableValue, setEditableValue] = useState(value);
3231
const inputRef = useRef<HTMLInputElement | null>(null);
33-
34-
if (hasPendingChanges && editableValue === value) {
35-
setHasPendingChanges(false);
36-
}
37-
38-
const handleChange = useCallback(
39-
({target}) => {
40-
if (dataType === 'boolean') {
41-
setEditableValue(target.checked);
42-
overrideValueFn(path, target.checked);
43-
} else {
44-
setEditableValue(target.value);
45-
}
46-
setHasPendingChanges(true);
47-
},
48-
[dataType, overrideValueFn, path],
49-
);
50-
51-
const handleReset = useCallback(
52-
() => {
53-
setEditableValue(value);
54-
setHasPendingChanges(false);
55-
56-
if (inputRef.current !== null) {
57-
inputRef.current.focus();
58-
}
59-
},
60-
[value],
61-
);
32+
const {
33+
editableValue,
34+
hasPendingChanges,
35+
isValid,
36+
parsedValue,
37+
reset,
38+
update,
39+
} = useEditableValue(initialValue);
40+
41+
const handleChange = useCallback(({target}) => update(target.value), [
42+
update,
43+
]);
6244

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

68-
const {key} = event;
69-
70-
if (key === 'Enter') {
71-
if (dataType === 'number') {
72-
const parsedValue = parseFloat(editableValue);
73-
if (!Number.isNaN(parsedValue)) {
50+
switch (event.key) {
51+
case 'Enter':
52+
if (isValid && hasPendingChanges) {
7453
overrideValueFn(path, parsedValue);
7554
}
76-
} else {
77-
overrideValueFn(path, editableValue);
78-
}
79-
80-
// Don't reset the pending change flag here.
81-
// The inspected fiber won't be updated until after the next "inspectElement" message.
82-
// We'll reset that flag during a subsequent render.
83-
} else if (key === 'Escape') {
84-
setEditableValue(value);
85-
setHasPendingChanges(false);
55+
break;
56+
case 'Escape':
57+
reset();
58+
break;
59+
default:
60+
break;
8661
}
8762
},
88-
[editableValue, dataType, overrideValueFn, path, value],
63+
[hasPendingChanges, isValid, overrideValueFn, parsedValue, reset],
8964
);
9065

91-
// Render different input types based on the dataType
92-
let type = 'text';
93-
if (dataType === 'boolean') {
94-
type = 'checkbox';
95-
} else if (dataType === 'number') {
96-
type = 'number';
97-
}
98-
99-
let inputValue = value == null ? '' : value;
100-
if (hasPendingChanges) {
101-
inputValue = editableValue == null ? '' : editableValue;
102-
}
103-
10466
let placeholder = '';
105-
if (value === null) {
106-
placeholder = '(null)';
107-
} else if (value === undefined) {
67+
if (editableValue === undefined) {
10868
placeholder = '(undefined)';
109-
} else if (dataType === 'string') {
110-
placeholder = '(string)';
69+
} else {
70+
placeholder = 'Enter valid JSON';
11171
}
11272

11373
return (
11474
<Fragment>
115-
{dataType === 'boolean' && (
116-
<label className={styles.CheckboxLabel}>
117-
<input
118-
checked={inputValue}
119-
className={styles.Checkbox}
120-
onChange={handleChange}
121-
onKeyDown={handleKeyDown}
122-
ref={inputRef}
123-
type={type}
124-
/>
125-
</label>
126-
)}
127-
{dataType !== 'boolean' && (
128-
<input
129-
className={styles.Input}
130-
onChange={handleChange}
131-
onKeyDown={handleKeyDown}
132-
placeholder={placeholder}
133-
ref={inputRef}
134-
type={type}
135-
value={inputValue}
136-
/>
75+
<input
76+
autoComplete="new-password"
77+
className={isValid ? styles.Input : styles.Invalid}
78+
onChange={handleChange}
79+
onKeyDown={handleKeyDown}
80+
placeholder={placeholder}
81+
ref={inputRef}
82+
type="text"
83+
value={editableValue}
84+
/>
85+
{hasPendingChanges && (
86+
<Button
87+
className={styles.ResetButton}
88+
onClick={reset}
89+
title="Reset value">
90+
<ButtonIcon type="undo" />
91+
</Button>
13792
)}
138-
{hasPendingChanges &&
139-
dataType !== 'boolean' && (
140-
<Button
141-
className={styles.ResetButton}
142-
onClick={handleReset}
143-
title="Reset value">
144-
<ButtonIcon type="undo" />
145-
</Button>
146-
)}
14793
</Fragment>
14894
);
14995
}

packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,10 @@
4646
font-style: italic;
4747
padding-left: 0.75rem;
4848
}
49+
50+
.AddEntry {
51+
padding-left: 1rem;
52+
white-space: nowrap;
53+
display: flex;
54+
align-items: center;
55+
}

0 commit comments

Comments
 (0)