2019-08-27 10:54:01 -07:00
|
|
|
/**
|
2022-10-18 11:19:24 -04:00
|
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
2019-08-27 10:54:01 -07:00
|
|
|
*
|
|
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
|
|
|
|
*
|
|
|
|
|
* @flow
|
|
|
|
|
*/
|
2019-02-08 14:44:07 -05:00
|
|
|
|
|
|
|
|
// This context combines tree/selection state, search, and the owners stack.
|
|
|
|
|
// These values are managed together because changes in one often impact the others.
|
|
|
|
|
// Combining them enables us to avoid cascading renders.
|
|
|
|
|
//
|
|
|
|
|
// Changes to search state may impact tree state.
|
|
|
|
|
// For example, updating the selected search result also updates the tree's selected value.
|
2020-06-15 19:59:44 -04:00
|
|
|
// Search does not fundamentally change the tree though.
|
2019-02-08 14:44:07 -05:00
|
|
|
// It is also possible to update the selected tree value independently.
|
|
|
|
|
//
|
|
|
|
|
// Changes to owners state mask search and tree values.
|
2020-06-15 19:59:44 -04:00
|
|
|
// When owners stack is not empty, search is temporarily disabled,
|
2019-02-08 14:44:07 -05:00
|
|
|
// and tree values (e.g. num elements, selected element) are masked.
|
|
|
|
|
// Both tree and search values are restored when the owners stack is cleared.
|
|
|
|
|
//
|
|
|
|
|
// For this reason, changes to the tree context are processed in sequence: tree -> search -> owners
|
|
|
|
|
// This enables each section to potentially override (or mask) previous values.
|
|
|
|
|
|
2022-09-15 16:45:29 -04:00
|
|
|
import type {ReactContext} from 'shared/ReactTypes';
|
|
|
|
|
|
2020-02-21 19:45:20 -08:00
|
|
|
import * as React from 'react';
|
|
|
|
|
import {
|
2019-02-08 14:44:07 -05:00
|
|
|
createContext,
|
|
|
|
|
useCallback,
|
|
|
|
|
useContext,
|
2019-02-08 15:37:52 -05:00
|
|
|
useEffect,
|
2019-04-10 08:57:42 -07:00
|
|
|
useLayoutEffect,
|
2019-02-08 14:44:07 -05:00
|
|
|
useMemo,
|
|
|
|
|
useReducer,
|
2019-04-09 18:11:40 -07:00
|
|
|
useRef,
|
2021-05-12 11:28:14 -04:00
|
|
|
startTransition,
|
2019-02-08 14:44:07 -05:00
|
|
|
} from 'react';
|
2019-08-13 17:58:03 -07:00
|
|
|
import {createRegExp} from '../utils';
|
|
|
|
|
import {BridgeContext, StoreContext} from '../context';
|
2019-03-06 14:01:52 -08:00
|
|
|
import Store from '../../store';
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2023-10-10 18:10:17 +01:00
|
|
|
import type {Element} from 'react-devtools-shared/src/frontend/types';
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2022-09-09 16:03:48 -04:00
|
|
|
export type StateContext = {
|
2019-02-08 14:44:07 -05:00
|
|
|
// Tree
|
|
|
|
|
numElements: number,
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID: number | null,
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementID: number | null,
|
|
|
|
|
selectedElementIndex: number | null,
|
|
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
searchIndex: number | null,
|
|
|
|
|
searchResults: Array<number>,
|
|
|
|
|
searchText: string,
|
|
|
|
|
|
|
|
|
|
// Owners
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID: number | null,
|
2019-04-24 15:19:13 -07:00
|
|
|
ownerFlatTree: Array<Element> | null,
|
2019-04-21 12:16:42 -07:00
|
|
|
|
|
|
|
|
// Inspection element panel
|
|
|
|
|
inspectedElementID: number | null,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2022-09-09 16:03:48 -04:00
|
|
|
type ACTION_GO_TO_NEXT_SEARCH_RESULT = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'GO_TO_NEXT_SEARCH_RESULT',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_GO_TO_PREVIOUS_SEARCH_RESULT = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'GO_TO_PREVIOUS_SEARCH_RESULT',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_HANDLE_STORE_MUTATION = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'HANDLE_STORE_MUTATION',
|
2019-04-24 16:29:23 +01:00
|
|
|
payload: [Array<number>, Map<number, number>],
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_RESET_OWNER_STACK = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'RESET_OWNER_STACK',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_CHILD_ELEMENT_IN_TREE = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_CHILD_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_ELEMENT_AT_INDEX = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_ELEMENT_AT_INDEX',
|
|
|
|
|
payload: number | null,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_ELEMENT_BY_ID = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_ELEMENT_BY_ID',
|
|
|
|
|
payload: number | null,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_NEXT_ELEMENT_IN_TREE = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_NEXT_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {
|
2020-12-22 17:09:29 +01:00
|
|
|
type: 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_NEXT_SIBLING_IN_TREE = {
|
2020-09-01 20:03:44 -04:00
|
|
|
type: 'SELECT_NEXT_SIBLING_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_OWNER = {
|
2020-09-01 20:03:44 -04:00
|
|
|
type: 'SELECT_OWNER',
|
|
|
|
|
payload: number,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_PARENT_ELEMENT_IN_TREE = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_PARENT_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE = {
|
2020-12-22 17:09:29 +01:00
|
|
|
type: 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE = {
|
2020-09-01 20:03:44 -04:00
|
|
|
type: 'SELECT_PREVIOUS_SIBLING_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE = {
|
2020-09-01 20:03:44 -04:00
|
|
|
type: 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE = {
|
2020-09-01 20:03:44 -04:00
|
|
|
type: 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_SET_SEARCH_TEXT = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'SET_SEARCH_TEXT',
|
|
|
|
|
payload: string,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
|
|
|
|
type ACTION_UPDATE_INSPECTED_ELEMENT_ID = {
|
2019-04-22 10:19:59 -07:00
|
|
|
type: 'UPDATE_INSPECTED_ELEMENT_ID',
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
2019-04-22 10:19:59 -07:00
|
|
|
|
|
|
|
|
type Action =
|
|
|
|
|
| ACTION_GO_TO_NEXT_SEARCH_RESULT
|
|
|
|
|
| ACTION_GO_TO_PREVIOUS_SEARCH_RESULT
|
|
|
|
|
| ACTION_HANDLE_STORE_MUTATION
|
|
|
|
|
| ACTION_RESET_OWNER_STACK
|
|
|
|
|
| ACTION_SELECT_CHILD_ELEMENT_IN_TREE
|
|
|
|
|
| ACTION_SELECT_ELEMENT_AT_INDEX
|
|
|
|
|
| ACTION_SELECT_ELEMENT_BY_ID
|
|
|
|
|
| ACTION_SELECT_NEXT_ELEMENT_IN_TREE
|
2020-12-22 17:09:29 +01:00
|
|
|
| ACTION_SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
|
2020-09-01 20:03:44 -04:00
|
|
|
| ACTION_SELECT_NEXT_SIBLING_IN_TREE
|
|
|
|
|
| ACTION_SELECT_OWNER
|
2019-04-22 10:19:59 -07:00
|
|
|
| ACTION_SELECT_PARENT_ELEMENT_IN_TREE
|
|
|
|
|
| ACTION_SELECT_PREVIOUS_ELEMENT_IN_TREE
|
2020-12-22 17:09:29 +01:00
|
|
|
| ACTION_SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE
|
2020-09-01 20:03:44 -04:00
|
|
|
| ACTION_SELECT_PREVIOUS_SIBLING_IN_TREE
|
|
|
|
|
| ACTION_SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE
|
|
|
|
|
| ACTION_SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE
|
2019-04-22 10:19:59 -07:00
|
|
|
| ACTION_SET_SEARCH_TEXT
|
|
|
|
|
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
|
|
|
|
|
|
2019-05-09 15:52:50 -07:00
|
|
|
export type DispatcherContext = (action: Action) => void;
|
2019-04-22 10:19:59 -07:00
|
|
|
|
2023-01-31 08:25:05 -05:00
|
|
|
const TreeStateContext: ReactContext<StateContext> =
|
|
|
|
|
createContext<StateContext>(((null: any): StateContext));
|
2019-04-22 10:19:59 -07:00
|
|
|
TreeStateContext.displayName = 'TreeStateContext';
|
|
|
|
|
|
2023-01-31 08:25:05 -05:00
|
|
|
const TreeDispatcherContext: ReactContext<DispatcherContext> =
|
|
|
|
|
createContext<DispatcherContext>(((null: any): DispatcherContext));
|
2019-04-22 10:19:59 -07:00
|
|
|
TreeDispatcherContext.displayName = 'TreeDispatcherContext';
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2022-09-09 16:03:48 -04:00
|
|
|
type State = {
|
2019-02-08 14:44:07 -05:00
|
|
|
// Tree
|
|
|
|
|
numElements: number,
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID: number | null,
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementID: number | null,
|
|
|
|
|
selectedElementIndex: number | null,
|
|
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
searchIndex: number | null,
|
|
|
|
|
searchResults: Array<number>,
|
|
|
|
|
searchText: string,
|
|
|
|
|
|
|
|
|
|
// Owners
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID: number | null,
|
2019-04-24 15:19:13 -07:00
|
|
|
ownerFlatTree: Array<Element> | null,
|
2019-04-21 12:16:42 -07:00
|
|
|
|
|
|
|
|
// Inspection element panel
|
|
|
|
|
inspectedElementID: number | null,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
2019-02-08 14:44:07 -05:00
|
|
|
|
|
|
|
|
function reduceTreeState(store: Store, state: State, action: Action): State {
|
2020-09-01 20:03:44 -04:00
|
|
|
let {
|
|
|
|
|
numElements,
|
|
|
|
|
ownerSubtreeLeafElementID,
|
|
|
|
|
selectedElementIndex,
|
|
|
|
|
selectedElementID,
|
|
|
|
|
} = state;
|
2020-04-01 12:35:52 -07:00
|
|
|
const ownerID = state.ownerID;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-04-10 08:57:42 -07:00
|
|
|
let lookupIDForIndex = true;
|
|
|
|
|
|
2019-02-08 14:44:07 -05:00
|
|
|
// Base tree should ignore selected element changes when the owner's tree is active.
|
2019-05-09 11:47:22 -07:00
|
|
|
if (ownerID === null) {
|
2019-04-22 10:19:59 -07:00
|
|
|
switch (action.type) {
|
2019-02-08 14:44:07 -05:00
|
|
|
case 'HANDLE_STORE_MUTATION':
|
|
|
|
|
numElements = store.numElements;
|
|
|
|
|
|
|
|
|
|
// If the currently-selected Element has been removed from the tree, update selection state.
|
2019-04-24 16:29:23 +01:00
|
|
|
const removedIDs = action.payload[1];
|
|
|
|
|
// Find the closest parent that wasn't removed during this batch.
|
|
|
|
|
// We deduce the parent-child mapping from removedIDs (id -> parentID)
|
|
|
|
|
// because by now it's too late to read them from the store.
|
|
|
|
|
while (
|
2019-02-08 15:37:52 -05:00
|
|
|
selectedElementID !== null &&
|
2019-04-24 16:29:23 +01:00
|
|
|
removedIDs.has(selectedElementID)
|
2019-02-08 15:37:52 -05:00
|
|
|
) {
|
2019-04-24 16:29:23 +01:00
|
|
|
selectedElementID = ((removedIDs.get(
|
2019-08-13 17:58:03 -07:00
|
|
|
selectedElementID,
|
2019-04-24 16:29:23 +01:00
|
|
|
): any): number);
|
|
|
|
|
}
|
|
|
|
|
if (selectedElementID === 0) {
|
|
|
|
|
// The whole root was removed.
|
2019-02-08 15:37:52 -05:00
|
|
|
selectedElementIndex = null;
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
break;
|
2019-04-10 18:49:52 +01:00
|
|
|
case 'SELECT_CHILD_ELEMENT_IN_TREE':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-04-10 18:49:52 +01:00
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
const selectedElement = store.getElementAtIndex(
|
2019-08-13 17:58:03 -07:00
|
|
|
((selectedElementIndex: any): number),
|
2019-04-10 18:49:52 +01:00
|
|
|
);
|
2019-04-10 19:30:45 +01:00
|
|
|
if (
|
|
|
|
|
selectedElement !== null &&
|
|
|
|
|
selectedElement.children.length > 0 &&
|
|
|
|
|
!selectedElement.isCollapsed
|
|
|
|
|
) {
|
2019-04-10 18:49:52 +01:00
|
|
|
const firstChildID = selectedElement.children[0];
|
|
|
|
|
const firstChildIndex = store.getIndexOfElementID(firstChildID);
|
|
|
|
|
if (firstChildIndex !== null) {
|
|
|
|
|
selectedElementIndex = firstChildIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2019-02-08 14:44:07 -05:00
|
|
|
case 'SELECT_ELEMENT_AT_INDEX':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-04-22 10:19:59 -07:00
|
|
|
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
|
2019-02-08 14:44:07 -05:00
|
|
|
break;
|
|
|
|
|
case 'SELECT_ELEMENT_BY_ID':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-04-10 08:57:42 -07:00
|
|
|
// Skip lookup in this case; it would be redundant.
|
|
|
|
|
// It might also cause problems if the specified element was inside of a (not yet expanded) subtree.
|
|
|
|
|
lookupIDForIndex = false;
|
|
|
|
|
|
2019-04-22 10:19:59 -07:00
|
|
|
selectedElementID = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementIndex =
|
2019-04-22 10:19:59 -07:00
|
|
|
selectedElementID === null
|
2019-02-08 14:44:07 -05:00
|
|
|
? null
|
2019-04-22 10:19:59 -07:00
|
|
|
: store.getIndexOfElementID(selectedElementID);
|
2019-02-08 14:44:07 -05:00
|
|
|
break;
|
|
|
|
|
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-02-08 14:44:07 -05:00
|
|
|
if (
|
2019-04-05 08:08:02 -07:00
|
|
|
selectedElementIndex === null ||
|
|
|
|
|
selectedElementIndex + 1 >= numElements
|
2019-02-08 14:44:07 -05:00
|
|
|
) {
|
2019-04-05 08:08:02 -07:00
|
|
|
selectedElementIndex = 0;
|
|
|
|
|
} else {
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementIndex++;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2020-09-01 20:03:44 -04:00
|
|
|
case 'SELECT_NEXT_SIBLING_IN_TREE':
|
|
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
const selectedElement = store.getElementAtIndex(
|
|
|
|
|
((selectedElementIndex: any): number),
|
|
|
|
|
);
|
|
|
|
|
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
|
|
|
|
const parent = store.getElementByID(selectedElement.parentID);
|
|
|
|
|
if (parent !== null) {
|
|
|
|
|
const {children} = parent;
|
|
|
|
|
const selectedChildIndex = children.indexOf(selectedElement.id);
|
|
|
|
|
const nextChildID =
|
|
|
|
|
selectedChildIndex < children.length - 1
|
|
|
|
|
? children[selectedChildIndex + 1]
|
|
|
|
|
: children[0];
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(nextChildID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
if (
|
|
|
|
|
ownerSubtreeLeafElementID !== null &&
|
|
|
|
|
ownerSubtreeLeafElementID !== selectedElementID
|
|
|
|
|
) {
|
|
|
|
|
const leafElement = store.getElementByID(ownerSubtreeLeafElementID);
|
|
|
|
|
if (leafElement !== null) {
|
2022-10-04 15:39:26 -04:00
|
|
|
let currentElement: null | Element = leafElement;
|
2020-09-01 20:03:44 -04:00
|
|
|
while (currentElement !== null) {
|
|
|
|
|
if (currentElement.ownerID === selectedElementID) {
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(
|
|
|
|
|
currentElement.id,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
} else if (currentElement.ownerID !== 0) {
|
|
|
|
|
currentElement = store.getElementByID(currentElement.ownerID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
if (ownerSubtreeLeafElementID === null) {
|
|
|
|
|
// If this is the first time we're stepping through the owners tree,
|
|
|
|
|
// pin the current component as the owners list leaf.
|
|
|
|
|
// This will enable us to step back down to this component.
|
|
|
|
|
ownerSubtreeLeafElementID = selectedElementID;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const selectedElement = store.getElementAtIndex(
|
|
|
|
|
((selectedElementIndex: any): number),
|
|
|
|
|
);
|
|
|
|
|
if (selectedElement !== null && selectedElement.ownerID !== 0) {
|
|
|
|
|
const ownerIndex = store.getIndexOfElementID(
|
|
|
|
|
selectedElement.ownerID,
|
|
|
|
|
);
|
|
|
|
|
if (ownerIndex !== null) {
|
|
|
|
|
selectedElementIndex = ownerIndex;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2019-02-27 13:49:46 -08:00
|
|
|
case 'SELECT_PARENT_ELEMENT_IN_TREE':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-02-27 13:49:46 -08:00
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
const selectedElement = store.getElementAtIndex(
|
2019-08-13 17:58:03 -07:00
|
|
|
((selectedElementIndex: any): number),
|
2019-02-27 13:49:46 -08:00
|
|
|
);
|
2020-09-01 20:03:44 -04:00
|
|
|
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
2019-04-10 18:49:52 +01:00
|
|
|
const parentIndex = store.getIndexOfElementID(
|
2019-08-13 17:58:03 -07:00
|
|
|
selectedElement.parentID,
|
2019-04-10 18:49:52 +01:00
|
|
|
);
|
|
|
|
|
if (parentIndex !== null) {
|
|
|
|
|
selectedElementIndex = parentIndex;
|
|
|
|
|
}
|
2019-02-27 13:49:46 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2019-02-08 14:44:07 -05:00
|
|
|
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
2019-04-05 08:08:02 -07:00
|
|
|
if (selectedElementIndex === null || selectedElementIndex === 0) {
|
|
|
|
|
selectedElementIndex = numElements - 1;
|
|
|
|
|
} else {
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementIndex--;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2020-09-01 20:03:44 -04:00
|
|
|
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
|
|
|
|
|
ownerSubtreeLeafElementID = null;
|
|
|
|
|
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
const selectedElement = store.getElementAtIndex(
|
|
|
|
|
((selectedElementIndex: any): number),
|
|
|
|
|
);
|
|
|
|
|
if (selectedElement !== null && selectedElement.parentID !== 0) {
|
|
|
|
|
const parent = store.getElementByID(selectedElement.parentID);
|
|
|
|
|
if (parent !== null) {
|
|
|
|
|
const {children} = parent;
|
|
|
|
|
const selectedChildIndex = children.indexOf(selectedElement.id);
|
|
|
|
|
const nextChildID =
|
|
|
|
|
selectedChildIndex > 0
|
|
|
|
|
? children[selectedChildIndex - 1]
|
|
|
|
|
: children[children.length - 1];
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(nextChildID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2020-12-22 17:09:29 +01:00
|
|
|
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
|
2023-01-31 08:25:05 -05:00
|
|
|
const elementIndicesWithErrorsOrWarnings =
|
|
|
|
|
store.getElementsWithErrorsAndWarnings();
|
2022-05-05 11:46:57 -04:00
|
|
|
if (elementIndicesWithErrorsOrWarnings.length === 0) {
|
2020-12-22 17:09:29 +01:00
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let flatIndex = 0;
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
// Resume from the current position in the list.
|
|
|
|
|
// Otherwise step to the previous item, relative to the current selection.
|
|
|
|
|
for (
|
|
|
|
|
let i = elementIndicesWithErrorsOrWarnings.length - 1;
|
|
|
|
|
i >= 0;
|
|
|
|
|
i--
|
|
|
|
|
) {
|
|
|
|
|
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
|
|
|
|
if (index >= selectedElementIndex) {
|
|
|
|
|
flatIndex = i;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let prevEntry;
|
|
|
|
|
if (flatIndex === 0) {
|
|
|
|
|
prevEntry =
|
|
|
|
|
elementIndicesWithErrorsOrWarnings[
|
|
|
|
|
elementIndicesWithErrorsOrWarnings.length - 1
|
|
|
|
|
];
|
|
|
|
|
selectedElementID = prevEntry.id;
|
|
|
|
|
selectedElementIndex = prevEntry.index;
|
|
|
|
|
} else {
|
|
|
|
|
prevEntry = elementIndicesWithErrorsOrWarnings[flatIndex - 1];
|
|
|
|
|
selectedElementID = prevEntry.id;
|
|
|
|
|
selectedElementIndex = prevEntry.index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lookupIDForIndex = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE': {
|
2023-01-31 08:25:05 -05:00
|
|
|
const elementIndicesWithErrorsOrWarnings =
|
|
|
|
|
store.getElementsWithErrorsAndWarnings();
|
2022-05-05 11:46:57 -04:00
|
|
|
if (elementIndicesWithErrorsOrWarnings.length === 0) {
|
2020-12-22 17:09:29 +01:00
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let flatIndex = -1;
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
// Resume from the current position in the list.
|
|
|
|
|
// Otherwise step to the next item, relative to the current selection.
|
|
|
|
|
for (let i = 0; i < elementIndicesWithErrorsOrWarnings.length; i++) {
|
|
|
|
|
const {index} = elementIndicesWithErrorsOrWarnings[i];
|
|
|
|
|
if (index <= selectedElementIndex) {
|
|
|
|
|
flatIndex = i;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nextEntry;
|
|
|
|
|
if (flatIndex >= elementIndicesWithErrorsOrWarnings.length - 1) {
|
|
|
|
|
nextEntry = elementIndicesWithErrorsOrWarnings[0];
|
|
|
|
|
selectedElementID = nextEntry.id;
|
|
|
|
|
selectedElementIndex = nextEntry.index;
|
|
|
|
|
} else {
|
|
|
|
|
nextEntry = elementIndicesWithErrorsOrWarnings[flatIndex + 1];
|
|
|
|
|
selectedElementID = nextEntry.id;
|
|
|
|
|
selectedElementIndex = nextEntry.index;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lookupIDForIndex = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
default:
|
|
|
|
|
// React can bailout of no-op updates.
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep selected item ID and index in sync.
|
2019-04-10 08:57:42 -07:00
|
|
|
if (lookupIDForIndex && selectedElementIndex !== state.selectedElementIndex) {
|
2019-02-08 14:44:07 -05:00
|
|
|
if (selectedElementIndex === null) {
|
|
|
|
|
selectedElementID = null;
|
|
|
|
|
} else {
|
|
|
|
|
selectedElementID = store.getElementIDAtIndex(
|
2019-08-13 17:58:03 -07:00
|
|
|
((selectedElementIndex: any): number),
|
2019-02-08 14:44:07 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
|
|
|
|
|
numElements,
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID,
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementIndex,
|
|
|
|
|
selectedElementID,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function reduceSearchState(store: Store, state: State, action: Action): State {
|
|
|
|
|
let {
|
|
|
|
|
searchIndex,
|
|
|
|
|
searchResults,
|
|
|
|
|
searchText,
|
|
|
|
|
selectedElementID,
|
|
|
|
|
selectedElementIndex,
|
|
|
|
|
} = state;
|
2020-04-01 12:35:52 -07:00
|
|
|
const ownerID = state.ownerID;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-07-27 09:15:53 -07:00
|
|
|
const prevSearchIndex = searchIndex;
|
2019-04-04 15:59:06 +01:00
|
|
|
const prevSearchText = searchText;
|
2019-02-08 14:44:07 -05:00
|
|
|
const numPrevSearchResults = searchResults.length;
|
|
|
|
|
|
2019-04-09 00:06:13 +01:00
|
|
|
// We track explicitly whether search was requested because
|
|
|
|
|
// we might want to search even if search index didn't change.
|
|
|
|
|
// For example, if you press "next result" on a search with a single
|
|
|
|
|
// result but a different current selection, we'll set this to true.
|
|
|
|
|
let didRequestSearch = false;
|
|
|
|
|
|
2019-02-08 14:44:07 -05:00
|
|
|
// Search isn't supported when the owner's tree is active.
|
2019-05-09 11:47:22 -07:00
|
|
|
if (ownerID === null) {
|
2019-04-22 10:19:59 -07:00
|
|
|
switch (action.type) {
|
2019-02-08 14:44:07 -05:00
|
|
|
case 'GO_TO_NEXT_SEARCH_RESULT':
|
|
|
|
|
if (numPrevSearchResults > 0) {
|
2019-04-09 00:06:13 +01:00
|
|
|
didRequestSearch = true;
|
2019-02-08 14:44:07 -05:00
|
|
|
searchIndex =
|
2022-09-12 16:22:50 -04:00
|
|
|
// $FlowFixMe[unsafe-addition] addition with possible null/undefined value
|
2019-02-08 14:44:07 -05:00
|
|
|
searchIndex + 1 < numPrevSearchResults ? searchIndex + 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'GO_TO_PREVIOUS_SEARCH_RESULT':
|
|
|
|
|
if (numPrevSearchResults > 0) {
|
2019-04-09 00:06:13 +01:00
|
|
|
didRequestSearch = true;
|
2019-02-08 14:44:07 -05:00
|
|
|
searchIndex =
|
|
|
|
|
((searchIndex: any): number) > 0
|
|
|
|
|
? ((searchIndex: any): number) - 1
|
|
|
|
|
: numPrevSearchResults - 1;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'HANDLE_STORE_MUTATION':
|
|
|
|
|
if (searchText !== '') {
|
2023-01-31 08:25:05 -05:00
|
|
|
const [addedElementIDs, removedElementIDs] =
|
|
|
|
|
(action: ACTION_HANDLE_STORE_MUTATION).payload;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-04-24 16:29:23 +01:00
|
|
|
removedElementIDs.forEach((parentID, id) => {
|
2019-02-08 14:44:07 -05:00
|
|
|
// Prune this item from the search results.
|
|
|
|
|
const index = searchResults.indexOf(id);
|
|
|
|
|
if (index >= 0) {
|
|
|
|
|
searchResults = searchResults
|
|
|
|
|
.slice(0, index)
|
|
|
|
|
.concat(searchResults.slice(index + 1));
|
|
|
|
|
|
|
|
|
|
// If the results are now empty, also deselect things.
|
|
|
|
|
if (searchResults.length === 0) {
|
|
|
|
|
searchIndex = null;
|
|
|
|
|
} else if (((searchIndex: any): number) >= searchResults.length) {
|
|
|
|
|
searchIndex = searchResults.length - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
addedElementIDs.forEach(id => {
|
2019-02-21 14:56:11 -08:00
|
|
|
const element = ((store.getElementByID(id): any): Element);
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-02-21 14:56:11 -08:00
|
|
|
// It's possible that multiple tree operations will fire before this action has run.
|
|
|
|
|
// So it's important to check for elements that may have been added and then removed.
|
|
|
|
|
if (element !== null) {
|
2019-08-13 17:58:03 -07:00
|
|
|
const {displayName} = element;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-02-21 14:56:11 -08:00
|
|
|
// Add this item to the search results if it matches.
|
|
|
|
|
const regExp = createRegExp(searchText);
|
|
|
|
|
if (displayName !== null && regExp.test(displayName)) {
|
|
|
|
|
const newElementIndex = ((store.getIndexOfElementID(
|
2019-08-13 17:58:03 -07:00
|
|
|
id,
|
2019-02-21 14:56:11 -08:00
|
|
|
): any): number);
|
|
|
|
|
|
|
|
|
|
let foundMatch = false;
|
|
|
|
|
for (let index = 0; index < searchResults.length; index++) {
|
2019-08-13 21:59:07 -07:00
|
|
|
const resultID = searchResults[index];
|
2019-02-21 14:56:11 -08:00
|
|
|
if (
|
|
|
|
|
newElementIndex <
|
2019-08-13 21:59:07 -07:00
|
|
|
((store.getIndexOfElementID(resultID): any): number)
|
2019-02-21 14:56:11 -08:00
|
|
|
) {
|
|
|
|
|
foundMatch = true;
|
|
|
|
|
searchResults = searchResults
|
|
|
|
|
.slice(0, index)
|
2019-08-13 21:59:07 -07:00
|
|
|
.concat(resultID)
|
2019-02-21 14:56:11 -08:00
|
|
|
.concat(searchResults.slice(index));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!foundMatch) {
|
|
|
|
|
searchResults = searchResults.concat(id);
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
|
2019-02-21 14:56:11 -08:00
|
|
|
searchIndex = searchIndex === null ? 0 : searchIndex;
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SET_SEARCH_TEXT':
|
|
|
|
|
searchIndex = null;
|
|
|
|
|
searchResults = [];
|
2019-04-22 10:19:59 -07:00
|
|
|
searchText = (action: ACTION_SET_SEARCH_TEXT).payload;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
|
|
|
|
if (searchText !== '') {
|
|
|
|
|
const regExp = createRegExp(searchText);
|
|
|
|
|
store.roots.forEach(rootID => {
|
|
|
|
|
recursivelySearchTree(store, rootID, regExp, searchResults);
|
|
|
|
|
});
|
|
|
|
|
if (searchResults.length > 0) {
|
2019-07-27 09:15:53 -07:00
|
|
|
if (prevSearchIndex === null) {
|
|
|
|
|
if (selectedElementIndex !== null) {
|
|
|
|
|
searchIndex = getNearestResultIndex(
|
|
|
|
|
store,
|
|
|
|
|
searchResults,
|
2019-08-13 17:58:03 -07:00
|
|
|
selectedElementIndex,
|
2019-07-27 09:15:53 -07:00
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
searchIndex = 0;
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
} else {
|
2019-07-27 09:15:53 -07:00
|
|
|
searchIndex = Math.min(
|
|
|
|
|
((prevSearchIndex: any): number),
|
2019-08-13 17:58:03 -07:00
|
|
|
searchResults.length - 1,
|
2019-07-27 09:15:53 -07:00
|
|
|
);
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// React can bailout of no-op updates.
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-10 15:08:13 +01:00
|
|
|
if (searchText !== prevSearchText) {
|
2019-04-10 15:25:03 +01:00
|
|
|
const newSearchIndex = searchResults.indexOf(selectedElementID);
|
|
|
|
|
if (newSearchIndex === -1) {
|
2019-04-10 15:08:13 +01:00
|
|
|
// Only move the selection if the new query
|
|
|
|
|
// doesn't match the current selection anymore.
|
|
|
|
|
didRequestSearch = true;
|
|
|
|
|
} else {
|
|
|
|
|
// Selected item still matches the new search query.
|
|
|
|
|
// Adjust the index to reflect its position in new results.
|
2019-04-10 15:25:03 +01:00
|
|
|
searchIndex = newSearchIndex;
|
2019-04-10 15:08:13 +01:00
|
|
|
}
|
2019-04-09 00:06:13 +01:00
|
|
|
}
|
2019-04-13 16:19:32 +01:00
|
|
|
if (didRequestSearch && searchIndex !== null) {
|
|
|
|
|
selectedElementID = ((searchResults[searchIndex]: any): number);
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(
|
2019-08-13 17:58:03 -07:00
|
|
|
((selectedElementID: any): number),
|
2019-04-13 16:19:32 +01:00
|
|
|
);
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
|
|
|
|
|
selectedElementID,
|
|
|
|
|
selectedElementIndex,
|
|
|
|
|
|
|
|
|
|
searchIndex,
|
|
|
|
|
searchResults,
|
|
|
|
|
searchText,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function reduceOwnersState(store: Store, state: State, action: Action): State {
|
|
|
|
|
let {
|
|
|
|
|
numElements,
|
|
|
|
|
selectedElementID,
|
|
|
|
|
selectedElementIndex,
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID,
|
2019-04-22 10:19:59 -07:00
|
|
|
ownerFlatTree,
|
2019-02-08 14:44:07 -05:00
|
|
|
} = state;
|
2020-04-01 12:35:52 -07:00
|
|
|
const {searchIndex, searchResults, searchText} = state;
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-02-12 09:45:00 -05:00
|
|
|
let prevSelectedElementIndex = selectedElementIndex;
|
|
|
|
|
|
2019-04-22 10:19:59 -07:00
|
|
|
switch (action.type) {
|
2019-02-08 14:44:07 -05:00
|
|
|
case 'HANDLE_STORE_MUTATION':
|
2019-05-09 11:47:22 -07:00
|
|
|
if (ownerID !== null) {
|
2019-05-10 08:07:27 -07:00
|
|
|
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(
|
2019-08-13 17:58:03 -07:00
|
|
|
element => element.id === selectedElementID,
|
2019-05-10 08:07:27 -07:00
|
|
|
);
|
|
|
|
|
}
|
2019-04-10 17:28:36 +01:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (selectedElementID !== null) {
|
|
|
|
|
// Mutation might have caused the index of this ID to shift.
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(selectedElementID);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (selectedElementIndex === -1) {
|
|
|
|
|
// If we couldn't find this ID after mutation, unselect it.
|
|
|
|
|
selectedElementIndex = null;
|
|
|
|
|
selectedElementID = null;
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'RESET_OWNER_STACK':
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID = null;
|
|
|
|
|
ownerFlatTree = null;
|
2019-04-07 17:40:53 +01:00
|
|
|
selectedElementIndex =
|
|
|
|
|
selectedElementID !== null
|
|
|
|
|
? store.getIndexOfElementID(selectedElementID)
|
|
|
|
|
: null;
|
2019-02-08 14:44:07 -05:00
|
|
|
break;
|
|
|
|
|
case 'SELECT_ELEMENT_AT_INDEX':
|
2019-04-22 10:19:59 -07:00
|
|
|
if (ownerFlatTree !== null) {
|
|
|
|
|
selectedElementIndex = (action: ACTION_SELECT_ELEMENT_AT_INDEX).payload;
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_ELEMENT_BY_ID':
|
2019-04-22 10:19:59 -07:00
|
|
|
if (ownerFlatTree !== null) {
|
|
|
|
|
const payload = (action: ACTION_SELECT_ELEMENT_BY_ID).payload;
|
2019-06-12 15:13:51 -07:00
|
|
|
if (payload === null) {
|
|
|
|
|
selectedElementIndex = null;
|
|
|
|
|
} else {
|
|
|
|
|
selectedElementIndex = ownerFlatTree.findIndex(
|
2019-08-13 17:58:03 -07:00
|
|
|
element => element.id === payload,
|
2019-06-12 15:13:51 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If the selected element is outside of the current owners list,
|
|
|
|
|
// exit the list and select the element in the main tree.
|
|
|
|
|
// This supports features like toggling Suspense.
|
|
|
|
|
if (selectedElementIndex !== null && selectedElementIndex < 0) {
|
|
|
|
|
ownerID = null;
|
|
|
|
|
ownerFlatTree = null;
|
|
|
|
|
selectedElementIndex = store.getIndexOfElementID(payload);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
2019-04-22 10:19:59 -07:00
|
|
|
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
|
2019-02-08 14:44:07 -05:00
|
|
|
if (selectedElementIndex === null) {
|
|
|
|
|
selectedElementIndex = 0;
|
2019-04-22 10:19:59 -07:00
|
|
|
} else if (selectedElementIndex + 1 < ownerFlatTree.length) {
|
2019-02-08 14:44:07 -05:00
|
|
|
selectedElementIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
2019-04-22 10:19:59 -07:00
|
|
|
if (ownerFlatTree !== null && ownerFlatTree.length > 0) {
|
2019-02-08 14:44:07 -05:00
|
|
|
if (selectedElementIndex !== null && selectedElementIndex > 0) {
|
|
|
|
|
selectedElementIndex--;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case 'SELECT_OWNER':
|
2019-04-11 14:00:27 -07:00
|
|
|
// 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) {
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID = (action: ACTION_SELECT_OWNER).payload;
|
|
|
|
|
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
2019-04-11 14:00:27 -07:00
|
|
|
|
|
|
|
|
// Always force reset selection to be the top of the new owner tree.
|
|
|
|
|
selectedElementIndex = 0;
|
|
|
|
|
prevSelectedElementIndex = null;
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
// React can bailout of no-op updates.
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Changes in the selected owner require re-calculating the owners tree.
|
|
|
|
|
if (
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerFlatTree !== state.ownerFlatTree ||
|
2019-04-22 10:19:59 -07:00
|
|
|
action.type === 'HANDLE_STORE_MUTATION'
|
2019-02-08 14:44:07 -05:00
|
|
|
) {
|
2019-05-09 11:47:22 -07:00
|
|
|
if (ownerFlatTree === null) {
|
2019-02-08 14:44:07 -05:00
|
|
|
numElements = store.numElements;
|
|
|
|
|
} else {
|
2019-04-22 10:19:59 -07:00
|
|
|
numElements = ownerFlatTree.length;
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keep selected item ID and index in sync.
|
2019-02-12 09:45:00 -05:00
|
|
|
if (selectedElementIndex !== prevSelectedElementIndex) {
|
2019-02-08 14:44:07 -05:00
|
|
|
if (selectedElementIndex === null) {
|
|
|
|
|
selectedElementID = null;
|
2019-05-09 11:47:22 -07:00
|
|
|
} else {
|
|
|
|
|
if (ownerFlatTree !== null) {
|
|
|
|
|
selectedElementID = ownerFlatTree[selectedElementIndex].id;
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
|
|
|
|
|
numElements,
|
|
|
|
|
selectedElementID,
|
|
|
|
|
selectedElementIndex,
|
|
|
|
|
|
|
|
|
|
searchIndex,
|
|
|
|
|
searchResults,
|
|
|
|
|
searchText,
|
|
|
|
|
|
2019-05-09 11:47:22 -07:00
|
|
|
ownerID,
|
2019-04-22 10:19:59 -07:00
|
|
|
ownerFlatTree,
|
2019-02-08 14:44:07 -05:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-21 12:16:42 -07:00
|
|
|
function reduceSuspenseState(
|
|
|
|
|
store: Store,
|
|
|
|
|
state: State,
|
2019-08-13 17:58:03 -07:00
|
|
|
action: Action,
|
2019-04-21 12:16:42 -07:00
|
|
|
): State {
|
2019-08-13 17:58:03 -07:00
|
|
|
const {type} = action;
|
2019-04-21 12:16:42 -07:00
|
|
|
switch (type) {
|
|
|
|
|
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
2019-05-09 11:47:22 -07:00
|
|
|
if (state.inspectedElementID !== state.selectedElementID) {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
inspectedElementID: state.selectedElementID,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
break;
|
2019-04-21 12:16:42 -07:00
|
|
|
default:
|
2019-05-09 11:47:22 -07:00
|
|
|
break;
|
2019-04-21 12:16:42 -07:00
|
|
|
}
|
2019-05-09 11:47:22 -07:00
|
|
|
|
|
|
|
|
// React can bailout of no-op updates.
|
|
|
|
|
return state;
|
2019-04-21 12:16:42 -07:00
|
|
|
}
|
|
|
|
|
|
2022-09-09 16:03:48 -04:00
|
|
|
type Props = {
|
2019-05-09 14:21:22 -07:00
|
|
|
children: React$Node,
|
|
|
|
|
|
|
|
|
|
// Used for automated testing
|
2019-09-25 10:46:27 -07:00
|
|
|
defaultInspectedElementID?: ?number,
|
2019-05-09 14:21:22 -07:00
|
|
|
defaultOwnerID?: ?number,
|
2019-05-10 09:13:54 -07:00
|
|
|
defaultSelectedElementID?: ?number,
|
|
|
|
|
defaultSelectedElementIndex?: ?number,
|
2022-09-09 16:03:48 -04:00
|
|
|
};
|
2019-02-22 13:24:03 -08:00
|
|
|
|
2021-12-03 16:23:48 -05:00
|
|
|
// TODO Remove TreeContextController wrapper element once global Context.write API exists.
|
2019-05-10 09:13:54 -07:00
|
|
|
function TreeContextController({
|
|
|
|
|
children,
|
2019-09-25 10:46:27 -07:00
|
|
|
defaultInspectedElementID,
|
2019-05-10 09:13:54 -07:00
|
|
|
defaultOwnerID,
|
|
|
|
|
defaultSelectedElementID,
|
|
|
|
|
defaultSelectedElementIndex,
|
2022-09-15 16:45:29 -04:00
|
|
|
}: Props): React.Node {
|
2019-02-16 08:29:26 -08:00
|
|
|
const bridge = useContext(BridgeContext);
|
2019-02-08 14:44:07 -05:00
|
|
|
const store = useContext(StoreContext);
|
2019-02-16 08:29:26 -08:00
|
|
|
|
2019-02-08 15:37:52 -05:00
|
|
|
const initialRevision = useMemo(() => store.revision, [store]);
|
2019-02-08 14:44:07 -05:00
|
|
|
|
|
|
|
|
// This reducer is created inline because it needs access to the Store.
|
|
|
|
|
// The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
|
|
|
|
|
// so it's okay for the reducer to have an empty dependencies array.
|
|
|
|
|
const reducer = useMemo(
|
2023-01-31 08:25:05 -05:00
|
|
|
() =>
|
|
|
|
|
(state: State, action: Action): State => {
|
|
|
|
|
const {type} = action;
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'GO_TO_NEXT_SEARCH_RESULT':
|
|
|
|
|
case 'GO_TO_PREVIOUS_SEARCH_RESULT':
|
|
|
|
|
case 'HANDLE_STORE_MUTATION':
|
|
|
|
|
case 'RESET_OWNER_STACK':
|
|
|
|
|
case 'SELECT_ELEMENT_AT_INDEX':
|
|
|
|
|
case 'SELECT_ELEMENT_BY_ID':
|
|
|
|
|
case 'SELECT_CHILD_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_NEXT_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
|
|
|
|
|
case 'SELECT_NEXT_SIBLING_IN_TREE':
|
|
|
|
|
case 'SELECT_OWNER_LIST_NEXT_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_OWNER_LIST_PREVIOUS_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_PARENT_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
|
|
|
|
case 'SELECT_PREVIOUS_ELEMENT_WITH_ERROR_OR_WARNING_IN_TREE':
|
|
|
|
|
case 'SELECT_PREVIOUS_SIBLING_IN_TREE':
|
|
|
|
|
case 'SELECT_OWNER':
|
|
|
|
|
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
|
|
|
|
case 'SET_SEARCH_TEXT':
|
|
|
|
|
state = reduceTreeState(store, state, action);
|
|
|
|
|
state = reduceSearchState(store, state, action);
|
|
|
|
|
state = reduceOwnersState(store, state, action);
|
|
|
|
|
state = reduceSuspenseState(store, state, action);
|
|
|
|
|
|
|
|
|
|
// If the selected ID is in a collapsed subtree, reset the selected index to null.
|
|
|
|
|
// We'll know the correct index after the layout effect will toggle the tree,
|
|
|
|
|
// and the store tree is mutated to account for that.
|
|
|
|
|
if (
|
|
|
|
|
state.selectedElementID !== null &&
|
|
|
|
|
store.isInsideCollapsedSubTree(state.selectedElementID)
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
...state,
|
|
|
|
|
selectedElementIndex: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
2019-04-20 16:20:03 +01:00
|
|
|
|
2023-01-31 08:25:05 -05:00
|
|
|
return state;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unrecognized action "${type}"`);
|
|
|
|
|
}
|
|
|
|
|
},
|
2019-08-13 17:58:03 -07:00
|
|
|
[store],
|
2019-02-08 14:44:07 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [state, dispatch] = useReducer(reducer, {
|
|
|
|
|
// Tree
|
|
|
|
|
numElements: store.numElements,
|
2020-09-01 20:03:44 -04:00
|
|
|
ownerSubtreeLeafElementID: null,
|
2019-05-10 09:13:54 -07:00
|
|
|
selectedElementID:
|
|
|
|
|
defaultSelectedElementID == null ? null : defaultSelectedElementID,
|
|
|
|
|
selectedElementIndex:
|
|
|
|
|
defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex,
|
2019-02-08 14:44:07 -05:00
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
searchIndex: null,
|
|
|
|
|
searchResults: [],
|
|
|
|
|
searchText: '',
|
|
|
|
|
|
|
|
|
|
// Owners
|
2019-05-09 14:21:22 -07:00
|
|
|
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
|
2019-04-22 10:19:59 -07:00
|
|
|
ownerFlatTree: null,
|
2019-04-21 12:16:42 -07:00
|
|
|
|
|
|
|
|
// Inspection element panel
|
2019-09-25 10:46:27 -07:00
|
|
|
inspectedElementID:
|
|
|
|
|
defaultInspectedElementID == null ? null : defaultInspectedElementID,
|
2019-02-08 14:44:07 -05:00
|
|
|
});
|
|
|
|
|
|
2019-04-21 12:16:42 -07:00
|
|
|
const dispatchWrapper = useCallback(
|
2019-04-22 10:19:59 -07:00
|
|
|
(action: Action) => {
|
2021-03-10 08:52:19 -05:00
|
|
|
dispatch(action);
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
dispatch({type: 'UPDATE_INSPECTED_ELEMENT_ID'});
|
|
|
|
|
});
|
2019-04-21 12:16:42 -07:00
|
|
|
},
|
2019-08-13 17:58:03 -07:00
|
|
|
[dispatch],
|
2019-04-21 12:16:42 -07:00
|
|
|
);
|
|
|
|
|
|
2019-02-16 08:29:26 -08:00
|
|
|
// Listen for host element selections.
|
2020-01-09 13:54:11 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleSelectFiber = (id: number) =>
|
|
|
|
|
dispatchWrapper({type: 'SELECT_ELEMENT_BY_ID', payload: id});
|
|
|
|
|
bridge.addListener('selectFiber', handleSelectFiber);
|
|
|
|
|
return () => bridge.removeListener('selectFiber', handleSelectFiber);
|
|
|
|
|
}, [bridge, dispatchWrapper]);
|
2019-02-16 08:29:26 -08:00
|
|
|
|
2019-04-10 08:57:42 -07:00
|
|
|
// If a newly-selected search result or inspection selection is inside of a collapsed subtree, auto expand it.
|
|
|
|
|
// This needs to be a layout effect to avoid temporarily flashing an incorrect selection.
|
|
|
|
|
const prevSelectedElementID = useRef<number | null>(null);
|
2020-01-09 13:54:11 +00:00
|
|
|
useLayoutEffect(() => {
|
|
|
|
|
if (state.selectedElementID !== prevSelectedElementID.current) {
|
|
|
|
|
prevSelectedElementID.current = state.selectedElementID;
|
|
|
|
|
|
|
|
|
|
if (state.selectedElementID !== null) {
|
2020-04-01 12:35:52 -07:00
|
|
|
const element = store.getElementByID(state.selectedElementID);
|
2020-01-09 13:54:11 +00:00
|
|
|
if (element !== null && element.parentID > 0) {
|
|
|
|
|
store.toggleIsCollapsed(element.parentID, false);
|
2019-04-09 18:11:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
2020-01-09 13:54:11 +00:00
|
|
|
}
|
|
|
|
|
}, [state.selectedElementID, store]);
|
2019-04-09 18:11:40 -07:00
|
|
|
|
2019-02-08 14:44:07 -05:00
|
|
|
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
|
2020-01-09 13:54:11 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleStoreMutated = ([addedElementIDs, removedElementIDs]: [
|
|
|
|
|
Array<number>,
|
|
|
|
|
Map<number, number>,
|
|
|
|
|
]) => {
|
|
|
|
|
dispatchWrapper({
|
|
|
|
|
type: 'HANDLE_STORE_MUTATION',
|
|
|
|
|
payload: [addedElementIDs, removedElementIDs],
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Since this is a passive effect, the tree may have been mutated before our initial subscription.
|
|
|
|
|
if (store.revision !== initialRevision) {
|
|
|
|
|
// At the moment, we can treat this as a mutation.
|
|
|
|
|
// We don't know which Elements were newly added/removed, but that should be okay in this case.
|
|
|
|
|
// It would only impact the search state, which is unlikely to exist yet at this point.
|
|
|
|
|
dispatchWrapper({
|
|
|
|
|
type: 'HANDLE_STORE_MUTATION',
|
|
|
|
|
payload: [[], new Map()],
|
|
|
|
|
});
|
|
|
|
|
}
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2020-01-09 13:54:11 +00:00
|
|
|
store.addListener('mutated', handleStoreMutated);
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2020-01-09 13:54:11 +00:00
|
|
|
return () => store.removeListener('mutated', handleStoreMutated);
|
|
|
|
|
}, [dispatchWrapper, initialRevision, store]);
|
2019-02-08 14:44:07 -05:00
|
|
|
|
2019-04-22 10:19:59 -07:00
|
|
|
return (
|
|
|
|
|
<TreeStateContext.Provider value={state}>
|
|
|
|
|
<TreeDispatcherContext.Provider value={dispatchWrapper}>
|
|
|
|
|
{children}
|
|
|
|
|
</TreeDispatcherContext.Provider>
|
|
|
|
|
</TreeStateContext.Provider>
|
|
|
|
|
);
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
|
|
|
|
function recursivelySearchTree(
|
|
|
|
|
store: Store,
|
|
|
|
|
elementID: number,
|
|
|
|
|
regExp: RegExp,
|
2019-08-13 17:58:03 -07:00
|
|
|
searchResults: Array<number>,
|
2019-02-08 14:44:07 -05:00
|
|
|
): void {
|
2019-08-13 17:58:03 -07:00
|
|
|
const {children, displayName, hocDisplayNames} = ((store.getElementByID(
|
|
|
|
|
elementID,
|
2019-02-08 14:44:07 -05:00
|
|
|
): any): Element);
|
2019-08-05 11:44:48 -05:00
|
|
|
|
|
|
|
|
if (displayName != null && regExp.test(displayName) === true) {
|
|
|
|
|
searchResults.push(elementID);
|
|
|
|
|
} else if (
|
|
|
|
|
hocDisplayNames != null &&
|
|
|
|
|
hocDisplayNames.length > 0 &&
|
|
|
|
|
hocDisplayNames.some(name => regExp.test(name)) === true
|
|
|
|
|
) {
|
|
|
|
|
searchResults.push(elementID);
|
2019-02-08 14:44:07 -05:00
|
|
|
}
|
2019-08-05 11:44:48 -05:00
|
|
|
|
2019-02-08 14:44:07 -05:00
|
|
|
children.forEach(childID =>
|
2019-08-13 17:58:03 -07:00
|
|
|
recursivelySearchTree(store, childID, regExp, searchResults),
|
2019-02-08 14:44:07 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-07-27 09:15:53 -07:00
|
|
|
function getNearestResultIndex(
|
|
|
|
|
store: Store,
|
2019-07-27 13:04:20 -03:00
|
|
|
searchResults: Array<number>,
|
2019-08-13 17:58:03 -07:00
|
|
|
selectedElementIndex: number,
|
2019-07-27 09:15:53 -07:00
|
|
|
): number {
|
|
|
|
|
const index = searchResults.findIndex(id => {
|
2019-08-13 21:59:07 -07:00
|
|
|
const innerIndex = store.getIndexOfElementID(id);
|
|
|
|
|
return innerIndex !== null && innerIndex >= selectedElementIndex;
|
2019-07-27 09:15:53 -07:00
|
|
|
});
|
2019-07-27 13:04:20 -03:00
|
|
|
|
2019-07-27 09:15:53 -07:00
|
|
|
return index === -1 ? 0 : index;
|
2019-07-27 13:04:20 -03:00
|
|
|
}
|
|
|
|
|
|
2019-08-13 17:58:03 -07:00
|
|
|
export {TreeDispatcherContext, TreeStateContext, TreeContextController};
|