main
js 331 lines 8.9 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9 import * as React from 'react';
10 import {
11 Fragment,
12 useCallback,
13 useContext,
14 useLayoutEffect,
15 useReducer,
16 useRef,
17 useState,
18 } from 'react';
19 import Button from '../Button';
20 import ButtonIcon from '../ButtonIcon';
21 import Toggle from '../Toggle';
22 import ElementBadges from './ElementBadges';
23 import {OwnersListContext, useChangeOwnerAction} from './OwnersListContext';
24 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
25 import {useIsOverflowing} from '../hooks';
26 import {StoreContext} from '../context';
27 import Tooltip from '../Components/reach-ui/tooltip';
28 import {
29 Menu,
30 MenuList,
31 MenuButton,
32 MenuItem,
33 } from '../Components/reach-ui/menu-button';
34
35 import type {SerializedElement} from 'react-devtools-shared/src/frontend/types';
36
37 import styles from './OwnersStack.css';
38
39 type SelectOwner = (owner: SerializedElement | null) => void;
40
41 type ACTION_UPDATE_OWNER_ID = {
42 type: 'UPDATE_OWNER_ID',
43 ownerID: number | null,
44 owners: Array<SerializedElement>,
45 };
46 type ACTION_UPDATE_SELECTED_INDEX = {
47 type: 'UPDATE_SELECTED_INDEX',
48 selectedIndex: number,
49 };
50
51 type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
52
53 type State = {
54 ownerID: number | null,
55 owners: Array<SerializedElement>,
56 selectedIndex: number,
57 };
58
59 function dialogReducer(state: State, action: Action) {
60 switch (action.type) {
61 case 'UPDATE_OWNER_ID':
62 const selectedIndex = action.owners.findIndex(
63 owner => owner.id === action.ownerID,
64 );
65 return {
66 ownerID: action.ownerID,
67 owners: action.owners,
68 selectedIndex,
69 };
70 case 'UPDATE_SELECTED_INDEX':
71 return {
72 ...state,
73 selectedIndex: action.selectedIndex,
74 };
75 default:
76 throw new Error(`Invalid action "${action.type}"`);
77 }
78 }
79
80 type OwnerStackFlatListProps = {
81 owners: Array<SerializedElement>,
82 selectedIndex: number,
83 selectOwner: SelectOwner,
84 setElementsTotalWidth: (width: number) => void,
85 };
86
87 function OwnerStackFlatList({
88 owners,
89 selectedIndex,
90 selectOwner,
91 setElementsTotalWidth,
92 }: OwnerStackFlatListProps): React.Node {
93 const containerRef = useRef<HTMLDivElement | null>(null);
94 useLayoutEffect(() => {
95 const container = containerRef.current;
96 if (container === null) {
97 return;
98 }
99
100 const ResizeObserver = container.ownerDocument.defaultView.ResizeObserver;
101 const observer = new ResizeObserver(entries => {
102 const entry = entries[0];
103 setElementsTotalWidth(entry.contentRect.width);
104 });
105
106 observer.observe(container);
107 return observer.disconnect.bind(observer);
108 }, []);
109
110 return (
111 <div className={styles.OwnerStackFlatListContainer} ref={containerRef}>
112 {owners.map((owner, index) => (
113 <Fragment key={index}>
114 <ElementView
115 owner={owner}
116 isSelected={index === selectedIndex}
117 selectOwner={selectOwner}
118 />
119 {index < owners.length - 1 && (
120 <span className={styles.OwnerStackFlatListSeparator}>»</span>
121 )}
122 </Fragment>
123 ))}
124 </div>
125 );
126 }
127
128 export default function OwnerStack(): React.Node {
129 const read = useContext(OwnersListContext);
130 const {ownerID} = useContext(TreeStateContext);
131 const treeDispatch = useContext(TreeDispatcherContext);
132 const changeOwnerAction = useChangeOwnerAction();
133
134 const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
135 ownerID: null,
136 owners: [],
137 selectedIndex: 0,
138 });
139
140 // When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners.
141 // We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed.
142 if (ownerID === null) {
143 dispatch({
144 type: 'UPDATE_OWNER_ID',
145 ownerID: null,
146 owners: [],
147 });
148 } else if (ownerID !== state.ownerID) {
149 const isInStore =
150 state.owners.findIndex(owner => owner.id === ownerID) >= 0;
151 dispatch({
152 type: 'UPDATE_OWNER_ID',
153 ownerID,
154 owners: isInStore ? state.owners : read(ownerID) || [],
155 });
156 }
157
158 const {owners, selectedIndex} = state;
159
160 const selectOwner = useCallback<SelectOwner>(
161 (owner: SerializedElement | null) => {
162 if (owner !== null) {
163 const index = owners.indexOf(owner);
164 dispatch({
165 type: 'UPDATE_SELECTED_INDEX',
166 selectedIndex: index >= 0 ? index : 0,
167 });
168 changeOwnerAction(owner.id);
169 } else {
170 dispatch({
171 type: 'UPDATE_SELECTED_INDEX',
172 selectedIndex: 0,
173 });
174 treeDispatch({type: 'RESET_OWNER_STACK'});
175 }
176 },
177 [owners, treeDispatch],
178 );
179
180 const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
181 const elementsBarRef = useRef<HTMLDivElement | null>(null);
182 const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
183
184 const selectedOwner = owners[selectedIndex];
185
186 return (
187 <div className={styles.OwnerStack}>
188 <div className={styles.Bar} ref={elementsBarRef}>
189 {isOverflowing ? (
190 <Fragment>
191 <ElementsDropdown
192 owners={owners}
193 selectedIndex={selectedIndex}
194 selectOwner={selectOwner}
195 />
196 <BackToOwnerButton
197 owners={owners}
198 selectedIndex={selectedIndex}
199 selectOwner={selectOwner}
200 />
201 {selectedOwner != null && (
202 <ElementView
203 owner={selectedOwner}
204 isSelected={true}
205 selectOwner={selectOwner}
206 />
207 )}
208 </Fragment>
209 ) : (
210 <OwnerStackFlatList
211 owners={owners}
212 selectedIndex={selectedIndex}
213 selectOwner={selectOwner}
214 setElementsTotalWidth={setElementsTotalWidth}
215 />
216 )}
217 </div>
218 <div className={styles.VRule} />
219 <Button onClick={() => selectOwner(null)} title="Back to tree view">
220 <ButtonIcon type="close" />
221 </Button>
222 </div>
223 );
224 }
225
226 type ElementsDropdownProps = {
227 owners: Array<SerializedElement>,
228 selectedIndex: number,
229 selectOwner: SelectOwner,
230 };
231 function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
232 const store = useContext(StoreContext);
233
234 const menuItems = [];
235 for (let index = owners.length - 1; index >= 0; index--) {
236 const owner = owners[index];
237 const isInStore = store.containsElement(owner.id);
238 menuItems.push(
239 <MenuItem
240 key={owner.id}
241 className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
242 onSelect={() => (isInStore ? selectOwner(owner) : null)}>
243 {owner.displayName}
244
245 <ElementBadges
246 hocDisplayNames={owner.hocDisplayNames}
247 environmentName={owner.env}
248 compiledWithForget={owner.compiledWithForget}
249 className={styles.BadgesBlock}
250 />
251 </MenuItem>,
252 );
253 }
254
255 return (
256 <Menu>
257 <MenuButton className={styles.MenuButton}>
258 <Tooltip label="Open elements dropdown">
259 <span className={styles.MenuButtonContent} tabIndex={-1}>
260 <ButtonIcon type="more" />
261 </span>
262 </Tooltip>
263 </MenuButton>
264 <MenuList className={styles.Modal}>{menuItems}</MenuList>
265 </Menu>
266 );
267 }
268
269 type ElementViewProps = {
270 isSelected: boolean,
271 owner: SerializedElement,
272 selectOwner: SelectOwner,
273 ...
274 };
275 function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
276 const store = useContext(StoreContext);
277
278 const {displayName, hocDisplayNames, compiledWithForget} = owner;
279 const isInStore = store.containsElement(owner.id);
280
281 const handleChange = useCallback(() => {
282 if (isInStore) {
283 selectOwner(owner);
284 }
285 }, [isInStore, selectOwner, owner]);
286
287 return (
288 <Toggle
289 className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
290 isChecked={isSelected}
291 onChange={handleChange}>
292 {displayName}
293
294 <ElementBadges
295 hocDisplayNames={hocDisplayNames}
296 environmentName={owner.env}
297 compiledWithForget={compiledWithForget}
298 className={styles.BadgesBlock}
299 />
300 </Toggle>
301 );
302 }
303
304 type BackToOwnerButtonProps = {
305 owners: Array<SerializedElement>,
306 selectedIndex: number,
307 selectOwner: SelectOwner,
308 };
309 function BackToOwnerButton({
310 owners,
311 selectedIndex,
312 selectOwner,
313 }: BackToOwnerButtonProps) {
314 const store = useContext(StoreContext);
315
316 if (selectedIndex <= 0) {
317 return null;
318 }
319
320 const owner = owners[selectedIndex - 1];
321 const isInStore = store.containsElement(owner.id);
322
323 return (
324 <Button
325 className={isInStore ? undefined : styles.NotInStore}
326 onClick={() => (isInStore ? selectOwner(owner) : null)}
327 title={`Up to ${owner.displayName || 'owner'}`}>
328 <ButtonIcon type="previous" />
329 </Button>
330 );
331 }