/
ashipov
/
react
Обзор
Документация
Войти
/
ashipov
/
react
Код
Запросы
0
Задачи 2.0
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
586 строк
17 KB
Sebastian Markbåge
[DevTools] Make a non-editable name of KeyValue clickable (#34095)
04 авг 2025, 16:27
Не верифицирован
04 авг 2025, 16:27
8e3db09
Код
Авторство
О чём код?
/** * 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 {useTransition, useContext, useRef, useState, useMemo} from 'react'; import {OptionsContext} from '../context'; import EditableName from './EditableName'; import EditableValue from './EditableValue'; import NewArrayValue from './NewArrayValue'; import NewKeyValue from './NewKeyValue'; import LoadingAnimation from './LoadingAnimation'; import ExpandCollapseToggle from './ExpandCollapseToggle'; import {alphaSortEntries, getMetaValueLabel} from '../utils'; import {meta} from '../../../hydration'; import Store from '../../store'; import {parseHookPathForEdit} from './utils'; import styles from './KeyValue.css'; import Button from 'react-devtools-shared/src/devtools/views/Button'; import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; import isArray from 'react-devtools-shared/src/isArray'; import {InspectedElementContext} from './InspectedElementContext'; import {PROTOCOLS_SUPPORTED_AS_LINKS_IN_KEY_VALUE} from './constants'; import KeyValueContextMenuContainer from './KeyValueContextMenuContainer'; import {ContextMenuContext} from '../context'; import type {ContextMenuContextType} from '../context'; import type {InspectedElement} from 'react-devtools-shared/src/frontend/types'; import type {Element} from 'react-devtools-shared/src/frontend/types'; import type {Element as ReactElement} from 'react'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; // $FlowFixMe[method-unbinding] const hasOwnProperty = Object.prototype.hasOwnProperty; type Type = 'props' | 'state' | 'context' | 'hooks'; type KeyValueProps = { alphaSort: boolean, bridge: FrontendBridge, canDeletePaths: boolean, canEditValues: boolean, canRenamePaths: boolean, canRenamePathsAtDepth?: (depth: number) => boolean, depth: number, element: Element, hidden: boolean, hookID?: ?number, hookName?: ?string, inspectedElement: InspectedElement, isDirectChildOfAnArray?: boolean, name: string, path: Array<any>, pathRoot: Type, store: Store, value: any, }; export default function KeyValue({ alphaSort, bridge, canDeletePaths, canEditValues, canRenamePaths, canRenamePathsAtDepth, depth, element, inspectedElement, isDirectChildOfAnArray, hidden, hookID, hookName, name, path, pathRoot, store, value, }: KeyValueProps): React.Node { const {readOnly: readOnlyGlobalFlag} = useContext(OptionsContext); canDeletePaths = !readOnlyGlobalFlag && canDeletePaths; canEditValues = !readOnlyGlobalFlag && canEditValues; canRenamePaths = !readOnlyGlobalFlag && canRenamePaths; const {id} = inspectedElement; const fullPath = useMemo(() => [pathRoot, ...path], [pathRoot, path]); const [isOpen, setIsOpen] = useState<boolean>(false); const contextMenuTriggerRef = useRef(null); const {inspectPaths} = useContext(InspectedElementContext); const {viewAttributeSourceFunction} = useContext<ContextMenuContextType>(ContextMenuContext); let isInspectable = false; let isReadOnlyBasedOnMetadata = false; if (value !== null && typeof value === 'object') { isInspectable = value[meta.inspectable] && value[meta.size] !== 0; isReadOnlyBasedOnMetadata = value[meta.readonly]; } const [isInspectPathsPending, startInspectPathsTransition] = useTransition(); const toggleIsOpen = () => { if (isOpen) { setIsOpen(false); } else { setIsOpen(true); if (isInspectable) { startInspectPathsTransition(() => { inspectPaths([pathRoot, ...path]); }); } } }; const dataType = typeof value; const isSimpleType = dataType === 'number' || dataType === 'string' || dataType === 'boolean' || value == null; const pathType = value !== null && typeof value === 'object' && hasOwnProperty.call(value, meta.type) ? value[meta.type] : typeof value; const pathIsFunction = pathType === 'function'; const style = { paddingLeft: `${(depth - 1) * 0.75}rem`, }; const overrideValue = (newPath: Array<string | number>, newValue: any) => { if (hookID != null) { newPath = parseHookPathForEdit(newPath); } const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('overrideValueAtPath', { hookID, id, path: newPath, rendererID, type: pathRoot, value: newValue, }); } }; const deletePath = (pathToDelete: Array<string | number>) => { if (hookID != null) { pathToDelete = parseHookPathForEdit(pathToDelete); } const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('deletePath', { hookID, id, path: pathToDelete, rendererID, type: pathRoot, }); } }; const renamePath = ( oldPath: Array<string | number>, newPath: Array<string | number>, ) => { if (newPath[newPath.length - 1] === '') { // Deleting the key suggests an intent to delete the whole path. if (canDeletePaths) { deletePath(oldPath); } } else { if (hookID != null) { oldPath = parseHookPathForEdit(oldPath); newPath = parseHookPathForEdit(newPath); } const rendererID = store.getRendererIDForElement(id); if (rendererID !== null) { bridge.send('renamePath', { hookID, id, newPath, oldPath, rendererID, type: pathRoot, }); } } }; // TRICKY This is a bit of a hack to account for context and hooks. // In these cases, paths can be renamed but only at certain depths. // The special "value" wrapper for context shouldn't be editable. // Only certain types of hooks should be editable. let canRenameTheCurrentPath = canRenamePaths; if (canRenameTheCurrentPath && typeof canRenamePathsAtDepth === 'function') { canRenameTheCurrentPath = canRenamePathsAtDepth(depth); } const hasChildren = typeof value === 'object' && value !== null && (canEditValues || (isArray(value) && value.length > 0) || Object.entries(value).length > 0); let renderedName; if (isDirectChildOfAnArray) { if (canDeletePaths) { renderedName = ( <DeleteToggle name={name} deletePath={deletePath} path={path} /> ); } else { renderedName = ( <span className={styles.Name} onClick={isInspectable || hasChildren ? toggleIsOpen : null}> {name} {!!hookName && <span className={styles.HookName}>({hookName})</span>} <span className={styles.AfterName}>:</span> </span> ); } } else if (canRenameTheCurrentPath) { renderedName = ( <> <EditableName allowEmpty={canDeletePaths} className={styles.EditableName} initialValue={name} overrideName={renamePath} path={path} /> <span className={styles.AfterName}>:</span> </> ); } else { renderedName = ( <span className={styles.Name} data-testname="NonEditableName" onClick={isInspectable || hasChildren ? toggleIsOpen : null}> {name} {!!hookName && <span className={styles.HookName}>({hookName})</span>} <span className={styles.AfterName}>:</span> </span> ); } let children = null; if (isSimpleType) { let displayValue = value; if (dataType === 'string') { displayValue = `"${value}"`; } else if (dataType === 'boolean') { displayValue = value ? 'true' : 'false'; } else if (value === null) { displayValue = 'null'; } else if (value === undefined) { displayValue = 'undefined'; } else if (isNaN(value)) { displayValue = 'NaN'; } let shouldDisplayValueAsLink = false; if ( dataType === 'string' && PROTOCOLS_SUPPORTED_AS_LINKS_IN_KEY_VALUE.some(protocolPrefix => value.startsWith(protocolPrefix), ) ) { shouldDisplayValueAsLink = true; } children = ( <KeyValueContextMenuContainer key="root" anchorElementRef={contextMenuTriggerRef} attributeSourceCanBeInspected={false} canBeCopiedToClipboard={true} store={store} bridge={bridge} id={id} path={fullPath}> <div data-testname="KeyValue" className={styles.Item} hidden={hidden} ref={contextMenuTriggerRef} style={style}> <div className={styles.ExpandCollapseToggleSpacer} /> {renderedName} {canEditValues ? ( <EditableValue overrideValue={overrideValue} path={path} value={value} /> ) : shouldDisplayValueAsLink ? ( <a className={styles.Link} href={value} target="_blank" rel="noopener noreferrer"> {displayValue} </a> ) : ( <span className={styles.Value} data-testname="NonEditableValue"> {displayValue} </span> )} </div> </KeyValueContextMenuContainer> ); } else if (pathIsFunction && viewAttributeSourceFunction != null) { children = ( <KeyValueContextMenuContainer key="root" anchorElementRef={contextMenuTriggerRef} attributeSourceCanBeInspected={true} canBeCopiedToClipboard={false} store={store} bridge={bridge} id={id} path={fullPath}> <div data-testname="KeyValue" className={styles.Item} hidden={hidden} ref={contextMenuTriggerRef} style={style}> <div className={styles.ExpandCollapseToggleSpacer} /> {renderedName} <span className={styles.Link} onClick={() => { viewAttributeSourceFunction(id, fullPath); }}> {getMetaValueLabel(value)} </span> </div> </KeyValueContextMenuContainer> ); } else if ( hasOwnProperty.call(value, meta.type) && !hasOwnProperty.call(value, meta.unserializable) ) { children = ( <KeyValueContextMenuContainer key="root" anchorElementRef={contextMenuTriggerRef} attributeSourceCanBeInspected={false} canBeCopiedToClipboard={true} store={store} bridge={bridge} id={id} path={fullPath}> <div data-testname="KeyValue" className={styles.Item} hidden={hidden} ref={contextMenuTriggerRef} style={style}> {isInspectable ? ( <ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} /> ) : ( <div className={styles.ExpandCollapseToggleSpacer} /> )} {renderedName} <span className={styles.Value} onClick={isInspectable ? toggleIsOpen : undefined}> {getMetaValueLabel(value)} </span> </div> </KeyValueContextMenuContainer> ); if (isInspectPathsPending) { children = ( <> {children} <div className={styles.Item} style={style}> <div className={styles.ExpandCollapseToggleSpacer} /> <LoadingAnimation /> </div> </> ); } } else { if (isArray(value)) { const displayName = getMetaValueLabel(value); children = value.map((innerValue, index) => ( <KeyValue key={index} alphaSort={alphaSort} bridge={bridge} canDeletePaths={canDeletePaths && !isReadOnlyBasedOnMetadata} canEditValues={canEditValues && !isReadOnlyBasedOnMetadata} canRenamePaths={canRenamePaths && !isReadOnlyBasedOnMetadata} canRenamePathsAtDepth={canRenamePathsAtDepth} depth={depth + 1} element={element} hookID={hookID} inspectedElement={inspectedElement} isDirectChildOfAnArray={true} hidden={hidden || !isOpen} name={index} path={path.concat(index)} pathRoot={pathRoot} store={store} value={value[index]} /> )); if (canEditValues && !isReadOnlyBasedOnMetadata) { children.push( <NewArrayValue key="NewKeyValue" bridge={bridge} depth={depth + 1} hidden={hidden || !isOpen} hookID={hookID} index={value.length} element={element} inspectedElement={inspectedElement} path={path} store={store} type={pathRoot} />, ); } children.unshift( <KeyValueContextMenuContainer key={`${depth}-root`} anchorElementRef={contextMenuTriggerRef} attributeSourceCanBeInspected={pathIsFunction} canBeCopiedToClipboard={!pathIsFunction} store={store} bridge={bridge} id={id} path={fullPath}> <div data-testname="KeyValue" className={styles.Item} hidden={hidden} ref={contextMenuTriggerRef} style={style}> {hasChildren ? ( <ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} /> ) : ( <div className={styles.ExpandCollapseToggleSpacer} /> )} {renderedName} <span className={styles.Value} onClick={hasChildren ? toggleIsOpen : undefined}> {displayName} </span> </div> </KeyValueContextMenuContainer>, ); } else { // TRICKY // It's important to use Object.entries() rather than Object.keys() // because of the hidden meta Symbols used for hydration and unserializable values. const entries = Object.entries(value); if (alphaSort) { entries.sort(alphaSortEntries); } const displayName = getMetaValueLabel(value); children = entries.map(([key, keyValue]): ReactElement<any> => ( <KeyValue key={key} alphaSort={alphaSort} bridge={bridge} canDeletePaths={canDeletePaths && !isReadOnlyBasedOnMetadata} canEditValues={canEditValues && !isReadOnlyBasedOnMetadata} canRenamePaths={canRenamePaths && !isReadOnlyBasedOnMetadata} canRenamePathsAtDepth={canRenamePathsAtDepth} depth={depth + 1} element={element} hookID={hookID} inspectedElement={inspectedElement} hidden={hidden || !isOpen} name={key} path={path.concat(key)} pathRoot={pathRoot} store={store} value={keyValue} /> )); if (canEditValues && !isReadOnlyBasedOnMetadata) { children.push( <NewKeyValue key="NewKeyValue" bridge={bridge} depth={depth + 1} element={element} hidden={hidden || !isOpen} hookID={hookID} inspectedElement={inspectedElement} path={path} store={store} type={pathRoot} />, ); } children.unshift( <KeyValueContextMenuContainer key={`${depth}-root`} anchorElementRef={contextMenuTriggerRef} attributeSourceCanBeInspected={pathIsFunction} canBeCopiedToClipboard={!pathIsFunction} store={store} bridge={bridge} id={id} path={fullPath}> <div data-testname="KeyValue" className={styles.Item} hidden={hidden} ref={contextMenuTriggerRef} style={style}> {hasChildren ? ( <ExpandCollapseToggle isOpen={isOpen} setIsOpen={toggleIsOpen} /> ) : ( <div className={styles.ExpandCollapseToggleSpacer} /> )} {renderedName} <span className={styles.Value} onClick={hasChildren ? toggleIsOpen : undefined}> {displayName} </span> </div> </KeyValueContextMenuContainer>, ); } } return children; } // $FlowFixMe[missing-local-annot] function DeleteToggle({deletePath, name, path}) { // $FlowFixMe[missing-local-annot] const handleClick = event => { event.stopPropagation(); deletePath(path); }; return ( <> <Button className={styles.DeleteArrayItemButton} onClick={handleClick} title="Delete entry"> <ButtonIcon type="delete" /> </Button> <span className={styles.Name}> {name} <span className={styles.AfterName}>:</span> </span> </> ); }