src/devtools/views/StoreInspector/KeyValue.js (206 lines of code) (raw):
/**
* 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 { useCallback, useEffect, useMemo, useState } from 'react';
import type { Element } from 'react';
// import EditableValue from './EditableValue';
import ExpandCollapseToggle from '../Components/ExpandCollapseToggle';
import { alphaSortEntries } from '../utils';
import styles from './KeyValue.css';
import type { StoreRecords } from '../../../types.js';
type KeyValueProps = {|
alphaSort: boolean,
depth: number,
hidden?: boolean,
keyName: string,
path: Array<any>,
value: mixed,
records: StoreRecords,
setSelectedRecordID: (id: string) => void,
|};
const REF = '__ref';
const REFS = '__refs';
function getDisplayValue(value) {
return typeof value === 'string'
? `"${value}"`
: typeof value === 'boolean' || typeof value === 'number'
? value.toString()
: value === null
? 'null'
: 'undefined';
}
export default function KeyValue({
alphaSort,
depth,
hidden,
keyName,
path,
value,
records,
setSelectedRecordID,
}: KeyValueProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [wasEverOpen, setWasEverOpen] = useState<boolean>(isOpen);
useEffect(() => {
if (isOpen && !wasEverOpen) {
setWasEverOpen(true);
}
}, [isOpen, wasEverOpen]);
const sortedEntries = useMemo(
() =>
value !== null && typeof value === 'object'
? alphaSort
? Object.entries(value).sort(alphaSortEntries)
: Object.entries(value)
: [],
[value, alphaSort]
);
const selectRecordID = useCallback(() => {
// When this is called, the variable value is the next record referenced by the current record.
const nextRecID =
value !== null &&
typeof value === 'object' &&
value.hasOwnProperty('__id')
? ((value.__id: any): string)
: null;
if (nextRecID != null) {
setSelectedRecordID(nextRecID);
}
}, [value, setSelectedRecordID]);
const toggleIsOpen = useCallback(() => {
setIsOpen(!isOpen);
}, [isOpen]);
const isSimpleType = typeof value !== 'object' && value !== null;
const style = {
paddingLeft: `${(depth - 1) * 0.75}rem`,
};
// If the value is a reference to another record
// Edge case: the id or __id of a record is a reference but we don't want to
// expand it
if (
value !== null &&
records.hasOwnProperty(value) &&
typeof value === 'string' &&
records[value] != null &&
(keyName === REF || path[path.length - 2] === REFS)
) {
const referencedRecord = records[value];
return (
<div key="root" className={styles.Item} hidden={hidden} style={style}>
<span className={styles.Value}>
<KeyValue
key={keyName}
alphaSort={true}
depth={depth + 1}
keyName={keyName}
path={[keyName]}
value={referencedRecord}
records={records}
setSelectedRecordID={setSelectedRecordID}
/>
</span>
</div>
);
} else if (isSimpleType) {
const displayValue = getDisplayValue(value);
return (
<div key="root" className={styles.Item} hidden={hidden} style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
<span className={styles.Name}>{keyName}</span>
<span className={styles.Value}>{displayValue}</span>
</div>
);
} else {
if (Array.isArray(value)) {
const hasChildren = value.length > 0;
const children = wasEverOpen
? value.map<React.Node>((innerValue, mapIndex): React.Node => (
<KeyValue
key={mapIndex}
alphaSort={alphaSort}
depth={depth + 1}
hidden={hidden || !isOpen}
keyName={mapIndex.toString()}
path={path.concat(mapIndex)}
value={value[mapIndex]}
records={records}
setSelectedRecordID={setSelectedRecordID}
/>
))
: [];
children.unshift(
<div
key={`${depth}-root`}
className={styles.Item}
hidden={hidden}
style={style}
>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
<span
className={styles.Name}
onClick={hasChildren ? toggleIsOpen : undefined}
>
{keyName}
</span>
<span>
Array
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
</span>
</div>
);
return children;
} else {
const hasChildren = sortedEntries.length > 0;
// sortedEntries contains an array of tuples which contain a string and a value.
// In some cases, the string is a __ref. In this case, there should be no other values
// within the array because __ref is a reference to another record.
const entryReference = sortedEntries[0];
const recordFieldKey = entryReference == null ? null : entryReference[0];
const nextReferencedRecordID =
entryReference == null ? null : entryReference[1];
// In the case of a reference to another record, we want the displayName/value, to be
// the id of the next record, so that users can click on it like a link
const displayName =
sortedEntries === []
? 'Object'
: recordFieldKey !== REF || path[path.length - 2] === REFS
? keyName === REF &&
value !== null &&
typeof value === 'object' &&
value.hasOwnProperty('__id')
? ((value.__id: any): string)
: 'Object'
: typeof nextReferencedRecordID === 'string' &&
records[nextReferencedRecordID] != null
? ((records[nextReferencedRecordID].__typename: any): string)
: 'Object';
const children = wasEverOpen
? sortedEntries.map<Element<any>>(([entriesName, entriesVal]) => (
<KeyValue
key={entriesName}
alphaSort={alphaSort}
depth={depth + 1}
hidden={hidden || !isOpen}
keyName={entriesName}
path={path.concat(entriesName)}
value={entriesVal}
records={records}
setSelectedRecordID={setSelectedRecordID}
/>
))
: [];
children.unshift(
<div
key={`${depth}-root`}
className={styles.Item}
hidden={hidden}
style={style}
>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
<span
className={styles.Name}
onClick={hasChildren ? toggleIsOpen : undefined}
>
{keyName}
</span>
<span onClick={keyName === REF ? selectRecordID : null}>
{`${displayName || ''} `}
{hasChildren ? '' : <span className={styles.Empty}>(empty)</span>}
</span>
</div>
);
return children;
}
}
}