mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge pull request #264 from bvaughn/filter-owners-list
Fetch owners list from renderer (using suspense)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element that includes filtered components: mount 1`] = `
|
||||
[root]
|
||||
▾ <Grandparent>
|
||||
<Child>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element that includes filtered components: owners for "Child" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 7,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 9,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Child",
|
||||
"id": 8,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: mount 1`] = `
|
||||
[root]
|
||||
▾ <Grandparent>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: owners for "Child" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 3,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: owners for "Parent" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should include the current element even if there are no other owners: mount 1`] = `
|
||||
[root]
|
||||
<Grandparent>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should include the current element even if there are no other owners: owners for "Grandparent" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 5,
|
||||
},
|
||||
]
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type { Element } from 'src/devtools/views/Components/types';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('OwnersListContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let OwnersListContext;
|
||||
let OwnersListContextController;
|
||||
let StoreContext;
|
||||
let TreeContextController;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
OwnersListContext = require('src/devtools/views/Components/OwnersListContext')
|
||||
.OwnersListContext;
|
||||
OwnersListContextController = require('src/devtools/views/Components/OwnersListContext')
|
||||
.OwnersListContextController;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContextController = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeContextController;
|
||||
});
|
||||
|
||||
const Contexts = ({ children, defaultOwnerID = null }) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController defaultOwnerID={defaultOwnerID}>
|
||||
<OwnersListContextController>{children}</OwnersListContextController>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
|
||||
it('should fetch the owners list for the selected element', async done => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const parent = ((store.getElementAtIndex(1): any): Element);
|
||||
const firstChild = ((store.getElementAtIndex(2): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actSuspense(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={parent.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={parent} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
didFinish = false;
|
||||
await utils.actSuspense(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={firstChild.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={firstChild} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fetch the owners list for the selected element that includes filtered components', async done => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const firstChild = ((store.getElementAtIndex(1): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actSuspense(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={firstChild.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={firstChild} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should include the current element even if there are no other owners', async done => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const grandparent = ((store.getElementAtIndex(0): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actSuspense(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={grandparent.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={grandparent} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -6,42 +6,7 @@ describe('Store component filters', () => {
|
||||
let TestUtils;
|
||||
let Types;
|
||||
let store;
|
||||
|
||||
const createElementTypeFilter = (elementType, isEnabled = true) => ({
|
||||
type: Types.ComponentFilterElementType,
|
||||
isEnabled,
|
||||
value: elementType,
|
||||
});
|
||||
|
||||
const createDisplayNameFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterDisplayName,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
|
||||
const createLocationFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterLocation,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
let utils;
|
||||
|
||||
const act = (callback: Function) => {
|
||||
TestUtils.act(() => {
|
||||
@@ -59,6 +24,7 @@ describe('Store component filters', () => {
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Types = require('src/types');
|
||||
utils = require('./utils');
|
||||
});
|
||||
|
||||
it('should throw if filters are updated while profiling', () => {
|
||||
@@ -89,7 +55,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
utils.createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -98,7 +64,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -107,8 +73,8 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
createElementTypeFilter(Types.ElementTypeFunction),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass),
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -117,8 +83,8 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass, false),
|
||||
createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass, false),
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -134,7 +100,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeRoot),
|
||||
utils.createElementTypeFilter(Types.ElementTypeRoot),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -159,13 +125,17 @@ describe('Store component filters', () => {
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Foo')]));
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('Foo')])
|
||||
);
|
||||
expect(store).toMatchSnapshot('2: filter "Foo"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Ba')]));
|
||||
act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
|
||||
expect(store).toMatchSnapshot('3: filter "Ba"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('B.z')]));
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('B.z')])
|
||||
);
|
||||
expect(store).toMatchSnapshot('4: filter "B.z"');
|
||||
});
|
||||
|
||||
@@ -178,7 +148,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter(__filename.replace(__dirname, '')),
|
||||
utils.createLocationFilter(__filename.replace(__dirname, '')),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -189,7 +159,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter('this:is:a:made:up:path'),
|
||||
utils.createLocationFilter('this:is:a:made:up:path'),
|
||||
])
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
import type {
|
||||
DispatcherContext,
|
||||
StateContext,
|
||||
} from 'src/devtools/views/Components/TreeContext';
|
||||
|
||||
describe('TreeListContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let StoreContext;
|
||||
let TreeContext;
|
||||
|
||||
let dispatch: DispatcherContext;
|
||||
let state: StateContext;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContext = require('src/devtools/views/Components/TreeContext');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset between tests
|
||||
dispatch = ((null: any): DispatcherContext);
|
||||
state = ((null: any): StateContext);
|
||||
});
|
||||
|
||||
const Capture = () => {
|
||||
dispatch = React.useContext(TreeContext.TreeDispatcherContext);
|
||||
state = React.useContext(TreeContext.TreeStateContext);
|
||||
return null;
|
||||
};
|
||||
|
||||
const Contexts = () => {
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContext.TreeContextController>
|
||||
<Capture />
|
||||
</TreeContext.TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('tree state', () => {
|
||||
it('should select the next and previous elements in the tree', () => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select first element');
|
||||
|
||||
while (
|
||||
state.selectedElementIndex !== null &&
|
||||
state.selectedElementIndex < store.numElements - 1
|
||||
) {
|
||||
const index = ((state.selectedElementIndex: any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot(`3: select element after (${index})`);
|
||||
}
|
||||
|
||||
while (
|
||||
state.selectedElementIndex !== null &&
|
||||
state.selectedElementIndex > 0
|
||||
) {
|
||||
const index = ((state.selectedElementIndex: any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot(`4: select element before (${index})`);
|
||||
}
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('5: select previous wraps around to last');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('6: select next wraps around to first');
|
||||
});
|
||||
|
||||
it('should select child elements', () => {
|
||||
const Grandparent = () => (
|
||||
<React.Fragment>
|
||||
<Parent />
|
||||
<Parent />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select first element');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: select Parent');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: select Child');
|
||||
|
||||
const previousState = state;
|
||||
|
||||
// There are no more children to select, so this should be a no-op
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toEqual(previousState);
|
||||
});
|
||||
|
||||
it('should select parent elements and then collapse', () => {
|
||||
const Grandparent = () => (
|
||||
<React.Fragment>
|
||||
<Parent />
|
||||
<Parent />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select last child');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: select Parent');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: select Grandparent');
|
||||
|
||||
const previousState = state;
|
||||
|
||||
// There are no more ancestors to select, so this should be a no-op
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toEqual(previousState);
|
||||
});
|
||||
|
||||
it('should clear selection if the selected element is unmounted', async done => {
|
||||
const Grandparent = props => props.children || null;
|
||||
const Parent = props => props.children || null;
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Grandparent>
|
||||
<Parent>
|
||||
<Child />
|
||||
<Child />
|
||||
</Parent>
|
||||
</Grandparent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 3 })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select second child');
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
ReactDOM.render(
|
||||
<Grandparent>
|
||||
<Parent />
|
||||
</Grandparent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
expect(state).toMatchSnapshot(
|
||||
'3: remove children (parent should now be selected)'
|
||||
);
|
||||
|
||||
await utils.actSuspense(() => ReactDOM.unmountComponentAtNode(container));
|
||||
expect(state).toMatchSnapshot(
|
||||
'4: unmount root (nothing should be selected)'
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search state', () => {
|
||||
it('should find elements matching search text', () => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'f' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: search for "f"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'q' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: search for "q"');
|
||||
});
|
||||
|
||||
it('should select the next and previous items within the search results', () => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Baz />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: go to second result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: go to third result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('5: go to second result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('6: go to first result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('7: wrap to last result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('8: wrap to first result');
|
||||
});
|
||||
|
||||
it('should add newly mounted elements to the search results set if they match the current text', async done => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: mount Baz');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should remove unmounted elements from the search results set', async done => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: go to second result');
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: unmount Baz');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('owners state', () => {
|
||||
it('should support entering and existing the owners tree view', () => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||||
|
||||
utils.act(() => dispatch({ type: 'RESET_OWNER_STACK' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: final state');
|
||||
});
|
||||
|
||||
it('should remove an element from the owners list if it is unmounted', async done => {
|
||||
const Grandparent = ({ count }) => <Parent count={count} />;
|
||||
const Parent = ({ count }) =>
|
||||
new Array(count).fill(true).map((_, index) => <Child key={index} />);
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Grandparent count={2} />, container));
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
ReactDOM.render(<Grandparent count={1} />, container)
|
||||
);
|
||||
expect(state).toMatchSnapshot('3: remove second child');
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
ReactDOM.render(<Grandparent count={0} />, container)
|
||||
);
|
||||
expect(state).toMatchSnapshot('4: remove first child');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should exit the owners list if the current owner is unmounted', async done => {
|
||||
const Parent = props => props.children || null;
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Parent>
|
||||
<Child />
|
||||
</Parent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let childID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: childID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: child owners tree');
|
||||
|
||||
await utils.actSuspense(() => ReactDOM.render(<Parent />, container));
|
||||
expect(state).toMatchSnapshot('3: remove child');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(0): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: parent owners tree');
|
||||
|
||||
await utils.actSuspense(() => ReactDOM.unmountComponentAtNode(container));
|
||||
expect(state).toMatchSnapshot('5: unmount root');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
export function act(callback: Function): void {
|
||||
const TestUtils = require('react-dom/test-utils');
|
||||
TestUtils.act(() => {
|
||||
@@ -54,6 +56,56 @@ export function beforeEachProfiling(): void {
|
||||
);
|
||||
}
|
||||
|
||||
export function createElementTypeFilter(
|
||||
elementType: ElementType,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
return {
|
||||
type: Types.ComponentFilterElementType,
|
||||
isEnabled,
|
||||
value: elementType,
|
||||
};
|
||||
}
|
||||
|
||||
export function createDisplayNameFilter(
|
||||
source: string,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterDisplayName,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
}
|
||||
|
||||
export function createLocationFilter(
|
||||
source: string,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterLocation,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRendererID(): number {
|
||||
if (global.agent == null) {
|
||||
throw Error('Agent unavailable.');
|
||||
|
||||
+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}"`);
|
||||
|
||||
+34
-1
@@ -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,35 @@ export function attach(
|
||||
}
|
||||
}
|
||||
|
||||
function getOwnersList(id: number): Array<Owner> | null {
|
||||
let fiber = findCurrentFiberUsingSlowPathById(id);
|
||||
if (fiber == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { _debugOwner } = fiber;
|
||||
|
||||
const owners = [
|
||||
{
|
||||
displayName: getDisplayNameForFiber(fiber) || 'Unknown',
|
||||
id,
|
||||
},
|
||||
];
|
||||
|
||||
if (_debugOwner) {
|
||||
let owner = _debugOwner;
|
||||
while (owner !== null) {
|
||||
owners.unshift({
|
||||
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) {
|
||||
@@ -2385,6 +2417,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,113 @@ 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 = action.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: 0,
|
||||
});
|
||||
|
||||
// When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners.
|
||||
// We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed.
|
||||
if (ownerID === null) {
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID: null,
|
||||
owners: [],
|
||||
});
|
||||
} else if (ownerID !== state.ownerID) {
|
||||
const isInStore =
|
||||
state.owners.findIndex(owner => owner.id === ownerID) >= 0;
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID,
|
||||
owners: isInStore ? 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 +129,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 +140,7 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
setElementsTotalWidth(elementsTotalWidth);
|
||||
}, [elementsBarRef, isOverflowing, ownerStack.length]);
|
||||
}, [elementsBarRef, isOverflowing, owners.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
@@ -56,28 +148,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 +189,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 +230,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 +258,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,13 +298,13 @@ 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 => (
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
displayName={owner.displayName}
|
||||
displayName={owner.displayName || 'Unknown'}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
/>
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import Store from '../../store';
|
||||
|
||||
import type { Element } from './types';
|
||||
|
||||
type StateContext = {|
|
||||
export type StateContext = {|
|
||||
// Tree
|
||||
numElements: number,
|
||||
selectedElementID: number | null,
|
||||
@@ -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,
|
||||
@@ -118,7 +117,7 @@ type Action =
|
||||
| ACTION_SET_SEARCH_TEXT
|
||||
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
|
||||
|
||||
type DispatcherContext = (action: Action) => void;
|
||||
export type DispatcherContext = (action: Action) => void;
|
||||
|
||||
const TreeStateContext = createContext<StateContext>(
|
||||
((null: any): StateContext)
|
||||
@@ -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,30 +446,20 @@ 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 (ownerID !== null) {
|
||||
if (!store.containsElement(ownerID)) {
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementID = null;
|
||||
} else {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === selectedElementID
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (selectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
@@ -491,13 +473,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 +514,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 +529,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 +543,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 +561,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
searchResults,
|
||||
searchText,
|
||||
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
};
|
||||
}
|
||||
@@ -619,20 +574,30 @@ 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 |};
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|
||||
// Used for automated testing
|
||||
defaultOwnerID?: ?number,
|
||||
|};
|
||||
|
||||
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
|
||||
function TreeContextController({ children }: Props) {
|
||||
function TreeContextController({ children, defaultOwnerID }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
@@ -696,8 +661,7 @@ function TreeContextController({ children }: Props) {
|
||||
searchText: '',
|
||||
|
||||
// Owners
|
||||
ownerStack: [],
|
||||
ownerStackIndex: null,
|
||||
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Inspection element panel
|
||||
|
||||
@@ -31,10 +31,15 @@ export type Element = {|
|
||||
|};
|
||||
|
||||
export type Owner = {|
|
||||
displayName: string,
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
|};
|
||||
|
||||
export type OwnersList = {|
|
||||
id: number,
|
||||
owners: Array<Owner> | null,
|
||||
|};
|
||||
|
||||
export type InspectedElement = {|
|
||||
id: number,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user