@samitouri / QOS-React-2 / commits / 8cd8b29015

Added support for editable hook values (pending facebook/react/pull/14906)

Brian Vaughn committed Feb 20, 2019 at 15:58 UTC 8cd8b29015646b65f7db7c1b39a0c9afa19612c9
11 files changed +206 -46
src/backend/ReactDebugHooks.js
+7 -3
@@ -225,6 +225,7 @@ type ReactCurrentDispatcher = {
225 };
226
227 type HooksNode = {
228 + nativeHookIndex: number,
229 name: string,
230 value: mixed,
231 subHooks: Array<HooksNode>,
@@ -366,6 +367,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
367 const rootChildren = [];
368 let prevStack = null;
369 let levelChildren = rootChildren;
370 + let nativeHookIndex = 0;
371 const stackOfChildren = [];
372 for (let i = 0; i < readHookLog.length; i++) {
373 const hook = readHookLog[i];
@@ -399,6 +401,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
401 levelChildren.push({
402 name: parseCustomHookName(stack[j - 1].functionName),
403 value: undefined,
404 + nativeHookIndex: -1,
405 subHooks: children,
406 });
407 stackOfChildren.push(levelChildren);
@@ -409,12 +412,13 @@ function buildTree(rootStack, readHookLog): HooksTree {
412 levelChildren.push({
413 name: hook.primitive,
414 value: hook.value,
415 + nativeHookIndex: hook.primitive === 'DebugValue' ? -1 : nativeHookIndex++,
416 subHooks: [],
417 });
418 }
419
420 // Associate custom hook values (useDebugValue() hook entries) with the correct hooks.
417 - rollupDebugValues(rootChildren, null);
421 + processDebugValues(rootChildren, null);
422
423 return rootChildren;
424 }
@@ -423,7 +427,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
427 // That hook adds the user-provided values to the hooks tree.
428 // This method removes those values (so they don't appear in DevTools),
429 // and bubbles them up to the "value" attribute of their parent custom hook.
426 -function rollupDebugValues(
430 +function processDebugValues(
431 hooksTree: HooksTree,
432 parentHooksNode: HooksNode | null
433 ): void {
@@ -436,7 +440,7 @@ function rollupDebugValues(
440 i--;
441 debugValueHooksNodes.push(hooksNode);
442 } else {
439 - rollupDebugValues(hooksNode.subHooks, hooksNode);
443 + processDebugValues(hooksNode.subHooks, hooksNode);
444 }
445 }
446
src/backend/agent.js
+24
@@ -23,6 +23,14 @@ type InspectSelectParams = {|
23 rendererID: number,
24 |};
25
26 +type OverrideHookParams = {|
27 + id: number,
28 + nativeHookIndex: number,
29 + path: Array<string | number>,
30 + rendererID: number,
31 + value: any,
32 +|};
33 +
34 type SetInParams = {|
35 id: number,
36 path: Array<string | number>,
@@ -40,6 +48,7 @@ export default class Agent extends EventEmitter {
48 bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
49 bridge.addListener('inspectElement', this.inspectElement);
50 bridge.addListener('overrideContext', this.overrideContext);
51 + bridge.addListener('overrideHook', this.overrideHook);
52 bridge.addListener('overrideProps', this.overrideProps);
53 bridge.addListener('overrideState', this.overrideState);
54 bridge.addListener('selectElement', this.selectElement);
@@ -122,6 +131,21 @@ export default class Agent extends EventEmitter {
131 }
132 };
133
134 + overrideHook = ({
135 + id,
136 + nativeHookIndex,
137 + path,
138 + rendererID,
139 + value,
140 + }: OverrideHookParams) => {
141 + const renderer = this._rendererInterfaces[rendererID];
142 + if (renderer == null) {
143 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
144 + } else {
145 + renderer.setInHook(id, nativeHookIndex, path, value);
146 + }
147 + };
148 +
149 overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
150 const renderer = this._rendererInterfaces[rendererID];
151 if (renderer == null) {
src/backend/index.js
+6 -2
@@ -1,11 +1,15 @@
1 // @flow
2
3 -import type { Hook, ReactRenderer, RendererInterface } from './types';
3 +import type { DevToolsHook, ReactRenderer, RendererInterface } from './types';
4 import Agent from './agent';
5
6 import { attach } from './renderer';
7
8 -export function initBackend(hook: Hook, agent: Agent, global: Object): void {
8 +export function initBackend(
9 + hook: DevToolsHook,
10 + agent: Agent,
11 + global: Object
12 +): void {
13 const subs = [
14 hook.sub(
15 'renderer-attached',
src/backend/renderer.js
+22 -4
@@ -24,8 +24,8 @@ import { getUID } from '../utils';
24 import { inspectHooksOfFiber } from './ReactDebugHooks';
25
26 import type {
27 + DevToolsHook,
28 Fiber,
28 - Hook,
29 ReactRenderer,
30 FiberData,
31 RendererInterface,
@@ -151,7 +151,7 @@ function getInternalReactConstants(version) {
151 }
152
153 export function attach(
154 - hook: Hook,
154 + hook: DevToolsHook,
155 rendererID: number,
156 renderer: ReactRenderer,
157 global: Object
@@ -194,7 +194,7 @@ export function attach(
194 DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
195 } = ReactSymbols;
196
197 - const { overrideProps } = renderer;
197 + const { overrideHook, overrideProps } = renderer;
198
199 const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
200 if (__DEBUG__) {
@@ -1141,7 +1141,10 @@ export function attach(
1141 return {
1142 id,
1143
1144 - // Does the current renderer support editable props/state/hooks?
1144 + // Does the current renderer support editable hooks?
1145 + canEditHooks: typeof overrideHook === 'function',
1146 +
1147 + // Does the current renderer support editable function props?
1148 canEditFunctionProps: typeof overrideProps === 'function',
1149
1150 // Inspectable properties.
@@ -1163,6 +1166,20 @@ export function attach(
1166 };
1167 }
1168
1169 + function setInHook(
1170 + id: number,
1171 + nativeHookIndex: number,
1172 + path: Array<string | number>,
1173 + value: any
1174 + ) {
1175 + const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1176 + if (fiber !== null) {
1177 + if (typeof overrideHook === 'function') {
1178 + overrideHook(fiber, nativeHookIndex, path, value);
1179 + }
1180 + }
1181 + }
1182 +
1183 function setInProps(id: number, path: Array<string | number>, value: any) {
1184 const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
1185 if (fiber !== null) {
@@ -1216,6 +1233,7 @@ export function attach(
1233 cleanup,
1234 renderer,
1235 setInContext,
1236 + setInHook,
1237 setInProps,
1238 setInState,
1239 walkTree,
src/backend/types.js
+19 -2
@@ -33,6 +33,16 @@ export type ReactRenderer = {
33 findFiberByHostInstance: (hostInstance: NativeType) => ?Fiber,
34 version: string,
35 bundleType: BundleType,
36 +
37 + // 16.9+
38 + overrideHook?: ?(
39 + fiber: Object,
40 + nativeHookIndex: number,
41 + path: Array<string | number>,
42 + value: any
43 + ) => void,
44 +
45 + // 16.7+
46 overrideProps?: ?(
47 fiber: Object,
48 path: Array<string | number>,
@@ -55,15 +65,21 @@ export type RendererInterface = {
65 inspectElement: (id: number) => InspectedElement | null,
66 renderer: ReactRenderer | null,
67 selectElement: (id: number) => void,
68 + setInContext: (id: number, path: Array<string | number>, value: any) => void,
69 + setInHook: (
70 + id: number,
71 + nativeHookIndex: number,
72 + path: Array<string | number>,
73 + value: any
74 + ) => void,
75 setInProps: (id: number, path: Array<string | number>, value: any) => void,
76 setInState: (id: number, path: Array<string | number>, value: any) => void,
60 - setInContext: (id: number, path: Array<string | number>, value: any) => void,
77 walkTree: () => void,
78 };
79
80 export type Handler = (data: any) => void;
81
66 -export type Hook = {
82 +export type DevToolsHook = {
83 listeners: { [key: string]: Array<Handler> },
84 rendererInterfaces: Map<RendererID, RendererInterface>,
85 renderers: Map<RendererID, ReactRenderer>,
@@ -83,6 +99,7 @@ export type Hook = {
99 };
100
101 export type HooksNode = {
102 + nativeHookIndex: number,
103 name: string,
104 value: mixed,
105 subHooks: Array<HooksNode>,
src/devtools/types.js
+3
@@ -48,6 +48,9 @@ export type Owner = {|
48 export type InspectedElement = {|
49 id: number,
50
51 + // Does the current renderer support editable hooks?
52 + canEditHooks: boolean,
53 +
54 // Does the current renderer support editable function props?
55 canEditFunctionProps: boolean,
56
src/devtools/views/HooksTree.css
+8 -1
@@ -3,15 +3,22 @@
3 border-top: 1px solid var(--color-border);
4 }
5
6 -.HooksNode {
6 +.Hook {
7 padding-left: 0.75rem;
8 }
9
10 .NameValueRow {
11 + display: flex;
12 }
13
14 .Name {
15 color: var(--color-attribute-name);
16 + flex: 0 0 auto;
17 +}
18 +.Name:after {
19 + content: ': ';
20 + color: var(--color-text-color);
21 + margin-right: 0.5rem;
22 }
23
24 .Value {
src/devtools/views/HooksTree.js
+95 -24
@@ -1,36 +1,67 @@
1 // @flow
2
3 -import React from 'react';
4 -import { KeyValue } from './InspectedElementTree';
3 +import React, { useContext } from 'react';
4 +import { BridgeContext, StoreContext } from './context';
5 +import { EditableValue, KeyValue } from './InspectedElementTree';
6 import styles from './HooksTree.css';
7
8 import type { HooksNode, HooksTree } from 'src/backend/types';
9
9 -export function HooksTreeView({ hooksTree }: { hooksTree: HooksTree | null }) {
10 - if (hooksTree === null) {
10 +type HooksTreeViewProps = {|
11 + canEditHooks: boolean,
12 + hooks: HooksTree | null,
13 + id: number,
14 +|};
15 +
16 +export function HooksTreeView({ canEditHooks, hooks, id }: HooksTreeViewProps) {
17 + if (hooks === null) {
18 return null;
19 } else {
20 return (
21 <div className={styles.HooksTreeView}>
22 <div className={styles.Item}>hooks</div>
16 - <InnerHooksTreeView hooksTree={hooksTree} />
23 + <InnerHooksTreeView canEditHooks={canEditHooks} hooks={hooks} id={id} />
24 </div>
25 );
26 }
27 }
28
22 -export function InnerHooksTreeView({ hooksTree }: { hooksTree: HooksTree }) {
29 +type InnerHooksTreeViewProps = {|
30 + canEditHooks: boolean,
31 + hooks: HooksTree,
32 + id: number,
33 +|};
34 +
35 +export function InnerHooksTreeView({
36 + canEditHooks,
37 + hooks,
38 + id,
39 +}: InnerHooksTreeViewProps) {
40 // $FlowFixMe "Missing type annotation for U" whatever that means
24 - return hooksTree.map((hooksNode, index) => (
25 - <HooksNodeView key={index} hooksNode={hooksTree[index]} />
41 + return hooks.map((hook, index) => (
42 + <HookView
43 + key={index}
44 + canEditHooks={canEditHooks}
45 + hook={hooks[index]}
46 + id={id}
47 + />
48 ));
49 }
50
29 -function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
30 - const { name, subHooks, value } = hooksNode;
51 +type HookViewProps = {|
52 + canEditHooks: boolean,
53 + hook: HooksNode,
54 + id: number,
55 + path?: Array<any>,
56 +|};
57 +
58 +function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
59 + const { name, nativeHookIndex, subHooks, value } = hook;
60 +
61 + const bridge = useContext(BridgeContext);
62 + const store = useContext(StoreContext);
63
64 // TODO Add click and key handlers for toggling element open/close state.
33 - // TODO Support editable props
65
66 const isCustomHook = subHooks.length > 0;
67
@@ -40,7 +71,11 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
71 let isComplexDisplayValue = false;
72
73 // Format data for display to mimic the props/state/context for now.
43 - if (type === 'number' || type === 'string' || type === 'boolean') {
74 + if (type === 'string') {
75 + displayValue = `"${((value: any): string)}"`;
76 + } else if (type === 'boolean') {
77 + displayValue = value ? 'true' : 'false';
78 + } else if (type === 'number') {
79 displayValue = value;
80 } else if (value === null) {
81 displayValue = 'null';
@@ -57,39 +92,75 @@ function HooksNodeView({ hooksNode }: { hooksNode: HooksNode }) {
92 if (isCustomHook) {
93 if (isComplexDisplayValue) {
94 return (
60 - <div className={styles.HooksNode}>
95 + <div className={styles.Hook}>
96 <div className={styles.NameValueRow}>
62 - <span className={styles.Name}>{name}: </span>
97 + <span className={styles.Name}>{name}</span>
98 </div>
99 <KeyValue depth={1} name="DebugValue" value={value} />
65 - <InnerHooksTreeView hooksTree={subHooks} />
100 + <InnerHooksTreeView
101 + canEditHooks={canEditHooks}
102 + hooks={subHooks}
103 + id={id}
104 + />
105 </div>
106 );
107 } else {
108 return (
70 - <div className={styles.HooksNode}>
109 + <div className={styles.Hook}>
110 <div className={styles.NameValueRow}>
72 - <span className={styles.Name}>{name}: </span> {/* $FlowFixMe */}
111 + <span className={styles.Name}>{name}</span> {/* $FlowFixMe */}
112 <span className={styles.Value}>{displayValue}</span>
113 </div>
75 - <InnerHooksTreeView hooksTree={subHooks} />
114 + <InnerHooksTreeView
115 + canEditHooks={canEditHooks}
116 + hooks={subHooks}
117 + id={id}
118 + />
119 </div>
120 );
121 }
122 } else {
123 + let overrideValueFn = null;
124 + if (canEditHooks && name === 'State') {
125 + overrideValueFn = (path: Array<string | number>, value: any) => {
126 + const rendererID = store.getRendererIDForElement(id);
127 + bridge.send('overrideHook', {
128 + id,
129 + nativeHookIndex,
130 + path,
131 + rendererID,
132 + value,
133 + });
134 + };
135 + }
136 +
137 if (isComplexDisplayValue) {
138 return (
82 - <div className={styles.HooksNode}>
83 - <KeyValue depth={0} name={name} value={value} />
139 + <div className={styles.Hook}>
140 + <KeyValue
141 + depth={0}
142 + name={name}
143 + overrideValueFn={overrideValueFn}
144 + value={value}
145 + />
146 </div>
147 );
148 } else {
149 return (
88 - <div className={styles.HooksNode}>
150 + <div className={styles.Hook}>
151 <div className={styles.NameValueRow}>
90 - <span className={styles.Name}>{name}: </span>
91 - {/* $FlowFixMe */}
92 - <span className={styles.Value}>{displayValue}</span>
152 + <span className={styles.Name}>{name}</span>
153 + {typeof overrideValueFn === 'function' ? (
154 + <EditableValue
155 + dataType={type}
156 + overrideValueFn={overrideValueFn}
157 + path={[]}
158 + value={value}
159 + />
160 + ) : (
161 + // $FlowFixMe Cannot create span element because in property children
162 + <span className={styles.Value}>{displayValue}</span>
163 + )}
164 </div>
165 </div>
166 );
src/devtools/views/InspectedElementTree.js
+8 -3
@@ -26,7 +26,6 @@ export default function InspectedElementTree({
26 return null;
27 } else {
28 // TODO Add click and key handlers for toggling element open/close state.
29 - // TODO Support editable props
29 return (
30 <div className={styles.InspectedElementTree}>
31 <div className={styles.Item}>{label}</div>
@@ -164,7 +163,7 @@ type EditableValueProps = {|
163 value: any,
164 |};
165
167 -function EditableValue({
166 +export function EditableValue({
167 dataType,
168 overrideValueFn,
169 path,
@@ -214,7 +213,13 @@ function EditableValue({
213 onKeyDown={handleKeyDown}
214 onKeyPress={handleKeyPress}
215 type={type}
217 - value={dataType === 'boolean' ? undefined : editableValue || ''}
216 + value={
217 + dataType === 'boolean'
218 + ? undefined
219 + : editableValue != null
220 + ? editableValue
221 + : ''
222 + }
223 />
224 </label>
225 );
src/devtools/views/SelectedElement.js
+11 -4
@@ -107,7 +107,15 @@ function InspectedElementView({
107 inspectedElement,
108 }: InspectedElementViewProps) {
109 const { id, type } = element;
110 - const { context, hooks, owners, props, state } = inspectedElement;
110 + const {
111 + canEditFunctionProps,
112 + canEditHooks,
113 + context,
114 + hooks,
115 + owners,
116 + props,
117 + state,
118 + } = inspectedElement;
119
120 const { ownerStack } = useContext(TreeContext);
121 const bridge = useContext(BridgeContext);
@@ -129,8 +137,7 @@ function InspectedElementView({
137 const rendererID = store.getRendererIDForElement(id);
138 bridge.send('overrideState', { id, path, rendererID, value });
139 };
132 - } else if (type === ElementTypeFunction) {
133 - // TODO Only enable this if renderer.canEditFunctionProps is true!
140 + } else if (type === ElementTypeFunction && canEditFunctionProps) {
141 overridePropsFn = (path: Array<string | number>, value: any) => {
142 const rendererID = store.getRendererIDForElement(id);
143 bridge.send('overrideProps', { id, path, rendererID, value });
@@ -150,7 +157,7 @@ function InspectedElementView({
157 data={state}
158 overrideValueFn={overrideStateFn}
159 />
153 - <HooksTree hooksTree={hooks} />
160 + <HooksTree canEditHooks={canEditHooks} hooks={hooks} id={id} />
161 <InspectedElementTree
162 label="context"
163 data={context}
src/hook.js
+3 -3
@@ -7,11 +7,11 @@
7 * @flow
8 */
9
10 -import type { Hook } from 'src/backend/types';
10 +import type { DevToolsHook } from 'src/backend/types';
11
12 declare var window: any;
13
14 -export function installHook(target: any): Hook | null {
14 +export function installHook(target: any): DevToolsHook | null {
15 if (target.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) return null;
16
17 function detectReactBuildType(renderer) {
@@ -155,7 +155,7 @@ export function installHook(target: any): Hook | null {
155 const listeners = {};
156 const renderers = new Map();
157
158 - const hook: Hook = {
158 + const hook: DevToolsHook = {
159 rendererInterfaces,
160 listeners,
161 renderers,