/** * Copyright (c) Facebook, Inc. and its 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} 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 useContextMenu from '../../ContextMenu/useContextMenu'; 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 type {InspectedElement} from './types'; import type {Element} from 'react-devtools-shared/src/devtools/views/Components/types'; import type {Element as ReactElement} from 'react'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; 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, 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) { const {readOnly: readOnlyGlobalFlag} = useContext(OptionsContext); canDeletePaths = !readOnlyGlobalFlag && canDeletePaths; canEditValues = !readOnlyGlobalFlag && canEditValues; canRenamePaths = !readOnlyGlobalFlag && canRenamePaths; const {id} = inspectedElement; const [isOpen, setIsOpen] = useState(false); const contextMenuTriggerRef = useRef(null); const {inspectPaths} = useContext(InspectedElementContext); 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]); }); } } }; useContextMenu({ data: { path: [pathRoot, ...path], type: value !== null && typeof value === 'object' && hasOwnProperty.call(value, meta.type) ? value[meta.type] : typeof value, }, id: 'InspectedElement', ref: contextMenuTriggerRef, }); const dataType = typeof value; const isSimpleType = dataType === 'number' || dataType === 'string' || dataType === 'boolean' || value == null; const style = { paddingLeft: `${(depth - 1) * 0.75}rem`, }; const overrideValue = (newPath, newValue) => { 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 => { 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, newPath) => { 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); } let renderedName; if (isDirectChildOfAnArray) { if (canDeletePaths) { renderedName = ( ); } else { renderedName = ( {name} {!!hookName && ({hookName})} ); } } else if (canRenameTheCurrentPath) { renderedName = ( ); } else { renderedName = ( {name} {!!hookName && ({hookName})} ); } 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'; } let shouldDisplayValueAsLink = false; if ( dataType === 'string' && PROTOCOLS_SUPPORTED_AS_LINKS_IN_KEY_VALUE.some(protocolPrefix => value.startsWith(protocolPrefix), ) ) { shouldDisplayValueAsLink = true; } children = (