mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fetch owners list from renderer (using suspense)
Owners in the list may have been filtered out of the Store, but in the owners list view- it's important to still show them. The frontend cannot do this on its own, so this list needs to come from the renderer interface.
This commit is contained in:
+17
-5
@@ -16,6 +16,7 @@ import type {
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { OwnersList } from 'src/devtools/views/Components/types';
|
||||
import type { Bridge, ComponentFilter } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
@@ -29,7 +30,7 @@ const debug = (methodName, ...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
type InspectSelectParams = {|
|
||||
type ElementAndRendererID = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
@@ -99,6 +100,7 @@ export default class Agent extends EventEmitter {
|
||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
|
||||
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
|
||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||
bridge.addListener('inspectElement', this.inspectElement);
|
||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||
bridge.addListener('overrideContext', this.overrideContext);
|
||||
@@ -303,7 +305,17 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
getOwnersList = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
const owners = renderer.getOwnersList(id);
|
||||
this._bridge.send('ownersList', ({ id, owners }: OwnersList));
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -312,7 +324,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
logElementToConsole = ({ id, rendererID }: InspectSelectParams) => {
|
||||
logElementToConsole = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -340,7 +352,7 @@ export default class Agent extends EventEmitter {
|
||||
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
|
||||
};
|
||||
|
||||
selectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
selectElement = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -506,7 +518,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
|
||||
viewElementSource = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
|
||||
+30
-16
@@ -50,7 +50,10 @@ import type {
|
||||
ReactRenderer,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { InspectedElement } from 'src/devtools/views/Components/types';
|
||||
import type {
|
||||
InspectedElement,
|
||||
Owner,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
|
||||
function getInternalReactConstants(version) {
|
||||
@@ -1685,6 +1688,30 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function getOwnersList(id: number): Array<Owner> | null {
|
||||
let fiber = findCurrentFiberUsingSlowPathById(id);
|
||||
if (fiber == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _debugOwner } = fiber;
|
||||
|
||||
let owners = null;
|
||||
if (_debugOwner) {
|
||||
owners = [];
|
||||
let owner = _debugOwner;
|
||||
while (owner !== null) {
|
||||
owners.push({
|
||||
displayName: getDisplayNameForFiber(owner) || 'Unknown',
|
||||
id: getFiberID(getPrimaryFiber(owner)),
|
||||
});
|
||||
owner = owner._debugOwner || null;
|
||||
}
|
||||
}
|
||||
|
||||
return owners;
|
||||
}
|
||||
|
||||
function inspectElementRaw(id: number): InspectedElement | null {
|
||||
let fiber = findCurrentFiberUsingSlowPathById(id);
|
||||
if (fiber == null) {
|
||||
@@ -1692,7 +1719,6 @@ export function attach(
|
||||
}
|
||||
|
||||
const {
|
||||
_debugOwner,
|
||||
_debugSource,
|
||||
stateNode,
|
||||
memoizedProps,
|
||||
@@ -1764,19 +1790,6 @@ export function attach(
|
||||
context = { value: context };
|
||||
}
|
||||
|
||||
let owners = null;
|
||||
if (_debugOwner) {
|
||||
owners = [];
|
||||
let owner = _debugOwner;
|
||||
while (owner !== null) {
|
||||
owners.push({
|
||||
displayName: getDisplayNameForFiber(owner) || 'Unknown',
|
||||
id: getFiberID(getPrimaryFiber(owner)),
|
||||
});
|
||||
owner = owner._debugOwner || null;
|
||||
}
|
||||
}
|
||||
|
||||
const isTimedOutSuspense =
|
||||
tag === SuspenseComponent && memoizedState !== null;
|
||||
|
||||
@@ -1812,7 +1825,7 @@ export function attach(
|
||||
state: usesHooks ? null : memoizedState,
|
||||
|
||||
// List of owners
|
||||
owners,
|
||||
owners: getOwnersList(id),
|
||||
|
||||
// Location of component in source coude.
|
||||
source: _debugSource,
|
||||
@@ -2385,6 +2398,7 @@ export function attach(
|
||||
getFiberCommits,
|
||||
getInteractions,
|
||||
findNativeByFiberID,
|
||||
getOwnersList,
|
||||
getPathForElement,
|
||||
getProfilingDataForDownload,
|
||||
getProfilingSummary,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
import type { InspectedElement } from 'src/devtools/views/Components/types';
|
||||
import type {
|
||||
InspectedElement,
|
||||
Owner,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
|
||||
type BundleType =
|
||||
| 0 // PROD
|
||||
@@ -175,6 +178,7 @@ export type RendererInterface = {
|
||||
) => number | null,
|
||||
getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
|
||||
getInteractions: (rootID: number) => InteractionsBackend,
|
||||
getOwnersList: (id: number) => Array<Owner> | null,
|
||||
getProfilingDataForDownload: (rootID: number) => Object,
|
||||
getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
|
||||
getPathForElement: (id: number) => Array<PathFrame> | null,
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Suspense } from 'react';
|
||||
import Tree from './Tree';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import { InspectedElementContextController } from './InspectedElementContext';
|
||||
import { OwnersListContextController } from './OwnersListContext';
|
||||
import portaledContent from '../portaledContent';
|
||||
import { ModalDialog } from '../ModalDialog';
|
||||
|
||||
@@ -12,19 +13,21 @@ import styles from './Components.css';
|
||||
function Components(_: {||}) {
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
<div className={styles.Components}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<InspectedElementContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</InspectedElementContextController>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
</div>
|
||||
<OwnersListContextController>
|
||||
<InspectedElementContextController>
|
||||
<div className={styles.Components}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
</div>
|
||||
</InspectedElementContextController>
|
||||
</OwnersListContextController>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ type Props = {
|
||||
|
||||
export default function ElementView({ data, index, style }: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerFlatTree, ownerStack, selectedElementID } = useContext(
|
||||
const { ownerFlatTree, ownerID, selectedElementID } = useContext(
|
||||
TreeStateContext
|
||||
);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
@@ -168,7 +168,7 @@ export default function ElementView({ data, index, style }: Props) {
|
||||
}}
|
||||
>
|
||||
<span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
|
||||
{ownerStack.length === 0 ? (
|
||||
{ownerID === null ? (
|
||||
<ExpandCollapseToggle element={element} store={store} />
|
||||
) : null}
|
||||
<span className={styles.Component}>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { createResource } from '../../cache';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { TreeStateContext } from './TreeContext';
|
||||
|
||||
import type {
|
||||
Element,
|
||||
Owner,
|
||||
OwnersList,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource, Thenable } from '../../cache';
|
||||
|
||||
type Context = (id: number) => Array<Owner> | null;
|
||||
|
||||
const OwnersListContext = createContext<Context>(((null: any): Context));
|
||||
OwnersListContext.displayName = 'OwnersListContext';
|
||||
|
||||
type ResolveFn = (ownersList: Array<Owner> | null) => void;
|
||||
type InProgressRequest = {|
|
||||
promise: Thenable<Array<Owner>>,
|
||||
resolveFn: ResolveFn,
|
||||
|};
|
||||
|
||||
const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
|
||||
const resource: Resource<Element, Element, Array<Owner>> = createResource(
|
||||
(element: Element) => {
|
||||
let request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
return request.promise;
|
||||
}
|
||||
|
||||
let resolveFn = ((null: any): ResolveFn);
|
||||
const promise = new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
});
|
||||
|
||||
inProgressRequests.set(element, { promise, resolveFn });
|
||||
|
||||
return promise;
|
||||
},
|
||||
(element: Element) => element,
|
||||
{ useWeakMap: true }
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
function OwnersListContextController({ children }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
|
||||
const read = useCallback(
|
||||
(id: number) => {
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
return resource.read(element);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[store]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onOwnersList = (ownersList: OwnersList) => {
|
||||
const id = ownersList.id;
|
||||
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(element);
|
||||
request.resolveFn(ownersList.owners);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('ownersList', onOwnersList);
|
||||
return () => bridge.removeListener('ownersList', onOwnersList);
|
||||
}, [bridge, store]);
|
||||
|
||||
// This effect requests an updated owners list any time the selected owner changes
|
||||
useEffect(() => {
|
||||
if (ownerID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(ownerID);
|
||||
|
||||
bridge.send('getOwnersList', { id: ownerID, rendererID });
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [bridge, ownerID, store]);
|
||||
|
||||
return (
|
||||
<OwnersListContext.Provider value={read}>
|
||||
{children}
|
||||
</OwnersListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { OwnersListContext, OwnersListContextController };
|
||||
@@ -91,3 +91,8 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.NotInStore,
|
||||
.NotInStore:hover {
|
||||
color: var(--color-dimmest);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -12,22 +13,111 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Toggle from '../Toggle';
|
||||
import { OwnersListContext } from './OwnersListContext';
|
||||
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
|
||||
import { StoreContext } from '../context';
|
||||
import { useIsOverflowing } from '../hooks';
|
||||
import { StoreContext } from '../context';
|
||||
|
||||
import type { Element } from './types';
|
||||
import type { Owner } from './types';
|
||||
|
||||
import styles from './OwnersStack.css';
|
||||
|
||||
type SelectOwner = (owner: Owner | null) => void;
|
||||
|
||||
type ACTION_UPDATE_OWNER_ID = {|
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID: number | null,
|
||||
owners: Array<Owner>,
|
||||
|};
|
||||
type ACTION_UPDATE_SELECTED_INDEX = {|
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: number,
|
||||
|};
|
||||
|
||||
type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
|
||||
|
||||
type State = {|
|
||||
ownerID: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
|};
|
||||
|
||||
function dialogReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_OWNER_ID':
|
||||
const selectedIndex = state.owners.findIndex(
|
||||
owner => owner.id === action.ownerID
|
||||
);
|
||||
return {
|
||||
ownerID: action.ownerID,
|
||||
owners: action.owners,
|
||||
selectedIndex,
|
||||
};
|
||||
case 'UPDATE_SELECTED_INDEX':
|
||||
return {
|
||||
...state,
|
||||
selectedIndex: action.selectedIndex,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Invalid action "${action.type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const read = useContext(OwnersListContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const [state, dispatch] = useReducer<State, Action>(dialogReducer, {
|
||||
ownerID: null,
|
||||
owners: [],
|
||||
selectedIndex: -1,
|
||||
});
|
||||
|
||||
// TODO (owners) Explain this and use reducer with ownerID too to avoid inf. loop
|
||||
if (ownerID === null) {
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID: null,
|
||||
owners: [],
|
||||
});
|
||||
} else if (ownerID !== state.ownerID) {
|
||||
const isInList = state.owners.findIndex(owner => owner.id === ownerID) >= 0;
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID,
|
||||
owners: isInList ? state.owners : read(ownerID) || [],
|
||||
});
|
||||
}
|
||||
|
||||
const { owners, selectedIndex } = state;
|
||||
|
||||
const selectOwner = useCallback<SelectOwner>(
|
||||
(owner: Owner | null) => {
|
||||
if (owner !== null) {
|
||||
const index = owners.indexOf(owner);
|
||||
dispatch({
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: index >= 0 ? index : 0,
|
||||
});
|
||||
treeDispatch({ type: 'SELECT_OWNER', payload: owner.id });
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: 0,
|
||||
});
|
||||
treeDispatch({ type: 'RESET_OWNER_STACK' });
|
||||
}
|
||||
},
|
||||
[owners, treeDispatch]
|
||||
);
|
||||
|
||||
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
|
||||
const elementsBarRef = useRef<HTMLDivElement | null>(null);
|
||||
const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
|
||||
|
||||
const selectedOwner = owners[selectedIndex];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// If we're already overflowing, then we don't need to re-measure items.
|
||||
// That's because once the owners stack is open, it can only get larger (by driling in).
|
||||
@@ -37,7 +127,7 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
let elementsTotalWidth = 0;
|
||||
for (let i = 0; i < ownerStack.length; i++) {
|
||||
for (let i = 0; i < owners.length; i++) {
|
||||
const element = elementsBarRef.current.children[i];
|
||||
const computedStyle = getComputedStyle(element);
|
||||
|
||||
@@ -48,7 +138,7 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
setElementsTotalWidth(elementsTotalWidth);
|
||||
}, [elementsBarRef, isOverflowing, ownerStack.length]);
|
||||
}, [elementsBarRef, isOverflowing, owners.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
@@ -56,28 +146,38 @@ export default function OwnerStack() {
|
||||
{isOverflowing && (
|
||||
<Fragment>
|
||||
<ElementsDropdown
|
||||
ownerStack={ownerStack}
|
||||
ownerStackIndex={ownerStackIndex}
|
||||
owners={owners}
|
||||
selectedIndex={selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
<BackToOwnerButton
|
||||
ownerStack={ownerStack}
|
||||
ownerStackIndex={ownerStackIndex}
|
||||
/>
|
||||
<ElementView
|
||||
id={ownerStack[((ownerStackIndex: any): number)]}
|
||||
index={ownerStackIndex}
|
||||
owners={owners}
|
||||
selectedIndex={selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
{selectedOwner != null && (
|
||||
<ElementView
|
||||
owner={selectedOwner}
|
||||
isSelected
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
{!isOverflowing &&
|
||||
ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
owners.map((owner, index) => (
|
||||
<ElementView
|
||||
key={index}
|
||||
owner={owner}
|
||||
isSelected={index === selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
|
||||
onClick={() => selectOwner(null)}
|
||||
title="Back to tree view"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
@@ -87,26 +187,28 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
type ElementsDropdownProps = {
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
selectOwner: SelectOwner,
|
||||
};
|
||||
function ElementsDropdown({
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
owners,
|
||||
selectedIndex,
|
||||
selectOwner,
|
||||
}: ElementsDropdownProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const menuItems = [];
|
||||
for (let index = ownerStack.length - 1; index >= 0; index--) {
|
||||
const id = ownerStack[index];
|
||||
for (let index = owners.length - 1; index >= 0; index--) {
|
||||
const owner = owners[index];
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={id}
|
||||
className={styles.Component}
|
||||
onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
|
||||
key={owner.id}
|
||||
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
|
||||
onSelect={() => (isInStore ? selectOwner(owner) : null)}
|
||||
>
|
||||
{((store.getElementByID(id): any): Element).displayName}
|
||||
{owner.displayName}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
@@ -126,28 +228,26 @@ function ElementsDropdown({
|
||||
}
|
||||
|
||||
type ElementViewProps = {
|
||||
id: number,
|
||||
index: number | null,
|
||||
isSelected: boolean,
|
||||
owner: Owner,
|
||||
selectOwner: SelectOwner,
|
||||
};
|
||||
function ElementView({ id, index }: ElementViewProps) {
|
||||
function ElementView({ isSelected, owner, selectOwner }: ElementViewProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerStackIndex } = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const { displayName } = ((store.getElementByID(id): any): Element);
|
||||
|
||||
const isChecked = ownerStackIndex === index;
|
||||
const { displayName } = owner;
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
dispatch({ type: 'SELECT_OWNER', payload: id });
|
||||
if (isInStore) {
|
||||
selectOwner(owner);
|
||||
}
|
||||
}, [dispatch, id, isChecked]);
|
||||
}, [isInStore, selectOwner, owner]);
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
className={styles.Component}
|
||||
isChecked={isChecked}
|
||||
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
|
||||
isChecked={isSelected}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{displayName}
|
||||
@@ -156,32 +256,32 @@ function ElementView({ id, index }: ElementViewProps) {
|
||||
}
|
||||
|
||||
type BackToOwnerButtonProps = {|
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
selectOwner: SelectOwner,
|
||||
|};
|
||||
function BackToOwnerButton({
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
owners,
|
||||
selectedIndex,
|
||||
selectOwner,
|
||||
}: BackToOwnerButtonProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
if (ownerStackIndex === null || ownerStackIndex === 0) {
|
||||
if (selectedIndex <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ownerID = ownerStack[ownerStackIndex - 1];
|
||||
const owner = store.getElementByID(ownerID);
|
||||
const owner = owners[selectedIndex - 1];
|
||||
if (owner == null) {
|
||||
debugger;
|
||||
}
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: 'SELECT_OWNER',
|
||||
payload: ownerID,
|
||||
})
|
||||
}
|
||||
title={`Up to ${(owner !== null && owner.displayName) || 'owner'}`}
|
||||
className={isInStore ? undefined : styles.NotInStore}
|
||||
onClick={() => (isInStore ? selectOwner(owner) : null)}
|
||||
title={`Up to ${owner.displayName || 'owner'}`}
|
||||
>
|
||||
<ButtonIcon type="previous" />
|
||||
</Button>
|
||||
|
||||
@@ -227,7 +227,7 @@ function InspectedElementView({
|
||||
state,
|
||||
} = inspectedElement;
|
||||
|
||||
const { ownerStack } = useContext(TreeStateContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
@@ -298,7 +298,7 @@ function InspectedElementView({
|
||||
overrideValueFn={overrideContextFn}
|
||||
/>
|
||||
|
||||
{ownerStack.length === 0 && owners !== null && owners.length > 0 && (
|
||||
{ownerID === null && owners !== null && owners.length > 0 && (
|
||||
<div className={styles.Owners}>
|
||||
<div className={styles.OwnersHeader}>rendered by</div>
|
||||
{owners.map(owner => (
|
||||
|
||||
@@ -37,3 +37,14 @@
|
||||
margin: 0 0.5rem;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.Loading {
|
||||
height: 100%;
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
font-size: var(--font-size-sans-large);
|
||||
color: var(--color-dim);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
Suspense,
|
||||
useState,
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -38,7 +39,7 @@ export default function Tree(props: Props) {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {
|
||||
numElements,
|
||||
ownerStack,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
selectedElementID,
|
||||
@@ -277,7 +278,9 @@ export default function Tree(props: Props) {
|
||||
<div className={styles.SearchInput}>
|
||||
<InspectHostNodesToggle />
|
||||
<div className={styles.VRule} />
|
||||
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<Suspense fallback={<Loading />}>
|
||||
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
|
||||
</Suspense>
|
||||
<div className={styles.VRule} />
|
||||
<ToggleComponentFiltersModalButton />
|
||||
</div>
|
||||
@@ -317,7 +320,7 @@ export default function Tree(props: Props) {
|
||||
}
|
||||
|
||||
function InnerElementType({ style, ...rest }) {
|
||||
const { ownerStack } = useContext(TreeStateContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
|
||||
// The list may need to scroll horizontally due to deeply nested elements.
|
||||
// We don't know the maximum scroll width up front, because we're windowing.
|
||||
@@ -346,10 +349,9 @@ function InnerElementType({ style, ...rest }) {
|
||||
|
||||
// We shouldn't retain this width across different conceptual trees though,
|
||||
// so when the user opens the "owners tree" view, we should discard the previous width.
|
||||
const hasOwnerStack = ownerStack.length > 0;
|
||||
const [prevHasOwnerStack, setPrevHasOwnerStack] = useState(hasOwnerStack);
|
||||
if (hasOwnerStack !== prevHasOwnerStack) {
|
||||
setPrevHasOwnerStack(hasOwnerStack);
|
||||
const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
|
||||
if (ownerID !== prevOwnerID) {
|
||||
setPrevOwnerID(ownerID);
|
||||
setMinWidth(null);
|
||||
}
|
||||
|
||||
@@ -371,3 +373,7 @@ function InnerElementType({ style, ...rest }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div className={styles.Loading}>Loading...</div>;
|
||||
}
|
||||
|
||||
@@ -50,9 +50,8 @@ type StateContext = {|
|
||||
searchText: string,
|
||||
|
||||
// Owners
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
@@ -142,8 +141,7 @@ type State = {|
|
||||
searchText: string,
|
||||
|
||||
// Owners
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Inspection element panel
|
||||
@@ -151,17 +149,12 @@ type State = {|
|
||||
|};
|
||||
|
||||
function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
numElements,
|
||||
ownerStack,
|
||||
selectedElementIndex,
|
||||
selectedElementID,
|
||||
} = state;
|
||||
let { numElements, ownerID, selectedElementIndex, selectedElementID } = state;
|
||||
|
||||
let lookupIDForIndex = true;
|
||||
|
||||
// Base tree should ignore selected element changes when the owner's tree is active.
|
||||
if (ownerStack.length === 0) {
|
||||
if (ownerID === null) {
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
numElements = store.numElements;
|
||||
@@ -276,7 +269,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
|
||||
function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
ownerStack,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
@@ -295,7 +288,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
let didRequestSearch = false;
|
||||
|
||||
// Search isn't supported when the owner's tree is active.
|
||||
if (ownerStack.length === 0) {
|
||||
if (ownerID === null) {
|
||||
switch (action.type) {
|
||||
case 'GO_TO_NEXT_SEARCH_RESULT':
|
||||
if (numPrevSearchResults > 0) {
|
||||
@@ -442,9 +435,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
numElements,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
@@ -454,25 +446,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
if (ownerStack.length > 0) {
|
||||
let indexOfRemovedItem = -1;
|
||||
for (let i = 0; i < ownerStack.length; i++) {
|
||||
if (store.getElementByID(ownerStack[i]) === null) {
|
||||
indexOfRemovedItem = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (indexOfRemovedItem >= 0) {
|
||||
ownerStack = ownerStack.slice(0, indexOfRemovedItem);
|
||||
if (ownerStack.length === 0) {
|
||||
ownerFlatTree = null;
|
||||
ownerStackIndex = null;
|
||||
} else {
|
||||
ownerStackIndex = ownerStack.length - 1;
|
||||
}
|
||||
}
|
||||
if (selectedElementID !== null && ownerFlatTree !== null) {
|
||||
if (ownerID !== null) {
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
if (selectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === selectedElementID
|
||||
@@ -491,13 +467,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
break;
|
||||
case 'RESET_OWNER_STACK':
|
||||
ownerStack = [];
|
||||
ownerStackIndex = null;
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementIndex =
|
||||
selectedElementID !== null
|
||||
? store.getIndexOfElementID(selectedElementID)
|
||||
: null;
|
||||
ownerFlatTree = null;
|
||||
break;
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
if (ownerFlatTree !== null) {
|
||||
@@ -533,33 +508,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
// If the Store doesn't have any owners metadata, don't drill into an empty stack.
|
||||
// This is a confusing user experience.
|
||||
if (store.hasOwnerMetadata) {
|
||||
const id = (action: ACTION_SELECT_OWNER).payload;
|
||||
ownerStackIndex = ownerStack.indexOf(id);
|
||||
ownerID = (action: ACTION_SELECT_OWNER).payload;
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
|
||||
// Always force reset selection to be the top of the new owner tree.
|
||||
selectedElementIndex = 0;
|
||||
prevSelectedElementIndex = null;
|
||||
|
||||
// If this owner is already in the current stack, just select it.
|
||||
// Otherwise, create a new stack.
|
||||
if (ownerStackIndex < 0) {
|
||||
// Add this new owner, and fill in the owners above it as well.
|
||||
ownerStack = [];
|
||||
let currentOwnerID = id;
|
||||
while (currentOwnerID !== 0) {
|
||||
ownerStack.unshift(currentOwnerID);
|
||||
currentOwnerID = ((store.getElementByID(
|
||||
currentOwnerID
|
||||
): any): Element).ownerID;
|
||||
}
|
||||
ownerStackIndex = ownerStack.length - 1;
|
||||
|
||||
if (searchText !== '') {
|
||||
searchIndex = null;
|
||||
searchResults = [];
|
||||
searchText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -569,17 +523,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
|
||||
// Changes in the selected owner require re-calculating the owners tree.
|
||||
if (
|
||||
ownerStackIndex !== state.ownerStackIndex ||
|
||||
ownerStack !== state.ownerStack ||
|
||||
ownerFlatTree !== state.ownerFlatTree ||
|
||||
action.type === 'HANDLE_STORE_MUTATION'
|
||||
) {
|
||||
if (ownerStackIndex === null) {
|
||||
ownerFlatTree = null;
|
||||
if (ownerFlatTree === null) {
|
||||
numElements = store.numElements;
|
||||
} else {
|
||||
ownerFlatTree = store.getOwnersListForElement(
|
||||
ownerStack[ownerStackIndex]
|
||||
);
|
||||
numElements = ownerFlatTree.length;
|
||||
}
|
||||
}
|
||||
@@ -588,9 +537,10 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
if (selectedElementIndex !== prevSelectedElementIndex) {
|
||||
if (selectedElementIndex === null) {
|
||||
selectedElementID = null;
|
||||
} else if (ownerFlatTree !== null) {
|
||||
selectedElementID =
|
||||
ownerFlatTree[((selectedElementIndex: any): number)].id;
|
||||
} else {
|
||||
if (ownerFlatTree !== null) {
|
||||
selectedElementID = ownerFlatTree[selectedElementIndex].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,8 +555,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
searchResults,
|
||||
searchText,
|
||||
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
};
|
||||
}
|
||||
@@ -619,14 +568,19 @@ function reduceSuspenseState(
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
if (state.inspectedElementID !== state.selectedElementID) {
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
break;
|
||||
}
|
||||
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
}
|
||||
|
||||
type Props = {| children: React$Node |};
|
||||
@@ -696,8 +650,7 @@ function TreeContextController({ children }: Props) {
|
||||
searchText: '',
|
||||
|
||||
// Owners
|
||||
ownerStack: [],
|
||||
ownerStackIndex: null,
|
||||
ownerID: null,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Inspection element panel
|
||||
|
||||
@@ -35,6 +35,11 @@ export type Owner = {|
|
||||
id: number,
|
||||
|};
|
||||
|
||||
export type OwnersList = {|
|
||||
id: number,
|
||||
owners: Array<Owner> | null,
|
||||
|};
|
||||
|
||||
export type InspectedElement = {|
|
||||
id: number,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user