@samitouri / QOS-React-2 / commits / a4212dcdce

Support editable props, state, and context values

Brian Vaughn committed Feb 17, 2019 at 13:07 UTC a4212dcdce1b5cbaf8d436871b1fe8968814a358
15 files changed +428 -37
shells/dev/app/EditableProps/EditableProps.css new
+13
@@ -0,0 +1,13 @@
1 +.App {
2 + /* GitHub.com frontend fonts */
3 + font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
4 + sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
5 + font-size: 14px;
6 + line-height: 1.5;
7 +}
8 +
9 +.Header {
10 + font-size: 1.5rem;
11 + font-weight: bold;
12 + margin-bottom: 0.5rem;
13 +}
shells/dev/app/EditableProps/EditableProps.js new
+57
@@ -0,0 +1,57 @@
1 +// @flow
2 +
3 +import React, { createContext, Component, Fragment } from 'react';
4 +import styles from './EditableProps.css';
5 +
6 +type StatefulFunctionProps = {| count: number |};
7 +
8 +function StatefulFunction({ count }: StatefulFunctionProps) {
9 + return <li>Count: {count}</li>;
10 +}
11 +
12 +const BoolContext = createContext(true);
13 +// $FlowFixMe Flow does not yet know about Context.displayName
14 +BoolContext.displayName = 'BoolContext';
15 +
16 +type Props = {| name: string, toggle: boolean |};
17 +type State = {| cities: Array<string>, state: string |};
18 +
19 +class StatefulClass extends Component<Props, State> {
20 + static contextType = BoolContext;
21 +
22 + state: State = {
23 + cities: ['San Francisco', 'San Jose'],
24 + state: 'California',
25 + };
26 +
27 + handleChange = ({ target }) =>
28 + this.setState({
29 + state: target.value,
30 + });
31 +
32 + render() {
33 + return (
34 + <Fragment>
35 + <li>Name: {this.props.name}</li>
36 + <li>Toggle: {this.props.toggle ? 'true' : 'false'}</li>
37 + <li>
38 + State: <input value={this.state.state} onChange={this.handleChange} />
39 + </li>
40 + <li>Cities: {this.state.cities.join(', ')}</li>
41 + <li>Context: {this.context ? 'true' : 'false'}</li>
42 + </Fragment>
43 + );
44 + }
45 +}
46 +
47 +export default function EditableProps() {
48 + return (
49 + <div className={styles.App}>
50 + <div className={styles.Header}>Editable props</div>
51 + <ul>
52 + <StatefulClass name="Brian" toggle={true} />
53 + <StatefulFunction count={1} />
54 + </ul>
55 + </div>
56 + );
57 +}
shells/dev/app/EditableProps/index.js new
+5
@@ -0,0 +1,5 @@
1 +// @flow
2 +
3 +import EditableProps from './EditableProps';
4 +
5 +export default EditableProps;
shells/dev/app/index.js
+2
@@ -4,6 +4,7 @@
4
5 import { createElement } from 'react';
6 import { render, unmountComponentAtNode } from 'react-dom';
7 +import EditableProps from './EditableProps';
8 import ElementTypes from './ElementTypes';
9 import InspectableElements from './InspectableElements';
10 import ToDoList from './ToDoList';
@@ -24,6 +25,7 @@ function mountTestApp() {
25 mountHelper(ToDoList);
26 mountHelper(InspectableElements);
27 mountHelper(ElementTypes);
28 + mountHelper(EditableProps);
29 }
30
31 function unmountTestApp() {
src/backend/agent.js
+44 -2
@@ -18,6 +18,18 @@ const debug = (methodName, ...args) => {
18 }
19 };
20
21 +type InspectSelectParams = {|
22 + id: number,
23 + rendererID: number,
24 +|};
25 +
26 +type SetInParams = {|
27 + id: number,
28 + path: Array<string | number>,
29 + rendererID: number,
30 + value: any,
31 +|};
32 +
33 export default class Agent extends EventEmitter {
34 _bridge: Bridge = ((null: any): Bridge);
35 _rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
@@ -27,6 +39,9 @@ export default class Agent extends EventEmitter {
39
40 bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
41 bridge.addListener('inspectElement', this.inspectElement);
42 + bridge.addListener('overrideContext', this.overrideContext);
43 + bridge.addListener('overrideProps', this.overrideProps);
44 + bridge.addListener('overrideState', this.overrideState);
45 bridge.addListener('selectElement', this.selectElement);
46 bridge.addListener('startInspectingDOM', this.startInspectingDOM);
47 bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
@@ -80,7 +95,7 @@ export default class Agent extends EventEmitter {
95 }
96 };
97
83 - inspectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
98 + inspectElement = ({ id, rendererID }: InspectSelectParams) => {
99 const renderer = this._rendererInterfaces[rendererID];
100 if (renderer == null) {
101 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -89,7 +104,7 @@ export default class Agent extends EventEmitter {
104 }
105 };
106
92 - selectElement = ({ id, rendererID }: { id: number, rendererID: number }) => {
107 + selectElement = ({ id, rendererID }: InspectSelectParams) => {
108 const renderer = this._rendererInterfaces[rendererID];
109 if (renderer == null) {
110 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
@@ -98,6 +113,33 @@ export default class Agent extends EventEmitter {
113 }
114 };
115
116 + overrideContext = ({ id, path, rendererID, value }: SetInParams) => {
117 + const renderer = this._rendererInterfaces[rendererID];
118 + if (renderer == null) {
119 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
120 + } else {
121 + renderer.setInContext(id, path, value);
122 + }
123 + };
124 +
125 + overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
126 + const renderer = this._rendererInterfaces[rendererID];
127 + if (renderer == null) {
128 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
129 + } else {
130 + renderer.setInProps(id, path, value);
131 + }
132 + };
133 +
134 + overrideState = ({ id, path, rendererID, value }: SetInParams) => {
135 + const renderer = this._rendererInterfaces[rendererID];
136 + if (renderer == null) {
137 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
138 + } else {
139 + renderer.setInState(id, path, value);
140 + }
141 + };
142 +
143 setRendererInterface(
144 rendererID: RendererID,
145 rendererInterface: RendererInterface
src/backend/renderer.js
+62 -7
@@ -2,7 +2,8 @@
2
3 import { gte } from 'semver';
4 import {
5 - ElementTypeClassOrFunction,
5 + ElementTypeClass,
6 + ElementTypeFunction,
7 ElementTypeContext,
8 ElementTypeForwardRef,
9 ElementTypeMemo,
@@ -12,7 +13,7 @@ import {
13 ElementTypeSuspense,
14 } from 'src/devtools/types';
15 import { getDisplayName, utfEncodeString } from '../utils';
15 -import { cleanForBridge } from './utils';
16 +import { cleanForBridge, copyWithSet, setInObject } from './utils';
17 import {
18 __DEBUG__,
19 TREE_OPERATION_ADD,
@@ -193,6 +194,8 @@ export function attach(
194 DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
195 } = ReactSymbols;
196
197 + const { overrideProps } = renderer;
198 +
199 const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
200 if (__DEBUG__) {
201 const fiberData = getDataForFiber(fiber);
@@ -295,13 +298,19 @@ export function attach(
298
299 switch (tag) {
300 case ClassComponent:
298 - case FunctionComponent:
301 case IncompleteClassComponent:
302 + fiberData = {
303 + displayName: getDisplayName(resolvedType),
304 + key,
305 + type: ElementTypeClass,
306 + };
307 + break;
308 + case FunctionComponent:
309 case IndeterminateComponent:
310 fiberData = {
311 displayName: getDisplayName(resolvedType),
312 key,
304 - type: ElementTypeClassOrFunction,
313 + type: ElementTypeFunction,
314 };
315 break;
316 case ForwardRef:
@@ -1072,7 +1081,7 @@ export function attach(
1081 tag === IncompleteClassComponent ||
1082 tag === IndeterminateComponent
1083 ) {
1075 - if (stateNode && Object.keys(stateNode.context).length > 0) {
1084 + if (stateNode && stateNode.context != null) {
1085 context = stateNode.context;
1086 }
1087 } else if (
@@ -1135,7 +1144,7 @@ export function attach(
1144 id,
1145
1146 // Does the current renderer support editable props/state/hooks?
1138 - canEditValues: false, // TODO
1147 + canEditFunctionProps: typeof overrideProps === 'function',
1148
1149 // Inspectable properties.
1150 // TODO Review sanitization approach for the below inspectable values.
@@ -1156,6 +1165,49 @@ export function attach(
1165 };
1166 }
1167
1168 + function setInProps(id: number, path: Array<string | number>, value: any) {
1169 + const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1170 + if (fiber !== null) {
1171 + const instance = fiber.stateNode;
1172 + if (instance === null) {
1173 + if (typeof overrideProps === 'function') {
1174 + overrideProps(fiber, path, value);
1175 + }
1176 + } else {
1177 + fiber.pendingProps = copyWithSet(instance.props, path, value);
1178 + instance.forceUpdate();
1179 + }
1180 + }
1181 + }
1182 +
1183 + function setInState(id: number, path: Array<string | number>, value: any) {
1184 + const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1185 + if (fiber !== null) {
1186 + const instance = fiber.stateNode;
1187 + setInObject(instance.state, path, value);
1188 + instance.forceUpdate();
1189 + }
1190 + }
1191 +
1192 + function setInContext(id: number, path: Array<string | number>, value: any) {
1193 + // To simplify hydration and display of primative context values (e.g. number, string)
1194 + // the inspectElement() method wraps context in a {value: ...} object.
1195 + // We need to remove the first part of the path (the "value") before continuing.
1196 + path = path.slice(1);
1197 +
1198 + const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1199 + if (fiber !== null) {
1200 + const instance = fiber.stateNode;
1201 + if (path.length === 0) {
1202 + // Simple context value
1203 + instance.context = value;
1204 + } else {
1205 + setInObject(instance.context, path, value);
1206 + }
1207 + instance.forceUpdate();
1208 + }
1209 + }
1210 +
1211 return {
1212 getFiberIDFromNative,
1213 getNativeFromReactElement,
@@ -1164,7 +1216,10 @@ export function attach(
1216 inspectElement,
1217 selectElement,
1218 cleanup,
1167 - walkTree,
1219 renderer,
1220 + setInContext,
1221 + setInProps,
1222 + setInState,
1223 + walkTree,
1224 };
1225 }
src/backend/types.js
+3
@@ -55,6 +55,9 @@ export type RendererInterface = {
55 inspectElement: (id: number) => InspectedElement | null,
56 renderer: ReactRenderer | null,
57 selectElement: (id: number) => void,
58 + setInProps: (id: number, path: Array<string | number>, value: any) => void,
59 + setInState: (id: number, path: Array<string | number>, value: any) => void,
60 + setInContext: (id: number, path: Array<string | number>, value: any) => void,
61 walkTree: () => void,
62 };
63
src/backend/utils.js
+34
@@ -16,3 +16,37 @@ export function cleanForBridge(data: Object | null): DehydratedData | null {
16 return null;
17 }
18 }
19 +
20 +export function copyWithSet(
21 + obj: Object | Array<any>,
22 + path: Array<string | number>,
23 + value: any,
24 + index: number = 0
25 +): Object | Array<any> {
26 + if (index >= path.length) {
27 + return value;
28 + }
29 + const key = path[index];
30 + const updated = Array.isArray(obj) ? obj.slice() : { ...obj };
31 + // $FlowFixMe number or string is fine here
32 + updated[key] = copyWithSet(obj[key], path, value, index + 1);
33 + return updated;
34 +}
35 +
36 +export function setInObject(
37 + object: Object,
38 + path: Array<string | number>,
39 + value: any
40 +) {
41 + const last = path.pop();
42 + if (object != null) {
43 + const parent: Object = path.reduce(
44 + // $FlowFixMe
45 + (reduced, attribute) => reduced[attribute],
46 + object
47 + );
48 + if (parent) {
49 + parent[last] = value;
50 + }
51 + }
52 +}
src/devtools/types.js
+12 -11
@@ -1,18 +1,19 @@
1 // @flow
2
3 -export const ElementTypeClassOrFunction = 1;
4 -export const ElementTypeContext = 2;
5 -export const ElementTypeForwardRef = 3;
6 -export const ElementTypeMemo = 4;
7 -export const ElementTypeOtherOrUnknown = 5;
8 -export const ElementTypeProfiler = 6;
9 -export const ElementTypeRoot = 7;
10 -export const ElementTypeSuspense = 8;
3 +export const ElementTypeClass = 1;
4 +export const ElementTypeFunction = 2;
5 +export const ElementTypeContext = 3;
6 +export const ElementTypeForwardRef = 4;
7 +export const ElementTypeMemo = 5;
8 +export const ElementTypeOtherOrUnknown = 6;
9 +export const ElementTypeProfiler = 7;
10 +export const ElementTypeRoot = 8;
11 +export const ElementTypeSuspense = 9;
12
13 // Different types of elements displayed in the Elements tree.
14 // These types may be used to visually distinguish types,
15 // or to enable/disable certain functionality.
15 -export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
16 +export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
17
18 // Each element on the frontend corresponds to a Fiber on the backend.
19 // Some of its information (e.g. id, type, displayName) come from the backend.
@@ -47,8 +48,8 @@ export type Owner = {|
48 export type InspectedElement = {|
49 id: number,
50
50 - // Does the current renderer support editable props/state/hooks?
51 - canEditValues: boolean,
51 + // Does the current renderer support editable function props?
52 + canEditFunctionProps: boolean,
53
54 // Inspectable properties.
55 context: Object | null,
src/devtools/views/Element.js
+3 -2
@@ -1,7 +1,7 @@
1 // @flow
2
3 import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4 -import { ElementTypeClassOrFunction } from 'src/devtools/types';
4 +import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types';
5 import { createRegExp } from './utils';
6 import { TreeContext } from './TreeContext';
7
@@ -42,7 +42,8 @@ export default function ElementView({ index, style }: Props) {
42 );
43
44 const isSelected = selectedElementID === id;
45 - const showDollarR = isSelected && type === ElementTypeClassOrFunction;
45 + const showDollarR =
46 + isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
47
48 // TODO styles.SelectedElement is 100% width but it doesn't take horizontal overflow into account.
49
src/devtools/views/InspectedElementTree.css
+28
@@ -7,16 +7,44 @@
7 }
8
9 .Item {
10 + display: flex;
11 }
12
13 .Name {
14 color: var(--color-attribute-name);
15 + flex: 0 0 auto;
16 +}
17 +.Name:after {
18 + content: ': ';
19 + color: var(--color-text-color);
20 + margin-right: 0.5rem;
21 }
22
23 .Value {
24 color: var(--color-attribute-value);
25 }
26
27 +.ValueInputLabel {
28 + flex: 1 1 100%;
29 +}
30 +.ValueInputLabel:focus-within {
31 + background-color: var(--color-button-background-focus);
32 +}
33 +
34 +.ValueInput {
35 + background: none;
36 + border: 1px solid transparent;
37 + color: var(--color-attribute-editable-value);
38 + border-radius: 0.125rem;
39 + width: 100%;
40 + font-family: var(--font-family-monospace);
41 + font-size: var(--font-size-monospace-normal);
42 +}
43 +.ValueInput:focus {
44 + background-color: var(--color-button-background-focus);
45 + outline: none;
46 +}
47 +
48 .None {
49 color: var(--color-dimmer);
50 font-style: italic;
src/devtools/views/InspectedElementTree.js
+106 -8
@@ -1,19 +1,23 @@
1 // @flow
2
3 -import React from 'react';
3 +import React, { useCallback, useState } from 'react';
4 import { getMetaValueLabel } from './utils';
5 import { meta } from '../../hydration';
6 import styles from './InspectedElementTree.css';
7
8 +type OverrideValueFn = (path: Array<string | number>, value: any) => void;
9 +
10 type Props = {|
11 data: Object | null,
12 label: string,
13 + overrideValueFn?: ?OverrideValueFn,
14 showWhenEmpty?: boolean,
15 |};
16
17 export default function InspectedElementTree({
18 data,
19 label,
20 + overrideValueFn,
21 showWhenEmpty = false,
22 }: Props) {
23 const isEmpty = data === null || Object.keys(data).length === 0;
@@ -33,6 +37,8 @@ export default function InspectedElementTree({
37 key={name}
38 depth={1}
39 name={name}
40 + overrideValueFn={overrideValueFn}
41 + path={[name]}
42 value={(data: any)[name]}
43 />
44 ))}
@@ -44,10 +50,18 @@ export default function InspectedElementTree({
50 type KeyValueProps = {|
51 depth: number,
52 name: string,
53 + overrideValueFn?: ?OverrideValueFn,
54 + path?: Array<any>,
55 value: any,
56 |};
57
50 -export function KeyValue({ depth, name, value }: KeyValueProps) {
58 +export function KeyValue({
59 + depth,
60 + name,
61 + overrideValueFn,
62 + path = [],
63 + value,
64 +}: KeyValueProps) {
65 const dataType = typeof value;
66 const isSimpleType =
67 dataType === 'number' ||
@@ -72,14 +86,24 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
86
87 children = (
88 <div key="root" className={styles.Item} style={{ paddingLeft }}>
75 - <span className={styles.Name}>{name}</span>:{' '}
76 - <span className={styles.Value}>{displayValue}</span>
89 + <span className={styles.Name}>{name}</span>
90 + {typeof overrideValueFn === 'function' ? (
91 + <EditableValue
92 + dataType={dataType}
93 + overrideValueFn={overrideValueFn}
94 + path={path}
95 + value={value}
96 + />
97 + ) : (
98 + <span className={styles.Value}>{displayValue}</span>
99 + )}
100 </div>
101 );
102 } else if (value.hasOwnProperty(meta.type)) {
103 + // TODO Is this type even necessary? Can we just drop it?
104 children = (
105 <div key="root" className={styles.Item} style={{ paddingLeft }}>
82 - <span className={styles.Name}>{name}</span>:{' '}
106 + <span className={styles.Name}>{name}</span>
107 <span className={styles.Value}>{getMetaValueLabel(value)}</span>
108 </div>
109 );
@@ -90,6 +114,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
114 key={index}
115 depth={depth + 1}
116 name={index}
117 + overrideValueFn={overrideValueFn}
118 + path={path.concat(index)}
119 value={value[index]}
120 />
121 ));
@@ -99,13 +125,21 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
125 className={styles.Item}
126 style={{ paddingLeft }}
127 >
102 - <span className={styles.Name}>{name}</span>: <span>Array</span>
128 + <span className={styles.Name}>{name}</span>
129 + <span>Array</span>
130 </div>
131 );
132 } else {
133 // $FlowFixMe
134 children = Object.entries(value).map(([name, value]) => (
108 - <KeyValue key={name} depth={depth + 1} name={name} value={value} />
135 + <KeyValue
136 + key={name}
137 + depth={depth + 1}
138 + name={name}
139 + overrideValueFn={overrideValueFn}
140 + path={path.concat(name)}
141 + value={value}
142 + />
143 ));
144 children.unshift(
145 <div
@@ -113,7 +147,8 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
147 className={styles.Item}
148 style={{ paddingLeft }}
149 >
116 - <span className={styles.Name}>{name}</span>: <span>Object</span>
150 + <span className={styles.Name}>{name}</span>
151 + <span>Object</span>
152 </div>
153 );
154 }
@@ -121,3 +156,66 @@ export function KeyValue({ depth, name, value }: KeyValueProps) {
156
157 return children;
158 }
159 +
160 +type EditableValueProps = {|
161 + dataType: string,
162 + overrideValueFn: OverrideValueFn,
163 + path: Array<string | number>,
164 + value: any,
165 +|};
166 +
167 +function EditableValue({
168 + dataType,
169 + overrideValueFn,
170 + path,
171 + value,
172 +}: EditableValueProps) {
173 + const [editableValue, setEditableValue] = useState(value);
174 +
175 + const handleChange = useCallback(
176 + ({ target }) => {
177 + if (dataType === 'boolean') {
178 + setEditableValue(target.checked);
179 + overrideValueFn(path, target.checked);
180 + } else if (dataType === 'number') {
181 + setEditableValue(parseFloat(target.value));
182 + } else {
183 + setEditableValue(target.value);
184 + }
185 + },
186 + [dataType, setEditableValue]
187 + );
188 +
189 + const handleKeyPress = useCallback(
190 + ({ key }) => {
191 + if (key === 'Enter') {
192 + overrideValueFn(path, editableValue);
193 + }
194 + },
195 + [path, editableValue, overrideValueFn]
196 + );
197 +
198 + const handleKeyDown = useCallback(event => event.stopPropagation(), []);
199 +
200 + // Render different input types based on the dataType
201 + let type = 'text';
202 + if (dataType === 'boolean') {
203 + type = 'checkbox';
204 + } else if (dataType === 'number') {
205 + type = 'number';
206 + }
207 +
208 + return (
209 + <label className={styles.ValueInputLabel}>
210 + <input
211 + checked={dataType === 'boolean' ? editableValue : undefined}
212 + className={styles.ValueInput}
213 + onChange={handleChange}
214 + onKeyDown={handleKeyDown}
215 + onKeyPress={handleKeyPress}
216 + type={type}
217 + value={dataType === 'boolean' ? undefined : editableValue || ''}
218 + />
219 + </label>
220 + );
221 +}
src/devtools/views/SelectedElement.js
+56 -7
@@ -15,9 +15,10 @@ import HooksTree from './HooksTree';
15 import InspectedElementTree from './InspectedElementTree';
16 import { hydrate } from 'src/hydration';
17 import styles from './SelectedElement.css';
18 +import { ElementTypeClass, ElementTypeFunction } from '../types';
19
20 import type { InspectedElement } from '../types';
20 -import type { DehydratedData } from 'src/devtools/types';
21 +import type { DehydratedData, Element } from 'src/devtools/types';
22
23 export type Props = {||};
24
@@ -87,26 +88,74 @@ export default function SelectedElement(_: Props) {
88 )}
89
90 {inspectedElement !== null && (
90 - <InspectedElementView inspectedElement={inspectedElement} />
91 + <InspectedElementView
92 + element={element}
93 + inspectedElement={inspectedElement}
94 + />
95 )}
96 </div>
97 );
98 }
99
100 type InspectedElementViewProps = {|
101 + element: Element,
102 inspectedElement: InspectedElement,
103 |};
104
100 -function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
101 - let { context, hooks, owners, props, state } = inspectedElement;
105 +function InspectedElementView({
106 + element,
107 + inspectedElement,
108 +}: InspectedElementViewProps) {
109 + const { id, type } = element;
110 + const { context, hooks, owners, props, state } = inspectedElement;
111 +
112 const { ownerStack } = useContext(TreeContext);
113 + const bridge = useContext(BridgeContext);
114 + const store = useContext(StoreContext);
115 +
116 + let overrideContextFn = null;
117 + let overridePropsFn = null;
118 + let overrideStateFn = null;
119 + if (type === ElementTypeClass) {
120 + overrideContextFn = (path: Array<string | number>, value: any) => {
121 + const rendererID = store.getRendererIDForElement(id);
122 + bridge.send('overrideContext', { id, path, rendererID, value });
123 + };
124 + overridePropsFn = (path: Array<string | number>, value: any) => {
125 + const rendererID = store.getRendererIDForElement(id);
126 + bridge.send('overrideProps', { id, path, rendererID, value });
127 + };
128 + overrideStateFn = (path: Array<string | number>, value: any) => {
129 + const rendererID = store.getRendererIDForElement(id);
130 + bridge.send('overrideState', { id, path, rendererID, value });
131 + };
132 + } else if (type === ElementTypeFunction) {
133 + // TODO Only enable this if renderer.canEditFunctionProps is true!
134 + overridePropsFn = (path: Array<string | number>, value: any) => {
135 + const rendererID = store.getRendererIDForElement(id);
136 + bridge.send('overrideProps', { id, path, rendererID, value });
137 + };
138 + }
139
140 return (
141 <div className={styles.InspectedElement}>
106 - <InspectedElementTree label="props" data={props} showWhenEmpty />
107 - <InspectedElementTree label="state" data={state} />
142 + <InspectedElementTree
143 + label="props"
144 + data={props}
145 + overrideValueFn={overridePropsFn}
146 + showWhenEmpty
147 + />
148 + <InspectedElementTree
149 + label="state"
150 + data={state}
151 + overrideValueFn={overrideStateFn}
152 + />
153 <HooksTree hooksTree={hooks} />
109 - <InspectedElementTree label="context" data={context} />
154 + <InspectedElementTree
155 + label="context"
156 + data={context}
157 + overrideValueFn={overrideContextFn}
158 + />
159
160 {ownerStack.length === 0 && owners !== null && owners.length > 0 && (
161 <div className={styles.Owners}>
src/devtools/views/SettingsContext.js
+1
@@ -118,6 +118,7 @@ function updateDisplayDensity(displayDensity: DisplayDensity): void {
118 function updateThemeVariables(theme: Theme): void {
119 updateStyleHelper(theme, 'color-attribute-name');
120 updateStyleHelper(theme, 'color-attribute-value');
121 + updateStyleHelper(theme, 'color-attribute-editable-value');
122 updateStyleHelper(theme, 'color-background');
123 updateStyleHelper(theme, 'color-border');
124 updateStyleHelper(theme, 'color-button-background');
src/devtools/views/root.css
+2
@@ -6,6 +6,7 @@
6 /* Light theme */
7 --light-color-attribute-name: #ef6632;
8 --light-color-attribute-value: #1a1aa6;
9 + --light-color-attribute-editable-value: #1a1aa6;
10 --light-color-background: #ffffff;
11 --light-color-button-background: #ffffff;
12 --light-color-button-background-focus: #ebf1fb;
@@ -31,6 +32,7 @@
32 /* Dark theme */
33 --dark-color-attribute-name: #9d87d2;
34 --dark-color-attribute-value: #cedae0;
35 + --dark-color-attribute-editable-value: yellow;
36 --dark-color-background: #282c34;
37 --dark-color-button-background: #282c34;
38 --dark-color-button-background-focus: #3d424a;