Files
react/packages/react-devtools-shared/src/devtools/views/Components/Element.js
Ruslan Lesiutin a15bbe1475 refactor: data source for errors and warnings tracking is now in Store (#31010)
Stacked on https://github.com/facebook/react/pull/31009.

1. Instead of keeping `showInlineWarningsAndErrors` in `Settings`
context (which was removed in
https://github.com/facebook/react/pull/30610), `Store` will now have a
boolean flag, which controls if the UI should be displaying information
about errors and warnings.
2. The errors and warnings counters in the Tree view are now counting
only unique errors. This makes more sense, because it is part of the
Elements Tree view, so ideally it should be showing number of components
with errors and number of components of warnings. Consider this example:
2.1. Warning for element `A` was emitted once and warning for element
`B` was emitted twice.
2.2. With previous implementation, we would show `3 ⚠️`, because in
total there were 3 warnings in total. If user tries to iterate through
these, it will only take 2 steps to do the full cycle, because there are
only 2 elements with warnings (with one having same warning, which was
emitted twice).
2.3 With current implementation, we would show `2 ⚠️`. Inspecting the
element with doubled warning will still show the warning counter (2)
before the warning message.

With these changes, the feature correctly works.
https://fburl.com/a7fw92m4
2024-09-24 19:51:21 +01:00

261 lines
7.2 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {Fragment, useContext, useMemo, useState} from 'react';
import Store from 'react-devtools-shared/src/devtools/store';
import ButtonIcon from '../ButtonIcon';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {StoreContext} from '../context';
import {useSubscription} from '../hooks';
import {logEvent} from 'react-devtools-shared/src/Logger';
import IndexableElementBadges from './IndexableElementBadges';
import IndexableDisplayName from './IndexableDisplayName';
import type {ItemData} from './Tree';
import type {Element as ElementType} from 'react-devtools-shared/src/frontend/types';
import styles from './Element.css';
import Icon from '../Icon';
type Props = {
data: ItemData,
index: number,
style: Object,
...
};
export default function Element({data, index, style}: Props): React.Node {
const store = useContext(StoreContext);
const {ownerFlatTree, ownerID, selectedElementID} =
useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const element =
ownerFlatTree !== null
? ownerFlatTree[index]
: store.getElementAtIndex(index);
const [isHovered, setIsHovered] = useState(false);
const {isNavigatingWithKeyboard, onElementMouseEnter, treeFocused} = data;
const id = element === null ? null : element.id;
const isSelected = selectedElementID === id;
const errorsAndWarningsSubscription = useMemo(
() => ({
getCurrentValue: () =>
element === null
? {errorCount: 0, warningCount: 0}
: store.getErrorAndWarningCountForElementID(element.id),
subscribe: (callback: Function) => {
store.addListener('mutated', callback);
return () => store.removeListener('mutated', callback);
},
}),
[store, element],
);
const {errorCount, warningCount} = useSubscription<{
errorCount: number,
warningCount: number,
}>(errorsAndWarningsSubscription);
const handleDoubleClick = () => {
if (id !== null) {
dispatch({type: 'SELECT_OWNER', payload: id});
}
};
// $FlowFixMe[missing-local-annot]
const handleClick = ({metaKey}) => {
if (id !== null) {
logEvent({
event_name: 'select-element',
metadata: {source: 'click-element'},
});
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: metaKey ? null : id,
});
}
};
const handleMouseEnter = () => {
setIsHovered(true);
if (id !== null) {
onElementMouseEnter(id);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
};
// $FlowFixMe[missing-local-annot]
const handleKeyDoubleClick = event => {
// Double clicks on key value are used for text selection (if the text has been truncated).
// They should not enter the owners tree view.
event.stopPropagation();
event.preventDefault();
};
// Handle elements that are removed from the tree while an async render is in progress.
if (element == null) {
console.warn(`<Element> Could not find element at index ${index}`);
// This return needs to happen after hooks, since hooks can't be conditional.
return null;
}
const {
depth,
displayName,
hocDisplayNames,
isStrictModeNonCompliant,
key,
compiledWithForget,
} = element;
// Only show strict mode non-compliance badges for top level elements.
// Showing an inline badge for every element in the tree would be noisy.
const showStrictModeBadge = isStrictModeNonCompliant && depth === 0;
let className = styles.Element;
if (isSelected) {
className = treeFocused
? styles.SelectedElement
: styles.InactiveSelectedElement;
} else if (isHovered && !isNavigatingWithKeyboard) {
className = styles.HoveredElement;
}
return (
<div
className={className}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleClick}
onDoubleClick={handleDoubleClick}
style={style}
data-testname="ComponentTreeListItem"
data-depth={depth}>
{/* This wrapper is used by Tree for measurement purposes. */}
<div
className={styles.Wrapper}
style={{
// Left offset presents the appearance of a nested tree structure.
// We must use padding rather than margin/left because of the selected background color.
transform: `translateX(calc(${depth} * var(--indentation-size)))`,
}}>
{ownerID === null && (
<ExpandCollapseToggle element={element} store={store} />
)}
<IndexableDisplayName displayName={displayName} id={id} />
{key && (
<Fragment>
&nbsp;<span className={styles.KeyName}>key</span>="
<span
className={styles.KeyValue}
title={key}
onDoubleClick={handleKeyDoubleClick}>
{key}
</span>
"
</Fragment>
)}
<IndexableElementBadges
hocDisplayNames={hocDisplayNames}
compiledWithForget={compiledWithForget}
elementID={id}
className={styles.BadgesBlock}
/>
{errorCount > 0 && (
<Icon
type="error"
className={
isSelected && treeFocused
? styles.ErrorIconContrast
: styles.ErrorIcon
}
/>
)}
{warningCount > 0 && (
<Icon
type="warning"
className={
isSelected && treeFocused
? styles.WarningIconContrast
: styles.WarningIcon
}
/>
)}
{showStrictModeBadge && (
<Icon
className={
isSelected && treeFocused
? styles.StrictModeContrast
: styles.StrictMode
}
title="This component is not running in StrictMode."
type="strict-mode-non-compliant"
/>
)}
</div>
</div>
);
}
// Prevent double clicks on toggle from drilling into the owner list.
// $FlowFixMe[missing-local-annot]
const swallowDoubleClick = event => {
event.preventDefault();
event.stopPropagation();
};
type ExpandCollapseToggleProps = {
element: ElementType,
store: Store,
};
function ExpandCollapseToggle({element, store}: ExpandCollapseToggleProps) {
const {children, id, isCollapsed} = element;
// $FlowFixMe[missing-local-annot]
const toggleCollapsed = event => {
event.preventDefault();
event.stopPropagation();
store.toggleIsCollapsed(id, !isCollapsed);
};
// $FlowFixMe[missing-local-annot]
const stopPropagation = event => {
// Prevent the row from selecting
event.stopPropagation();
};
if (children.length === 0) {
return <div className={styles.ExpandCollapseToggle} />;
}
return (
<div
className={styles.ExpandCollapseToggle}
onMouseDown={stopPropagation}
onClick={toggleCollapsed}
onDoubleClick={swallowDoubleClick}>
<ButtonIcon type={isCollapsed ? 'collapsed' : 'expanded'} />
</div>
);
}