@samitouri / QOS-React-2 / commits / 6f118497e8

Updated editable hooks UI and added limited undo feature

Brian Vaughn committed Feb 21, 2019 at 13:24 UTC 6f118497e8f979061c27aa2fd7f10cf2b8b61b9b
7 files changed +150 -29
shells/dev/app/EditableProps/EditableProps.js
+80 -6
@@ -1,16 +1,61 @@
1 // @flow
2
3 -import React, { createContext, Component, Fragment } from 'react';
3 +import React, {
4 + createContext,
5 + Component,
6 + Fragment,
7 + useCallback,
8 + useDebugValue,
9 + useEffect,
10 + useReducer,
11 + useState,
12 +} from 'react';
13 import styles from './EditableProps.css';
14
6 -type StatefulFunctionProps = {| count: number |};
15 +const initialData = { foo: 'FOO', bar: 'BAR' };
16
8 -function StatefulFunction({ count }: StatefulFunctionProps) {
9 - return <li>Count: {count}</li>;
17 +function reducer(state, action) {
18 + switch (action.type) {
19 + case 'swap':
20 + return { foo: state.bar, bar: state.foo };
21 + default:
22 + throw new Error();
23 + }
24 +}
25 +
26 +type StatefulFunctionProps = {| name: string |};
27 +
28 +function StatefulFunction({ name }: StatefulFunctionProps) {
29 + const [count, updateCount] = useState(0);
30 + const debouncedCount = useDebounce(count, 1000);
31 + const handleUpdateCountClick = useCallback(() => updateCount(count + 1), [
32 + count,
33 + ]);
34 +
35 + const [data, dispatch] = useReducer(reducer, initialData);
36 + const handleUpdateReducerClick = useCallback(
37 + () => dispatch({ type: 'swap' }),
38 + []
39 + );
40 +
41 + return (
42 + <Fragment>
43 + <li>Name: {name}</li>
44 + <li>
45 + <button onClick={handleUpdateCountClick}>
46 + Debounced count: {debouncedCount}
47 + </button>
48 + </li>
49 + <li>
50 + <button onClick={handleUpdateReducerClick}>
51 + Reducer state: {JSON.stringify(data, null, 2)}
52 + </button>
53 + </li>
54 + </Fragment>
55 + );
56 }
57
58 const BoolContext = createContext(true);
13 -// $FlowFixMe Flow does not yet know about Context.displayName
59 BoolContext.displayName = 'BoolContext';
60
61 type Props = {| name: string, toggle: boolean |};
@@ -50,8 +95,37 @@ export default function EditableProps() {
95 <div className={styles.Header}>Editable props</div>
96 <ul>
97 <StatefulClass name="Brian" toggle={true} />
53 - <StatefulFunction count={1} />
98 + <StatefulFunction name="Brian" />
99 </ul>
100 </div>
101 );
102 }
103 +
104 +// Below copied from https://usehooks.com/
105 +function useDebounce(value, delay) {
106 + // State and setters for debounced value
107 + const [debouncedValue, setDebouncedValue] = useState(value);
108 +
109 + // Show the value in DevTools
110 + useDebugValue(debouncedValue);
111 +
112 + useEffect(
113 + () => {
114 + // Update debounced value after delay
115 + const handler = setTimeout(() => {
116 + setDebouncedValue(value);
117 + }, delay);
118 +
119 + // Cancel the timeout if value changes (also on delay change or unmount)
120 + // This is how we prevent debounced value from updating if value is changed ...
121 + // .. within the delay period. Timeout gets cleared and restarted.
122 + return () => {
123 + clearTimeout(handler);
124 + };
125 + },
126 + [value, delay] // Only re-call effect if value or delay changes
127 + );
128 +
129 + return debouncedValue;
130 +}
131 +// Above copied from https://usehooks.com/
src/backend/ReactDebugHooks.js
+6 -2
@@ -226,6 +226,7 @@ type ReactCurrentDispatcher = {
226
227 type HooksNode = {
228 index: number,
229 + isEditable: boolean,
230 name: string,
231 value: mixed,
232 subHooks: Array<HooksNode>,
@@ -402,6 +403,7 @@ function buildTree(rootStack, readHookLog): HooksTree {
403 name: parseCustomHookName(stack[j - 1].functionName),
404 value: undefined,
405 index: -1,
406 + isEditable: false,
407 subHooks: children,
408 });
409 stackOfChildren.push(levelChildren);
@@ -409,10 +411,12 @@ function buildTree(rootStack, readHookLog): HooksTree {
411 }
412 prevStack = stack;
413 }
414 + const { primitive } = hook;
415 levelChildren.push({
413 - name: hook.primitive,
416 + index: primitive === 'DebugValue' ? -1 : index++,
417 + isEditable: primitive === 'Reducer' || primitive === 'State',
418 + name: primitive,
419 value: hook.value,
415 - index: hook.primitive === 'DebugValue' ? -1 : index++,
420 subHooks: [],
421 });
422 }
src/backend/types.js
+1
@@ -100,6 +100,7 @@ export type DevToolsHook = {
100
101 export type HooksNode = {
102 index: number,
103 + isEditable: boolean,
104 name: string,
105 value: mixed,
106 subHooks: Array<HooksNode>,
src/devtools/views/ButtonIcon.js
+9
@@ -10,6 +10,7 @@ type Props = {|
10 | 'down'
11 | 'more'
12 | 'search'
13 + | 'undo'
14 | 'up'
15 | 'view-dom'
16 | 'view-source',
@@ -33,6 +34,9 @@ export default function ButtonIcon({ type }: Props) {
34 case 'search':
35 pathData = PATH_SEARCH;
36 break;
37 + case 'undo':
38 + pathData = PATH_UNDO;
39 + break;
40 case 'up':
41 pathData = PATH_UP;
42 break;
@@ -85,6 +89,11 @@ const PATH_SEARCH = `
89 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z
90 `;
91
92 +const PATH_UNDO = `
93 + M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88
94 + 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z
95 +`;
96 +
97 const PATH_UP = 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z';
98
99 const PATH_VIEW_DOM = `
src/devtools/views/HooksTree.js
+3 -2
@@ -56,7 +56,7 @@ type HookViewProps = {|
56 |};
57
58 function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
59 - const { name, index, subHooks, value } = hook;
59 + const { name, index, isEditable, subHooks, value } = hook;
60
61 const bridge = useContext(BridgeContext);
62 const store = useContext(StoreContext);
@@ -121,7 +121,8 @@ function HookView({ canEditHooks, hook, id, path = [] }: HookViewProps) {
121 }
122 } else {
123 let overrideValueFn = null;
124 - if (canEditHooks && name === 'State') {
124 + // TODO Maybe read editable value from debug hook?
125 + if (canEditHooks && isEditable) {
126 overrideValueFn = (path: Array<string | number>, value: any) => {
127 const rendererID = store.getRendererIDForElement(id);
128 bridge.send('overrideHook', {
src/devtools/views/InspectedElementTree.css
+6 -1
@@ -32,11 +32,11 @@
32 }
33
34 .ValueInput {
35 + width: 100%;
36 background: none;
37 border: 1px solid transparent;
38 color: var(--color-attribute-editable-value);
39 border-radius: 0.125rem;
39 - width: 100%;
40 font-family: var(--font-family-monospace);
41 font-size: var(--font-size-monospace-normal);
42 }
@@ -55,3 +55,8 @@
55 font-style: italic;
56 padding-left: 0.75rem;
57 }
58 +
59 +.ResetButton {
60 + flex: 0 0 auto;
61 + padding: 0 0.5rem;
62 +}
src/devtools/views/InspectedElementTree.js
+45 -18
@@ -1,6 +1,8 @@
1 // @flow
2
3 -import React, { useCallback, useState } from 'react';
3 +import React, { Fragment, useCallback, useState } from 'react';
4 +import Button from './Button';
5 +import ButtonIcon from './ButtonIcon';
6 import { getMetaValueLabel } from './utils';
7 import { meta } from '../../hydration';
8 import styles from './InspectedElementTree.css';
@@ -169,8 +171,13 @@ export function EditableValue({
171 path,
172 value,
173 }: EditableValueProps) {
174 + const [hasPendingChanges, setHasPendingChanges] = useState(false);
175 const [editableValue, setEditableValue] = useState(value);
176
177 + if (hasPendingChanges && editableValue === value) {
178 + setHasPendingChanges(false);
179 + }
180 +
181 const handleChange = useCallback(
182 ({ target }) => {
183 if (dataType === 'boolean') {
@@ -181,14 +188,24 @@ export function EditableValue({
188 } else {
189 setEditableValue(target.value);
190 }
191 + setHasPendingChanges(true);
192 },
193 [dataType, overrideValueFn, path]
194 );
195
196 + const handleReset = useCallback(() => {
197 + setEditableValue(value);
198 + setHasPendingChanges(false);
199 + }, [value]);
200 +
201 const handleKeyPress = useCallback(
202 ({ key }) => {
203 if (key === 'Enter') {
204 overrideValueFn(path, editableValue);
205 +
206 + // Don't reset the pending change flag here.
207 + // The inspected fiber won't be updated until after the next "inspectElement" message.
208 + // We'll reset that flag during a subsequent render.
209 }
210 },
211 [path, editableValue, overrideValueFn]
@@ -204,23 +221,33 @@ export function EditableValue({
221 type = 'number';
222 }
223
224 + let inputValue = value;
225 + if (hasPendingChanges) {
226 + inputValue = editableValue == null ? '' : editableValue;
227 + }
228 +
229 return (
208 - <label className={styles.ValueInputLabel}>
209 - <input
210 - checked={dataType === 'boolean' ? editableValue : undefined}
211 - className={styles.ValueInput}
212 - onChange={handleChange}
213 - onKeyDown={handleKeyDown}
214 - onKeyPress={handleKeyPress}
215 - type={type}
216 - value={
217 - dataType === 'boolean'
218 - ? undefined
219 - : editableValue != null
220 - ? editableValue
221 - : ''
222 - }
223 - />
224 - </label>
230 + <Fragment>
231 + <label className={styles.ValueInputLabel}>
232 + <input
233 + checked={dataType === 'boolean' ? inputValue : undefined}
234 + className={styles.ValueInput}
235 + onChange={handleChange}
236 + onKeyDown={handleKeyDown}
237 + onKeyPress={handleKeyPress}
238 + type={type}
239 + value={dataType === 'boolean' ? undefined : inputValue}
240 + />
241 + </label>
242 + {hasPendingChanges && (
243 + <Button
244 + className={styles.ResetButton}
245 + onClick={handleReset}
246 + title="Reset value"
247 + >
248 + <ButtonIcon type="undo" />
249 + </Button>
250 + )}
251 + </Fragment>
252 );
253 }