@samitouri / QOS-React / commits / 50d9451f32

Improve DevTools editing interface (#19774)

* Improve DevTools editing interface This commit adds the ability to rename or delete keys in the props/state/hooks/context editor and adds tests to cover this functionality. DevTools will degrade gracefully for older versions of React that do not inject the new reconciler rename* or delete* methods. Specifically, this commit includes the following changes: * Adds unit tests (for modern and legacy renderers) to cover overriding props, renaming keys, and deleting keys. * Refactor backend override methods to reduce redundant Bridge/Agent listeners and methods. * Inject new (DEV-only) methods from reconciler into DevTools to rename and delete paths. * Refactor 'inspected element' UI components to improve readability. * Improve auto-size input to better mimic Chrome's Style editor panel. (See this Code Sandbox for a proof of concept.) It also contains the following code cleanup: * Additional unit tests have been added for modifying values as well as renaming or deleting paths. * Four new DEV-only methods have been added to the reconciler to be injected into the DevTools hook: overrideHookStateDeletePath, overrideHookStateRenamePath, overridePropsDeletePath, and overridePropsRenamePath. (DevTools will degrade gracefully for older renderers without these methods.) * I also took this as an opportunity to refactor some of the existing code in a few places: * Rather than the backend implementing separate methods for editing props, state, hooks, and context– there are now three methods: deletePath, renamePath, and overrideValueAtPath that accept a type argument to differentiate between props, state, context, or hooks. * The various UI components for the DevTools frontend have been refactored to remove some unnecessary repetition. This commit also adds temporary support for override* commands with mismatched backend/frontend versions: * Add message forwarding for older backend methods (overrideContext, overrideHookState, overrideProps, and overrideState) to the new overrideValueAtPath method. This was done in both the frontend Bridge (for newer frontends passing messages to older embedded backends) and in the backend Agent (for older frontends passing messages to newer backends). We do this because React Native embeds the React DevTools backend, but cannot control which version of the frontend users use. * Additional unit tests have been added as well to cover the older frontend to newer backend case. Our DevTools test infra does not make it easy to write tests for the other way around.

Brian Vaughn committed Sep 18, 2020 at 11:07 UTC 50d9451f320a9aaf94304209193562cc385567d8
47 files changed +4516 -1252
packages/react-devtools-shared/package.json
+1
@@ -11,6 +11,7 @@
11 "@reach/menu-button": "^0.1.17",
12 "@reach/tooltip": "^0.2.2",
13 "clipboard-js": "^0.3.6",
14 + "json5": "^2.1.3",
15 "local-storage-fallback": "^4.1.1",
16 "lodash.throttle": "^4.1.1",
17 "memoize-one": "^3.1.1",
packages/react-devtools-shared/src/__tests__/__snapshots__/inspectedElementContext-test.js.snap
+29 -29
@@ -1,34 +1,5 @@
1 // Jest Snapshot v1, https://goo.gl/fbAQLP
2
3 -exports[`InspectedElementContext display complex values of useDebugValue: DisplayedComplexValue 1`] = `
4 -{
5 - "id": 2,
6 - "owners": null,
7 - "context": null,
8 - "hooks": [
9 - {
10 - "id": null,
11 - "isStateEditable": false,
12 - "name": "DebuggableHook",
13 - "value": {
14 - "foo": 2
15 - },
16 - "subHooks": [
17 - {
18 - "id": 0,
19 - "isStateEditable": true,
20 - "name": "State",
21 - "value": 1,
22 - "subHooks": []
23 - }
24 - ]
25 - }
26 - ],
27 - "props": {},
28 - "state": null
29 -}
30 -`;
31 -
3 exports[`InspectedElementContext should dehydrate complex nested values when requested: 1: Initially inspect element 1`] = `
4 {
5 "id": 2,
@@ -65,6 +36,35 @@ exports[`InspectedElementContext should dehydrate complex nested values when req
36 }
37 `;
38
39 +exports[`InspectedElementContext should display complex values of useDebugValue: DisplayedComplexValue 1`] = `
40 +{
41 + "id": 2,
42 + "owners": null,
43 + "context": null,
44 + "hooks": [
45 + {
46 + "id": null,
47 + "isStateEditable": false,
48 + "name": "DebuggableHook",
49 + "value": {
50 + "foo": 2
51 + },
52 + "subHooks": [
53 + {
54 + "id": 0,
55 + "isStateEditable": true,
56 + "name": "State",
57 + "value": 1,
58 + "subHooks": []
59 + }
60 + ]
61 + }
62 + ],
63 + "props": {},
64 + "state": null
65 +}
66 +`;
67 +
68 exports[`InspectedElementContext should include updates for nested values that were previously hydrated: 1: Initially inspect element 1`] = `
69 {
70 "id": 2,
packages/react-devtools-shared/src/__tests__/editing-test.js new
+1111
@@ -0,0 +1,1111 @@
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 type {FrontendBridge} from 'react-devtools-shared/src/bridge';
11 +import type Store from 'react-devtools-shared/src/devtools/store';
12 +
13 +describe('editing interface', () => {
14 + let PropTypes;
15 + let React;
16 + let ReactDOM;
17 + let bridge: FrontendBridge;
18 + let store: Store;
19 + let utils;
20 +
21 + const flushPendingUpdates = () => {
22 + jest.runOnlyPendingTimers();
23 + };
24 +
25 + beforeEach(() => {
26 + utils = require('./utils');
27 +
28 + bridge = global.bridge;
29 + store = global.store;
30 + store.collapseNodesByDefault = false;
31 +
32 + PropTypes = require('prop-types');
33 + React = require('react');
34 + ReactDOM = require('react-dom');
35 + });
36 +
37 + describe('props', () => {
38 + let committedClassProps;
39 + let committedFunctionProps;
40 + let classID;
41 + let functionID;
42 +
43 + async function mountTestApp() {
44 + class ClassComponent extends React.Component {
45 + componentDidMount() {
46 + committedClassProps = this.props;
47 + }
48 + componentDidUpdate() {
49 + committedClassProps = this.props;
50 + }
51 + render() {
52 + return null;
53 + }
54 + }
55 +
56 + function FunctionComponent(props) {
57 + React.useLayoutEffect(() => {
58 + committedFunctionProps = props;
59 + });
60 + return null;
61 + }
62 +
63 + const container = document.createElement('div');
64 + await utils.actAsync(() =>
65 + ReactDOM.render(
66 + <>
67 + <ClassComponent
68 + array={[1, 2, 3]}
69 + object={{nested: 'initial'}}
70 + shallow="initial"
71 + />
72 + ,
73 + <FunctionComponent
74 + array={[1, 2, 3]}
75 + object={{nested: 'initial'}}
76 + shallow="initial"
77 + />
78 + ,
79 + </>,
80 + container,
81 + ),
82 + );
83 +
84 + classID = ((store.getElementIDAtIndex(0): any): number);
85 + functionID = ((store.getElementIDAtIndex(1): any): number);
86 +
87 + expect(committedClassProps).toStrictEqual({
88 + array: [1, 2, 3],
89 + object: {
90 + nested: 'initial',
91 + },
92 + shallow: 'initial',
93 + });
94 + expect(committedFunctionProps).toStrictEqual({
95 + array: [1, 2, 3],
96 + object: {
97 + nested: 'initial',
98 + },
99 + shallow: 'initial',
100 + });
101 + }
102 +
103 + it('should have editable values', async () => {
104 + await mountTestApp();
105 +
106 + function overrideProps(id, path, value) {
107 + const rendererID = utils.getRendererID();
108 + bridge.send('overrideValueAtPath', {
109 + id,
110 + path,
111 + rendererID,
112 + type: 'props',
113 + value,
114 + });
115 + flushPendingUpdates();
116 + }
117 +
118 + overrideProps(classID, ['shallow'], 'updated');
119 + expect(committedClassProps).toStrictEqual({
120 + array: [1, 2, 3],
121 + object: {
122 + nested: 'initial',
123 + },
124 + shallow: 'updated',
125 + });
126 + overrideProps(classID, ['object', 'nested'], 'updated');
127 + expect(committedClassProps).toStrictEqual({
128 + array: [1, 2, 3],
129 + object: {
130 + nested: 'updated',
131 + },
132 + shallow: 'updated',
133 + });
134 + overrideProps(classID, ['array', 1], 'updated');
135 + expect(committedClassProps).toStrictEqual({
136 + array: [1, 'updated', 3],
137 + object: {
138 + nested: 'updated',
139 + },
140 + shallow: 'updated',
141 + });
142 +
143 + overrideProps(functionID, ['shallow'], 'updated');
144 + expect(committedFunctionProps).toStrictEqual({
145 + array: [1, 2, 3],
146 + object: {
147 + nested: 'initial',
148 + },
149 + shallow: 'updated',
150 + });
151 + overrideProps(functionID, ['object', 'nested'], 'updated');
152 + expect(committedFunctionProps).toStrictEqual({
153 + array: [1, 2, 3],
154 + object: {
155 + nested: 'updated',
156 + },
157 + shallow: 'updated',
158 + });
159 + overrideProps(functionID, ['array', 1], 'updated');
160 + expect(committedFunctionProps).toStrictEqual({
161 + array: [1, 'updated', 3],
162 + object: {
163 + nested: 'updated',
164 + },
165 + shallow: 'updated',
166 + });
167 + });
168 +
169 + // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
170 + it('should still support overriding prop values with legacy backend methods', async () => {
171 + await mountTestApp();
172 +
173 + function overrideProps(id, path, value) {
174 + const rendererID = utils.getRendererID();
175 + bridge.send('overrideProps', {
176 + id,
177 + path,
178 + rendererID,
179 + value,
180 + });
181 + flushPendingUpdates();
182 + }
183 +
184 + overrideProps(classID, ['object', 'nested'], 'updated');
185 + expect(committedClassProps).toStrictEqual({
186 + array: [1, 2, 3],
187 + object: {
188 + nested: 'updated',
189 + },
190 + shallow: 'initial',
191 + });
192 +
193 + overrideProps(functionID, ['shallow'], 'updated');
194 + expect(committedFunctionProps).toStrictEqual({
195 + array: [1, 2, 3],
196 + object: {
197 + nested: 'initial',
198 + },
199 + shallow: 'updated',
200 + });
201 + });
202 +
203 + it('should have editable paths', async () => {
204 + await mountTestApp();
205 +
206 + function renamePath(id, oldPath, newPath) {
207 + const rendererID = utils.getRendererID();
208 + bridge.send('renamePath', {
209 + id,
210 + oldPath,
211 + newPath,
212 + rendererID,
213 + type: 'props',
214 + });
215 + flushPendingUpdates();
216 + }
217 +
218 + renamePath(classID, ['shallow'], ['after']);
219 + expect(committedClassProps).toStrictEqual({
220 + array: [1, 2, 3],
221 + object: {
222 + nested: 'initial',
223 + },
224 + after: 'initial',
225 + });
226 + renamePath(classID, ['object', 'nested'], ['object', 'after']);
227 + expect(committedClassProps).toStrictEqual({
228 + array: [1, 2, 3],
229 + object: {
230 + after: 'initial',
231 + },
232 + after: 'initial',
233 + });
234 +
235 + renamePath(functionID, ['shallow'], ['after']);
236 + expect(committedFunctionProps).toStrictEqual({
237 + array: [1, 2, 3],
238 + object: {
239 + nested: 'initial',
240 + },
241 + after: 'initial',
242 + });
243 + renamePath(functionID, ['object', 'nested'], ['object', 'after']);
244 + expect(committedFunctionProps).toStrictEqual({
245 + array: [1, 2, 3],
246 + object: {
247 + after: 'initial',
248 + },
249 + after: 'initial',
250 + });
251 + });
252 +
253 + it('should enable adding new object properties and array values', async () => {
254 + await mountTestApp();
255 +
256 + function overrideProps(id, path, value) {
257 + const rendererID = utils.getRendererID();
258 + bridge.send('overrideValueAtPath', {
259 + id,
260 + path,
261 + rendererID,
262 + type: 'props',
263 + value,
264 + });
265 + flushPendingUpdates();
266 + }
267 +
268 + overrideProps(classID, ['new'], 'value');
269 + expect(committedClassProps).toStrictEqual({
270 + array: [1, 2, 3],
271 + object: {
272 + nested: 'initial',
273 + },
274 + shallow: 'initial',
275 + new: 'value',
276 + });
277 +
278 + overrideProps(classID, ['object', 'new'], 'value');
279 + expect(committedClassProps).toStrictEqual({
280 + array: [1, 2, 3],
281 + object: {
282 + nested: 'initial',
283 + new: 'value',
284 + },
285 + shallow: 'initial',
286 + new: 'value',
287 + });
288 +
289 + overrideProps(classID, ['array', 3], 'new value');
290 + expect(committedClassProps).toStrictEqual({
291 + array: [1, 2, 3, 'new value'],
292 + object: {
293 + nested: 'initial',
294 + new: 'value',
295 + },
296 + shallow: 'initial',
297 + new: 'value',
298 + });
299 +
300 + overrideProps(functionID, ['new'], 'value');
301 + expect(committedFunctionProps).toStrictEqual({
302 + array: [1, 2, 3],
303 + object: {
304 + nested: 'initial',
305 + },
306 + shallow: 'initial',
307 + new: 'value',
308 + });
309 +
310 + overrideProps(functionID, ['object', 'new'], 'value');
311 + expect(committedFunctionProps).toStrictEqual({
312 + array: [1, 2, 3],
313 + object: {
314 + nested: 'initial',
315 + new: 'value',
316 + },
317 + shallow: 'initial',
318 + new: 'value',
319 + });
320 +
321 + overrideProps(functionID, ['array', 3], 'new value');
322 + expect(committedFunctionProps).toStrictEqual({
323 + array: [1, 2, 3, 'new value'],
324 + object: {
325 + nested: 'initial',
326 + new: 'value',
327 + },
328 + shallow: 'initial',
329 + new: 'value',
330 + });
331 + });
332 +
333 + it('should have deletable keys', async () => {
334 + await mountTestApp();
335 +
336 + function deletePath(id, path) {
337 + const rendererID = utils.getRendererID();
338 + bridge.send('deletePath', {
339 + id,
340 + path,
341 + rendererID,
342 + type: 'props',
343 + });
344 + flushPendingUpdates();
345 + }
346 +
347 + deletePath(classID, ['shallow']);
348 + expect(committedClassProps).toStrictEqual({
349 + array: [1, 2, 3],
350 + object: {
351 + nested: 'initial',
352 + },
353 + });
354 + deletePath(classID, ['object', 'nested']);
355 + expect(committedClassProps).toStrictEqual({
356 + array: [1, 2, 3],
357 + object: {},
358 + });
359 + deletePath(classID, ['array', 1]);
360 + expect(committedClassProps).toStrictEqual({
361 + array: [1, 3],
362 + object: {},
363 + });
364 +
365 + deletePath(functionID, ['shallow']);
366 + expect(committedFunctionProps).toStrictEqual({
367 + array: [1, 2, 3],
368 + object: {
369 + nested: 'initial',
370 + },
371 + });
372 + deletePath(functionID, ['object', 'nested']);
373 + expect(committedFunctionProps).toStrictEqual({
374 + array: [1, 2, 3],
375 + object: {},
376 + });
377 + deletePath(functionID, ['array', 1]);
378 + expect(committedFunctionProps).toStrictEqual({
379 + array: [1, 3],
380 + object: {},
381 + });
382 + });
383 + });
384 +
385 + describe('state', () => {
386 + let committedState;
387 + let id;
388 +
389 + async function mountTestApp() {
390 + class ClassComponent extends React.Component {
391 + state = {
392 + array: [1, 2, 3],
393 + object: {
394 + nested: 'initial',
395 + },
396 + shallow: 'initial',
397 + };
398 + componentDidMount() {
399 + committedState = this.state;
400 + }
401 + componentDidUpdate() {
402 + committedState = this.state;
403 + }
404 + render() {
405 + return null;
406 + }
407 + }
408 +
409 + const container = document.createElement('div');
410 + await utils.actAsync(() =>
411 + ReactDOM.render(
412 + <ClassComponent object={{nested: 'initial'}} shallow="initial" />,
413 + container,
414 + ),
415 + );
416 +
417 + id = ((store.getElementIDAtIndex(0): any): number);
418 +
419 + expect(committedState).toStrictEqual({
420 + array: [1, 2, 3],
421 + object: {
422 + nested: 'initial',
423 + },
424 + shallow: 'initial',
425 + });
426 + }
427 +
428 + it('should have editable values', async () => {
429 + await mountTestApp();
430 +
431 + function overrideState(path, value) {
432 + const rendererID = utils.getRendererID();
433 + bridge.send('overrideValueAtPath', {
434 + id,
435 + path,
436 + rendererID,
437 + type: 'state',
438 + value,
439 + });
440 + flushPendingUpdates();
441 + }
442 +
443 + overrideState(['shallow'], 'updated');
444 + expect(committedState).toStrictEqual({
445 + array: [1, 2, 3],
446 + object: {nested: 'initial'},
447 + shallow: 'updated',
448 + });
449 +
450 + overrideState(['object', 'nested'], 'updated');
451 + expect(committedState).toStrictEqual({
452 + array: [1, 2, 3],
453 + object: {nested: 'updated'},
454 + shallow: 'updated',
455 + });
456 +
457 + overrideState(['array', 1], 'updated');
458 + expect(committedState).toStrictEqual({
459 + array: [1, 'updated', 3],
460 + object: {nested: 'updated'},
461 + shallow: 'updated',
462 + });
463 + });
464 +
465 + // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
466 + it('should still support overriding state values with legacy backend methods', async () => {
467 + await mountTestApp();
468 +
469 + function overrideState(path, value) {
470 + const rendererID = utils.getRendererID();
471 + bridge.send('overrideState', {
472 + id,
473 + path,
474 + rendererID,
475 + value,
476 + });
477 + flushPendingUpdates();
478 + }
479 +
480 + overrideState(['array', 1], 'updated');
481 + expect(committedState).toStrictEqual({
482 + array: [1, 'updated', 3],
483 + object: {nested: 'initial'},
484 + shallow: 'initial',
485 + });
486 + });
487 +
488 + it('should have editable paths', async () => {
489 + await mountTestApp();
490 +
491 + function renamePath(oldPath, newPath) {
492 + const rendererID = utils.getRendererID();
493 + bridge.send('renamePath', {
494 + id,
495 + oldPath,
496 + newPath,
497 + rendererID,
498 + type: 'state',
499 + });
500 + flushPendingUpdates();
501 + }
502 +
503 + renamePath(['shallow'], ['after']);
504 + expect(committedState).toStrictEqual({
505 + array: [1, 2, 3],
506 + object: {
507 + nested: 'initial',
508 + },
509 + after: 'initial',
510 + });
511 +
512 + renamePath(['object', 'nested'], ['object', 'after']);
513 + expect(committedState).toStrictEqual({
514 + array: [1, 2, 3],
515 + object: {
516 + after: 'initial',
517 + },
518 + after: 'initial',
519 + });
520 + });
521 +
522 + it('should enable adding new object properties and array values', async () => {
523 + await mountTestApp();
524 +
525 + function overrideState(path, value) {
526 + const rendererID = utils.getRendererID();
527 + bridge.send('overrideValueAtPath', {
528 + id,
529 + path,
530 + rendererID,
531 + type: 'state',
532 + value,
533 + });
534 + flushPendingUpdates();
535 + }
536 +
537 + overrideState(['new'], 'value');
538 + expect(committedState).toStrictEqual({
539 + array: [1, 2, 3],
540 + object: {
541 + nested: 'initial',
542 + },
543 + shallow: 'initial',
544 + new: 'value',
545 + });
546 +
547 + overrideState(['object', 'new'], 'value');
548 + expect(committedState).toStrictEqual({
549 + array: [1, 2, 3],
550 + object: {
551 + nested: 'initial',
552 + new: 'value',
553 + },
554 + shallow: 'initial',
555 + new: 'value',
556 + });
557 +
558 + overrideState(['array', 3], 'new value');
559 + expect(committedState).toStrictEqual({
560 + array: [1, 2, 3, 'new value'],
561 + object: {
562 + nested: 'initial',
563 + new: 'value',
564 + },
565 + shallow: 'initial',
566 + new: 'value',
567 + });
568 + });
569 +
570 + it('should have deletable keys', async () => {
571 + await mountTestApp();
572 +
573 + function deletePath(path) {
574 + const rendererID = utils.getRendererID();
575 + bridge.send('deletePath', {
576 + id,
577 + path,
578 + rendererID,
579 + type: 'state',
580 + });
581 + flushPendingUpdates();
582 + }
583 +
584 + deletePath(['shallow']);
585 + expect(committedState).toStrictEqual({
586 + array: [1, 2, 3],
587 + object: {
588 + nested: 'initial',
589 + },
590 + });
591 +
592 + deletePath(['object', 'nested']);
593 + expect(committedState).toStrictEqual({
594 + array: [1, 2, 3],
595 + object: {},
596 + });
597 +
598 + deletePath(['array', 1]);
599 + expect(committedState).toStrictEqual({
600 + array: [1, 3],
601 + object: {},
602 + });
603 + });
604 + });
605 +
606 + describe('hooks', () => {
607 + let committedState;
608 + let hookID;
609 + let id;
610 +
611 + async function mountTestApp() {
612 + function FunctionComponent() {
613 + const [state] = React.useState({
614 + array: [1, 2, 3],
615 + object: {
616 + nested: 'initial',
617 + },
618 + shallow: 'initial',
619 + });
620 + React.useLayoutEffect(() => {
621 + committedState = state;
622 + });
623 + return null;
624 + }
625 +
626 + const container = document.createElement('div');
627 + await utils.actAsync(() =>
628 + ReactDOM.render(<FunctionComponent />, container),
629 + );
630 +
631 + hookID = 0; // index
632 + id = ((store.getElementIDAtIndex(0): any): number);
633 +
634 + expect(committedState).toStrictEqual({
635 + array: [1, 2, 3],
636 + object: {
637 + nested: 'initial',
638 + },
639 + shallow: 'initial',
640 + });
641 + }
642 +
643 + it('should have editable values', async () => {
644 + await mountTestApp();
645 +
646 + function overrideHookState(path, value) {
647 + const rendererID = utils.getRendererID();
648 + bridge.send('overrideValueAtPath', {
649 + hookID,
650 + id,
651 + path,
652 + rendererID,
653 + type: 'hooks',
654 + value,
655 + });
656 + flushPendingUpdates();
657 + }
658 +
659 + overrideHookState(['shallow'], 'updated');
660 + expect(committedState).toStrictEqual({
661 + array: [1, 2, 3],
662 + object: {
663 + nested: 'initial',
664 + },
665 + shallow: 'updated',
666 + });
667 +
668 + overrideHookState(['object', 'nested'], 'updated');
669 + expect(committedState).toStrictEqual({
670 + array: [1, 2, 3],
671 + object: {
672 + nested: 'updated',
673 + },
674 + shallow: 'updated',
675 + });
676 +
677 + overrideHookState(['array', 1], 'updated');
678 + expect(committedState).toStrictEqual({
679 + array: [1, 'updated', 3],
680 + object: {
681 + nested: 'updated',
682 + },
683 + shallow: 'updated',
684 + });
685 + });
686 +
687 + // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
688 + it('should still support overriding hook values with legacy backend methods', async () => {
689 + await mountTestApp();
690 +
691 + function overrideHookState(path, value) {
692 + const rendererID = utils.getRendererID();
693 + bridge.send('overrideHookState', {
694 + hookID,
695 + id,
696 + path,
697 + rendererID,
698 + value,
699 + });
700 + flushPendingUpdates();
701 + }
702 +
703 + overrideHookState(['shallow'], 'updated');
704 + expect(committedState).toStrictEqual({
705 + array: [1, 2, 3],
706 + object: {
707 + nested: 'initial',
708 + },
709 + shallow: 'updated',
710 + });
711 + });
712 +
713 + it('should have editable paths', async () => {
714 + await mountTestApp();
715 +
716 + function renamePath(oldPath, newPath) {
717 + const rendererID = utils.getRendererID();
718 + bridge.send('renamePath', {
719 + id,
720 + hookID,
721 + oldPath,
722 + newPath,
723 + rendererID,
724 + type: 'hooks',
725 + });
726 + flushPendingUpdates();
727 + }
728 +
729 + renamePath(['shallow'], ['after']);
730 + expect(committedState).toStrictEqual({
731 + array: [1, 2, 3],
732 + object: {
733 + nested: 'initial',
734 + },
735 + after: 'initial',
736 + });
737 +
738 + renamePath(['object', 'nested'], ['object', 'after']);
739 + expect(committedState).toStrictEqual({
740 + array: [1, 2, 3],
741 + object: {
742 + after: 'initial',
743 + },
744 + after: 'initial',
745 + });
746 + });
747 +
748 + it('should enable adding new object properties and array values', async () => {
749 + await mountTestApp();
750 +
751 + function overrideHookState(path, value) {
752 + const rendererID = utils.getRendererID();
753 + bridge.send('overrideValueAtPath', {
754 + hookID,
755 + id,
756 + path,
757 + rendererID,
758 + type: 'hooks',
759 + value,
760 + });
761 + flushPendingUpdates();
762 + }
763 +
764 + overrideHookState(['new'], 'value');
765 + expect(committedState).toStrictEqual({
766 + array: [1, 2, 3],
767 + object: {
768 + nested: 'initial',
769 + },
770 + shallow: 'initial',
771 + new: 'value',
772 + });
773 +
774 + overrideHookState(['object', 'new'], 'value');
775 + expect(committedState).toStrictEqual({
776 + array: [1, 2, 3],
777 + object: {
778 + nested: 'initial',
779 + new: 'value',
780 + },
781 + shallow: 'initial',
782 + new: 'value',
783 + });
784 +
785 + overrideHookState(['array', 3], 'new value');
786 + expect(committedState).toStrictEqual({
787 + array: [1, 2, 3, 'new value'],
788 + object: {
789 + nested: 'initial',
790 + new: 'value',
791 + },
792 + shallow: 'initial',
793 + new: 'value',
794 + });
795 + });
796 +
797 + it('should have deletable keys', async () => {
798 + await mountTestApp();
799 +
800 + function deletePath(path) {
801 + const rendererID = utils.getRendererID();
802 + bridge.send('deletePath', {
803 + hookID,
804 + id,
805 + path,
806 + rendererID,
807 + type: 'hooks',
808 + });
809 + flushPendingUpdates();
810 + }
811 +
812 + deletePath(['shallow']);
813 + expect(committedState).toStrictEqual({
814 + array: [1, 2, 3],
815 + object: {
816 + nested: 'initial',
817 + },
818 + });
819 +
820 + deletePath(['object', 'nested']);
821 + expect(committedState).toStrictEqual({
822 + array: [1, 2, 3],
823 + object: {},
824 + });
825 +
826 + deletePath(['array', 1]);
827 + expect(committedState).toStrictEqual({
828 + array: [1, 3],
829 + object: {},
830 + });
831 + });
832 + });
833 +
834 + describe('context', () => {
835 + let committedContext;
836 + let id;
837 +
838 + async function mountTestApp() {
839 + class LegacyContextProvider extends React.Component<any> {
840 + static childContextTypes = {
841 + array: PropTypes.array,
842 + object: PropTypes.object,
843 + shallow: PropTypes.string,
844 + };
845 + getChildContext() {
846 + return {
847 + array: [1, 2, 3],
848 + object: {
849 + nested: 'initial',
850 + },
851 + shallow: 'initial',
852 + };
853 + }
854 + render() {
855 + return this.props.children;
856 + }
857 + }
858 +
859 + class ClassComponent extends React.Component<any> {
860 + static contextTypes = {
861 + array: PropTypes.array,
862 + object: PropTypes.object,
863 + shallow: PropTypes.string,
864 + };
865 + componentDidMount() {
866 + committedContext = this.context;
867 + }
868 + componentDidUpdate() {
869 + committedContext = this.context;
870 + }
871 + render() {
872 + return null;
873 + }
874 + }
875 +
876 + const container = document.createElement('div');
877 + await utils.actAsync(() =>
878 + ReactDOM.render(
879 + <LegacyContextProvider>
880 + <ClassComponent />
881 + </LegacyContextProvider>,
882 + container,
883 + ),
884 + );
885 +
886 + // This test only covers Class components.
887 + // Function components using legacy context are not editable.
888 +
889 + id = ((store.getElementIDAtIndex(1): any): number);
890 +
891 + expect(committedContext).toStrictEqual({
892 + array: [1, 2, 3],
893 + object: {
894 + nested: 'initial',
895 + },
896 + shallow: 'initial',
897 + });
898 + }
899 +
900 + it('should have editable values', async () => {
901 + await mountTestApp();
902 +
903 + function overrideContext(path, value) {
904 + const rendererID = utils.getRendererID();
905 +
906 + // To simplify hydration and display of primitive context values (e.g. number, string)
907 + // the inspectElement() method wraps context in a {value: ...} object.
908 + path = ['value', ...path];
909 +
910 + bridge.send('overrideValueAtPath', {
911 + id,
912 + path,
913 + rendererID,
914 + type: 'context',
915 + value,
916 + });
917 + flushPendingUpdates();
918 + }
919 +
920 + overrideContext(['shallow'], 'updated');
921 + expect(committedContext).toStrictEqual({
922 + array: [1, 2, 3],
923 + object: {
924 + nested: 'initial',
925 + },
926 + shallow: 'updated',
927 + });
928 +
929 + overrideContext(['object', 'nested'], 'updated');
930 + expect(committedContext).toStrictEqual({
931 + array: [1, 2, 3],
932 + object: {
933 + nested: 'updated',
934 + },
935 + shallow: 'updated',
936 + });
937 +
938 + overrideContext(['array', 1], 'updated');
939 + expect(committedContext).toStrictEqual({
940 + array: [1, 'updated', 3],
941 + object: {
942 + nested: 'updated',
943 + },
944 + shallow: 'updated',
945 + });
946 + });
947 +
948 + // Tests the combination of older frontend (DevTools UI) with newer backend (embedded within a renderer).
949 + it('should still support overriding context values with legacy backend methods', async () => {
950 + await mountTestApp();
951 +
952 + function overrideContext(path, value) {
953 + const rendererID = utils.getRendererID();
954 +
955 + // To simplify hydration and display of primitive context values (e.g. number, string)
956 + // the inspectElement() method wraps context in a {value: ...} object.
957 + path = ['value', ...path];
958 +
959 + bridge.send('overrideContext', {
960 + id,
961 + path,
962 + rendererID,
963 + value,
964 + });
965 + flushPendingUpdates();
966 + }
967 +
968 + overrideContext(['object', 'nested'], 'updated');
969 + expect(committedContext).toStrictEqual({
970 + array: [1, 2, 3],
971 + object: {
972 + nested: 'updated',
973 + },
974 + shallow: 'initial',
975 + });
976 + });
977 +
978 + it('should have editable paths', async () => {
979 + await mountTestApp();
980 +
981 + function renamePath(oldPath, newPath) {
982 + const rendererID = utils.getRendererID();
983 +
984 + // To simplify hydration and display of primitive context values (e.g. number, string)
985 + // the inspectElement() method wraps context in a {value: ...} object.
986 + oldPath = ['value', ...oldPath];
987 + newPath = ['value', ...newPath];
988 +
989 + bridge.send('renamePath', {
990 + id,
991 + oldPath,
992 + newPath,
993 + rendererID,
994 + type: 'context',
995 + });
996 + flushPendingUpdates();
997 + }
998 +
999 + renamePath(['shallow'], ['after']);
1000 + expect(committedContext).toStrictEqual({
1001 + array: [1, 2, 3],
1002 + object: {
1003 + nested: 'initial',
1004 + },
1005 + after: 'initial',
1006 + });
1007 +
1008 + renamePath(['object', 'nested'], ['object', 'after']);
1009 + expect(committedContext).toStrictEqual({
1010 + array: [1, 2, 3],
1011 + object: {
1012 + after: 'initial',
1013 + },
1014 + after: 'initial',
1015 + });
1016 + });
1017 +
1018 + it('should enable adding new object properties and array values', async () => {
1019 + await mountTestApp();
1020 +
1021 + function overrideContext(path, value) {
1022 + const rendererID = utils.getRendererID();
1023 +
1024 + // To simplify hydration and display of primitive context values (e.g. number, string)
1025 + // the inspectElement() method wraps context in a {value: ...} object.
1026 + path = ['value', ...path];
1027 +
1028 + bridge.send('overrideValueAtPath', {
1029 + id,
1030 + path,
1031 + rendererID,
1032 + type: 'context',
1033 + value,
1034 + });
1035 + flushPendingUpdates();
1036 + }
1037 +
1038 + overrideContext(['new'], 'value');
1039 + expect(committedContext).toStrictEqual({
1040 + array: [1, 2, 3],
1041 + object: {
1042 + nested: 'initial',
1043 + },
1044 + shallow: 'initial',
1045 + new: 'value',
1046 + });
1047 +
1048 + overrideContext(['object', 'new'], 'value');
1049 + expect(committedContext).toStrictEqual({
1050 + array: [1, 2, 3],
1051 + object: {
1052 + nested: 'initial',
1053 + new: 'value',
1054 + },
1055 + shallow: 'initial',
1056 + new: 'value',
1057 + });
1058 +
1059 + overrideContext(['array', 3], 'new value');
1060 + expect(committedContext).toStrictEqual({
1061 + array: [1, 2, 3, 'new value'],
1062 + object: {
1063 + nested: 'initial',
1064 + new: 'value',
1065 + },
1066 + shallow: 'initial',
1067 + new: 'value',
1068 + });
1069 + });
1070 +
1071 + it('should have deletable keys', async () => {
1072 + await mountTestApp();
1073 +
1074 + function deletePath(path) {
1075 + const rendererID = utils.getRendererID();
1076 +
1077 + // To simplify hydration and display of primitive context values (e.g. number, string)
1078 + // the inspectElement() method wraps context in a {value: ...} object.
1079 + path = ['value', ...path];
1080 +
1081 + bridge.send('deletePath', {
1082 + id,
1083 + path,
1084 + rendererID,
1085 + type: 'context',
1086 + });
1087 + flushPendingUpdates();
1088 + }
1089 +
1090 + deletePath(['shallow']);
1091 + expect(committedContext).toStrictEqual({
1092 + array: [1, 2, 3],
1093 + object: {
1094 + nested: 'initial',
1095 + },
1096 + });
1097 +
1098 + deletePath(['object', 'nested']);
1099 + expect(committedContext).toStrictEqual({
1100 + array: [1, 2, 3],
1101 + object: {},
1102 + });
1103 +
1104 + deletePath(['array', 1]);
1105 + expect(committedContext).toStrictEqual({
1106 + array: [1, 3],
1107 + object: {},
1108 + });
1109 + });
1110 + });
1111 +});
packages/react-devtools-shared/src/__tests__/inspectedElementContext-test.js
+1 -1
@@ -1705,7 +1705,7 @@ describe('InspectedElementContext', () => {
1705 done();
1706 });
1707
1708 - it('display complex values of useDebugValue', async done => {
1708 + it('should display complex values of useDebugValue', async done => {
1709 let getInspectedElementPath: GetInspectedElementPath = ((null: any): GetInspectedElementPath);
1710 let inspectedElement = null;
1711 function Suspender({target}) {
packages/react-devtools-shared/src/__tests__/legacy/editing-test.js new
+669
@@ -0,0 +1,669 @@
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 type {FrontendBridge} from 'react-devtools-shared/src/bridge';
11 +import type Store from 'react-devtools-shared/src/devtools/store';
12 +
13 +describe('editing interface', () => {
14 + let PropTypes;
15 + let React;
16 + let ReactDOM;
17 + let bridge: FrontendBridge;
18 + let store: Store;
19 +
20 + const act = (callback: Function) => {
21 + callback();
22 +
23 + jest.runAllTimers(); // Flush Bridge operations
24 + };
25 +
26 + const flushPendingUpdates = () => {
27 + jest.runOnlyPendingTimers();
28 + };
29 +
30 + beforeEach(() => {
31 + bridge = global.bridge;
32 + store = global.store;
33 + store.collapseNodesByDefault = false;
34 +
35 + PropTypes = require('prop-types');
36 +
37 + // Redirect all React/ReactDOM requires to the v15 UMD.
38 + // We use the UMD because Jest doesn't enable us to mock deep imports (e.g. "react/lib/Something").
39 + jest.mock('react', () => jest.requireActual('react-15/dist/react.js'));
40 + jest.mock('react-dom', () =>
41 + jest.requireActual('react-dom-15/dist/react-dom.js'),
42 + );
43 +
44 + React = require('react');
45 + ReactDOM = require('react-dom');
46 + });
47 +
48 + describe('props', () => {
49 + let committedProps;
50 + let id;
51 +
52 + function mountTestApp() {
53 + class ClassComponent extends React.Component {
54 + componentDidMount() {
55 + committedProps = this.props;
56 + }
57 + componentDidUpdate() {
58 + committedProps = this.props;
59 + }
60 + render() {
61 + return null;
62 + }
63 + }
64 +
65 + act(() =>
66 + ReactDOM.render(
67 + <ClassComponent
68 + array={[1, 2, 3]}
69 + object={{nested: 'initial'}}
70 + shallow="initial"
71 + />,
72 + document.createElement('div'),
73 + ),
74 + );
75 +
76 + id = ((store.getElementIDAtIndex(0): any): number);
77 +
78 + expect(committedProps).toStrictEqual({
79 + array: [1, 2, 3],
80 + object: {
81 + nested: 'initial',
82 + },
83 + shallow: 'initial',
84 + });
85 + }
86 +
87 + it('should have editable values', () => {
88 + mountTestApp();
89 +
90 + function overrideProps(path, value) {
91 + const rendererID = ((store.getRendererIDForElement(id): any): number);
92 + bridge.send('overrideValueAtPath', {
93 + id,
94 + path,
95 + rendererID,
96 + type: 'props',
97 + value,
98 + });
99 + flushPendingUpdates();
100 + }
101 +
102 + overrideProps(['shallow'], 'updated');
103 + expect(committedProps).toStrictEqual({
104 + array: [1, 2, 3],
105 + object: {
106 + nested: 'initial',
107 + },
108 + shallow: 'updated',
109 + });
110 + overrideProps(['object', 'nested'], 'updated');
111 + expect(committedProps).toStrictEqual({
112 + array: [1, 2, 3],
113 + object: {
114 + nested: 'updated',
115 + },
116 + shallow: 'updated',
117 + });
118 + overrideProps(['array', 1], 'updated');
119 + expect(committedProps).toStrictEqual({
120 + array: [1, 'updated', 3],
121 + object: {
122 + nested: 'updated',
123 + },
124 + shallow: 'updated',
125 + });
126 + });
127 +
128 + it('should have editable paths', () => {
129 + mountTestApp();
130 +
131 + function renamePath(oldPath, newPath) {
132 + const rendererID = ((store.getRendererIDForElement(id): any): number);
133 + bridge.send('renamePath', {
134 + id,
135 + oldPath,
136 + newPath,
137 + rendererID,
138 + type: 'props',
139 + });
140 + flushPendingUpdates();
141 + }
142 +
143 + renamePath(['shallow'], ['after']);
144 + expect(committedProps).toStrictEqual({
145 + array: [1, 2, 3],
146 + object: {
147 + nested: 'initial',
148 + },
149 + after: 'initial',
150 + });
151 + renamePath(['object', 'nested'], ['object', 'after']);
152 + expect(committedProps).toStrictEqual({
153 + array: [1, 2, 3],
154 + object: {
155 + after: 'initial',
156 + },
157 + after: 'initial',
158 + });
159 + });
160 +
161 + it('should enable adding new object properties and array values', async () => {
162 + await mountTestApp();
163 +
164 + function overrideProps(path, value) {
165 + const rendererID = ((store.getRendererIDForElement(id): any): number);
166 + bridge.send('overrideValueAtPath', {
167 + id,
168 + path,
169 + rendererID,
170 + type: 'props',
171 + value,
172 + });
173 + flushPendingUpdates();
174 + }
175 +
176 + overrideProps(['new'], 'value');
177 + expect(committedProps).toStrictEqual({
178 + array: [1, 2, 3],
179 + object: {
180 + nested: 'initial',
181 + },
182 + shallow: 'initial',
183 + new: 'value',
184 + });
185 +
186 + overrideProps(['object', 'new'], 'value');
187 + expect(committedProps).toStrictEqual({
188 + array: [1, 2, 3],
189 + object: {
190 + nested: 'initial',
191 + new: 'value',
192 + },
193 + shallow: 'initial',
194 + new: 'value',
195 + });
196 +
197 + overrideProps(['array', 3], 'new value');
198 + expect(committedProps).toStrictEqual({
199 + array: [1, 2, 3, 'new value'],
200 + object: {
201 + nested: 'initial',
202 + new: 'value',
203 + },
204 + shallow: 'initial',
205 + new: 'value',
206 + });
207 + });
208 +
209 + it('should have deletable keys', () => {
210 + mountTestApp();
211 +
212 + function deletePath(path) {
213 + const rendererID = ((store.getRendererIDForElement(id): any): number);
214 + bridge.send('deletePath', {
215 + id,
216 + path,
217 + rendererID,
218 + type: 'props',
219 + });
220 + flushPendingUpdates();
221 + }
222 +
223 + deletePath(['shallow']);
224 + expect(committedProps).toStrictEqual({
225 + array: [1, 2, 3],
226 + object: {
227 + nested: 'initial',
228 + },
229 + });
230 + deletePath(['object', 'nested']);
231 + expect(committedProps).toStrictEqual({
232 + array: [1, 2, 3],
233 + object: {},
234 + });
235 + deletePath(['array', 1]);
236 + expect(committedProps).toStrictEqual({
237 + array: [1, 3],
238 + object: {},
239 + });
240 + });
241 + });
242 +
243 + describe('state', () => {
244 + let committedState;
245 + let id;
246 +
247 + function mountTestApp() {
248 + class ClassComponent extends React.Component {
249 + state = {
250 + array: [1, 2, 3],
251 + object: {
252 + nested: 'initial',
253 + },
254 + shallow: 'initial',
255 + };
256 + componentDidMount() {
257 + committedState = this.state;
258 + }
259 + componentDidUpdate() {
260 + committedState = this.state;
261 + }
262 + render() {
263 + return null;
264 + }
265 + }
266 +
267 + act(() =>
268 + ReactDOM.render(
269 + <ClassComponent object={{nested: 'initial'}} shallow="initial" />,
270 + document.createElement('div'),
271 + ),
272 + );
273 +
274 + id = ((store.getElementIDAtIndex(0): any): number);
275 +
276 + expect(committedState).toStrictEqual({
277 + array: [1, 2, 3],
278 + object: {
279 + nested: 'initial',
280 + },
281 + shallow: 'initial',
282 + });
283 + }
284 +
285 + it('should have editable values', () => {
286 + mountTestApp();
287 +
288 + function overrideState(path, value) {
289 + const rendererID = ((store.getRendererIDForElement(id): any): number);
290 + bridge.send('overrideValueAtPath', {
291 + id,
292 + path,
293 + rendererID,
294 + type: 'state',
295 + value,
296 + });
297 + flushPendingUpdates();
298 + }
299 +
300 + overrideState(['shallow'], 'updated');
301 + expect(committedState).toStrictEqual({
302 + array: [1, 2, 3],
303 + object: {nested: 'initial'},
304 + shallow: 'updated',
305 + });
306 +
307 + overrideState(['object', 'nested'], 'updated');
308 + expect(committedState).toStrictEqual({
309 + array: [1, 2, 3],
310 + object: {nested: 'updated'},
311 + shallow: 'updated',
312 + });
313 +
314 + overrideState(['array', 1], 'updated');
315 + expect(committedState).toStrictEqual({
316 + array: [1, 'updated', 3],
317 + object: {nested: 'updated'},
318 + shallow: 'updated',
319 + });
320 + });
321 +
322 + it('should have editable paths', () => {
323 + mountTestApp();
324 +
325 + function renamePath(oldPath, newPath) {
326 + const rendererID = ((store.getRendererIDForElement(id): any): number);
327 + bridge.send('renamePath', {
328 + id,
329 + oldPath,
330 + newPath,
331 + rendererID,
332 + type: 'state',
333 + });
334 + flushPendingUpdates();
335 + }
336 +
337 + renamePath(['shallow'], ['after']);
338 + expect(committedState).toStrictEqual({
339 + array: [1, 2, 3],
340 + object: {
341 + nested: 'initial',
342 + },
343 + after: 'initial',
344 + });
345 +
346 + renamePath(['object', 'nested'], ['object', 'after']);
347 + expect(committedState).toStrictEqual({
348 + array: [1, 2, 3],
349 + object: {
350 + after: 'initial',
351 + },
352 + after: 'initial',
353 + });
354 + });
355 +
356 + it('should enable adding new object properties and array values', async () => {
357 + await mountTestApp();
358 +
359 + function overrideState(path, value) {
360 + const rendererID = ((store.getRendererIDForElement(id): any): number);
361 + bridge.send('overrideValueAtPath', {
362 + id,
363 + path,
364 + rendererID,
365 + type: 'state',
366 + value,
367 + });
368 + flushPendingUpdates();
369 + }
370 +
371 + overrideState(['new'], 'value');
372 + expect(committedState).toStrictEqual({
373 + array: [1, 2, 3],
374 + object: {
375 + nested: 'initial',
376 + },
377 + shallow: 'initial',
378 + new: 'value',
379 + });
380 +
381 + overrideState(['object', 'new'], 'value');
382 + expect(committedState).toStrictEqual({
383 + array: [1, 2, 3],
384 + object: {
385 + nested: 'initial',
386 + new: 'value',
387 + },
388 + shallow: 'initial',
389 + new: 'value',
390 + });
391 +
392 + overrideState(['array', 3], 'new value');
393 + expect(committedState).toStrictEqual({
394 + array: [1, 2, 3, 'new value'],
395 + object: {
396 + nested: 'initial',
397 + new: 'value',
398 + },
399 + shallow: 'initial',
400 + new: 'value',
401 + });
402 + });
403 +
404 + it('should have deletable keys', () => {
405 + mountTestApp();
406 +
407 + function deletePath(path) {
408 + const rendererID = ((store.getRendererIDForElement(id): any): number);
409 + bridge.send('deletePath', {
410 + id,
411 + path,
412 + rendererID,
413 + type: 'state',
414 + });
415 + flushPendingUpdates();
416 + }
417 +
418 + deletePath(['shallow']);
419 + expect(committedState).toStrictEqual({
420 + array: [1, 2, 3],
421 + object: {
422 + nested: 'initial',
423 + },
424 + });
425 +
426 + deletePath(['object', 'nested']);
427 + expect(committedState).toStrictEqual({
428 + array: [1, 2, 3],
429 + object: {},
430 + });
431 +
432 + deletePath(['array', 1]);
433 + expect(committedState).toStrictEqual({
434 + array: [1, 3],
435 + object: {},
436 + });
437 + });
438 + });
439 +
440 + describe('context', () => {
441 + let committedContext;
442 + let id;
443 +
444 + function mountTestApp() {
445 + class LegacyContextProvider extends React.Component<any> {
446 + static childContextTypes = {
447 + array: PropTypes.array,
448 + object: PropTypes.object,
449 + shallow: PropTypes.string,
450 + };
451 + getChildContext() {
452 + return {
453 + array: [1, 2, 3],
454 + object: {
455 + nested: 'initial',
456 + },
457 + shallow: 'initial',
458 + };
459 + }
460 + render() {
461 + return this.props.children;
462 + }
463 + }
464 +
465 + class ClassComponent extends React.Component<any> {
466 + static contextTypes = {
467 + array: PropTypes.array,
468 + object: PropTypes.object,
469 + shallow: PropTypes.string,
470 + };
471 + componentDidMount() {
472 + committedContext = this.context;
473 + }
474 + componentDidUpdate() {
475 + committedContext = this.context;
476 + }
477 + render() {
478 + return null;
479 + }
480 + }
481 +
482 + act(() =>
483 + ReactDOM.render(
484 + <LegacyContextProvider>
485 + <ClassComponent />
486 + </LegacyContextProvider>,
487 + document.createElement('div'),
488 + ),
489 + );
490 +
491 + // This test only covers Class components.
492 + // Function components using legacy context are not editable.
493 +
494 + id = ((store.getElementIDAtIndex(1): any): number);
495 +
496 + expect(committedContext).toStrictEqual({
497 + array: [1, 2, 3],
498 + object: {
499 + nested: 'initial',
500 + },
501 + shallow: 'initial',
502 + });
503 + }
504 +
505 + it('should have editable values', () => {
506 + mountTestApp();
507 +
508 + function overrideContext(path, value) {
509 + const rendererID = ((store.getRendererIDForElement(id): any): number);
510 +
511 + bridge.send('overrideValueAtPath', {
512 + id,
513 + path,
514 + rendererID,
515 + type: 'context',
516 + value,
517 + });
518 + flushPendingUpdates();
519 + }
520 +
521 + overrideContext(['shallow'], 'updated');
522 + expect(committedContext).toStrictEqual({
523 + array: [1, 2, 3],
524 + object: {
525 + nested: 'initial',
526 + },
527 + shallow: 'updated',
528 + });
529 +
530 + overrideContext(['object', 'nested'], 'updated');
531 + expect(committedContext).toStrictEqual({
532 + array: [1, 2, 3],
533 + object: {
534 + nested: 'updated',
535 + },
536 + shallow: 'updated',
537 + });
538 +
539 + overrideContext(['array', 1], 'updated');
540 + expect(committedContext).toStrictEqual({
541 + array: [1, 'updated', 3],
542 + object: {
543 + nested: 'updated',
544 + },
545 + shallow: 'updated',
546 + });
547 + });
548 +
549 + it('should have editable paths', () => {
550 + mountTestApp();
551 +
552 + function renamePath(oldPath, newPath) {
553 + const rendererID = ((store.getRendererIDForElement(id): any): number);
554 +
555 + bridge.send('renamePath', {
556 + id,
557 + oldPath,
558 + newPath,
559 + rendererID,
560 + type: 'context',
561 + });
562 + flushPendingUpdates();
563 + }
564 +
565 + renamePath(['shallow'], ['after']);
566 + expect(committedContext).toStrictEqual({
567 + array: [1, 2, 3],
568 + object: {
569 + nested: 'initial',
570 + },
571 + after: 'initial',
572 + });
573 +
574 + renamePath(['object', 'nested'], ['object', 'after']);
575 + expect(committedContext).toStrictEqual({
576 + array: [1, 2, 3],
577 + object: {
578 + after: 'initial',
579 + },
580 + after: 'initial',
581 + });
582 + });
583 +
584 + it('should enable adding new object properties and array values', async () => {
585 + await mountTestApp();
586 +
587 + function overrideContext(path, value) {
588 + const rendererID = ((store.getRendererIDForElement(id): any): number);
589 +
590 + bridge.send('overrideValueAtPath', {
591 + id,
592 + path,
593 + rendererID,
594 + type: 'context',
595 + value,
596 + });
597 + flushPendingUpdates();
598 + }
599 +
600 + overrideContext(['new'], 'value');
601 + expect(committedContext).toStrictEqual({
602 + array: [1, 2, 3],
603 + object: {
604 + nested: 'initial',
605 + },
606 + shallow: 'initial',
607 + new: 'value',
608 + });
609 +
610 + overrideContext(['object', 'new'], 'value');
611 + expect(committedContext).toStrictEqual({
612 + array: [1, 2, 3],
613 + object: {
614 + nested: 'initial',
615 + new: 'value',
616 + },
617 + shallow: 'initial',
618 + new: 'value',
619 + });
620 +
621 + overrideContext(['array', 3], 'new value');
622 + expect(committedContext).toStrictEqual({
623 + array: [1, 2, 3, 'new value'],
624 + object: {
625 + nested: 'initial',
626 + new: 'value',
627 + },
628 + shallow: 'initial',
629 + new: 'value',
630 + });
631 + });
632 +
633 + it('should have deletable keys', () => {
634 + mountTestApp();
635 +
636 + function deletePath(path) {
637 + const rendererID = ((store.getRendererIDForElement(id): any): number);
638 +
639 + bridge.send('deletePath', {
640 + id,
641 + path,
642 + rendererID,
643 + type: 'context',
644 + });
645 + flushPendingUpdates();
646 + }
647 +
648 + deletePath(['shallow']);
649 + expect(committedContext).toStrictEqual({
650 + array: [1, 2, 3],
651 + object: {
652 + nested: 'initial',
653 + },
654 + });
655 +
656 + deletePath(['object', 'nested']);
657 + expect(committedContext).toStrictEqual({
658 + array: [1, 2, 3],
659 + object: {},
660 + });
661 +
662 + deletePath(['array', 1]);
663 + expect(committedContext).toStrictEqual({
664 + array: [1, 3],
665 + object: {},
666 + });
667 + });
668 + });
669 +});
packages/react-devtools-shared/src/backend/NativeStyleEditor/setupNativeStyleEditor.js
+14 -7
@@ -224,14 +224,16 @@ function renameStyle(
224 customStyle[oldName] = undefined;
225 }
226
227 - agent.overrideProps({
227 + agent.overrideValueAtPath({
228 + type: 'props',
229 id,
230 rendererID,
231 path: ['style', lastIndex],
232 value: customStyle,
233 });
234 } else {
234 - agent.overrideProps({
235 + agent.overrideValueAtPath({
236 + type: 'props',
237 id,
238 rendererID,
239 path: ['style'],
@@ -247,14 +249,16 @@ function renameStyle(
249 customStyle[oldName] = undefined;
250 }
251
250 - agent.overrideProps({
252 + agent.overrideValueAtPath({
253 + type: 'props',
254 id,
255 rendererID,
256 path: ['style'],
257 value: customStyle,
258 });
259 } else {
257 - agent.overrideProps({
260 + agent.overrideValueAtPath({
261 + type: 'props',
262 id,
263 rendererID,
264 path: ['style'],
@@ -298,14 +302,16 @@ function setStyle(
302 typeof style[lastLength] === 'object' &&
303 !Array.isArray(style[lastLength])
304 ) {
301 - agent.overrideProps({
305 + agent.overrideValueAtPath({
306 + type: 'props',
307 id,
308 rendererID,
309 path: ['style', lastLength, name],
310 value,
311 });
312 } else {
308 - agent.overrideProps({
313 + agent.overrideValueAtPath({
314 + type: 'props',
315 id,
316 rendererID,
317 path: ['style'],
@@ -313,7 +319,8 @@ function setStyle(
319 });
320 }
321 } else {
316 - agent.overrideProps({
322 + agent.overrideValueAtPath({
323 + type: 'props',
324 id,
325 rendererID,
326 path: ['style'],
packages/react-devtools-shared/src/backend/agent.js
+163 -38
@@ -79,10 +79,40 @@ type OverrideHookParams = {|
79 hookID: number,
80 path: Array<string | number>,
81 rendererID: number,
82 + wasForwarded?: boolean,
83 value: any,
84 |};
85
86 type SetInParams = {|
87 + id: number,
88 + path: Array<string | number>,
89 + rendererID: number,
90 + wasForwarded?: boolean,
91 + value: any,
92 +|};
93 +
94 +type PathType = 'props' | 'hooks' | 'state' | 'context';
95 +
96 +type DeletePathParams = {|
97 + type: PathType,
98 + hookID?: ?number,
99 + id: number,
100 + path: Array<string | number>,
101 + rendererID: number,
102 +|};
103 +
104 +type RenamePathParams = {|
105 + type: PathType,
106 + hookID?: ?number,
107 + id: number,
108 + oldPath: Array<string | number>,
109 + newPath: Array<string | number>,
110 + rendererID: number,
111 +|};
112 +
113 +type OverrideValueAtPathParams = {|
114 + type: PathType,
115 + hookID?: ?number,
116 id: number,
117 path: Array<string | number>,
118 rendererID: number,
@@ -140,17 +170,16 @@ export default class Agent extends EventEmitter<{|
170 this._bridge = bridge;
171
172 bridge.addListener('copyElementPath', this.copyElementPath);
173 + bridge.addListener('deletePath', this.deletePath);
174 bridge.addListener('getProfilingData', this.getProfilingData);
175 bridge.addListener('getProfilingStatus', this.getProfilingStatus);
176 bridge.addListener('getOwnersList', this.getOwnersList);
177 bridge.addListener('inspectElement', this.inspectElement);
178 bridge.addListener('logElementToConsole', this.logElementToConsole);
148 - bridge.addListener('overrideContext', this.overrideContext);
149 - bridge.addListener('overrideHookState', this.overrideHookState);
150 - bridge.addListener('overrideProps', this.overrideProps);
151 - bridge.addListener('overrideState', this.overrideState);
179 bridge.addListener('overrideSuspense', this.overrideSuspense);
180 + bridge.addListener('overrideValueAtPath', this.overrideValueAtPath);
181 bridge.addListener('reloadAndProfile', this.reloadAndProfile);
182 + bridge.addListener('renamePath', this.renamePath);
183 bridge.addListener('setTraceUpdatesEnabled', this.setTraceUpdatesEnabled);
184 bridge.addListener('startProfiling', this.startProfiling);
185 bridge.addListener('stopProfiling', this.stopProfiling);
@@ -168,6 +197,14 @@ export default class Agent extends EventEmitter<{|
197 bridge.addListener('viewAttributeSource', this.viewAttributeSource);
198 bridge.addListener('viewElementSource', this.viewElementSource);
199
200 + // Temporarily support older standalone front-ends sending commands to newer embedded backends.
201 + // We do this because React Native embeds the React DevTools backend,
202 + // but cannot control which version of the frontend users use.
203 + bridge.addListener('overrideContext', this.overrideContext);
204 + bridge.addListener('overrideHookState', this.overrideHookState);
205 + bridge.addListener('overrideProps', this.overrideProps);
206 + bridge.addListener('overrideState', this.overrideState);
207 +
208 if (this._isProfiling) {
209 bridge.send('profilingStatus', true);
210 }
@@ -198,6 +235,15 @@ export default class Agent extends EventEmitter<{|
235 }
236 };
237
238 + deletePath = ({hookID, id, path, rendererID, type}: DeletePathParams) => {
239 + const renderer = this._rendererInterfaces[rendererID];
240 + if (renderer == null) {
241 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
242 + } else {
243 + renderer.deletePath(type, id, hookID, path);
244 + }
245 + };
246 +
247 getInstanceAndStyle({
248 id,
249 rendererID,
@@ -288,71 +334,150 @@ export default class Agent extends EventEmitter<{|
334 }
335 };
336
291 - reloadAndProfile = (recordChangeDescriptions: boolean) => {
292 - sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
293 - sessionStorageSetItem(
294 - SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
295 - recordChangeDescriptions ? 'true' : 'false',
296 - );
297 -
298 - // This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
299 - // In that case, the shell must also listen for this specific message to know when it needs to reload the app.
300 - // The agent can't do this in a way that is renderer agnostic.
301 - this._bridge.send('reloadAppForProfiling');
337 + overrideSuspense = ({
338 + id,
339 + rendererID,
340 + forceFallback,
341 + }: OverrideSuspenseParams) => {
342 + const renderer = this._rendererInterfaces[rendererID];
343 + if (renderer == null) {
344 + console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
345 + } else {
346 + renderer.overrideSuspense(id, forceFallback);
347 + }
348 };
349
304 - overrideContext = ({id, path, rendererID, value}: SetInParams) => {
350 + overrideValueAtPath = ({
351 + hookID,
352 + id,
353 + path,
354 + rendererID,
355 + type,
356 + value,
357 + }: OverrideValueAtPathParams) => {
358 const renderer = this._rendererInterfaces[rendererID];
359 if (renderer == null) {
360 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
361 } else {
309 - renderer.setInContext(id, path, value);
362 + renderer.overrideValueAtPath(type, id, hookID, path, value);
363 + }
364 + };
365 +
366 + // Temporarily support older standalone front-ends by forwarding the older message types
367 + // to the new "overrideValueAtPath" command the backend is now listening to.
368 + overrideContext = ({
369 + id,
370 + path,
371 + rendererID,
372 + wasForwarded,
373 + value,
374 + }: SetInParams) => {
375 + // Don't forward a message that's already been forwarded by the front-end Bridge.
376 + // We only need to process the override command once!
377 + if (!wasForwarded) {
378 + this.overrideValueAtPath({
379 + id,
380 + path,
381 + rendererID,
382 + type: 'context',
383 + value,
384 + });
385 }
386 };
387
388 + // Temporarily support older standalone front-ends by forwarding the older message types
389 + // to the new "overrideValueAtPath" command the backend is now listening to.
390 overrideHookState = ({
391 id,
392 hookID,
393 path,
394 rendererID,
395 + wasForwarded,
396 value,
397 }: OverrideHookParams) => {
320 - const renderer = this._rendererInterfaces[rendererID];
321 - if (renderer == null) {
322 - console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
323 - } else {
324 - renderer.setInHook(id, hookID, path, value);
398 + // Don't forward a message that's already been forwarded by the front-end Bridge.
399 + // We only need to process the override command once!
400 + if (!wasForwarded) {
401 + this.overrideValueAtPath({
402 + id,
403 + path,
404 + rendererID,
405 + type: 'hooks',
406 + value,
407 + });
408 }
409 };
410
328 - overrideProps = ({id, path, rendererID, value}: SetInParams) => {
329 - const renderer = this._rendererInterfaces[rendererID];
330 - if (renderer == null) {
331 - console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
332 - } else {
333 - renderer.setInProps(id, path, value);
411 + // Temporarily support older standalone front-ends by forwarding the older message types
412 + // to the new "overrideValueAtPath" command the backend is now listening to.
413 + overrideProps = ({
414 + id,
415 + path,
416 + rendererID,
417 + wasForwarded,
418 + value,
419 + }: SetInParams) => {
420 + // Don't forward a message that's already been forwarded by the front-end Bridge.
421 + // We only need to process the override command once!
422 + if (!wasForwarded) {
423 + this.overrideValueAtPath({
424 + id,
425 + path,
426 + rendererID,
427 + type: 'props',
428 + value,
429 + });
430 }
431 };
432
337 - overrideState = ({id, path, rendererID, value}: SetInParams) => {
338 - const renderer = this._rendererInterfaces[rendererID];
339 - if (renderer == null) {
340 - console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
341 - } else {
342 - renderer.setInState(id, path, value);
433 + // Temporarily support older standalone front-ends by forwarding the older message types
434 + // to the new "overrideValueAtPath" command the backend is now listening to.
435 + overrideState = ({
436 + id,
437 + path,
438 + rendererID,
439 + wasForwarded,
440 + value,
441 + }: SetInParams) => {
442 + // Don't forward a message that's already been forwarded by the front-end Bridge.
443 + // We only need to process the override command once!
444 + if (!wasForwarded) {
445 + this.overrideValueAtPath({
446 + id,
447 + path,
448 + rendererID,
449 + type: 'state',
450 + value,
451 + });
452 }
453 };
454
346 - overrideSuspense = ({
455 + reloadAndProfile = (recordChangeDescriptions: boolean) => {
456 + sessionStorageSetItem(SESSION_STORAGE_RELOAD_AND_PROFILE_KEY, 'true');
457 + sessionStorageSetItem(
458 + SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
459 + recordChangeDescriptions ? 'true' : 'false',
460 + );
461 +
462 + // This code path should only be hit if the shell has explicitly told the Store that it supports profiling.
463 + // In that case, the shell must also listen for this specific message to know when it needs to reload the app.
464 + // The agent can't do this in a way that is renderer agnostic.
465 + this._bridge.send('reloadAppForProfiling');
466 + };
467 +
468 + renamePath = ({
469 + hookID,
470 id,
471 + newPath,
472 + oldPath,
473 rendererID,
349 - forceFallback,
350 - }: OverrideSuspenseParams) => {
474 + type,
475 + }: RenamePathParams) => {
476 const renderer = this._rendererInterfaces[rendererID];
477 if (renderer == null) {
478 console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
479 } else {
355 - renderer.overrideSuspense(id, forceFallback);
480 + renderer.renamePath(type, id, hookID, oldPath, newPath);
481 }
482 };
483
packages/react-devtools-shared/src/backend/legacy/renderer.js
+107 -38
@@ -15,8 +15,20 @@ import {
15 ElementTypeOtherOrUnknown,
16 } from 'react-devtools-shared/src/types';
17 import {getUID, utfEncodeString, printOperationsArray} from '../../utils';
18 -import {cleanForBridge, copyToClipboard, copyWithSet} from '../utils';
19 -import {getDisplayName, getInObject} from 'react-devtools-shared/src/utils';
18 +import {
19 + cleanForBridge,
20 + copyToClipboard,
21 + copyWithDelete,
22 + copyWithRename,
23 + copyWithSet,
24 +} from '../utils';
25 +import {
26 + deletePathInObject,
27 + getDisplayName,
28 + getInObject,
29 + renamePathInObject,
30 + setInObject,
31 +} from 'react-devtools-shared/src/utils';
32 import {
33 __DEBUG__,
34 TREE_OPERATION_ADD,
@@ -770,11 +782,15 @@ export function attach(
782 return {
783 id,
784
773 - // Hooks did not exist in legacy versions
785 + // Does the current renderer support editable hooks and function props?
786 canEditHooks: false,
787 + canEditFunctionProps: false,
788
776 - // Does the current renderer support editable function props?
777 - canEditFunctionProps: true,
789 + // Does the current renderer support advanced editing interface?
790 + canEditHooksAndDeletePaths: false,
791 + canEditHooksAndRenamePaths: false,
792 + canEditFunctionPropsDeletePaths: false,
793 + canEditFunctionPropsRenamePaths: false,
794
795 // Suspense did not exist in legacy versions
796 canToggleSuspense: false,
@@ -873,53 +889,110 @@ export function attach(
889 global.$type = element.type;
890 }
891
876 - function setInProps(id: number, path: Array<string | number>, value: any) {
892 + function deletePath(
893 + type: 'context' | 'hooks' | 'props' | 'state',
894 + id: number,
895 + hookID: ?number,
896 + path: Array<string | number>,
897 + ): void {
898 const internalInstance = idToInternalInstanceMap.get(id);
899 if (internalInstance != null) {
879 - const element = internalInstance._currentElement;
880 - internalInstance._currentElement = {
881 - ...element,
882 - props: copyWithSet(element.props, path, value),
883 - };
884 - forceUpdate(internalInstance._instance);
900 + const publicInstance = internalInstance._instance;
901 + if (publicInstance != null) {
902 + switch (type) {
903 + case 'context':
904 + deletePathInObject(publicInstance.context, path);
905 + forceUpdate(publicInstance);
906 + break;
907 + case 'hooks':
908 + throw new Error('Hooks not supported by this renderer');
909 + case 'props':
910 + const element = internalInstance._currentElement;
911 + internalInstance._currentElement = {
912 + ...element,
913 + props: copyWithDelete(element.props, path),
914 + };
915 + forceUpdate(publicInstance);
916 + break;
917 + case 'state':
918 + deletePathInObject(publicInstance.state, path);
919 + forceUpdate(publicInstance);
920 + break;
921 + }
922 + }
923 }
924 }
925
888 - function setInState(id: number, path: Array<string | number>, value: any) {
926 + function renamePath(
927 + type: 'context' | 'hooks' | 'props' | 'state',
928 + id: number,
929 + hookID: ?number,
930 + oldPath: Array<string | number>,
931 + newPath: Array<string | number>,
932 + ): void {
933 const internalInstance = idToInternalInstanceMap.get(id);
934 if (internalInstance != null) {
935 const publicInstance = internalInstance._instance;
936 if (publicInstance != null) {
893 - setIn(publicInstance.state, path, value);
894 - forceUpdate(publicInstance);
937 + switch (type) {
938 + case 'context':
939 + renamePathInObject(publicInstance.context, oldPath, newPath);
940 + forceUpdate(publicInstance);
941 + break;
942 + case 'hooks':
943 + throw new Error('Hooks not supported by this renderer');
944 + case 'props':
945 + const element = internalInstance._currentElement;
946 + internalInstance._currentElement = {
947 + ...element,
948 + props: copyWithRename(element.props, oldPath, newPath),
949 + };
950 + forceUpdate(publicInstance);
951 + break;
952 + case 'state':
953 + renamePathInObject(publicInstance.state, oldPath, newPath);
954 + forceUpdate(publicInstance);
955 + break;
956 + }
957 }
958 }
959 }
960
899 - function setInContext(id: number, path: Array<string | number>, value: any) {
961 + function overrideValueAtPath(
962 + type: 'context' | 'hooks' | 'props' | 'state',
963 + id: number,
964 + hookID: ?number,
965 + path: Array<string | number>,
966 + value: any,
967 + ): void {
968 const internalInstance = idToInternalInstanceMap.get(id);
969 if (internalInstance != null) {
970 const publicInstance = internalInstance._instance;
971 if (publicInstance != null) {
904 - setIn(publicInstance.context, path, value);
905 - forceUpdate(publicInstance);
972 + switch (type) {
973 + case 'context':
974 + setInObject(publicInstance.context, path, value);
975 + forceUpdate(publicInstance);
976 + break;
977 + case 'hooks':
978 + throw new Error('Hooks not supported by this renderer');
979 + case 'props':
980 + const element = internalInstance._currentElement;
981 + internalInstance._currentElement = {
982 + ...element,
983 + props: copyWithSet(element.props, path, value),
984 + };
985 + forceUpdate(publicInstance);
986 + break;
987 + case 'state':
988 + setInObject(publicInstance.state, path, value);
989 + forceUpdate(publicInstance);
990 + break;
991 + }
992 }
993 }
994 }
995
910 - function setIn(obj: Object, path: Array<string | number>, value: any) {
911 - const last = path.pop();
912 - const parent = path.reduce(
913 - // $FlowFixMe
914 - (reduced, attr) => (reduced ? reduced[attr] : null),
915 - obj,
916 - );
917 - if (parent) {
918 - // $FlowFixMe
919 - parent[last] = value;
920 - }
921 - }
922 -
996 // v16+ only features
997 const getProfilingData = () => {
998 throw new Error('getProfilingData not supported by this renderer');
@@ -933,9 +1006,6 @@ export function attach(
1006 const overrideSuspense = () => {
1007 throw new Error('overrideSuspense not supported by this renderer');
1008 };
936 - const setInHook = () => {
937 - throw new Error('setInHook not supported by this renderer');
938 - };
1009 const startProfiling = () => {
1010 // Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
1011 };
@@ -973,6 +1043,7 @@ export function attach(
1043 return {
1044 cleanup,
1045 copyElementPath,
1046 + deletePath,
1047 flushInitialOperations,
1048 getBestMatchForTrackedPath,
1049 getDisplayNameForFiberID,
@@ -990,13 +1061,11 @@ export function attach(
1061 inspectElement,
1062 logElementToConsole,
1063 overrideSuspense,
1064 + overrideValueAtPath,
1065 + renamePath,
1066 prepareViewAttributeSource,
1067 prepareViewElementSource,
1068 renderer,
996 - setInContext,
997 - setInHook,
998 - setInProps,
999 - setInState,
1069 setTraceUpdatesEnabled,
1070 setTrackedPath,
1071 startProfiling,
packages/react-devtools-shared/src/backend/renderer.js
+182 -42
@@ -26,15 +26,23 @@ import {
26 ElementTypeSuspenseList,
27 } from 'react-devtools-shared/src/types';
28 import {
29 + deletePathInObject,
30 getDisplayName,
31 getDefaultComponentFilters,
32 getInObject,
33 getUID,
34 + renamePathInObject,
35 setInObject,
36 utfEncodeString,
37 } from 'react-devtools-shared/src/utils';
38 import {sessionStorageGetItem} from 'react-devtools-shared/src/storage';
37 -import {cleanForBridge, copyToClipboard, copyWithSet} from './utils';
39 +import {
40 + cleanForBridge,
41 + copyToClipboard,
42 + copyWithDelete,
43 + copyWithRename,
44 + copyWithSet,
45 +} from './utils';
46 import {
47 __DEBUG__,
48 SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
@@ -452,7 +460,11 @@ export function attach(
460
461 const {
462 overrideHookState,
463 + overrideHookStateDeletePath,
464 + overrideHookStateRenamePath,
465 overrideProps,
466 + overridePropsDeletePath,
467 + overridePropsRenamePath,
468 setSuspenseHandler,
469 scheduleUpdate,
470 } = renderer;
@@ -2360,12 +2372,20 @@ export function attach(
2372 return {
2373 id,
2374
2363 - // Does the current renderer support editable hooks?
2375 + // Does the current renderer support editable hooks and function props?
2376 canEditHooks: typeof overrideHookState === 'function',
2365 -
2366 - // Does the current renderer support editable function props?
2377 canEditFunctionProps: typeof overrideProps === 'function',
2378
2379 + // Does the current renderer support advanced editing interface?
2380 + canEditHooksAndDeletePaths:
2381 + typeof overrideHookStateDeletePath === 'function',
2382 + canEditHooksAndRenamePaths:
2383 + typeof overrideHookStateRenamePath === 'function',
2384 + canEditFunctionPropsDeletePaths:
2385 + typeof overridePropsDeletePath === 'function',
2386 + canEditFunctionPropsRenamePaths:
2387 + typeof overridePropsRenamePath === 'function',
2388 +
2389 canToggleSuspense:
2390 supportsTogglingSuspense &&
2391 // If it's showing the real content, we can always flip fallback.
@@ -2687,60 +2707,181 @@ export function attach(
2707 }
2708 }
2709
2690 - function setInHook(
2710 + function deletePath(
2711 + type: 'context' | 'hooks' | 'props' | 'state',
2712 id: number,
2692 - index: number,
2713 + hookID: ?number,
2714 path: Array<string | number>,
2694 - value: any,
2695 - ) {
2715 + ): void {
2716 const fiber = findCurrentFiberUsingSlowPathById(id);
2717 if (fiber !== null) {
2698 - if (typeof overrideHookState === 'function') {
2699 - overrideHookState(fiber, index, path, value);
2718 + const instance = fiber.stateNode;
2719 +
2720 + switch (type) {
2721 + case 'context':
2722 + // To simplify hydration and display of primitive context values (e.g. number, string)
2723 + // the inspectElement() method wraps context in a {value: ...} object.
2724 + // We need to remove the first part of the path (the "value") before continuing.
2725 + path = path.slice(1);
2726 +
2727 + switch (fiber.tag) {
2728 + case ClassComponent:
2729 + if (path.length === 0) {
2730 + // Simple context value (noop)
2731 + } else {
2732 + deletePathInObject(instance.context, path);
2733 + }
2734 + instance.forceUpdate();
2735 + break;
2736 + case FunctionComponent:
2737 + // Function components using legacy context are not editable
2738 + // because there's no instance on which to create a cloned, mutated context.
2739 + break;
2740 + }
2741 + break;
2742 + case 'hooks':
2743 + if (typeof overrideHookStateDeletePath === 'function') {
2744 + overrideHookStateDeletePath(fiber, ((hookID: any): number), path);
2745 + }
2746 + break;
2747 + case 'props':
2748 + if (instance === null) {
2749 + if (typeof overridePropsDeletePath === 'function') {
2750 + overridePropsDeletePath(fiber, path);
2751 + }
2752 + } else {
2753 + fiber.pendingProps = copyWithDelete(instance.props, path);
2754 + instance.forceUpdate();
2755 + }
2756 + break;
2757 + case 'state':
2758 + deletePathInObject(instance.state, path);
2759 + instance.forceUpdate();
2760 + break;
2761 }
2762 }
2763 }
2764
2704 - function setInProps(id: number, path: Array<string | number>, value: any) {
2765 + function renamePath(
2766 + type: 'context' | 'hooks' | 'props' | 'state',
2767 + id: number,
2768 + hookID: ?number,
2769 + oldPath: Array<string | number>,
2770 + newPath: Array<string | number>,
2771 + ): void {
2772 const fiber = findCurrentFiberUsingSlowPathById(id);
2773 if (fiber !== null) {
2774 const instance = fiber.stateNode;
2708 - if (instance === null) {
2709 - if (typeof overrideProps === 'function') {
2710 - overrideProps(fiber, path, value);
2711 - }
2712 - } else {
2713 - fiber.pendingProps = copyWithSet(instance.props, path, value);
2714 - instance.forceUpdate();
2775 +
2776 + switch (type) {
2777 + case 'context':
2778 + // To simplify hydration and display of primitive context values (e.g. number, string)
2779 + // the inspectElement() method wraps context in a {value: ...} object.
2780 + // We need to remove the first part of the path (the "value") before continuing.
2781 + oldPath = oldPath.slice(1);
2782 + newPath = newPath.slice(1);
2783 +
2784 + switch (fiber.tag) {
2785 + case ClassComponent:
2786 + if (oldPath.length === 0) {
2787 + // Simple context value (noop)
2788 + } else {
2789 + renamePathInObject(instance.context, oldPath, newPath);
2790 + }
2791 + instance.forceUpdate();
2792 + break;
2793 + case FunctionComponent:
2794 + // Function components using legacy context are not editable
2795 + // because there's no instance on which to create a cloned, mutated context.
2796 + break;
2797 + }
2798 + break;
2799 + case 'hooks':
2800 + if (typeof overrideHookStateRenamePath === 'function') {
2801 + overrideHookStateRenamePath(
2802 + fiber,
2803 + ((hookID: any): number),
2804 + oldPath,
2805 + newPath,
2806 + );
2807 + }
2808 + break;
2809 + case 'props':
2810 + if (instance === null) {
2811 + if (typeof overridePropsRenamePath === 'function') {
2812 + overridePropsRenamePath(fiber, oldPath, newPath);
2813 + }
2814 + } else {
2815 + fiber.pendingProps = copyWithRename(
2816 + instance.props,
2817 + oldPath,
2818 + newPath,
2819 + );
2820 + instance.forceUpdate();
2821 + }
2822 + break;
2823 + case 'state':
2824 + renamePathInObject(instance.state, oldPath, newPath);
2825 + instance.forceUpdate();
2826 + break;
2827 }
2828 }
2829 }
2830
2719 - function setInState(id: number, path: Array<string | number>, value: any) {
2831 + function overrideValueAtPath(
2832 + type: 'context' | 'hooks' | 'props' | 'state',
2833 + id: number,
2834 + hookID: ?number,
2835 + path: Array<string | number>,
2836 + value: any,
2837 + ): void {
2838 const fiber = findCurrentFiberUsingSlowPathById(id);
2839 if (fiber !== null) {
2840 const instance = fiber.stateNode;
2723 - setInObject(instance.state, path, value);
2724 - instance.forceUpdate();
2725 - }
2726 - }
2841
2728 - function setInContext(id: number, path: Array<string | number>, value: any) {
2729 - // To simplify hydration and display of primitive context values (e.g. number, string)
2730 - // the inspectElement() method wraps context in a {value: ...} object.
2731 - // We need to remove the first part of the path (the "value") before continuing.
2732 - path = path.slice(1);
2733 -
2734 - const fiber = findCurrentFiberUsingSlowPathById(id);
2735 - if (fiber !== null) {
2736 - const instance = fiber.stateNode;
2737 - if (path.length === 0) {
2738 - // Simple context value
2739 - instance.context = value;
2740 - } else {
2741 - setInObject(instance.context, path, value);
2842 + switch (type) {
2843 + case 'context':
2844 + // To simplify hydration and display of primitive context values (e.g. number, string)
2845 + // the inspectElement() method wraps context in a {value: ...} object.
2846 + // We need to remove the first part of the path (the "value") before continuing.
2847 + path = path.slice(1);
2848 +
2849 + switch (fiber.tag) {
2850 + case ClassComponent:
2851 + if (path.length === 0) {
2852 + // Simple context value
2853 + instance.context = value;
2854 + } else {
2855 + setInObject(instance.context, path, value);
2856 + }
2857 + instance.forceUpdate();
2858 + break;
2859 + case FunctionComponent:
2860 + // Function components using legacy context are not editable
2861 + // because there's no instance on which to create a cloned, mutated context.
2862 + break;
2863 + }
2864 + break;
2865 + case 'hooks':
2866 + if (typeof overrideHookState === 'function') {
2867 + overrideHookState(fiber, ((hookID: any): number), path, value);
2868 + }
2869 + break;
2870 + case 'props':
2871 + if (instance === null) {
2872 + if (typeof overrideProps === 'function') {
2873 + overrideProps(fiber, path, value);
2874 + }
2875 + } else {
2876 + fiber.pendingProps = copyWithSet(instance.props, path, value);
2877 + instance.forceUpdate();
2878 + }
2879 + break;
2880 + case 'state':
2881 + setInObject(instance.state, path, value);
2882 + instance.forceUpdate();
2883 + break;
2884 }
2743 - instance.forceUpdate();
2885 }
2886 }
2887
@@ -3192,6 +3333,7 @@ export function attach(
3333 return {
3334 cleanup,
3335 copyElementPath,
3336 + deletePath,
3337 findNativeNodesForFiberID,
3338 flushInitialOperations,
3339 getBestMatchForTrackedPath,
@@ -3208,11 +3350,9 @@ export function attach(
3350 prepareViewAttributeSource,
3351 prepareViewElementSource,
3352 overrideSuspense,
3353 + overrideValueAtPath,
3354 + renamePath,
3355 renderer,
3212 - setInContext,
3213 - setInHook,
3214 - setInProps,
3215 - setInState,
3356 setTraceUpdatesEnabled,
3357 setTrackedPath,
3358 startProfiling,
packages/react-devtools-shared/src/backend/types.js
+52 -11
@@ -97,12 +97,36 @@ export type ReactRenderer = {
97 path: Array<string | number>,
98 value: any,
99 ) => void,
100 + // 17+
101 + overrideHookStateDeletePath?: ?(
102 + fiber: Object,
103 + id: number,
104 + path: Array<string | number>,
105 + ) => void,
106 + // 17+
107 + overrideHookStateRenamePath?: ?(
108 + fiber: Object,
109 + id: number,
110 + oldPath: Array<string | number>,
111 + newPath: Array<string | number>,
112 + ) => void,
113 // 16.7+
114 overrideProps?: ?(
115 fiber: Object,
116 path: Array<string | number>,
117 value: any,
118 ) => void,
119 + // 17+
120 + overridePropsDeletePath?: ?(
121 + fiber: Object,
122 + path: Array<string | number>,
123 + ) => void,
124 + // 17+
125 + overridePropsRenamePath?: ?(
126 + fiber: Object,
127 + oldPath: Array<string | number>,
128 + newPath: Array<string | number>,
129 + ) => void,
130 // 16.9+
131 scheduleUpdate?: ?(fiber: Object) => void,
132 setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
@@ -184,12 +208,16 @@ export type InspectedElement = {|
208
209 displayName: string | null,
210
187 - // Does the current renderer support editable hooks?
211 + // Does the current renderer support editable hooks and function props?
212 canEditHooks: boolean,
189 -
190 - // Does the current renderer support editable function props?
213 canEditFunctionProps: boolean,
214
215 + // Does the current renderer support advanced editing interface?
216 + canEditHooksAndDeletePaths: boolean,
217 + canEditHooksAndRenamePaths: boolean,
218 + canEditFunctionPropsDeletePaths: boolean,
219 + canEditFunctionPropsRenamePaths: boolean,
220 +
221 // Is this Suspense, and can its value be overridden now?
222 canToggleSuspense: boolean,
223
@@ -261,9 +289,17 @@ export type InstanceAndStyle = {|
289 style: Object | null,
290 |};
291
292 +type Type = 'props' | 'hooks' | 'state' | 'context';
293 +
294 export type RendererInterface = {
295 cleanup: () => void,
296 copyElementPath: (id: number, path: Array<string | number>) => void,
297 + deletePath: (
298 + type: Type,
299 + id: number,
300 + hookID: ?number,
301 + path: Array<string | number>,
302 + ) => void,
303 findNativeNodesForFiberID: FindNativeNodesForFiberID,
304 flushInitialOperations: () => void,
305 getBestMatchForTrackedPath: () => PathMatch | null,
@@ -281,21 +317,26 @@ export type RendererInterface = {
317 ) => InspectedElementPayload,
318 logElementToConsole: (id: number) => void,
319 overrideSuspense: (id: number, forceFallback: boolean) => void,
320 + overrideValueAtPath: (
321 + type: Type,
322 + id: number,
323 + hook: ?number,
324 + path: Array<string | number>,
325 + value: any,
326 + ) => void,
327 prepareViewAttributeSource: (
328 id: number,
329 path: Array<string | number>,
330 ) => void,
331 prepareViewElementSource: (id: number) => void,
289 - renderer: ReactRenderer | null,
290 - setInContext: (id: number, path: Array<string | number>, value: any) => void,
291 - setInHook: (
332 + renamePath: (
333 + type: Type,
334 id: number,
293 - index: number,
294 - path: Array<string | number>,
295 - value: any,
335 + hookID: ?number,
336 + oldPath: Array<string | number>,
337 + newPath: Array<string | number>,
338 ) => void,
297 - setInProps: (id: number, path: Array<string | number>, value: any) => void,
298 - setInState: (id: number, path: Array<string | number>, value: any) => void,
339 + renderer: ReactRenderer | null,
340 setTraceUpdatesEnabled: (enabled: boolean) => void,
341 setTrackedPath: (path: Array<PathFrame> | null) => void,
342 startProfiling: (recordChangeDescriptions: boolean) => void,
packages/react-devtools-shared/src/backend/utils.js
+46
@@ -55,6 +55,52 @@ export function copyToClipboard(value: any): void {
55 }
56 }
57
58 +export function copyWithDelete(
59 + obj: Object | Array<any>,
60 + path: Array<string | number>,
61 + index: number = 0,
62 +): Object | Array<any> {
63 + const key = path[index];
64 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
65 + if (index + 1 === path.length) {
66 + if (Array.isArray(updated)) {
67 + updated.splice(((key: any): number), 1);
68 + } else {
69 + delete updated[key];
70 + }
71 + } else {
72 + // $FlowFixMe number or string is fine here
73 + updated[key] = copyWithDelete(obj[key], path, index + 1);
74 + }
75 + return updated;
76 +}
77 +
78 +// This function expects paths to be the same except for the final value.
79 +// e.g. ['path', 'to', 'foo'] and ['path', 'to', 'bar']
80 +export function copyWithRename(
81 + obj: Object | Array<any>,
82 + oldPath: Array<string | number>,
83 + newPath: Array<string | number>,
84 + index: number = 0,
85 +): Object | Array<any> {
86 + const oldKey = oldPath[index];
87 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
88 + if (index + 1 === oldPath.length) {
89 + const newKey = newPath[index];
90 + // $FlowFixMe number or string is fine here
91 + updated[newKey] = updated[oldKey];
92 + if (Array.isArray(updated)) {
93 + updated.splice(((oldKey: any): number), 1);
94 + } else {
95 + delete updated[oldKey];
96 + }
97 + } else {
98 + // $FlowFixMe number or string is fine here
99 + updated[oldKey] = copyWithRename(obj[oldKey], oldPath, newPath, index + 1);
100 + }
101 + return updated;
102 +}
103 +
104 export function copyWithSet(
105 obj: Object | Array<any>,
106 path: Array<string | number>,
packages/react-devtools-shared/src/bridge.js
+98 -4
@@ -38,6 +38,7 @@ type HighlightElementInDOM = {|
38 type OverrideValue = {|
39 ...ElementAndRendererID,
40 path: Array<string | number>,
41 + wasForwarded?: boolean,
42 value: any,
43 |};
44
@@ -46,6 +47,31 @@ type OverrideHookState = {|
47 hookID: number,
48 |};
49
50 +type PathType = 'props' | 'hooks' | 'state' | 'context';
51 +
52 +type DeletePath = {|
53 + ...ElementAndRendererID,
54 + type: PathType,
55 + hookID?: ?number,
56 + path: Array<string | number>,
57 +|};
58 +
59 +type RenamePath = {|
60 + ...ElementAndRendererID,
61 + type: PathType,
62 + hookID?: ?number,
63 + oldPath: Array<string | number>,
64 + newPath: Array<string | number>,
65 +|};
66 +
67 +type OverrideValueAtPath = {|
68 + ...ElementAndRendererID,
69 + type: PathType,
70 + hookID?: ?number,
71 + path: Array<string | number>,
72 + value: any,
73 +|};
74 +
75 type OverrideSuspense = {|
76 ...ElementAndRendererID,
77 forceFallback: boolean,
@@ -117,19 +143,18 @@ type BackendEvents = {|
143 type FrontendEvents = {|
144 clearNativeElementHighlight: [],
145 copyElementPath: [CopyElementPathParams],
146 + deletePath: [DeletePath],
147 getOwnersList: [ElementAndRendererID],
148 getProfilingData: [{|rendererID: RendererID|}],
149 getProfilingStatus: [],
150 highlightNativeElement: [HighlightElementInDOM],
151 inspectElement: [InspectElementParams],
152 logElementToConsole: [ElementAndRendererID],
126 - overrideContext: [OverrideValue],
127 - overrideHookState: [OverrideHookState],
128 - overrideProps: [OverrideValue],
129 - overrideState: [OverrideValue],
153 overrideSuspense: [OverrideSuspense],
154 + overrideValueAtPath: [OverrideValueAtPath],
155 profilingData: [ProfilingDataBackend],
156 reloadAndProfile: [boolean],
157 + renamePath: [RenamePath],
158 selectFiber: [number],
159 setTraceUpdatesEnabled: [boolean],
160 shutdown: [],
@@ -147,6 +172,21 @@ type FrontendEvents = {|
172 NativeStyleEditor_measure: [ElementAndRendererID],
173 NativeStyleEditor_renameAttribute: [NativeStyleEditor_RenameAttributeParams],
174 NativeStyleEditor_setValue: [NativeStyleEditor_SetValueParams],
175 +
176 + // Temporarily support newer standalone front-ends sending commands to older embedded backends.
177 + // We do this because React Native embeds the React DevTools backend,
178 + // but cannot control which version of the frontend users use.
179 + //
180 + // Note that nothing in the newer backend actually listens to these events,
181 + // but the new frontend still dispatches them (in case older backends are listening to them instead).
182 + //
183 + // Note that this approach does no support the combination of a newer backend with an older frontend.
184 + // It would be more work to suppot both approaches (and not run handlers twice)
185 + // so I chose to support the more likely/common scenario (and the one more difficult for an end user to "fix").
186 + overrideContext: [OverrideValue],
187 + overrideHookState: [OverrideHookState],
188 + overrideProps: [OverrideValue],
189 + overrideState: [OverrideValue],
190 |};
191
192 class Bridge<
@@ -171,6 +211,11 @@ class Bridge<
211 wall.listen((message: Message) => {
212 (this: any).emit(message.event, message.payload);
213 }) || null;
214 +
215 + // Temporarily support older standalone front-ends sending commands to newer embedded backends.
216 + // We do this because React Native embeds the React DevTools backend,
217 + // but cannot control which version of the frontend users use.
218 + this.addListener('overrideValueAtPath', this.overrideValueAtPath);
219 }
220
221 // Listening directly to the wall isn't advised.
@@ -267,6 +312,55 @@ class Bridge<
312 this._timeoutID = setTimeout(this._flush, BATCH_DURATION);
313 }
314 };
315 +
316 + // Temporarily support older standalone backends by forwarding "overrideValueAtPath" commands
317 + // to the older message types they may be listening to.
318 + overrideValueAtPath = ({
319 + id,
320 + path,
321 + rendererID,
322 + type,
323 + value,
324 + }: OverrideValueAtPath) => {
325 + switch (type) {
326 + case 'context':
327 + this.send('overrideContext', {
328 + id,
329 + path,
330 + rendererID,
331 + wasForwarded: true,
332 + value,
333 + });
334 + break;
335 + case 'hooks':
336 + this.send('overrideHookState', {
337 + id,
338 + path,
339 + rendererID,
340 + wasForwarded: true,
341 + value,
342 + });
343 + break;
344 + case 'props':
345 + this.send('overrideProps', {
346 + id,
347 + path,
348 + rendererID,
349 + wasForwarded: true,
350 + value,
351 + });
352 + break;
353 + case 'state':
354 + this.send('overrideState', {
355 + id,
356 + path,
357 + rendererID,
358 + wasForwarded: true,
359 + value,
360 + });
361 + break;
362 + }
363 + };
364 }
365
366 export type BackendBridge = Bridge<BackendEvents, FrontendEvents>;
packages/react-devtools-shared/src/devtools/utils.js
+3 -1
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import JSON5 from 'json5';
11 +
12 import type {Element} from './views/Components/types';
13 import type Store from './store';
14
@@ -109,7 +111,7 @@ export function smartParse(value: any) {
111 case 'undefined':
112 return undefined;
113 default:
112 - return JSON.parse(sanitizeForParse(value));
114 + return JSON5.parse(sanitizeForParse(value));
115 }
116 }
117
packages/react-devtools-shared/src/devtools/views/Components/CannotSuspendWarningMessage.js new
+44
@@ -0,0 +1,44 @@
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 * as React from 'react';
11 +import {useContext} from 'react';
12 +import {StoreContext} from '../context';
13 +import {
14 + ComponentFilterElementType,
15 + ElementTypeSuspense,
16 +} from 'react-devtools-shared/src/types';
17 +
18 +export default function CannotSuspendWarningMessage() {
19 + const store = useContext(StoreContext);
20 + const areSuspenseElementsHidden = !!store.componentFilters.find(
21 + filter =>
22 + filter.type === ComponentFilterElementType &&
23 + filter.value === ElementTypeSuspense &&
24 + filter.isEnabled,
25 + );
26 +
27 + // Has the user filtered out Suspense nodes from the tree?
28 + // If so, the selected element might actually be in a Suspense tree after all.
29 + if (areSuspenseElementsHidden) {
30 + return (
31 + <div>
32 + Suspended state cannot be toggled while Suspense components are hidden.
33 + Disable the filter and try again.
34 + </div>
35 + );
36 + } else {
37 + return (
38 + <div>
39 + The selected element is not within a Suspense container. Suspending it
40 + would cause an error.
41 + </div>
42 + );
43 + }
44 +}
packages/react-devtools-shared/src/devtools/views/Components/Components.css
+2 -2
@@ -19,7 +19,7 @@
19 overflow: auto;
20 }
21
22 -.SelectedElementWrapper {
22 +.InspectedElementWrapper {
23 flex: 1 1 35%;
24 overflow-x: hidden;
25 overflow-y: auto;
@@ -47,7 +47,7 @@
47 flex: 0 0 var(--vertical-resize-percentage);
48 }
49
50 - .SelectedElementWrapper {
50 + .InspectedElementWrapper {
51 flex: 1 1 50%;
52 }
53
packages/react-devtools-shared/src/devtools/views/Components/Components.js
+3 -3
@@ -25,7 +25,7 @@ import {
25 localStorageGetItem,
26 localStorageSetItem,
27 } from 'react-devtools-shared/src/storage';
28 -import SelectedElement from './SelectedElement';
28 +import InspectedElement from './InspectedElement';
29 import {ModalDialog} from '../ModalDialog';
30 import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
31 import {NativeStyleContextController} from './NativeStyleEditor/context';
@@ -165,10 +165,10 @@ function Components(_: {||}) {
165 <div className={styles.ResizeBarWrapper}>
166 <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
167 </div>
168 - <div className={styles.SelectedElementWrapper}>
168 + <div className={styles.InspectedElementWrapper}>
169 <NativeStyleContextController>
170 <Suspense fallback={<Loading />}>
171 - <SelectedElement />
171 + <InspectedElement />
172 </Suspense>
173 </NativeStyleContextController>
174 </div>
packages/react-devtools-shared/src/devtools/views/Components/EditableName.css
-1
@@ -1,6 +1,5 @@
1 .Input {
2 flex: 0 1 auto;
3 - padding: 1px;
3 box-shadow: 0px 1px 3px transparent;
4 color: var(--color-text);
5 }
packages/react-devtools-shared/src/devtools/views/Components/EditableName.js
+31 -10
@@ -12,27 +12,44 @@ import {useCallback, useState} from 'react';
12 import AutoSizeInput from './NativeStyleEditor/AutoSizeInput';
13 import styles from './EditableName.css';
14
15 -type OverrideNameFn = (name: string, value: any) => void;
15 +type Type = 'props' | 'state' | 'context' | 'hooks';
16 +type OverrideNameFn = (
17 + oldName: Array<string | number>,
18 + newName: Array<string | number>,
19 +) => void;
20
21 type EditableNameProps = {|
22 + allowEmpty?: boolean,
23 + allowWhiteSpace?: boolean,
24 autoFocus?: boolean,
25 + className?: string,
26 initialValue?: string,
20 - overrideNameFn: OverrideNameFn,
27 + overrideName: OverrideNameFn,
28 + path: Array<string | number>,
29 + type: Type,
30 |};
31
32 export default function EditableName({
33 + allowEmpty = false,
34 + allowWhiteSpace = false,
35 autoFocus = false,
36 + className = '',
37 initialValue = '',
26 - overrideNameFn,
38 + overrideName,
39 + path,
40 + type,
41 }: EditableNameProps) {
42 const [editableName, setEditableName] = useState(initialValue);
43 const [isValid, setIsValid] = useState(false);
44
45 const handleChange = useCallback(
46 ({target}) => {
33 - const value = target.value.trim();
47 + let value = target.value;
48 + if (!allowWhiteSpace) {
49 + value = value.trim();
50 + }
51
35 - if (value) {
52 + if (allowEmpty || value !== '') {
53 setIsValid(true);
54 } else {
55 setIsValid(false);
@@ -40,7 +57,7 @@ export default function EditableName({
57
58 setEditableName(value);
59 },
43 - [overrideNameFn],
60 + [overrideName],
61 );
62
63 const handleKeyDown = useCallback(
@@ -52,7 +69,11 @@ export default function EditableName({
69 case 'Enter':
70 case 'Tab':
71 if (isValid) {
55 - overrideNameFn(editableName);
72 + const basePath = path.slice(0, path.length - 1);
73 + overrideName(
74 + [...basePath, initialValue],
75 + [...basePath, editableName],
76 + );
77 }
78 break;
79 case 'Escape':
@@ -62,16 +83,16 @@ export default function EditableName({
83 break;
84 }
85 },
65 - [editableName, setEditableName, isValid, initialValue, overrideNameFn],
86 + [editableName, setEditableName, isValid, initialValue, overrideName],
87 );
88
89 return (
90 <AutoSizeInput
91 autoFocus={autoFocus}
71 - className={styles.Input}
92 + className={[styles.Input, className].join(' ')}
93 onChange={handleChange}
94 onKeyDown={handleKeyDown}
74 - placeholder="new prop"
95 + placeholder="new entry"
96 type="text"
97 value={editableName}
98 />
packages/react-devtools-shared/src/devtools/views/Components/EditableValue.js
+4 -4
@@ -16,14 +16,14 @@ type OverrideValueFn = (path: Array<string | number>, value: any) => void;
16
17 type EditableValueProps = {|
18 className?: string,
19 - overrideValueFn: OverrideValueFn,
19 + overrideValue: OverrideValueFn,
20 path: Array<string | number>,
21 value: any,
22 |};
23
24 export default function EditableValue({
25 className = '',
26 - overrideValueFn,
26 + overrideValue,
27 path,
28 value,
29 }: EditableValueProps) {
@@ -55,7 +55,7 @@ export default function EditableValue({
55 // So we read from target.checked rather than parsedValue (which has not yet updated).
56 // We also don't check isValid (because that hasn't changed yet either);
57 // we don't need to check it anyway, since target.checked is always a boolean.
58 - overrideValueFn(path, target.checked);
58 + overrideValue(path, target.checked);
59 };
60
61 const handleKeyDown = event => {
@@ -76,7 +76,7 @@ export default function EditableValue({
76
77 const applyChanges = () => {
78 if (isValid && hasPendingChanges) {
79 - overrideValueFn(path, parsedValue);
79 + overrideValue(path, parsedValue);
80 }
81 };
82
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.css new
+68
@@ -0,0 +1,68 @@
1 +.InspectedElement {
2 + display: flex;
3 + flex-direction: column;
4 + height: 100%;
5 + width: 100%;
6 + border-left: 1px solid var(--color-border);
7 + border-top: 1px solid var(--color-border);
8 +}
9 +
10 +.TitleRow {
11 + flex: 0 0 42px;
12 + display: flex;
13 + align-items: center;
14 + font-size: var(--font-size-monospace-large);
15 + border-bottom: 1px solid var(--color-border);
16 + padding: 0.5rem;
17 +}
18 +
19 +.Key {
20 + flex: 0 1 auto;
21 + padding-left: 0.25rem;
22 + padding-right: 0.125rem;
23 + line-height: 1rem;
24 + border-top-left-radius: 0.125rem;
25 + border-bottom-left-radius: 0.125rem;
26 + display: inline-block;
27 + background-color: var(--color-component-badge-background);
28 + color: var(--color-text);
29 + font-family: var(--font-family-monospace);
30 + font-size: var(--font-size-monospace-small);
31 + white-space: nowrap;
32 + overflow: hidden;
33 + text-overflow: ellipsis;
34 + max-width: 100%;
35 +}
36 +
37 +.KeyArrow {
38 + height: 1rem;
39 + width: 1rem;
40 + margin-right: -0.25rem;
41 + border: 0.5rem solid transparent;
42 + border-left: 0.5rem solid var(--color-component-badge-background);
43 +}
44 +
45 +.SelectedComponentName {
46 + flex: 1 1 auto;
47 + overflow: hidden;
48 + text-overflow: ellipsis;
49 + line-height: normal;
50 +}
51 +
52 +.Component {
53 + flex: 1 1 auto;
54 + color: var(--color-component-name);
55 + font-family: var(--font-family-monospace);
56 + font-size: var(--font-size-monospace-normal);
57 + white-space: nowrap;
58 + overflow: hidden;
59 + text-overflow: ellipsis;
60 + max-width: 100%;
61 +}
62 +
63 +.Loading {
64 + padding: 0.25rem;
65 + color: var(--color-dimmer);
66 + font-style: italic;
67 +}
68 +
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js new
+240
@@ -0,0 +1,240 @@
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 * as React from 'react';
11 +import {useCallback, useContext} from 'react';
12 +import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
13 +import {BridgeContext, StoreContext} from '../context';
14 +import Button from '../Button';
15 +import ButtonIcon from '../ButtonIcon';
16 +import {ModalDialogContext} from '../ModalDialog';
17 +import {InspectedElementContext} from './InspectedElementContext';
18 +import ViewElementSourceContext from './ViewElementSourceContext';
19 +import Toggle from '../Toggle';
20 +import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
21 +import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
22 +import InspectedElementView from './InspectedElementView';
23 +
24 +import styles from './InspectedElement.css';
25 +
26 +import type {InspectedElementContextType} from './InspectedElementContext';
27 +import type {InspectedElement} from './types';
28 +
29 +export type Props = {||};
30 +
31 +export default function InspectedElementWrapper(_: Props) {
32 + const {inspectedElementID} = useContext(TreeStateContext);
33 + const dispatch = useContext(TreeDispatcherContext);
34 + const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
35 + ViewElementSourceContext,
36 + );
37 + const bridge = useContext(BridgeContext);
38 + const store = useContext(StoreContext);
39 + const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
40 +
41 + const {
42 + copyInspectedElementPath,
43 + getInspectedElementPath,
44 + getInspectedElement,
45 + storeAsGlobal,
46 + } = useContext<InspectedElementContextType>(InspectedElementContext);
47 +
48 + const element =
49 + inspectedElementID !== null
50 + ? store.getElementByID(inspectedElementID)
51 + : null;
52 +
53 + const inspectedElement =
54 + inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
55 +
56 + const highlightElement = useCallback(() => {
57 + if (element !== null && inspectedElementID !== null) {
58 + const rendererID = store.getRendererIDForElement(inspectedElementID);
59 + if (rendererID !== null) {
60 + bridge.send('highlightNativeElement', {
61 + displayName: element.displayName,
62 + hideAfterTimeout: true,
63 + id: inspectedElementID,
64 + openNativeElementsPanel: true,
65 + rendererID,
66 + scrollIntoView: true,
67 + });
68 + }
69 + }
70 + }, [bridge, element, inspectedElementID, store]);
71 +
72 + const logElement = useCallback(() => {
73 + if (inspectedElementID !== null) {
74 + const rendererID = store.getRendererIDForElement(inspectedElementID);
75 + if (rendererID !== null) {
76 + bridge.send('logElementToConsole', {
77 + id: inspectedElementID,
78 + rendererID,
79 + });
80 + }
81 + }
82 + }, [bridge, inspectedElementID, store]);
83 +
84 + const viewSource = useCallback(() => {
85 + if (viewElementSourceFunction != null && inspectedElement !== null) {
86 + viewElementSourceFunction(
87 + inspectedElement.id,
88 + ((inspectedElement: any): InspectedElement),
89 + );
90 + }
91 + }, [inspectedElement, viewElementSourceFunction]);
92 +
93 + // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
94 + // To detect this case, we defer to an injected helper function (if present).
95 + const canViewSource =
96 + inspectedElement !== null &&
97 + inspectedElement.canViewSource &&
98 + viewElementSourceFunction !== null &&
99 + (canViewElementSourceFunction === null ||
100 + canViewElementSourceFunction(inspectedElement));
101 +
102 + const isSuspended =
103 + element !== null &&
104 + element.type === ElementTypeSuspense &&
105 + inspectedElement != null &&
106 + inspectedElement.state != null;
107 +
108 + const canToggleSuspense =
109 + inspectedElement != null && inspectedElement.canToggleSuspense;
110 +
111 + // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
112 + const toggleSuspended = useCallback(() => {
113 + let nearestSuspenseElement = null;
114 + let currentElement = element;
115 + while (currentElement !== null) {
116 + if (currentElement.type === ElementTypeSuspense) {
117 + nearestSuspenseElement = currentElement;
118 + break;
119 + } else if (currentElement.parentID > 0) {
120 + currentElement = store.getElementByID(currentElement.parentID);
121 + } else {
122 + currentElement = null;
123 + }
124 + }
125 +
126 + // If we didn't find a Suspense ancestor, we can't suspend.
127 + // Instead we can show a warning to the user.
128 + if (nearestSuspenseElement === null) {
129 + modalDialogDispatch({
130 + type: 'SHOW',
131 + content: <CannotSuspendWarningMessage />,
132 + });
133 + } else {
134 + const nearestSuspenseElementID = nearestSuspenseElement.id;
135 +
136 + // If we're suspending from an arbitrary (non-Suspense) component, select the nearest Suspense element in the Tree.
137 + // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
138 + if (nearestSuspenseElement !== element) {
139 + dispatch({
140 + type: 'SELECT_ELEMENT_BY_ID',
141 + payload: nearestSuspenseElementID,
142 + });
143 + }
144 +
145 + const rendererID = store.getRendererIDForElement(
146 + nearestSuspenseElementID,
147 + );
148 +
149 + // Toggle suspended
150 + if (rendererID !== null) {
151 + bridge.send('overrideSuspense', {
152 + id: nearestSuspenseElementID,
153 + rendererID,
154 + forceFallback: !isSuspended,
155 + });
156 + }
157 + }
158 + }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
159 +
160 + if (element === null) {
161 + return (
162 + <div className={styles.InspectedElement}>
163 + <div className={styles.TitleRow} />
164 + </div>
165 + );
166 + }
167 +
168 + return (
169 + <div className={styles.InspectedElement}>
170 + <div className={styles.TitleRow}>
171 + {element.key && (
172 + <>
173 + <div className={styles.Key} title={`key "${element.key}"`}>
174 + {element.key}
175 + </div>
176 + <div className={styles.KeyArrow} />
177 + </>
178 + )}
179 +
180 + <div className={styles.SelectedComponentName}>
181 + <div className={styles.Component} title={element.displayName}>
182 + {element.displayName}
183 + </div>
184 + </div>
185 +
186 + {canToggleSuspense && (
187 + <Toggle
188 + className={styles.IconButton}
189 + isChecked={isSuspended}
190 + onChange={toggleSuspended}
191 + title={
192 + isSuspended
193 + ? 'Unsuspend the selected component'
194 + : 'Suspend the selected component'
195 + }>
196 + <ButtonIcon type="suspend" />
197 + </Toggle>
198 + )}
199 + {store.supportsNativeInspection && (
200 + <Button
201 + className={styles.IconButton}
202 + onClick={highlightElement}
203 + title="Inspect the matching DOM element">
204 + <ButtonIcon type="view-dom" />
205 + </Button>
206 + )}
207 + <Button
208 + className={styles.IconButton}
209 + onClick={logElement}
210 + title="Log this component data to the console">
211 + <ButtonIcon type="log-data" />
212 + </Button>
213 + <Button
214 + className={styles.IconButton}
215 + disabled={!canViewSource}
216 + onClick={viewSource}
217 + title="View source for this element">
218 + <ButtonIcon type="view-source" />
219 + </Button>
220 + </div>
221 +
222 + {inspectedElement === null && (
223 + <div className={styles.Loading}>Loading...</div>
224 + )}
225 +
226 + {inspectedElement !== null && (
227 + <InspectedElementView
228 + key={
229 + inspectedElementID /* Force reset when selected Element changes */
230 + }
231 + copyInspectedElementPath={copyInspectedElementPath}
232 + element={element}
233 + getInspectedElementPath={getInspectedElementPath}
234 + inspectedElement={inspectedElement}
235 + storeAsGlobal={storeAsGlobal}
236 + />
237 + )}
238 + </div>
239 + );
240 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContext.js
+8
@@ -198,7 +198,11 @@ function InspectedElementContextController({children}: Props) {
198 case 'full-data':
199 const {
200 canEditFunctionProps,
201 + canEditFunctionPropsDeletePaths,
202 + canEditFunctionPropsRenamePaths,
203 canEditHooks,
204 + canEditHooksAndDeletePaths,
205 + canEditHooksAndRenamePaths,
206 canToggleSuspense,
207 canViewSource,
208 hasLegacyContext,
@@ -217,7 +221,11 @@ function InspectedElementContextController({children}: Props) {
221
222 const inspectedElement: InspectedElementFrontend = {
223 canEditFunctionProps,
224 + canEditFunctionPropsDeletePaths,
225 + canEditFunctionPropsRenamePaths,
226 canEditHooks,
227 + canEditHooksAndDeletePaths,
228 + canEditHooksAndRenamePaths,
229 canToggleSuspense,
230 canViewSource,
231 hasLegacyContext,
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementContextTree.js new
+99
@@ -0,0 +1,99 @@
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 {copy} from 'clipboard-js';
11 +import * as React from 'react';
12 +import Button from '../Button';
13 +import ButtonIcon from '../ButtonIcon';
14 +import KeyValue from './KeyValue';
15 +import {alphaSortEntries, serializeDataForCopy} from '../utils';
16 +import Store from '../../store';
17 +import styles from './InspectedElementSharedStyles.css';
18 +import {
19 + ElementTypeClass,
20 + ElementTypeFunction,
21 +} from 'react-devtools-shared/src/types';
22 +
23 +import type {GetInspectedElementPath} from './InspectedElementContext';
24 +import type {InspectedElement} from './types';
25 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
26 +
27 +type Props = {|
28 + bridge: FrontendBridge,
29 + getInspectedElementPath: GetInspectedElementPath,
30 + inspectedElement: InspectedElement,
31 + store: Store,
32 +|};
33 +
34 +export default function InspectedElementContextTree({
35 + bridge,
36 + getInspectedElementPath,
37 + inspectedElement,
38 + store,
39 +}: Props) {
40 + const {hasLegacyContext, context, type} = inspectedElement;
41 +
42 + const isReadOnly = type !== ElementTypeClass && type !== ElementTypeFunction;
43 +
44 + const entries = context != null ? Object.entries(context) : null;
45 + if (entries !== null) {
46 + entries.sort(alphaSortEntries);
47 + }
48 +
49 + const isEmpty = entries === null || entries.length === 0;
50 +
51 + const handleCopy = () => copy(serializeDataForCopy(((context: any): Object)));
52 +
53 + // We add an object with a "value" key as a wrapper around Context data
54 + // so that we can use the shared <KeyValue> component to display it.
55 + // This wrapper object can't be renamed.
56 + const canRenamePathsAtDepth = depth => depth > 1;
57 +
58 + if (isEmpty) {
59 + return null;
60 + } else {
61 + return (
62 + <div className={styles.InspectedElementTree}>
63 + <div className={styles.HeaderRow}>
64 + <div className={styles.Header}>
65 + {hasLegacyContext ? 'legacy context' : 'context'}
66 + </div>
67 + {!isEmpty && (
68 + <Button onClick={handleCopy} title="Copy to clipboard">
69 + <ButtonIcon type="copy" />
70 + </Button>
71 + )}
72 + </div>
73 + {isEmpty && <div className={styles.Empty}>None</div>}
74 + {!isEmpty &&
75 + (entries: any).map(([name, value]) => (
76 + <KeyValue
77 + key={name}
78 + alphaSort={true}
79 + bridge={bridge}
80 + canDeletePaths={!isReadOnly}
81 + canEditValues={!isReadOnly}
82 + canRenamePaths={!isReadOnly}
83 + canRenamePathsAtDepth={canRenamePathsAtDepth}
84 + type="context"
85 + depth={1}
86 + getInspectedElementPath={getInspectedElementPath}
87 + hidden={false}
88 + inspectedElement={inspectedElement}
89 + name={name}
90 + path={[name]}
91 + pathRoot="context"
92 + store={store}
93 + value={value}
94 + />
95 + ))}
96 + </div>
97 + );
98 + }
99 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.css renamed
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementHooksTree.js renamed
+101 -86
@@ -13,35 +13,35 @@ import {useCallback, useContext, useRef, useState} from 'react';
13 import {BridgeContext, StoreContext} from '../context';
14 import Button from '../Button';
15 import ButtonIcon from '../ButtonIcon';
16 -import EditableValue from './EditableValue';
16 import ExpandCollapseToggle from './ExpandCollapseToggle';
18 -import {InspectedElementContext} from './InspectedElementContext';
17 import KeyValue from './KeyValue';
18 import {getMetaValueLabel, serializeHooksForCopy} from '../utils';
21 -import styles from './HooksTree.css';
19 +import Store from '../../store';
20 +import styles from './InspectedElementHooksTree.css';
21 import useContextMenu from '../../ContextMenu/useContextMenu';
22 import {meta} from '../../../hydration';
23
25 -import type {InspectPath} from './SelectedElement';
24 +import type {InspectedElement} from './types';
25 +import type {GetInspectedElementPath} from './InspectedElementContext';
26 import type {HooksNode, HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
27 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
28
29 type HooksTreeViewProps = {|
29 - canEditHooks: boolean,
30 - hooks: HooksTree | null,
31 - id: number,
30 + bridge: FrontendBridge,
31 + getInspectedElementPath: GetInspectedElementPath,
32 + inspectedElement: InspectedElement,
33 + store: Store,
34 |};
35
34 -export function HooksTreeView({canEditHooks, hooks, id}: HooksTreeViewProps) {
35 - const {getInspectedElementPath} = useContext(InspectedElementContext);
36 - const inspectPath = useCallback(
37 - (path: Array<string | number>) => {
38 - getInspectedElementPath(id, ['hooks', ...path]);
39 - },
40 - [getInspectedElementPath, id],
41 - );
42 - const handleCopy = useCallback(() => copy(serializeHooksForCopy(hooks)), [
43 - hooks,
44 - ]);
36 +export function InspectedElementHooksTree({
37 + bridge,
38 + getInspectedElementPath,
39 + inspectedElement,
40 + store,
41 +}: HooksTreeViewProps) {
42 + const {hooks, id} = inspectedElement;
43 +
44 + const handleCopy = () => copy(serializeHooksForCopy(hooks));
45
46 if (hooks === null) {
47 return null;
@@ -55,10 +55,10 @@ export function HooksTreeView({canEditHooks, hooks, id}: HooksTreeViewProps) {
55 </Button>
56 </div>
57 <InnerHooksTreeView
58 - canEditHooks={canEditHooks}
58 hooks={hooks}
59 id={id}
61 - inspectPath={inspectPath}
60 + getInspectedElementPath={getInspectedElementPath}
61 + inspectedElement={inspectedElement}
62 path={[]}
63 />
64 </div>
@@ -67,44 +67,61 @@ export function HooksTreeView({canEditHooks, hooks, id}: HooksTreeViewProps) {
67 }
68
69 type InnerHooksTreeViewProps = {|
70 - canEditHooks: boolean,
70 + getInspectedElementPath: GetInspectedElementPath,
71 hooks: HooksTree,
72 id: number,
73 - inspectPath: InspectPath,
73 + inspectedElement: InspectedElement,
74 path: Array<string | number>,
75 |};
76
77 export function InnerHooksTreeView({
78 - canEditHooks,
78 + getInspectedElementPath,
79 hooks,
80 id,
81 - inspectPath,
81 + inspectedElement,
82 path,
83 }: InnerHooksTreeViewProps) {
84 // $FlowFixMe "Missing type annotation for U" whatever that means
85 return hooks.map((hook, index) => (
86 <HookView
87 key={index}
88 - canEditHooks={canEditHooks}
88 + getInspectedElementPath={getInspectedElementPath}
89 hook={hooks[index]}
90 id={id}
91 - inspectPath={inspectPath}
91 + inspectedElement={inspectedElement}
92 path={path.concat([index])}
93 />
94 ));
95 }
96
97 type HookViewProps = {|
98 - canEditHooks: boolean,
98 + getInspectedElementPath: GetInspectedElementPath,
99 hook: HooksNode,
100 id: number,
101 - inspectPath: InspectPath,
101 + inspectedElement: InspectedElement,
102 path: Array<string | number>,
103 |};
104
105 -function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
105 +function HookView({
106 + getInspectedElementPath,
107 + hook,
108 + id,
109 + inspectedElement,
110 + path,
111 +}: HookViewProps) {
112 + const {
113 + canEditHooks,
114 + canEditHooksAndDeletePaths,
115 + canEditHooksAndRenamePaths,
116 + } = inspectedElement;
117 const {name, id: hookID, isStateEditable, subHooks, value} = hook;
118
119 + const isReadOnly = hookID == null || !isStateEditable;
120 +
121 + const canDeletePaths = !isReadOnly && canEditHooksAndDeletePaths;
122 + const canEditValues = !isReadOnly && canEditHooks;
123 + const canRenamePaths = !isReadOnly && canEditHooksAndRenamePaths;
124 +
125 const bridge = useContext(BridgeContext);
126 const store = useContext(StoreContext);
127
@@ -127,7 +144,7 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
144 ? hook[(meta.type: any)]
145 : typeof value,
146 },
130 - id: 'SelectedElement',
147 + id: 'InspectedElement',
148 ref: contextMenuTriggerRef,
149 });
150
@@ -145,6 +162,10 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
162 );
163 }
164
165 + // Certain hooks are not editable at all (as identified by react-debug-tools).
166 + // Primative hook names (e.g. the "State" name for useState) are also never editable.
167 + const canRenamePathsAtDepth = depth => isStateEditable && depth > 1;
168 +
169 const isCustomHook = subHooks.length > 0;
170
171 const type = typeof value;
@@ -174,20 +195,28 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
195 if (isCustomHook) {
196 const subHooksView = Array.isArray(subHooks) ? (
197 <InnerHooksTreeView
177 - canEditHooks={canEditHooks}
198 + getInspectedElementPath={getInspectedElementPath}
199 hooks={subHooks}
200 id={id}
180 - inspectPath={inspectPath}
201 + inspectedElement={inspectedElement}
202 path={path.concat(['subHooks'])}
203 />
204 ) : (
205 <KeyValue
185 - depth={1}
206 alphaSort={false}
187 - inspectPath={inspectPath}
207 + bridge={bridge}
208 + canDeletePaths={canDeletePaths}
209 + canEditValues={canEditValues}
210 + canRenamePaths={canRenamePaths}
211 + canRenamePathsAtDepth={canRenamePathsAtDepth}
212 + depth={1}
213 + getInspectedElementPath={getInspectedElementPath}
214 + hookID={hookID}
215 + inspectedElement={inspectedElement}
216 name="subHooks"
217 path={path.concat(['subHooks'])}
190 - pathRoot="hooks"
218 + store={store}
219 + type="hooks"
220 value={subHooks}
221 />
222 );
@@ -208,12 +237,20 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
237 </div>
238 <div className={styles.Children} hidden={!isOpen}>
239 <KeyValue
211 - depth={1}
240 alphaSort={false}
213 - inspectPath={inspectPath}
241 + bridge={bridge}
242 + canDeletePaths={canDeletePaths}
243 + canEditValues={canEditValues}
244 + canRenamePaths={canRenamePaths}
245 + canRenamePathsAtDepth={canRenamePathsAtDepth}
246 + depth={1}
247 + getInspectedElementPath={getInspectedElementPath}
248 + hookID={hookID}
249 + inspectedElement={inspectedElement}
250 name="DebugValue"
251 path={path.concat(['value'])}
252 pathRoot="hooks"
253 + store={store}
254 value={value}
255 />
256 {subHooksView}
@@ -242,41 +279,24 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
279 );
280 }
281 } else {
245 - let overrideValueFn = null;
246 - // TODO Maybe read editable value from debug hook?
247 - if (canEditHooks && isStateEditable && hookID !== null) {
248 - overrideValueFn = (
249 - absolutePath: Array<string | number>,
250 - newValue: any,
251 - ) => {
252 - const rendererID = store.getRendererIDForElement(id);
253 - if (rendererID !== null) {
254 - bridge.send('overrideHookState', {
255 - id,
256 - hookID,
257 - // Hooks override function expects a relative path for the specified hook (id),
258 - // starting with its id within the (flat) hooks list structure.
259 - // This relative path does not include the fake tree structure DevTools uses for display,
260 - // so it's important that we remove that part of the path before sending the update.
261 - path: absolutePath.slice(path.length + 1),
262 - rendererID,
263 - value: newValue,
264 - });
265 - }
266 - };
267 - }
268 -
282 if (isComplexDisplayValue) {
283 return (
284 <div className={styles.Hook}>
285 <KeyValue
273 - depth={1}
286 alphaSort={false}
275 - inspectPath={inspectPath}
287 + bridge={bridge}
288 + canDeletePaths={canDeletePaths}
289 + canEditValues={canEditValues}
290 + canRenamePaths={canRenamePaths}
291 + canRenamePathsAtDepth={canRenamePathsAtDepth}
292 + depth={1}
293 + getInspectedElementPath={getInspectedElementPath}
294 + hookID={hookID}
295 + inspectedElement={inspectedElement}
296 name={name}
277 - overrideValueFn={overrideValueFn}
297 path={path.concat(['value'])}
298 pathRoot="hooks"
299 + store={store}
300 value={value}
301 />
302 </div>
@@ -284,27 +304,22 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
304 } else {
305 return (
306 <div className={styles.Hook}>
287 - <div ref={contextMenuTriggerRef} className={styles.NameValueRow}>
288 - <span className={styles.ExpandCollapseToggleSpacer} />
289 - <span
290 - className={
291 - typeof overrideValueFn === 'function'
292 - ? styles.EditableName
293 - : styles.Name
294 - }>
295 - {name}
296 - </span>
297 - {typeof overrideValueFn === 'function' ? (
298 - <EditableValue
299 - overrideValueFn={overrideValueFn}
300 - path={[]}
301 - value={value}
302 - />
303 - ) : (
304 - // $FlowFixMe Cannot create span element because in property children
305 - <span className={styles.Value}>{displayValue}</span>
306 - )}
307 - </div>
307 + <KeyValue
308 + alphaSort={false}
309 + bridge={bridge}
310 + canDeletePaths={false}
311 + canEditValues={canEditValues}
312 + canRenamePaths={false}
313 + depth={1}
314 + getInspectedElementPath={getInspectedElementPath}
315 + hookID={hookID}
316 + inspectedElement={inspectedElement}
317 + name={name}
318 + path={[]}
319 + pathRoot="hooks"
320 + store={store}
321 + value={value}
322 + />
323 </div>
324 );
325 }
@@ -312,4 +327,4 @@ function HookView({canEditHooks, hook, id, inspectPath, path}: HookViewProps) {
327 }
328
329 // $FlowFixMe
315 -export default React.memo(HooksTreeView);
330 +export default React.memo(InspectedElementHooksTree);
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementPropsTree.js new
+104
@@ -0,0 +1,104 @@
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 {copy} from 'clipboard-js';
11 +import * as React from 'react';
12 +import Button from '../Button';
13 +import ButtonIcon from '../ButtonIcon';
14 +import KeyValue from './KeyValue';
15 +import NewKeyValue from './NewKeyValue';
16 +import {alphaSortEntries, serializeDataForCopy} from '../utils';
17 +import Store from '../../store';
18 +import styles from './InspectedElementSharedStyles.css';
19 +import {ElementTypeClass} from 'react-devtools-shared/src/types';
20 +
21 +import type {InspectedElement} from './types';
22 +import type {GetInspectedElementPath} from './InspectedElementContext';
23 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
24 +
25 +type Props = {|
26 + bridge: FrontendBridge,
27 + getInspectedElementPath: GetInspectedElementPath,
28 + inspectedElement: InspectedElement,
29 + store: Store,
30 +|};
31 +
32 +export default function InspectedElementPropsTree({
33 + bridge,
34 + getInspectedElementPath,
35 + inspectedElement,
36 + store,
37 +}: Props) {
38 + const {
39 + canEditFunctionProps,
40 + canEditFunctionPropsDeletePaths,
41 + canEditFunctionPropsRenamePaths,
42 + props,
43 + type,
44 + } = inspectedElement;
45 +
46 + const canDeletePaths =
47 + type === ElementTypeClass || canEditFunctionPropsDeletePaths;
48 + const canEditValues = type === ElementTypeClass || canEditFunctionProps;
49 + const canRenamePaths =
50 + type === ElementTypeClass || canEditFunctionPropsRenamePaths;
51 +
52 + const entries = props != null ? Object.entries(props) : null;
53 + if (entries !== null) {
54 + entries.sort(alphaSortEntries);
55 + }
56 +
57 + const isEmpty = entries === null || entries.length === 0;
58 +
59 + const handleCopy = () => copy(serializeDataForCopy(((props: any): Object)));
60 +
61 + return (
62 + <div className={styles.InspectedElementTree}>
63 + <div className={styles.HeaderRow}>
64 + <div className={styles.Header}>props</div>
65 + {!isEmpty && (
66 + <Button onClick={handleCopy} title="Copy to clipboard">
67 + <ButtonIcon type="copy" />
68 + </Button>
69 + )}
70 + </div>
71 + {!isEmpty &&
72 + (entries: any).map(([name, value]) => (
73 + <KeyValue
74 + key={name}
75 + alphaSort={true}
76 + bridge={bridge}
77 + canDeletePaths={canDeletePaths}
78 + canEditValues={canEditValues}
79 + canRenamePaths={canRenamePaths}
80 + depth={1}
81 + getInspectedElementPath={getInspectedElementPath}
82 + hidden={false}
83 + inspectedElement={inspectedElement}
84 + name={name}
85 + path={[name]}
86 + pathRoot="props"
87 + store={store}
88 + value={value}
89 + />
90 + ))}
91 + {canEditValues && (
92 + <NewKeyValue
93 + bridge={bridge}
94 + depth={0}
95 + hidden={false}
96 + inspectedElement={inspectedElement}
97 + path={[]}
98 + store={store}
99 + type="props"
100 + />
101 + )}
102 + </div>
103 + );
104 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSharedStyles.css renamed
+1 -1
@@ -47,7 +47,7 @@
47 padding-left: 0.75rem;
48 }
49
50 -.AddEntry {
50 +.ToggleSuspenseRow {
51 padding-left: 1rem;
52 white-space: nowrap;
53 display: flex;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementStateTree.js new
+84
@@ -0,0 +1,84 @@
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 {copy} from 'clipboard-js';
11 +import * as React from 'react';
12 +import Button from '../Button';
13 +import ButtonIcon from '../ButtonIcon';
14 +import KeyValue from './KeyValue';
15 +import {alphaSortEntries, serializeDataForCopy} from '../utils';
16 +import Store from '../../store';
17 +import styles from './InspectedElementSharedStyles.css';
18 +
19 +import type {InspectedElement} from './types';
20 +import type {GetInspectedElementPath} from './InspectedElementContext';
21 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
22 +
23 +type Props = {|
24 + bridge: FrontendBridge,
25 + getInspectedElementPath: GetInspectedElementPath,
26 + inspectedElement: InspectedElement,
27 + store: Store,
28 +|};
29 +
30 +export default function InspectedElementStateTree({
31 + bridge,
32 + getInspectedElementPath,
33 + inspectedElement,
34 + store,
35 +}: Props) {
36 + const {state} = inspectedElement;
37 +
38 + const entries = state != null ? Object.entries(state) : null;
39 + if (entries !== null) {
40 + entries.sort(alphaSortEntries);
41 + }
42 +
43 + const isEmpty = entries === null || entries.length === 0;
44 +
45 + const handleCopy = () => copy(serializeDataForCopy(((state: any): Object)));
46 +
47 + if (isEmpty) {
48 + return null;
49 + } else {
50 + return (
51 + <div className={styles.InspectedElementTree}>
52 + <div className={styles.HeaderRow}>
53 + <div className={styles.Header}>state</div>
54 + {!isEmpty && (
55 + <Button onClick={handleCopy} title="Copy to clipboard">
56 + <ButtonIcon type="copy" />
57 + </Button>
58 + )}
59 + </div>
60 + {isEmpty && <div className={styles.Empty}>None</div>}
61 + {!isEmpty &&
62 + (entries: any).map(([name, value]) => (
63 + <KeyValue
64 + key={name}
65 + alphaSort={true}
66 + bridge={bridge}
67 + canDeletePaths={true}
68 + canEditValues={true}
69 + canRenamePaths={true}
70 + depth={1}
71 + getInspectedElementPath={getInspectedElementPath}
72 + hidden={false}
73 + inspectedElement={inspectedElement}
74 + name={name}
75 + path={[name]}
76 + pathRoot="state"
77 + store={store}
78 + value={value}
79 + />
80 + ))}
81 + </div>
82 + );
83 + }
84 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementSuspenseToggle.js new
+70
@@ -0,0 +1,70 @@
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 * as React from 'react';
11 +import EditableValue from './EditableValue';
12 +import Store from '../../store';
13 +import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
14 +import styles from './InspectedElementSharedStyles.css';
15 +
16 +import type {InspectedElement} from './types';
17 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
18 +
19 +type Props = {|
20 + bridge: FrontendBridge,
21 + inspectedElement: InspectedElement,
22 + store: Store,
23 +|};
24 +
25 +export default function InspectedElementSuspenseToggle({
26 + bridge,
27 + inspectedElement,
28 + store,
29 +}: Props) {
30 + const {canToggleSuspense, id, state, type} = inspectedElement;
31 +
32 + if (type !== ElementTypeSuspense) {
33 + return null;
34 + }
35 +
36 + const isSuspended = state !== null;
37 +
38 + const toggleSuspense = (path, value) => {
39 + const rendererID = store.getRendererIDForElement(id);
40 + if (rendererID !== null) {
41 + bridge.send('overrideSuspense', {
42 + id,
43 + rendererID,
44 + forceFallback: value,
45 + });
46 + }
47 + };
48 +
49 + return (
50 + <div className={styles.InspectedElementTree}>
51 + <div className={styles.HeaderRow}>
52 + <div className={styles.Header}>suspense</div>
53 + </div>
54 + <div className={styles.ToggleSuspenseRow}>
55 + <span className={styles.Name}>Suspended</span>
56 + {canToggleSuspense ? (
57 + // key is required to keep <EditableValue> and header row toggle button in sync
58 + <EditableValue
59 + key={isSuspended}
60 + overrideValue={toggleSuspense}
61 + path={['suspense', 'Suspended']}
62 + value={isSuspended}
63 + />
64 + ) : (
65 + <span className={styles.Value}>{isSuspended ? 'true' : 'false'}</span>
66 + )}
67 + </div>
68 + </div>
69 + );
70 +}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementTree.js deleted
-121
@@ -1,121 +0,0 @@
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 {copy} from 'clipboard-js';
11 -import * as React from 'react';
12 -import {useCallback, useState} from 'react';
13 -import Button from '../Button';
14 -import ButtonIcon from '../ButtonIcon';
15 -import KeyValue from './KeyValue';
16 -import EditableName from './EditableName';
17 -import EditableValue from './EditableValue';
18 -import {alphaSortEntries, serializeDataForCopy} from '../utils';
19 -import styles from './InspectedElementTree.css';
20 -
21 -import type {InspectPath} from './SelectedElement';
22 -
23 -type OverrideValueFn = (path: Array<string | number>, value: any) => void;
24 -
25 -type Props = {|
26 - data: Object | null,
27 - inspectPath?: InspectPath,
28 - label: string,
29 - overrideValueFn?: ?OverrideValueFn,
30 - pathRoot: string,
31 - showWhenEmpty?: boolean,
32 - canAddEntries?: boolean,
33 -|};
34 -
35 -export default function InspectedElementTree({
36 - data,
37 - inspectPath,
38 - label,
39 - overrideValueFn,
40 - pathRoot,
41 - canAddEntries = false,
42 - showWhenEmpty = false,
43 -}: Props) {
44 - const entries = data != null ? Object.entries(data) : null;
45 - if (entries !== null) {
46 - entries.sort(alphaSortEntries);
47 - }
48 -
49 - const [newPropKey, setNewPropKey] = useState<number>(0);
50 - const [newPropName, setNewPropName] = useState<string>('');
51 -
52 - const isEmpty = entries === null || entries.length === 0;
53 -
54 - const handleCopy = useCallback(
55 - () => copy(serializeDataForCopy(((data: any): Object))),
56 - [data],
57 - );
58 -
59 - const handleNewEntryValue = useCallback(
60 - (name, value) => {
61 - if (!newPropName) {
62 - return;
63 - }
64 -
65 - setNewPropName('');
66 - setNewPropKey(key => key + 1);
67 -
68 - if (typeof overrideValueFn === 'function') {
69 - overrideValueFn(name, value);
70 - }
71 - },
72 - [newPropName, overrideValueFn],
73 - );
74 -
75 - if (isEmpty && !showWhenEmpty && !canAddEntries) {
76 - return null;
77 - } else {
78 - return (
79 - <div className={styles.InspectedElementTree}>
80 - <div className={styles.HeaderRow}>
81 - <div className={styles.Header}>{label}</div>
82 - {!isEmpty && (
83 - <Button onClick={handleCopy} title="Copy to clipboard">
84 - <ButtonIcon type="copy" />
85 - </Button>
86 - )}
87 - </div>
88 - {isEmpty && !canAddEntries && <div className={styles.Empty}>None</div>}
89 - {!isEmpty &&
90 - (entries: any).map(([name, value]) => (
91 - <KeyValue
92 - key={name}
93 - alphaSort={true}
94 - pathRoot={pathRoot}
95 - depth={1}
96 - inspectPath={inspectPath}
97 - name={name}
98 - overrideValueFn={overrideValueFn}
99 - path={[name]}
100 - value={value}
101 - />
102 - ))}
103 - {canAddEntries && (
104 - <div className={styles.AddEntry} key={newPropKey}>
105 - <EditableName
106 - autoFocus={newPropKey > 0}
107 - overrideNameFn={setNewPropName}
108 - />
109 - :&nbsp;
110 - <EditableValue
111 - className={styles.EditableValue}
112 - overrideValueFn={handleNewEntryValue}
113 - path={[newPropName]}
114 - value={''}
115 - />
116 - </div>
117 - )}
118 - </div>
119 - );
120 - }
121 -}
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.css renamed
-65
@@ -1,54 +1,3 @@
1 -.SelectedElement {
2 - display: flex;
3 - flex-direction: column;
4 - height: 100%;
5 - width: 100%;
6 - border-left: 1px solid var(--color-border);
7 - border-top: 1px solid var(--color-border);
8 -}
9 -
10 -.TitleRow {
11 - flex: 0 0 42px;
12 - display: flex;
13 - align-items: center;
14 - font-size: var(--font-size-monospace-large);
15 - border-bottom: 1px solid var(--color-border);
16 - padding: 0.5rem;
17 -}
18 -
19 -.Key {
20 - flex: 0 1 auto;
21 - padding-left: 0.25rem;
22 - padding-right: 0.125rem;
23 - line-height: 1rem;
24 - border-top-left-radius: 0.125rem;
25 - border-bottom-left-radius: 0.125rem;
26 - display: inline-block;
27 - background-color: var(--color-component-badge-background);
28 - color: var(--color-text);
29 - font-family: var(--font-family-monospace);
30 - font-size: var(--font-size-monospace-small);
31 - white-space: nowrap;
32 - overflow: hidden;
33 - text-overflow: ellipsis;
34 - max-width: 100%;
35 -}
36 -
37 -.KeyArrow {
38 - height: 1rem;
39 - width: 1rem;
40 - margin-right: -0.25rem;
41 - border: 0.5rem solid transparent;
42 - border-left: 0.5rem solid var(--color-component-badge-background);
43 -}
44 -
45 -.SelectedComponentName {
46 - flex: 1 1 auto;
47 - overflow: hidden;
48 - text-overflow: ellipsis;
49 - line-height: normal;
50 -}
51 -
1 .Owners {
2 padding: 0.25rem;
3 border-top: 1px solid var(--color-border);
@@ -83,7 +32,6 @@
32 margin-left: 1rem;
33 }
34
86 -.Component,
35 .Owner {
36 color: var(--color-component-name);
37 font-family: var(--font-family-monospace);
@@ -94,10 +42,6 @@
42 max-width: 100%;
43 }
44
97 -.Component {
98 - flex: 1 1 auto;
99 -}
100 -
45 .InspectedElement {
46 overflow-x: hidden;
47 overflow-y: auto;
@@ -118,15 +62,6 @@
62 background-color: var(--color-button-background-focus);
63 }
64
121 -.Loading {
122 - padding: 0.25rem;
123 - color: var(--color-dimmer);
124 - font-style: italic;
125 -}
126 -
127 -.CannotSuspendWarningMessage {
128 -}
129 -
65 .NotInStore {
66 color: var(--color-dim);
67 cursor: default;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElementView.js new
+285
@@ -0,0 +1,285 @@
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 {copy} from 'clipboard-js';
11 +import * as React from 'react';
12 +import {Fragment, useCallback, useContext} from 'react';
13 +import {TreeDispatcherContext} from './TreeContext';
14 +import {BridgeContext, ContextMenuContext, StoreContext} from '../context';
15 +import ContextMenu from '../../ContextMenu/ContextMenu';
16 +import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
17 +import Button from '../Button';
18 +import ButtonIcon from '../ButtonIcon';
19 +import Icon from '../Icon';
20 +import HocBadges from './HocBadges';
21 +import InspectedElementContextTree from './InspectedElementContextTree';
22 +import InspectedElementHooksTree from './InspectedElementHooksTree';
23 +import InspectedElementPropsTree from './InspectedElementPropsTree';
24 +import InspectedElementStateTree from './InspectedElementStateTree';
25 +import InspectedElementSuspenseToggle from './InspectedElementSuspenseToggle';
26 +import NativeStyleEditor from './NativeStyleEditor';
27 +import Badge from './Badge';
28 +import {useHighlightNativeElement} from '../hooks';
29 +
30 +import styles from './InspectedElementView.css';
31 +
32 +import type {ContextMenuContextType} from '../context';
33 +import type {
34 + CopyInspectedElementPath,
35 + GetInspectedElementPath,
36 + StoreAsGlobal,
37 +} from './InspectedElementContext';
38 +import type {Element, InspectedElement, Owner} from './types';
39 +import type {ElementType} from 'react-devtools-shared/src/types';
40 +
41 +export type CopyPath = (path: Array<string | number>) => void;
42 +export type InspectPath = (path: Array<string | number>) => void;
43 +
44 +type Props = {|
45 + copyInspectedElementPath: CopyInspectedElementPath,
46 + element: Element,
47 + getInspectedElementPath: GetInspectedElementPath,
48 + inspectedElement: InspectedElement,
49 + storeAsGlobal: StoreAsGlobal,
50 +|};
51 +
52 +export default function InspectedElementView({
53 + copyInspectedElementPath,
54 + element,
55 + getInspectedElementPath,
56 + inspectedElement,
57 + storeAsGlobal,
58 +}: Props) {
59 + const {id} = element;
60 + const {
61 + owners,
62 + rendererPackageName,
63 + rendererVersion,
64 + rootType,
65 + source,
66 + } = inspectedElement;
67 +
68 + const bridge = useContext(BridgeContext);
69 + const store = useContext(StoreContext);
70 +
71 + const {
72 + isEnabledForInspectedElement: isContextMenuEnabledForInspectedElement,
73 + viewAttributeSourceFunction,
74 + } = useContext<ContextMenuContextType>(ContextMenuContext);
75 +
76 + const rendererLabel =
77 + rendererPackageName !== null && rendererVersion !== null
78 + ? `${rendererPackageName}@${rendererVersion}`
79 + : null;
80 + const showOwnersList = owners !== null && owners.length > 0;
81 + const showRenderedBy =
82 + showOwnersList || rendererLabel !== null || rootType !== null;
83 +
84 + return (
85 + <Fragment>
86 + <div className={styles.InspectedElement}>
87 + <HocBadges element={element} />
88 +
89 + <InspectedElementPropsTree
90 + bridge={bridge}
91 + getInspectedElementPath={getInspectedElementPath}
92 + inspectedElement={inspectedElement}
93 + store={store}
94 + />
95 +
96 + <InspectedElementSuspenseToggle
97 + bridge={bridge}
98 + inspectedElement={inspectedElement}
99 + store={store}
100 + />
101 +
102 + <InspectedElementStateTree
103 + bridge={bridge}
104 + getInspectedElementPath={getInspectedElementPath}
105 + inspectedElement={inspectedElement}
106 + store={store}
107 + />
108 +
109 + <InspectedElementHooksTree
110 + bridge={bridge}
111 + getInspectedElementPath={getInspectedElementPath}
112 + inspectedElement={inspectedElement}
113 + store={store}
114 + />
115 +
116 + <InspectedElementContextTree
117 + bridge={bridge}
118 + getInspectedElementPath={getInspectedElementPath}
119 + inspectedElement={inspectedElement}
120 + store={store}
121 + />
122 +
123 + <NativeStyleEditor />
124 +
125 + {showRenderedBy && (
126 + <div className={styles.Owners}>
127 + <div className={styles.OwnersHeader}>rendered by</div>
128 + {showOwnersList &&
129 + ((owners: any): Array<Owner>).map(owner => (
130 + <OwnerView
131 + key={owner.id}
132 + displayName={owner.displayName || 'Anonymous'}
133 + hocDisplayNames={owner.hocDisplayNames}
134 + id={owner.id}
135 + isInStore={store.containsElement(owner.id)}
136 + type={owner.type}
137 + />
138 + ))}
139 + {rootType !== null && (
140 + <div className={styles.OwnersMetaField}>{rootType}</div>
141 + )}
142 + {rendererLabel !== null && (
143 + <div className={styles.OwnersMetaField}>{rendererLabel}</div>
144 + )}
145 + </div>
146 + )}
147 +
148 + {source !== null && (
149 + <Source fileName={source.fileName} lineNumber={source.lineNumber} />
150 + )}
151 + </div>
152 +
153 + {isContextMenuEnabledForInspectedElement && (
154 + <ContextMenu id="InspectedElement">
155 + {data => (
156 + <Fragment>
157 + <ContextMenuItem
158 + onClick={() => copyInspectedElementPath(id, data.path)}
159 + title="Copy value to clipboard">
160 + <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
161 + value to clipboard
162 + </ContextMenuItem>
163 + <ContextMenuItem
164 + onClick={() => storeAsGlobal(id, data.path)}
165 + title="Store as global variable">
166 + <Icon
167 + className={styles.ContextMenuIcon}
168 + type="store-as-global-variable"
169 + />{' '}
170 + Store as global variable
171 + </ContextMenuItem>
172 + {viewAttributeSourceFunction !== null &&
173 + data.type === 'function' && (
174 + <ContextMenuItem
175 + onClick={() => viewAttributeSourceFunction(id, data.path)}
176 + title="Go to definition">
177 + <Icon className={styles.ContextMenuIcon} type="code" /> Go
178 + to definition
179 + </ContextMenuItem>
180 + )}
181 + </Fragment>
182 + )}
183 + </ContextMenu>
184 + )}
185 + </Fragment>
186 + );
187 +}
188 +
189 +// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
190 +function formatSourceForDisplay(fileName: string, lineNumber: string) {
191 + const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
192 +
193 + let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
194 +
195 + // In DEV, include code for a common special case:
196 + // prefer "folder/index.js" instead of just "index.js".
197 + if (/^index\./.test(nameOnly)) {
198 + const match = fileName.match(BEFORE_SLASH_RE);
199 + if (match) {
200 + const pathBeforeSlash = match[1];
201 + if (pathBeforeSlash) {
202 + const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
203 + nameOnly = folderName + '/' + nameOnly;
204 + }
205 + }
206 + }
207 +
208 + return `${nameOnly}:${lineNumber}`;
209 +}
210 +
211 +type SourceProps = {|
212 + fileName: string,
213 + lineNumber: string,
214 +|};
215 +
216 +function Source({fileName, lineNumber}: SourceProps) {
217 + const handleCopy = () => copy(`${fileName}:${lineNumber}`);
218 + return (
219 + <div className={styles.Source}>
220 + <div className={styles.SourceHeaderRow}>
221 + <div className={styles.SourceHeader}>source</div>
222 + <Button onClick={handleCopy} title="Copy to clipboard">
223 + <ButtonIcon type="copy" />
224 + </Button>
225 + </div>
226 + <div className={styles.SourceOneLiner}>
227 + {formatSourceForDisplay(fileName, lineNumber)}
228 + </div>
229 + </div>
230 + );
231 +}
232 +
233 +type OwnerViewProps = {|
234 + displayName: string,
235 + hocDisplayNames: Array<string> | null,
236 + id: number,
237 + isInStore: boolean,
238 + type: ElementType,
239 +|};
240 +
241 +function OwnerView({
242 + displayName,
243 + hocDisplayNames,
244 + id,
245 + isInStore,
246 + type,
247 +}: OwnerViewProps) {
248 + const dispatch = useContext(TreeDispatcherContext);
249 + const {
250 + highlightNativeElement,
251 + clearHighlightNativeElement,
252 + } = useHighlightNativeElement();
253 +
254 + const handleClick = useCallback(
255 + () =>
256 + dispatch({
257 + type: 'SELECT_ELEMENT_BY_ID',
258 + payload: id,
259 + }),
260 + [dispatch, id],
261 + );
262 +
263 + const onMouseEnter = () => highlightNativeElement(id);
264 +
265 + const onMouseLeave = clearHighlightNativeElement;
266 +
267 + return (
268 + <Button
269 + key={id}
270 + className={styles.OwnerButton}
271 + disabled={!isInStore}
272 + onClick={handleClick}
273 + onMouseEnter={onMouseEnter}
274 + onMouseLeave={onMouseLeave}>
275 + <span className={styles.OwnerContent}>
276 + <span
277 + className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
278 + title={displayName}>
279 + {displayName}
280 + </span>
281 + <Badge hocDisplayNames={hocDisplayNames} type={type} />
282 + </span>
283 + </Button>
284 + );
285 +}
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
+7 -3
@@ -12,9 +12,8 @@
12 flex: 0 0 auto;
13 cursor: default;
14 }
15 -.EditableName:after,
16 -.Name:after {
17 - content: ': ';
15 +
16 +.AfterName {
17 color: var(--color-text);
18 margin-right: 0.5rem;
19 }
@@ -40,3 +39,8 @@
39 .Empty {
40 color: var(--color-dimmer);
41 }
42 +
43 +.DeleteArrayItemButton {
44 + padding: 0;
45 + margin-right: 0.125rem;
46 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
+228 -59
@@ -9,64 +9,85 @@
9
10 import * as React from 'react';
11 import {useEffect, useRef, useState} from 'react';
12 +import EditableName from './EditableName';
13 import EditableValue from './EditableValue';
14 +import NewArrayValue from './NewArrayValue';
15 +import NewKeyValue from './NewKeyValue';
16 import ExpandCollapseToggle from './ExpandCollapseToggle';
17 import {alphaSortEntries, getMetaValueLabel} from '../utils';
18 import {meta} from '../../../hydration';
19 import useContextMenu from '../../ContextMenu/useContextMenu';
20 +import Store from '../../store';
21 +import {parseHookPathForEdit} from './utils';
22 import styles from './KeyValue.css';
23 +import Button from 'react-devtools-shared/src/devtools/views/Button';
24 +import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
25
26 +import type {InspectedElement} from './types';
27 import type {Element} from 'react';
20 -import type {InspectPath} from './SelectedElement';
28 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
29 +import type {GetInspectedElementPath} from './InspectedElementContext';
30
22 -type OverrideValueFn = (path: Array<string | number>, value: any) => void;
31 +type Type = 'props' | 'state' | 'context' | 'hooks';
32
33 type KeyValueProps = {|
34 alphaSort: boolean,
35 + bridge: FrontendBridge,
36 + canDeletePaths: boolean,
37 + canEditValues: boolean,
38 + canRenamePaths: boolean,
39 + canRenamePathsAtDepth?: (depth: number) => boolean,
40 depth: number,
27 - hidden?: boolean,
28 - inspectPath?: InspectPath,
29 - isReadOnly?: boolean,
41 + hidden: boolean,
42 + hookID?: ?number,
43 + getInspectedElementPath: GetInspectedElementPath,
44 + inspectedElement: InspectedElement,
45 + isDirectChildOfAnArray?: boolean,
46 name: string,
31 - overrideValueFn?: ?OverrideValueFn,
47 path: Array<any>,
33 - pathRoot: string,
48 + pathRoot: Type,
49 + store: Store,
50 value: any,
51 |};
52
53 export default function KeyValue({
54 alphaSort,
55 + bridge,
56 + canDeletePaths,
57 + canEditValues,
58 + canRenamePaths,
59 + canRenamePathsAtDepth,
60 depth,
40 - inspectPath,
41 - isReadOnly,
61 + getInspectedElementPath,
62 + inspectedElement,
63 + isDirectChildOfAnArray,
64 hidden,
65 + hookID,
66 name,
44 - overrideValueFn,
67 path,
68 pathRoot,
69 + store,
70 value,
71 }: KeyValueProps) {
72 + const {id} = inspectedElement;
73 +
74 const [isOpen, setIsOpen] = useState<boolean>(false);
75 const prevIsOpenRef = useRef(isOpen);
76 const contextMenuTriggerRef = useRef(null);
77
53 - const isInspectable =
54 - value !== null &&
55 - typeof value === 'object' &&
56 - value[meta.inspectable] &&
57 - value[meta.size] !== 0;
78 + let isInspectable = false;
79 + let isReadOnly = false;
80 + if (value !== null && typeof value === 'object') {
81 + isInspectable = value[meta.inspectable] && value[meta.size] !== 0;
82 + isReadOnly = value[meta.readonly];
83 + }
84
85 useEffect(() => {
60 - if (
61 - isInspectable &&
62 - isOpen &&
63 - !prevIsOpenRef.current &&
64 - typeof inspectPath === 'function'
65 - ) {
66 - inspectPath(path);
86 + if (isInspectable && isOpen && !prevIsOpenRef.current) {
87 + getInspectedElementPath(id, [pathRoot, ...path]);
88 }
89 prevIsOpenRef.current = isOpen;
69 - }, [inspectPath, isInspectable, isOpen, path]);
90 + }, [getInspectedElementPath, isInspectable, isOpen, path, pathRoot]);
91
92 const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
93
@@ -80,7 +101,7 @@ export default function KeyValue({
101 ? value[meta.type]
102 : typeof value,
103 },
83 - id: 'SelectedElement',
104 + id: 'InspectedElement',
105 ref: contextMenuTriggerRef,
106 });
107
@@ -95,6 +116,99 @@ export default function KeyValue({
116 paddingLeft: `${(depth - 1) * 0.75}rem`,
117 };
118
119 + const overrideValue = (newPath, newValue) => {
120 + if (hookID != null) {
121 + newPath = parseHookPathForEdit(newPath);
122 + }
123 +
124 + const rendererID = store.getRendererIDForElement(id);
125 + if (rendererID !== null) {
126 + bridge.send('overrideValueAtPath', {
127 + hookID,
128 + id,
129 + path: newPath,
130 + rendererID,
131 + type: pathRoot,
132 + value: newValue,
133 + });
134 + }
135 + };
136 +
137 + const deletePath = pathToDelete => {
138 + if (hookID != null) {
139 + pathToDelete = parseHookPathForEdit(pathToDelete);
140 + }
141 +
142 + const rendererID = store.getRendererIDForElement(id);
143 + if (rendererID !== null) {
144 + bridge.send('deletePath', {
145 + hookID,
146 + id,
147 + path: pathToDelete,
148 + rendererID,
149 + type: pathRoot,
150 + });
151 + }
152 + };
153 +
154 + const renamePath = (oldPath, newPath) => {
155 + if (newPath[newPath.length - 1] === '') {
156 + // Deleting the key suggests an intent to delete the whole path.
157 + if (canDeletePaths) {
158 + deletePath(oldPath);
159 + }
160 + } else {
161 + if (hookID != null) {
162 + oldPath = parseHookPathForEdit(oldPath);
163 + newPath = parseHookPathForEdit(newPath);
164 + }
165 +
166 + const rendererID = store.getRendererIDForElement(id);
167 + if (rendererID !== null) {
168 + bridge.send('renamePath', {
169 + hookID,
170 + id,
171 + newPath,
172 + oldPath,
173 + rendererID,
174 + type: pathRoot,
175 + });
176 + }
177 + }
178 + };
179 +
180 + // TRICKY This is a bit of a hack to account for context and hooks.
181 + // In these cases, paths can be renamed but only at certain depths.
182 + // The special "value" wrapper for context shouldn't be editable.
183 + // Only certain types of hooks should be editable.
184 + let canRenameTheCurrentPath = canRenamePaths;
185 + if (canRenameTheCurrentPath && typeof canRenamePathsAtDepth === 'function') {
186 + canRenameTheCurrentPath = canRenamePathsAtDepth(depth);
187 + }
188 +
189 + let renderedName;
190 + if (isDirectChildOfAnArray) {
191 + if (canDeletePaths) {
192 + renderedName = (
193 + <DeleteToggle name={name} deletePath={deletePath} path={path} />
194 + );
195 + } else {
196 + renderedName = <span className={styles.Name}>{name}</span>;
197 + }
198 + } else if (canRenameTheCurrentPath) {
199 + renderedName = (
200 + <EditableName
201 + allowEmpty={canDeletePaths}
202 + className={styles.EditableName}
203 + initialValue={name}
204 + overrideName={renamePath}
205 + path={path}
206 + />
207 + );
208 + } else {
209 + renderedName = <span className={styles.Name}>{name}</span>;
210 + }
211 +
212 let children = null;
213 if (isSimpleType) {
214 let displayValue = value;
@@ -108,23 +222,19 @@ export default function KeyValue({
222 displayValue = 'undefined';
223 }
224
111 - const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
112 -
225 children = (
226 <div
227 key="root"
116 - path={path}
228 className={styles.Item}
229 hidden={hidden}
230 ref={contextMenuTriggerRef}
231 style={style}>
232 <div className={styles.ExpandCollapseToggleSpacer} />
122 - <span className={isEditable ? styles.EditableName : styles.Name}>
123 - {name}
124 - </span>
125 - {isEditable ? (
233 + {renderedName}
234 + <div className={styles.AfterName}>:</div>
235 + {canEditValues ? (
236 <EditableValue
127 - overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
237 + overrideValue={overrideValue}
238 path={path}
239 value={value}
240 />
@@ -139,21 +249,18 @@ export default function KeyValue({
249 ) {
250 children = (
251 <div
142 - ref={contextMenuTriggerRef}
252 key="root"
253 className={styles.Item}
254 hidden={hidden}
255 + ref={contextMenuTriggerRef}
256 style={style}>
257 {isInspectable ? (
258 <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
259 ) : (
260 <div className={styles.ExpandCollapseToggleSpacer} />
261 )}
152 - <span
153 - className={styles.Name}
154 - onClick={isInspectable ? toggleIsOpen : undefined}>
155 - {name}
156 - </span>
262 + {renderedName}
263 + <div className={styles.AfterName}>:</div>
264 <span
265 className={styles.Value}
266 onClick={isInspectable ? toggleIsOpen : undefined}>
@@ -163,41 +270,64 @@ export default function KeyValue({
270 );
271 } else {
272 if (Array.isArray(value)) {
166 - const hasChildren = value.length > 0;
273 + const hasChildren = value.length > 0 || canEditValues;
274 const displayName = getMetaValueLabel(value);
275
276 children = value.map((innerValue, index) => (
277 <KeyValue
278 key={index}
279 alphaSort={alphaSort}
280 + bridge={bridge}
281 + canDeletePaths={canDeletePaths && !isReadOnly}
282 + canEditValues={canEditValues && !isReadOnly}
283 + canRenamePaths={canRenamePaths && !isReadOnly}
284 + canRenamePathsAtDepth={canRenamePathsAtDepth}
285 depth={depth + 1}
174 - inspectPath={inspectPath}
175 - isReadOnly={isReadOnly}
286 + getInspectedElementPath={getInspectedElementPath}
287 + hookID={hookID}
288 + inspectedElement={inspectedElement}
289 + isDirectChildOfAnArray={true}
290 hidden={hidden || !isOpen}
291 name={index}
178 - overrideValueFn={overrideValueFn}
292 path={path.concat(index)}
293 pathRoot={pathRoot}
294 + store={store}
295 value={value[index]}
296 />
297 ));
298 +
299 + if (canEditValues && !isReadOnly) {
300 + children.push(
301 + <NewArrayValue
302 + key="NewKeyValue"
303 + bridge={bridge}
304 + depth={depth + 1}
305 + hidden={hidden || !isOpen}
306 + hookID={hookID}
307 + index={value.length}
308 + getInspectedElementPath={getInspectedElementPath}
309 + inspectedElement={inspectedElement}
310 + path={path}
311 + store={store}
312 + type={pathRoot}
313 + />,
314 + );
315 + }
316 +
317 children.unshift(
318 <div
186 - ref={contextMenuTriggerRef}
319 key={`${depth}-root`}
320 className={styles.Item}
321 hidden={hidden}
322 + ref={contextMenuTriggerRef}
323 style={style}>
324 {hasChildren ? (
325 <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
326 ) : (
327 <div className={styles.ExpandCollapseToggleSpacer} />
328 )}
196 - <span
197 - className={styles.Name}
198 - onClick={hasChildren ? toggleIsOpen : undefined}>
199 - {name}
200 - </span>
329 + {renderedName}
330 + <div className={styles.AfterName}>:</div>
331 <span
332 className={styles.Value}
333 onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -214,42 +344,62 @@ export default function KeyValue({
344 entries.sort(alphaSortEntries);
345 }
346
217 - const hasChildren = entries.length > 0;
347 + const hasChildren = entries.length > 0 || canEditValues;
348 const displayName = getMetaValueLabel(value);
349
220 - const areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
350 children = entries.map<Element<any>>(([key, keyValue]) => (
351 <KeyValue
352 key={key}
353 alphaSort={alphaSort}
354 + bridge={bridge}
355 + canDeletePaths={canDeletePaths && !isReadOnly}
356 + canEditValues={canEditValues && !isReadOnly}
357 + canRenamePaths={canRenamePaths && !isReadOnly}
358 + canRenamePathsAtDepth={canRenamePathsAtDepth}
359 depth={depth + 1}
226 - inspectPath={inspectPath}
227 - isReadOnly={areChildrenReadOnly}
360 + getInspectedElementPath={getInspectedElementPath}
361 + hookID={hookID}
362 + inspectedElement={inspectedElement}
363 hidden={hidden || !isOpen}
364 name={key}
230 - overrideValueFn={overrideValueFn}
365 path={path.concat(key)}
366 pathRoot={pathRoot}
367 + store={store}
368 value={keyValue}
369 />
370 ));
371 +
372 + if (canEditValues && !isReadOnly) {
373 + children.push(
374 + <NewKeyValue
375 + key="NewKeyValue"
376 + bridge={bridge}
377 + depth={depth + 1}
378 + getInspectedElementPath={getInspectedElementPath}
379 + hidden={hidden || !isOpen}
380 + hookID={hookID}
381 + inspectedElement={inspectedElement}
382 + path={path}
383 + store={store}
384 + type={pathRoot}
385 + />,
386 + );
387 + }
388 +
389 children.unshift(
390 <div
238 - ref={contextMenuTriggerRef}
391 key={`${depth}-root`}
392 className={styles.Item}
393 hidden={hidden}
394 + ref={contextMenuTriggerRef}
395 style={style}>
396 {hasChildren ? (
397 <ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
398 ) : (
399 <div className={styles.ExpandCollapseToggleSpacer} />
400 )}
248 - <span
249 - className={styles.Name}
250 - onClick={hasChildren ? toggleIsOpen : undefined}>
251 - {name}
252 - </span>
401 + {renderedName}
402 + <div className={styles.AfterName}>:</div>
403 <span
404 className={styles.Value}
405 onClick={hasChildren ? toggleIsOpen : undefined}>
@@ -262,3 +412,22 @@ export default function KeyValue({
412
413 return children;
414 }
415 +
416 +function DeleteToggle({deletePath, name, path}) {
417 + const handleClick = event => {
418 + event.stopPropagation();
419 + deletePath(path);
420 + };
421 +
422 + return (
423 + <>
424 + <Button
425 + className={styles.DeleteArrayItemButton}
426 + onClick={handleClick}
427 + title="Delete entry">
428 + <ButtonIcon type="delete" />
429 + </Button>
430 + <span className={styles.Name}>{name}</span>
431 + </>
432 + );
433 +}
packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.css
-11
@@ -1,5 +1,4 @@
1 .Input {
2 - width: 0;
2 min-width: 0.5rem;
3 flex: 1 1 auto;
4 border: none;
@@ -14,13 +13,3 @@
13 .Input:focus {
14 border-color: var(--color-border);
15 }
17 -
18 -.HiddenDiv {
19 - position: absolute;
20 - top: 0;
21 - left: 0;
22 - visibility: hidden;
23 - height: 0;
24 - overflow: scroll;
25 - white-space: pre;
26 -}
packages/react-devtools-shared/src/devtools/views/Components/NativeStyleEditor/AutoSizeInput.js
+15 -67
@@ -8,7 +8,6 @@
8 */
9
10 import * as React from 'react';
11 -import {Fragment, useLayoutEffect, useRef} from 'react';
11 import styles from './AutoSizeInput.css';
12
13 type Props = {
@@ -22,17 +21,15 @@ type Props = {
21 export default function AutoSizeInput({
22 className,
23 onFocus,
25 - placeholder,
24 + placeholder = '',
25 value,
26 ...rest
27 }: Props) {
29 - const hiddenDivRef = useRef<HTMLDivElement | null>(null);
30 - const inputRef = useRef<HTMLInputElement | null>(null);
31 -
28 const onFocusWrapper = event => {
33 - if (inputRef.current !== null) {
34 - inputRef.current.selectionStart = 0;
35 - inputRef.current.selectionEnd = value.length;
29 + const input = event.target;
30 + if (input !== null) {
31 + input.selectionStart = 0;
32 + input.selectionEnd = value.length;
33 }
34
35 if (typeof onFocus === 'function') {
@@ -40,67 +37,18 @@ export default function AutoSizeInput({
37 }
38 };
39
43 - // Copy text styles from <input> to hidden sizing <div>
44 - useLayoutEffect(() => {
45 - if (
46 - typeof window.getComputedStyle !== 'function' ||
47 - inputRef.current === null
48 - ) {
49 - return;
50 - }
51 -
52 - const inputStyle = window.getComputedStyle(inputRef.current);
53 - if (!inputStyle) {
54 - return;
55 - }
56 -
57 - if (hiddenDivRef.current !== null) {
58 - const divStyle = hiddenDivRef.current.style;
59 - divStyle.border = inputStyle.border;
60 - divStyle.fontFamily = inputStyle.fontFamily;
61 - divStyle.fontSize = inputStyle.fontSize;
62 - divStyle.fontStyle = inputStyle.fontStyle;
63 - divStyle.fontWeight = inputStyle.fontWeight;
64 - divStyle.letterSpacing = inputStyle.letterSpacing;
65 - divStyle.padding = inputStyle.padding;
66 - }
67 - }, []);
68 -
69 - // Resize input any time text changes
70 - useLayoutEffect(() => {
71 - if (hiddenDivRef.current === null) {
72 - return;
73 - }
74 -
75 - const scrollWidth = hiddenDivRef.current.getBoundingClientRect().width;
76 - if (!scrollWidth) {
77 - return;
78 - }
79 -
80 - // Adding an extra pixel avoids a slight horizontal scroll when changing text selection/cursor.
81 - // Not sure why this is, but the old DevTools did a similar thing.
82 - const targetWidth = Math.ceil(scrollWidth) + 1;
83 -
84 - if (inputRef.current !== null) {
85 - inputRef.current.style.width = `${targetWidth}px`;
86 - }
87 - }, [value]);
88 -
40 const isEmpty = value === '' || value === '""';
41
42 return (
92 - <Fragment>
93 - <input
94 - ref={inputRef}
95 - className={`${className ? className : ''} ${styles.Input}`}
96 - onFocus={onFocusWrapper}
97 - placeholder={placeholder}
98 - value={isEmpty ? '' : value}
99 - {...rest}
100 - />
101 - <div ref={hiddenDivRef} className={styles.HiddenDiv}>
102 - {isEmpty ? placeholder : value}
103 - </div>
104 - </Fragment>
43 + <input
44 + className={[styles.Input, className].join(' ')}
45 + onFocus={onFocusWrapper}
46 + placeholder={placeholder}
47 + style={{
48 + width: `calc(${isEmpty ? placeholder.length : value.length}ch + 1px)`,
49 + }}
50 + value={isEmpty ? '' : value}
51 + {...rest}
52 + />
53 );
54 }
packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.css new
+19
@@ -0,0 +1,19 @@
1 +.NewArrayValue {
2 + white-space: nowrap;
3 + display: flex;
4 + align-items: center;
5 +}
6 +
7 +.EditableName {
8 + margin-left: 1rem;
9 +}
10 +
11 +.Invalid {
12 + background-color: var(--color-background-invalid);
13 + color: var(--color-text-invalid) !important;
14 +}
15 +
16 +.Input:focus,
17 +.Invalid:focus {
18 + background-color: var(--color-button-background-focus);
19 +}
\ No newline at end of file
packages/react-devtools-shared/src/devtools/views/Components/NewArrayValue.js new
+107
@@ -0,0 +1,107 @@
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 * as React from 'react';
11 +import {useState} from 'react';
12 +import Store from '../../store';
13 +import EditableName from './EditableName';
14 +import {smartParse} from '../../utils';
15 +import {parseHookPathForEdit} from './utils';
16 +import styles from './NewArrayValue.css';
17 +
18 +import type {InspectedElement} from './types';
19 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
20 +
21 +type Props = {|
22 + bridge: FrontendBridge,
23 + depth: number,
24 + hidden: boolean,
25 + hookID?: ?number,
26 + index: number,
27 + inspectedElement: InspectedElement,
28 + path: Array<string | number>,
29 + store: Store,
30 + type: 'props' | 'context' | 'hooks' | 'state',
31 +|};
32 +
33 +export default function NewArrayValue({
34 + bridge,
35 + depth,
36 + hidden,
37 + hookID,
38 + index,
39 + inspectedElement,
40 + path,
41 + store,
42 + type,
43 +}: Props) {
44 + const [key, setKey] = useState<number>(0);
45 + const [isInvalid, setIsInvalid] = useState(false);
46 +
47 + // This is a bit of an unusual usage of the EditableName component,
48 + // but otherwise it acts the way we want for a new Array entry.
49 + const overrideName = (oldPath, newPath) => {
50 + const value = newPath[newPath.length - 1];
51 +
52 + let parsedValue;
53 + let newIsInvalid = true;
54 + try {
55 + parsedValue = smartParse(value);
56 + newIsInvalid = false;
57 + } catch (error) {}
58 +
59 + if (isInvalid !== newIsInvalid) {
60 + setIsInvalid(newIsInvalid);
61 + }
62 +
63 + if (!newIsInvalid) {
64 + setKey(key + 1);
65 +
66 + const {id} = inspectedElement;
67 + const rendererID = store.getRendererIDForElement(id);
68 + if (rendererID !== null) {
69 + let basePath = path;
70 + if (hookID != null) {
71 + basePath = parseHookPathForEdit(basePath);
72 + }
73 +
74 + bridge.send('overrideValueAtPath', {
75 + type,
76 + hookID,
77 + id,
78 + path: [...basePath, index],
79 + rendererID,
80 + value: parsedValue,
81 + });
82 + }
83 + }
84 + };
85 +
86 + return (
87 + <div
88 + key={key}
89 + hidden={hidden}
90 + style={{
91 + paddingLeft: `${(depth - 1) * 0.75}rem`,
92 + }}>
93 + <div className={styles.NewArrayValue}>
94 + <EditableName
95 + allowWhiteSpace={true}
96 + autoFocus={key > 0}
97 + className={[styles.EditableName, isInvalid && styles.Invalid].join(
98 + ' ',
99 + )}
100 + initialValue=""
101 + overrideName={overrideName}
102 + path={path}
103 + />
104 + </div>
105 + </div>
106 + );
107 +}
packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.css new
+13
@@ -0,0 +1,13 @@
1 +.NewKeyValue {
2 + white-space: nowrap;
3 + display: flex;
4 + align-items: center;
5 +}
6 +
7 +.EditableName {
8 + margin-left: 1rem;
9 +}
10 +
11 +.EditableValue {
12 + min-width: 1rem;
13 +}
packages/react-devtools-shared/src/devtools/views/Components/NewKeyValue.js new
+100
@@ -0,0 +1,100 @@
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 * as React from 'react';
11 +import {useState} from 'react';
12 +import Store from '../../store';
13 +import EditableName from './EditableName';
14 +import EditableValue from './EditableValue';
15 +import {parseHookPathForEdit} from './utils';
16 +import styles from './NewKeyValue.css';
17 +
18 +import type {InspectedElement} from './types';
19 +import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
20 +
21 +type Props = {|
22 + bridge: FrontendBridge,
23 + depth: number,
24 + hidden: boolean,
25 + hookID?: ?number,
26 + inspectedElement: InspectedElement,
27 + path: Array<string | number>,
28 + store: Store,
29 + type: 'props' | 'state' | 'hooks' | 'context',
30 +|};
31 +
32 +export default function NewKeyValue({
33 + bridge,
34 + depth,
35 + hidden,
36 + hookID,
37 + inspectedElement,
38 + path,
39 + store,
40 + type,
41 +}: Props) {
42 + const [newPropKey, setNewPropKey] = useState<number>(0);
43 + const [newPropName, setNewPropName] = useState<string>('');
44 +
45 + const overrideNewEntryName = (oldPath, newPath) => {
46 + setNewPropName(newPath[newPath.length - 1]);
47 + };
48 +
49 + const overrideNewEntryValue = (newPath, value) => {
50 + if (!newPropName) {
51 + return;
52 + }
53 +
54 + setNewPropName('');
55 + setNewPropKey(newPropKey + 1);
56 +
57 + const {id} = inspectedElement;
58 + const rendererID = store.getRendererIDForElement(id);
59 + if (rendererID !== null) {
60 + let basePath = newPath;
61 + if (hookID != null) {
62 + basePath = parseHookPathForEdit(basePath);
63 + }
64 +
65 + bridge.send('overrideValueAtPath', {
66 + type,
67 + hookID,
68 + id,
69 + path: basePath,
70 + rendererID,
71 + value,
72 + });
73 + }
74 + };
75 +
76 + return (
77 + <div
78 + key={newPropKey}
79 + hidden={hidden}
80 + style={{
81 + paddingLeft: `${(depth - 1) * 0.75}rem`,
82 + }}>
83 + <div className={styles.NewKeyValue}>
84 + <EditableName
85 + autoFocus={newPropKey > 0}
86 + className={styles.EditableName}
87 + overrideName={overrideNewEntryName}
88 + path={[]}
89 + />
90 + :&nbsp;
91 + <EditableValue
92 + className={styles.EditableValue}
93 + overrideValue={overrideNewEntryValue}
94 + path={[...path, newPropName]}
95 + value={''}
96 + />
97 + </div>
98 + </div>
99 + );
100 +}
packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js deleted
-616
@@ -1,616 +0,0 @@
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 {copy} from 'clipboard-js';
11 -import * as React from 'react';
12 -import {Fragment, useCallback, useContext} from 'react';
13 -import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
14 -import {BridgeContext, ContextMenuContext, StoreContext} from '../context';
15 -import ContextMenu from '../../ContextMenu/ContextMenu';
16 -import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
17 -import Button from '../Button';
18 -import ButtonIcon from '../ButtonIcon';
19 -import Icon from '../Icon';
20 -import HooksTree from './HooksTree';
21 -import {ModalDialogContext} from '../ModalDialog';
22 -import HocBadges from './HocBadges';
23 -import InspectedElementTree from './InspectedElementTree';
24 -import {InspectedElementContext} from './InspectedElementContext';
25 -import ViewElementSourceContext from './ViewElementSourceContext';
26 -import NativeStyleEditor from './NativeStyleEditor';
27 -import Toggle from '../Toggle';
28 -import Badge from './Badge';
29 -import {useHighlightNativeElement} from '../hooks';
30 -import {
31 - ComponentFilterElementType,
32 - ElementTypeClass,
33 - ElementTypeForwardRef,
34 - ElementTypeFunction,
35 - ElementTypeMemo,
36 - ElementTypeSuspense,
37 -} from 'react-devtools-shared/src/types';
38 -
39 -import styles from './SelectedElement.css';
40 -
41 -import type {ContextMenuContextType} from '../context';
42 -import type {
43 - CopyInspectedElementPath,
44 - GetInspectedElementPath,
45 - InspectedElementContextType,
46 - StoreAsGlobal,
47 -} from './InspectedElementContext';
48 -import type {Element, InspectedElement, Owner} from './types';
49 -import type {ElementType} from 'react-devtools-shared/src/types';
50 -
51 -export type Props = {||};
52 -
53 -export default function SelectedElement(_: Props) {
54 - const {inspectedElementID} = useContext(TreeStateContext);
55 - const dispatch = useContext(TreeDispatcherContext);
56 - const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
57 - ViewElementSourceContext,
58 - );
59 - const bridge = useContext(BridgeContext);
60 - const store = useContext(StoreContext);
61 - const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
62 -
63 - const {
64 - copyInspectedElementPath,
65 - getInspectedElementPath,
66 - getInspectedElement,
67 - storeAsGlobal,
68 - } = useContext<InspectedElementContextType>(InspectedElementContext);
69 -
70 - const element =
71 - inspectedElementID !== null
72 - ? store.getElementByID(inspectedElementID)
73 - : null;
74 -
75 - const inspectedElement =
76 - inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
77 -
78 - const highlightElement = useCallback(() => {
79 - if (element !== null && inspectedElementID !== null) {
80 - const rendererID = store.getRendererIDForElement(inspectedElementID);
81 - if (rendererID !== null) {
82 - bridge.send('highlightNativeElement', {
83 - displayName: element.displayName,
84 - hideAfterTimeout: true,
85 - id: inspectedElementID,
86 - openNativeElementsPanel: true,
87 - rendererID,
88 - scrollIntoView: true,
89 - });
90 - }
91 - }
92 - }, [bridge, element, inspectedElementID, store]);
93 -
94 - const logElement = useCallback(() => {
95 - if (inspectedElementID !== null) {
96 - const rendererID = store.getRendererIDForElement(inspectedElementID);
97 - if (rendererID !== null) {
98 - bridge.send('logElementToConsole', {
99 - id: inspectedElementID,
100 - rendererID,
101 - });
102 - }
103 - }
104 - }, [bridge, inspectedElementID, store]);
105 -
106 - const viewSource = useCallback(() => {
107 - if (viewElementSourceFunction != null && inspectedElement !== null) {
108 - viewElementSourceFunction(
109 - inspectedElement.id,
110 - ((inspectedElement: any): InspectedElement),
111 - );
112 - }
113 - }, [inspectedElement, viewElementSourceFunction]);
114 -
115 - // In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
116 - // To detect this case, we defer to an injected helper function (if present).
117 - const canViewSource =
118 - inspectedElement !== null &&
119 - inspectedElement.canViewSource &&
120 - viewElementSourceFunction !== null &&
121 - (canViewElementSourceFunction === null ||
122 - canViewElementSourceFunction(inspectedElement));
123 -
124 - const isSuspended =
125 - element !== null &&
126 - element.type === ElementTypeSuspense &&
127 - inspectedElement != null &&
128 - inspectedElement.state != null;
129 -
130 - const canToggleSuspense =
131 - inspectedElement != null && inspectedElement.canToggleSuspense;
132 -
133 - // TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
134 - const toggleSuspended = useCallback(() => {
135 - let nearestSuspenseElement = null;
136 - let currentElement = element;
137 - while (currentElement !== null) {
138 - if (currentElement.type === ElementTypeSuspense) {
139 - nearestSuspenseElement = currentElement;
140 - break;
141 - } else if (currentElement.parentID > 0) {
142 - currentElement = store.getElementByID(currentElement.parentID);
143 - } else {
144 - currentElement = null;
145 - }
146 - }
147 -
148 - // If we didn't find a Suspense ancestor, we can't suspend.
149 - // Instead we can show a warning to the user.
150 - if (nearestSuspenseElement === null) {
151 - modalDialogDispatch({
152 - type: 'SHOW',
153 - content: <CannotSuspendWarningMessage />,
154 - });
155 - } else {
156 - const nearestSuspenseElementID = nearestSuspenseElement.id;
157 -
158 - // If we're suspending from an arbitrary (non-Suspense) component, select the nearest Suspense element in the Tree.
159 - // This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
160 - if (nearestSuspenseElement !== element) {
161 - dispatch({
162 - type: 'SELECT_ELEMENT_BY_ID',
163 - payload: nearestSuspenseElementID,
164 - });
165 - }
166 -
167 - const rendererID = store.getRendererIDForElement(
168 - nearestSuspenseElementID,
169 - );
170 -
171 - // Toggle suspended
172 - if (rendererID !== null) {
173 - bridge.send('overrideSuspense', {
174 - id: nearestSuspenseElementID,
175 - rendererID,
176 - forceFallback: !isSuspended,
177 - });
178 - }
179 - }
180 - }, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
181 -
182 - if (element === null) {
183 - return (
184 - <div className={styles.SelectedElement}>
185 - <div className={styles.TitleRow} />
186 - </div>
187 - );
188 - }
189 -
190 - return (
191 - <div className={styles.SelectedElement}>
192 - <div className={styles.TitleRow}>
193 - {element.key && (
194 - <>
195 - <div className={styles.Key} title={`key "${element.key}"`}>
196 - {element.key}
197 - </div>
198 - <div className={styles.KeyArrow} />
199 - </>
200 - )}
201 -
202 - <div className={styles.SelectedComponentName}>
203 - <div className={styles.Component} title={element.displayName}>
204 - {element.displayName}
205 - </div>
206 - </div>
207 -
208 - {canToggleSuspense && (
209 - <Toggle
210 - className={styles.IconButton}
211 - isChecked={isSuspended}
212 - onChange={toggleSuspended}
213 - title={
214 - isSuspended
215 - ? 'Unsuspend the selected component'
216 - : 'Suspend the selected component'
217 - }>
218 - <ButtonIcon type="suspend" />
219 - </Toggle>
220 - )}
221 - {store.supportsNativeInspection && (
222 - <Button
223 - className={styles.IconButton}
224 - onClick={highlightElement}
225 - title="Inspect the matching DOM element">
226 - <ButtonIcon type="view-dom" />
227 - </Button>
228 - )}
229 - <Button
230 - className={styles.IconButton}
231 - onClick={logElement}
232 - title="Log this component data to the console">
233 - <ButtonIcon type="log-data" />
234 - </Button>
235 - <Button
236 - className={styles.IconButton}
237 - disabled={!canViewSource}
238 - onClick={viewSource}
239 - title="View source for this element">
240 - <ButtonIcon type="view-source" />
241 - </Button>
242 - </div>
243 -
244 - {inspectedElement === null && (
245 - <div className={styles.Loading}>Loading...</div>
246 - )}
247 -
248 - {inspectedElement !== null && (
249 - <InspectedElementView
250 - key={
251 - inspectedElementID /* Force reset when selected Element changes */
252 - }
253 - copyInspectedElementPath={copyInspectedElementPath}
254 - element={element}
255 - getInspectedElementPath={getInspectedElementPath}
256 - inspectedElement={inspectedElement}
257 - storeAsGlobal={storeAsGlobal}
258 - />
259 - )}
260 - </div>
261 - );
262 -}
263 -
264 -export type CopyPath = (path: Array<string | number>) => void;
265 -export type InspectPath = (path: Array<string | number>) => void;
266 -
267 -type InspectedElementViewProps = {|
268 - copyInspectedElementPath: CopyInspectedElementPath,
269 - element: Element,
270 - getInspectedElementPath: GetInspectedElementPath,
271 - inspectedElement: InspectedElement,
272 - storeAsGlobal: StoreAsGlobal,
273 -|};
274 -
275 -const IS_SUSPENDED = 'Suspended';
276 -
277 -function InspectedElementView({
278 - copyInspectedElementPath,
279 - element,
280 - getInspectedElementPath,
281 - inspectedElement,
282 - storeAsGlobal,
283 -}: InspectedElementViewProps) {
284 - const {id, type} = element;
285 - const {
286 - canEditFunctionProps,
287 - canEditHooks,
288 - canToggleSuspense,
289 - hasLegacyContext,
290 - context,
291 - hooks,
292 - owners,
293 - props,
294 - rendererPackageName,
295 - rendererVersion,
296 - rootType,
297 - source,
298 - state,
299 - } = inspectedElement;
300 -
301 - const bridge = useContext(BridgeContext);
302 - const store = useContext(StoreContext);
303 -
304 - const {
305 - isEnabledForInspectedElement,
306 - viewAttributeSourceFunction,
307 - } = useContext<ContextMenuContextType>(ContextMenuContext);
308 -
309 - const inspectContextPath = useCallback(
310 - (path: Array<string | number>) => {
311 - getInspectedElementPath(id, ['context', ...path]);
312 - },
313 - [getInspectedElementPath, id],
314 - );
315 - const inspectPropsPath = useCallback(
316 - (path: Array<string | number>) => {
317 - getInspectedElementPath(id, ['props', ...path]);
318 - },
319 - [getInspectedElementPath, id],
320 - );
321 - const inspectStatePath = useCallback(
322 - (path: Array<string | number>) => {
323 - getInspectedElementPath(id, ['state', ...path]);
324 - },
325 - [getInspectedElementPath, id],
326 - );
327 -
328 - let overrideContextFn = null;
329 - let overridePropsFn = null;
330 - let overrideStateFn = null;
331 - let overrideSuspenseFn = null;
332 - if (type === ElementTypeClass) {
333 - overrideContextFn = (path: Array<string | number>, value: any) => {
334 - const rendererID = store.getRendererIDForElement(id);
335 - if (rendererID !== null) {
336 - bridge.send('overrideContext', {id, path, rendererID, value});
337 - }
338 - };
339 - overridePropsFn = (path: Array<string | number>, value: any) => {
340 - const rendererID = store.getRendererIDForElement(id);
341 - if (rendererID !== null) {
342 - bridge.send('overrideProps', {id, path, rendererID, value});
343 - }
344 - };
345 - overrideStateFn = (path: Array<string | number>, value: any) => {
346 - const rendererID = store.getRendererIDForElement(id);
347 - if (rendererID !== null) {
348 - bridge.send('overrideState', {id, path, rendererID, value});
349 - }
350 - };
351 - } else if (
352 - (type === ElementTypeFunction ||
353 - type === ElementTypeMemo ||
354 - type === ElementTypeForwardRef) &&
355 - canEditFunctionProps
356 - ) {
357 - overridePropsFn = (path: Array<string | number>, value: any) => {
358 - const rendererID = store.getRendererIDForElement(id);
359 - if (rendererID !== null) {
360 - bridge.send('overrideProps', {id, path, rendererID, value});
361 - }
362 - };
363 - } else if (type === ElementTypeSuspense && canToggleSuspense) {
364 - overrideSuspenseFn = (path: Array<string | number>, value: boolean) => {
365 - if (path.length !== 1 && path !== IS_SUSPENDED) {
366 - throw new Error('Unexpected path.');
367 - }
368 - const rendererID = store.getRendererIDForElement(id);
369 - if (rendererID !== null) {
370 - bridge.send('overrideSuspense', {
371 - id,
372 - rendererID,
373 - forceFallback: value,
374 - });
375 - }
376 - };
377 - }
378 -
379 - const rendererLabel =
380 - rendererPackageName !== null && rendererVersion !== null
381 - ? `${rendererPackageName}@${rendererVersion}`
382 - : null;
383 - const showOwnersList = owners !== null && owners.length > 0;
384 - const showRenderedBy =
385 - showOwnersList || rendererLabel !== null || rootType !== null;
386 -
387 - return (
388 - <Fragment>
389 - <div className={styles.InspectedElement}>
390 - <HocBadges element={element} />
391 - <InspectedElementTree
392 - label="props"
393 - data={props}
394 - inspectPath={inspectPropsPath}
395 - overrideValueFn={overridePropsFn}
396 - pathRoot="props"
397 - showWhenEmpty={true}
398 - canAddEntries={typeof overridePropsFn === 'function'}
399 - />
400 - {type === ElementTypeSuspense ? (
401 - <InspectedElementTree
402 - label="suspense"
403 - data={{
404 - [IS_SUSPENDED]: state !== null,
405 - }}
406 - overrideValueFn={overrideSuspenseFn}
407 - />
408 - ) : (
409 - <InspectedElementTree
410 - label="state"
411 - data={state}
412 - inspectPath={inspectStatePath}
413 - overrideValueFn={overrideStateFn}
414 - pathRoot="state"
415 - />
416 - )}
417 - <HooksTree canEditHooks={canEditHooks} hooks={hooks} id={id} />
418 - <InspectedElementTree
419 - label={hasLegacyContext ? 'legacy context' : 'context'}
420 - data={context}
421 - inspectPath={inspectContextPath}
422 - overrideValueFn={overrideContextFn}
423 - pathRoot="context"
424 - />
425 -
426 - <NativeStyleEditor />
427 -
428 - {showRenderedBy && (
429 - <div className={styles.Owners}>
430 - <div className={styles.OwnersHeader}>rendered by</div>
431 - {showOwnersList &&
432 - ((owners: any): Array<Owner>).map(owner => (
433 - <OwnerView
434 - key={owner.id}
435 - displayName={owner.displayName || 'Anonymous'}
436 - hocDisplayNames={owner.hocDisplayNames}
437 - id={owner.id}
438 - isInStore={store.containsElement(owner.id)}
439 - type={owner.type}
440 - />
441 - ))}
442 - {rootType !== null && (
443 - <div className={styles.OwnersMetaField}>{rootType}</div>
444 - )}
445 - {rendererLabel !== null && (
446 - <div className={styles.OwnersMetaField}>{rendererLabel}</div>
447 - )}
448 - </div>
449 - )}
450 -
451 - {source !== null && (
452 - <Source fileName={source.fileName} lineNumber={source.lineNumber} />
453 - )}
454 - </div>
455 -
456 - {isEnabledForInspectedElement && (
457 - <ContextMenu id="SelectedElement">
458 - {data => (
459 - <Fragment>
460 - <ContextMenuItem
461 - onClick={() => copyInspectedElementPath(id, data.path)}
462 - title="Copy value to clipboard">
463 - <Icon className={styles.ContextMenuIcon} type="copy" /> Copy
464 - value to clipboard
465 - </ContextMenuItem>
466 - <ContextMenuItem
467 - onClick={() => storeAsGlobal(id, data.path)}
468 - title="Store as global variable">
469 - <Icon
470 - className={styles.ContextMenuIcon}
471 - type="store-as-global-variable"
472 - />{' '}
473 - Store as global variable
474 - </ContextMenuItem>
475 - {viewAttributeSourceFunction !== null &&
476 - data.type === 'function' && (
477 - <ContextMenuItem
478 - onClick={() => viewAttributeSourceFunction(id, data.path)}
479 - title="Go to definition">
480 - <Icon className={styles.ContextMenuIcon} type="code" /> Go
481 - to definition
482 - </ContextMenuItem>
483 - )}
484 - </Fragment>
485 - )}
486 - </ContextMenu>
487 - )}
488 - </Fragment>
489 - );
490 -}
491 -
492 -// This function is based on describeComponentFrame() in packages/shared/ReactComponentStackFrame
493 -function formatSourceForDisplay(fileName: string, lineNumber: string) {
494 - const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
495 -
496 - let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
497 -
498 - // In DEV, include code for a common special case:
499 - // prefer "folder/index.js" instead of just "index.js".
500 - if (/^index\./.test(nameOnly)) {
501 - const match = fileName.match(BEFORE_SLASH_RE);
502 - if (match) {
503 - const pathBeforeSlash = match[1];
504 - if (pathBeforeSlash) {
505 - const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
506 - nameOnly = folderName + '/' + nameOnly;
507 - }
508 - }
509 - }
510 -
511 - return `${nameOnly}:${lineNumber}`;
512 -}
513 -
514 -type SourceProps = {|
515 - fileName: string,
516 - lineNumber: string,
517 -|};
518 -
519 -function Source({fileName, lineNumber}: SourceProps) {
520 - const handleCopy = () => copy(`${fileName}:${lineNumber}`);
521 - return (
522 - <div className={styles.Source}>
523 - <div className={styles.SourceHeaderRow}>
524 - <div className={styles.SourceHeader}>source</div>
525 - <Button onClick={handleCopy} title="Copy to clipboard">
526 - <ButtonIcon type="copy" />
527 - </Button>
528 - </div>
529 - <div className={styles.SourceOneLiner}>
530 - {formatSourceForDisplay(fileName, lineNumber)}
531 - </div>
532 - </div>
533 - );
534 -}
535 -
536 -type OwnerViewProps = {|
537 - displayName: string,
538 - hocDisplayNames: Array<string> | null,
539 - id: number,
540 - isInStore: boolean,
541 - type: ElementType,
542 -|};
543 -
544 -function OwnerView({
545 - displayName,
546 - hocDisplayNames,
547 - id,
548 - isInStore,
549 - type,
550 -}: OwnerViewProps) {
551 - const dispatch = useContext(TreeDispatcherContext);
552 - const {
553 - highlightNativeElement,
554 - clearHighlightNativeElement,
555 - } = useHighlightNativeElement();
556 -
557 - const handleClick = useCallback(
558 - () =>
559 - dispatch({
560 - type: 'SELECT_ELEMENT_BY_ID',
561 - payload: id,
562 - }),
563 - [dispatch, id],
564 - );
565 -
566 - const onMouseEnter = () => highlightNativeElement(id);
567 -
568 - const onMouseLeave = clearHighlightNativeElement;
569 -
570 - return (
571 - <Button
572 - key={id}
573 - className={styles.OwnerButton}
574 - disabled={!isInStore}
575 - onClick={handleClick}
576 - onMouseEnter={onMouseEnter}
577 - onMouseLeave={onMouseLeave}>
578 - <span className={styles.OwnerContent}>
579 - <span
580 - className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
581 - title={displayName}>
582 - {displayName}
583 - </span>
584 - <Badge hocDisplayNames={hocDisplayNames} type={type} />
585 - </span>
586 - </Button>
587 - );
588 -}
589 -
590 -function CannotSuspendWarningMessage() {
591 - const store = useContext(StoreContext);
592 - const areSuspenseElementsHidden = !!store.componentFilters.find(
593 - filter =>
594 - filter.type === ComponentFilterElementType &&
595 - filter.value === ElementTypeSuspense &&
596 - filter.isEnabled,
597 - );
598 -
599 - // Has the user filtered out Suspense nodes from the tree?
600 - // If so, the selected element might actually be in a Suspense tree after all.
601 - if (areSuspenseElementsHidden) {
602 - return (
603 - <div className={styles.CannotSuspendWarningMessage}>
604 - Suspended state cannot be toggled while Suspense components are hidden.
605 - Disable the filter and try again.
606 - </div>
607 - );
608 - } else {
609 - return (
610 - <div className={styles.CannotSuspendWarningMessage}>
611 - The selected element is not within a Suspense container. Suspending it
612 - would cause an error.
613 - </div>
614 - );
615 - }
616 -}
packages/react-devtools-shared/src/devtools/views/Components/types.js
+7 -3
@@ -59,12 +59,16 @@ export type OwnersList = {|
59 export type InspectedElement = {|
60 id: number,
61
62 - // Does the current renderer support editable hooks?
62 + // Does the current renderer support editable hooks and function props?
63 canEditHooks: boolean,
64 -
65 - // Does the current renderer support editable function props?
64 canEditFunctionProps: boolean,
65
66 + // Does the current renderer support advanced editing interface?
67 + canEditHooksAndDeletePaths: boolean,
68 + canEditHooksAndRenamePaths: boolean,
69 + canEditFunctionPropsDeletePaths: boolean,
70 + canEditFunctionPropsRenamePaths: boolean,
71 +
72 // Is this Suspense, and can its value be overridden now?
73 canToggleSuspense: boolean,
74
packages/react-devtools-shared/src/devtools/views/Components/utils.js new
+28
@@ -0,0 +1,28 @@
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 +/**
11 + * Converts nested hooks paths to the format expected by the backend.
12 + * e.g. [''] => ['']
13 + * e.g. [1, 'value', ...] => [...]
14 + * e.g. [2, 'subhooks', 1, 'value', ...] => [...]
15 + * e.g. [1, 'subhooks', 3, 'subhooks', 2, 'value', ...] => [...]
16 + */
17 +export function parseHookPathForEdit(
18 + path: Array<string | number>,
19 +): Array<string | number> {
20 + let index = 0;
21 + for (let i = 0; i < path.length; i++) {
22 + if (path[i] === 'value') {
23 + index = i + 1;
24 + break;
25 + }
26 + }
27 + return path.slice(index);
28 +}
packages/react-devtools-shared/src/utils.js
+39
@@ -374,6 +374,45 @@ export function getInObject(object: Object, path: Array<string | number>): any {
374 }, object);
375 }
376
377 +export function deletePathInObject(
378 + object: Object,
379 + path: Array<string | number>,
380 +) {
381 + const length = path.length;
382 + const last = path[length - 1];
383 + if (object != null) {
384 + const parent = getInObject(object, path.slice(0, length - 1));
385 + if (parent) {
386 + if (Array.isArray(parent)) {
387 + parent.splice(((last: any): number), 1);
388 + } else {
389 + delete parent[last];
390 + }
391 + }
392 + }
393 +}
394 +
395 +export function renamePathInObject(
396 + object: Object,
397 + oldPath: Array<string | number>,
398 + newPath: Array<string | number>,
399 +) {
400 + const length = oldPath.length;
401 + if (object != null) {
402 + const parent = getInObject(object, oldPath.slice(0, length - 1));
403 + if (parent) {
404 + const lastOld = oldPath[length - 1];
405 + const lastNew = newPath[length - 1];
406 + parent[lastNew] = parent[lastOld];
407 + if (Array.isArray(parent)) {
408 + parent.splice(((lastOld: any): number), 1);
409 + } else {
410 + delete parent[lastOld];
411 + }
412 + }
413 + }
414 +}
415 +
416 export function setInObject(
417 object: Object,
418 path: Array<string | number>,
packages/react-reconciler/src/ReactFiberReconciler.new.js
+166 -14
@@ -466,24 +466,106 @@ export function shouldSuspend(fiber: Fiber): boolean {
466 }
467
468 let overrideHookState = null;
469 +let overrideHookStateDeletePath = null;
470 +let overrideHookStateRenamePath = null;
471 let overrideProps = null;
472 +let overridePropsDeletePath = null;
473 +let overridePropsRenamePath = null;
474 let scheduleUpdate = null;
475 let setSuspenseHandler = null;
476
477 if (__DEV__) {
478 + const copyWithDeleteImpl = (
479 + obj: Object | Array<any>,
480 + path: Array<string | number>,
481 + index: number,
482 + ) => {
483 + const key = path[index];
484 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
485 + if (index + 1 === path.length) {
486 + if (Array.isArray(updated)) {
487 + updated.splice(((key: any): number), 1);
488 + } else {
489 + delete updated[key];
490 + }
491 + return updated;
492 + }
493 + // $FlowFixMe number or string is fine here
494 + updated[key] = copyWithDeleteImpl(obj[key], path, index + 1);
495 + return updated;
496 + };
497 +
498 + const copyWithDelete = (
499 + obj: Object | Array<any>,
500 + path: Array<string | number>,
501 + ): Object | Array<any> => {
502 + return copyWithDeleteImpl(obj, path, 0);
503 + };
504 +
505 + const copyWithRenameImpl = (
506 + obj: Object | Array<any>,
507 + oldPath: Array<string | number>,
508 + newPath: Array<string | number>,
509 + index: number,
510 + ) => {
511 + const oldKey = oldPath[index];
512 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
513 + if (index + 1 === oldPath.length) {
514 + const newKey = newPath[index];
515 + // $FlowFixMe number or string is fine here
516 + updated[newKey] = updated[oldKey];
517 + if (Array.isArray(updated)) {
518 + updated.splice(((oldKey: any): number), 1);
519 + } else {
520 + delete updated[oldKey];
521 + }
522 + } else {
523 + // $FlowFixMe number or string is fine here
524 + updated[oldKey] = copyWithRenameImpl(
525 + // $FlowFixMe number or string is fine here
526 + obj[oldKey],
527 + oldPath,
528 + newPath,
529 + index + 1,
530 + );
531 + }
532 + return updated;
533 + };
534 +
535 + const copyWithRename = (
536 + obj: Object | Array<any>,
537 + oldPath: Array<string | number>,
538 + newPath: Array<string | number>,
539 + ): Object | Array<any> => {
540 + if (oldPath.length !== newPath.length) {
541 + console.warn('copyWithRename() expects paths of the same length');
542 + return;
543 + } else {
544 + for (let i = 0; i < newPath.length - 1; i++) {
545 + if (oldPath[i] !== newPath[i]) {
546 + console.warn(
547 + 'copyWithRename() expects paths to be the same except for the deepest key',
548 + );
549 + return;
550 + }
551 + }
552 + }
553 + return copyWithRenameImpl(obj, oldPath, newPath, 0);
554 + };
555 +
556 const copyWithSetImpl = (
557 obj: Object | Array<any>,
558 path: Array<string | number>,
477 - idx: number,
559 + index: number,
560 value: any,
561 ) => {
480 - if (idx >= path.length) {
562 + if (index >= path.length) {
563 return value;
564 }
483 - const key = path[idx];
565 + const key = path[index];
566 const updated = Array.isArray(obj) ? obj.slice() : {...obj};
567 // $FlowFixMe number or string is fine here
486 - updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
568 + updated[key] = copyWithSetImpl(obj[key], path, index + 1, value);
569 return updated;
570 };
571
@@ -495,6 +577,17 @@ if (__DEV__) {
577 return copyWithSetImpl(obj, path, 0, value);
578 };
579
580 + const findHook = (fiber: Fiber, id: number) => {
581 + // For now, the "id" of stateful hooks is just the stateful hook index.
582 + // This may change in the future with e.g. nested hooks.
583 + let currentHook = fiber.memoizedState;
584 + while (currentHook !== null && id > 0) {
585 + currentHook = currentHook.next;
586 + id--;
587 + }
588 + return currentHook;
589 + };
590 +
591 // Support DevTools editable values for useState and useReducer.
592 overrideHookState = (
593 fiber: Fiber,
@@ -502,17 +595,54 @@ if (__DEV__) {
595 path: Array<string | number>,
596 value: any,
597 ) => {
505 - // For now, the "id" of stateful hooks is just the stateful hook index.
506 - // This may change in the future with e.g. nested hooks.
507 - let currentHook = fiber.memoizedState;
508 - while (currentHook !== null && id > 0) {
509 - currentHook = currentHook.next;
510 - id--;
598 + const hook = findHook(fiber, id);
599 + if (hook !== null) {
600 + const newState = copyWithSet(hook.memoizedState, path, value);
601 + hook.memoizedState = newState;
602 + hook.baseState = newState;
603 +
604 + // We aren't actually adding an update to the queue,
605 + // because there is no update we can add for useReducer hooks that won't trigger an error.
606 + // (There's no appropriate action type for DevTools overrides.)
607 + // As a result though, React will see the scheduled update as a noop and bailout.
608 + // Shallow cloning props works as a workaround for now to bypass the bailout check.
609 + fiber.memoizedProps = {...fiber.memoizedProps};
610 +
611 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
612 }
512 - if (currentHook !== null) {
513 - const newState = copyWithSet(currentHook.memoizedState, path, value);
514 - currentHook.memoizedState = newState;
515 - currentHook.baseState = newState;
613 + };
614 + overrideHookStateDeletePath = (
615 + fiber: Fiber,
616 + id: number,
617 + path: Array<string | number>,
618 + ) => {
619 + const hook = findHook(fiber, id);
620 + if (hook !== null) {
621 + const newState = copyWithDelete(hook.memoizedState, path);
622 + hook.memoizedState = newState;
623 + hook.baseState = newState;
624 +
625 + // We aren't actually adding an update to the queue,
626 + // because there is no update we can add for useReducer hooks that won't trigger an error.
627 + // (There's no appropriate action type for DevTools overrides.)
628 + // As a result though, React will see the scheduled update as a noop and bailout.
629 + // Shallow cloning props works as a workaround for now to bypass the bailout check.
630 + fiber.memoizedProps = {...fiber.memoizedProps};
631 +
632 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
633 + }
634 + };
635 + overrideHookStateRenamePath = (
636 + fiber: Fiber,
637 + id: number,
638 + oldPath: Array<string | number>,
639 + newPath: Array<string | number>,
640 + ) => {
641 + const hook = findHook(fiber, id);
642 + if (hook !== null) {
643 + const newState = copyWithRename(hook.memoizedState, oldPath, newPath);
644 + hook.memoizedState = newState;
645 + hook.baseState = newState;
646
647 // We aren't actually adding an update to the queue,
648 // because there is no update we can add for useReducer hooks that won't trigger an error.
@@ -533,6 +663,24 @@ if (__DEV__) {
663 }
664 scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
665 };
666 + overridePropsDeletePath = (fiber: Fiber, path: Array<string | number>) => {
667 + fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path);
668 + if (fiber.alternate) {
669 + fiber.alternate.pendingProps = fiber.pendingProps;
670 + }
671 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
672 + };
673 + overridePropsRenamePath = (
674 + fiber: Fiber,
675 + oldPath: Array<string | number>,
676 + newPath: Array<string | number>,
677 + ) => {
678 + fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath);
679 + if (fiber.alternate) {
680 + fiber.alternate.pendingProps = fiber.pendingProps;
681 + }
682 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
683 + };
684
685 scheduleUpdate = (fiber: Fiber) => {
686 scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
@@ -571,7 +719,11 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
719 rendererPackageName: devToolsConfig.rendererPackageName,
720 rendererConfig: devToolsConfig.rendererConfig,
721 overrideHookState,
722 + overrideHookStateDeletePath,
723 + overrideHookStateRenamePath,
724 overrideProps,
725 + overridePropsDeletePath,
726 + overridePropsRenamePath,
727 setSuspenseHandler,
728 scheduleUpdate,
729 currentDispatcherRef: ReactCurrentDispatcher,
packages/react-reconciler/src/ReactFiberReconciler.old.js
+166 -14
@@ -466,24 +466,106 @@ export function shouldSuspend(fiber: Fiber): boolean {
466 }
467
468 let overrideHookState = null;
469 +let overrideHookStateDeletePath = null;
470 +let overrideHookStateRenamePath = null;
471 let overrideProps = null;
472 +let overridePropsDeletePath = null;
473 +let overridePropsRenamePath = null;
474 let scheduleUpdate = null;
475 let setSuspenseHandler = null;
476
477 if (__DEV__) {
478 + const copyWithDeleteImpl = (
479 + obj: Object | Array<any>,
480 + path: Array<string | number>,
481 + index: number,
482 + ) => {
483 + const key = path[index];
484 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
485 + if (index + 1 === path.length) {
486 + if (Array.isArray(updated)) {
487 + updated.splice(((key: any): number), 1);
488 + } else {
489 + delete updated[key];
490 + }
491 + return updated;
492 + }
493 + // $FlowFixMe number or string is fine here
494 + updated[key] = copyWithDeleteImpl(obj[key], path, index + 1);
495 + return updated;
496 + };
497 +
498 + const copyWithDelete = (
499 + obj: Object | Array<any>,
500 + path: Array<string | number>,
501 + ): Object | Array<any> => {
502 + return copyWithDeleteImpl(obj, path, 0);
503 + };
504 +
505 + const copyWithRenameImpl = (
506 + obj: Object | Array<any>,
507 + oldPath: Array<string | number>,
508 + newPath: Array<string | number>,
509 + index: number,
510 + ) => {
511 + const oldKey = oldPath[index];
512 + const updated = Array.isArray(obj) ? obj.slice() : {...obj};
513 + if (index + 1 === oldPath.length) {
514 + const newKey = newPath[index];
515 + // $FlowFixMe number or string is fine here
516 + updated[newKey] = updated[oldKey];
517 + if (Array.isArray(updated)) {
518 + updated.splice(((oldKey: any): number), 1);
519 + } else {
520 + delete updated[oldKey];
521 + }
522 + } else {
523 + // $FlowFixMe number or string is fine here
524 + updated[oldKey] = copyWithRenameImpl(
525 + // $FlowFixMe number or string is fine here
526 + obj[oldKey],
527 + oldPath,
528 + newPath,
529 + index + 1,
530 + );
531 + }
532 + return updated;
533 + };
534 +
535 + const copyWithRename = (
536 + obj: Object | Array<any>,
537 + oldPath: Array<string | number>,
538 + newPath: Array<string | number>,
539 + ): Object | Array<any> => {
540 + if (oldPath.length !== newPath.length) {
541 + console.warn('copyWithRename() expects paths of the same length');
542 + return;
543 + } else {
544 + for (let i = 0; i < newPath.length - 1; i++) {
545 + if (oldPath[i] !== newPath[i]) {
546 + console.warn(
547 + 'copyWithRename() expects paths to be the same except for the deepest key',
548 + );
549 + return;
550 + }
551 + }
552 + }
553 + return copyWithRenameImpl(obj, oldPath, newPath, 0);
554 + };
555 +
556 const copyWithSetImpl = (
557 obj: Object | Array<any>,
558 path: Array<string | number>,
477 - idx: number,
559 + index: number,
560 value: any,
561 ) => {
480 - if (idx >= path.length) {
562 + if (index >= path.length) {
563 return value;
564 }
483 - const key = path[idx];
565 + const key = path[index];
566 const updated = Array.isArray(obj) ? obj.slice() : {...obj};
567 // $FlowFixMe number or string is fine here
486 - updated[key] = copyWithSetImpl(obj[key], path, idx + 1, value);
568 + updated[key] = copyWithSetImpl(obj[key], path, index + 1, value);
569 return updated;
570 };
571
@@ -495,6 +577,17 @@ if (__DEV__) {
577 return copyWithSetImpl(obj, path, 0, value);
578 };
579
580 + const findHook = (fiber: Fiber, id: number) => {
581 + // For now, the "id" of stateful hooks is just the stateful hook index.
582 + // This may change in the future with e.g. nested hooks.
583 + let currentHook = fiber.memoizedState;
584 + while (currentHook !== null && id > 0) {
585 + currentHook = currentHook.next;
586 + id--;
587 + }
588 + return currentHook;
589 + };
590 +
591 // Support DevTools editable values for useState and useReducer.
592 overrideHookState = (
593 fiber: Fiber,
@@ -502,17 +595,54 @@ if (__DEV__) {
595 path: Array<string | number>,
596 value: any,
597 ) => {
505 - // For now, the "id" of stateful hooks is just the stateful hook index.
506 - // This may change in the future with e.g. nested hooks.
507 - let currentHook = fiber.memoizedState;
508 - while (currentHook !== null && id > 0) {
509 - currentHook = currentHook.next;
510 - id--;
598 + const hook = findHook(fiber, id);
599 + if (hook !== null) {
600 + const newState = copyWithSet(hook.memoizedState, path, value);
601 + hook.memoizedState = newState;
602 + hook.baseState = newState;
603 +
604 + // We aren't actually adding an update to the queue,
605 + // because there is no update we can add for useReducer hooks that won't trigger an error.
606 + // (There's no appropriate action type for DevTools overrides.)
607 + // As a result though, React will see the scheduled update as a noop and bailout.
608 + // Shallow cloning props works as a workaround for now to bypass the bailout check.
609 + fiber.memoizedProps = {...fiber.memoizedProps};
610 +
611 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
612 }
512 - if (currentHook !== null) {
513 - const newState = copyWithSet(currentHook.memoizedState, path, value);
514 - currentHook.memoizedState = newState;
515 - currentHook.baseState = newState;
613 + };
614 + overrideHookStateDeletePath = (
615 + fiber: Fiber,
616 + id: number,
617 + path: Array<string | number>,
618 + ) => {
619 + const hook = findHook(fiber, id);
620 + if (hook !== null) {
621 + const newState = copyWithDelete(hook.memoizedState, path);
622 + hook.memoizedState = newState;
623 + hook.baseState = newState;
624 +
625 + // We aren't actually adding an update to the queue,
626 + // because there is no update we can add for useReducer hooks that won't trigger an error.
627 + // (There's no appropriate action type for DevTools overrides.)
628 + // As a result though, React will see the scheduled update as a noop and bailout.
629 + // Shallow cloning props works as a workaround for now to bypass the bailout check.
630 + fiber.memoizedProps = {...fiber.memoizedProps};
631 +
632 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
633 + }
634 + };
635 + overrideHookStateRenamePath = (
636 + fiber: Fiber,
637 + id: number,
638 + oldPath: Array<string | number>,
639 + newPath: Array<string | number>,
640 + ) => {
641 + const hook = findHook(fiber, id);
642 + if (hook !== null) {
643 + const newState = copyWithRename(hook.memoizedState, oldPath, newPath);
644 + hook.memoizedState = newState;
645 + hook.baseState = newState;
646
647 // We aren't actually adding an update to the queue,
648 // because there is no update we can add for useReducer hooks that won't trigger an error.
@@ -533,6 +663,24 @@ if (__DEV__) {
663 }
664 scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
665 };
666 + overridePropsDeletePath = (fiber: Fiber, path: Array<string | number>) => {
667 + fiber.pendingProps = copyWithDelete(fiber.memoizedProps, path);
668 + if (fiber.alternate) {
669 + fiber.alternate.pendingProps = fiber.pendingProps;
670 + }
671 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
672 + };
673 + overridePropsRenamePath = (
674 + fiber: Fiber,
675 + oldPath: Array<string | number>,
676 + newPath: Array<string | number>,
677 + ) => {
678 + fiber.pendingProps = copyWithRename(fiber.memoizedProps, oldPath, newPath);
679 + if (fiber.alternate) {
680 + fiber.alternate.pendingProps = fiber.pendingProps;
681 + }
682 + scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
683 + };
684
685 scheduleUpdate = (fiber: Fiber) => {
686 scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp);
@@ -571,7 +719,11 @@ export function injectIntoDevTools(devToolsConfig: DevToolsConfig): boolean {
719 rendererPackageName: devToolsConfig.rendererPackageName,
720 rendererConfig: devToolsConfig.rendererConfig,
721 overrideHookState,
722 + overrideHookStateDeletePath,
723 + overrideHookStateRenamePath,
724 overrideProps,
725 + overridePropsDeletePath,
726 + overridePropsRenamePath,
727 setSuspenseHandler,
728 scheduleUpdate,
729 currentDispatcherRef: ReactCurrentDispatcher,
yarn.lock
+1 -1
@@ -8766,7 +8766,7 @@ json5@^2.1.0:
8766 dependencies:
8767 minimist "^1.2.0"
8768
8769 -json5@^2.1.2:
8769 +json5@^2.1.2, json5@^2.1.3:
8770 version "2.1.3"
8771 resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.3.tgz#c9b0f7fa9233bfe5807fe66fcf3a5617ed597d43"
8772 integrity sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==