LCOV - code coverage report
Current view: top level - dashql-app/src/ui/json - json_nested_open.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 97.7 % 129 126
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : import * as styles from './json_view.module.css';
       3              : 
       4              : import { useNestedExpansionDispatch, useNestedExpansionState } from './json_nested_state.js';
       5              : import { useJsonViewerState } from './json_view_state.js';
       6              : import { JsonCopyButton } from './json_copy_button.js';
       7              : import { JsonItemCount } from './json_item_count.js';
       8              : import { JsonArrowSymbol } from './json_arrow_symbol.js';
       9              : import { JsonEllipsisSymbol } from './json_ellipsis_symbol.js';
      10              : import { JsonKeyName } from './json_key_name.js';
      11              : 
      12              : export interface NestedOpenProps {
      13              :     expandKey: string;
      14              :     keyName?: (string | number);
      15              :     keyPath: (string | number)[];
      16              :     level: number;
      17              :     value: object;
      18              :     initialValue: object;
      19              :     parentValue?: object;
      20              : }
      21              : 
      22            1 : export function JsonNestedOpen(props: NestedOpenProps) {
      23              :     // Is the node expanded?
      24            1 :     const nodeExpansions = useNestedExpansionState();
      25            1 :     const dispatchExpands = useNestedExpansionDispatch();
      26            1 :     const { variant, onExpand, collapsed, shouldExpandNodeInitially } = useJsonViewerState();
      27            1 :     const defaultExpanded =
      28            1 :         typeof collapsed === 'boolean' ? !collapsed : typeof collapsed === 'number' ? props.level <= collapsed : true;
      29            1 :     let isExpanded = nodeExpansions[props.expandKey] ?? (shouldExpandNodeInitially ? true : defaultExpanded);
      30            1 :     const shouldExpand =
      31            1 :         shouldExpandNodeInitially && shouldExpandNodeInitially(isExpanded, {
      32            1 :             level: props.level,
      33            1 :             keyName: props.keyName,
      34            1 :             keyPath: props.keyPath,
      35            1 :             value: props.value,
      36            1 :             parentValue: props.parentValue
      37            1 :         });
      38            1 :     if (nodeExpansions[props.expandKey] === undefined && shouldExpandNodeInitially) {
      39            1 :         isExpanded = shouldExpand === true;
      40              :     }
      41              : 
      42              :     // Click handler
      43            1 :     const click = () => {
      44            1 :         const opt = { expand: !isExpanded, value: props.value, keyName: props.keyName };
      45            1 :         onExpand && onExpand(opt);
      46            1 :         dispatchExpands({ [props.expandKey]: opt.expand });
      47            1 :     };
      48              : 
      49            1 :     const style: React.CSSProperties = { display: 'inline-flex', alignItems: 'center' };
      50            1 :     const len = Object.keys(props.value ?? {}).length;
      51            1 :     const isObject = typeof props.value === 'object';
      52            1 :     const isArray = Array.isArray(props.value);
      53            1 :     const isMySet = props.value instanceof Set;
      54            1 :     const showArrow = len !== 0 && (isArray || isMySet || isObject);
      55            1 :     const childProps = {
      56            1 :         keyName: props.keyName,
      57            1 :         keyPath: props.keyPath,
      58            1 :         value: props.value,
      59            1 :         parentValue: props.parentValue
      60            1 :     };
      61            1 :     if (variant === 'explorer') {
      62              :         // The whole row toggles the node, the arrow button stays the accessible control
      63            1 :         return (
      64            1 :             <div className={styles.explorer_row} onClick={showArrow ? click : undefined}>
      65            1 :                 {showArrow ? (
      66            1 :                     <button
      67            1 :                         type="button"
      68            1 :                         className={styles.expand_button}
      69            1 :                         aria-label={isExpanded ? 'Collapse value' : 'Expand value'}
      70            1 :                         aria-expanded={isExpanded}
      71            1 :                         onClick={(e) => { e.stopPropagation(); click(); }}
      72              :                     >
      73            1 :                         <JsonArrowSymbol isExpanded={isExpanded} />
      74            1 :                     </button>
      75              :                 ) : (
      76            1 :                     <span className={styles.explorer_arrow_spacer} />
      77              :                 )}
      78            1 :                 {(props.keyName || typeof props.keyName === 'number') && (
      79            1 :                     <JsonKeyName {...childProps} />
      80              :                 )}
      81            1 :                 <ExplorerSummary value={props.value} initialValue={props.initialValue} />
      82            1 :                 <JsonCopyButton
      83            1 :                     keyName={props.keyName!}
      84            1 :                     value={props.value}
      85            1 :                     expandKey={props.expandKey}
      86            1 :                     parentValue={props.parentValue}
      87            1 :                     keyPath={props.keyPath}
      88            1 :                 />
      89            1 :             </div>
      90              :         );
      91              :     }
      92            1 :     return (
      93            1 :         <span style={style}>
      94            1 :             {showArrow && (
      95            1 :                 <button
      96            1 :                     type="button"
      97            1 :                     className={styles.expand_button}
      98            1 :                     aria-label={isExpanded ? 'Collapse JSON value' : 'Expand JSON value'}
      99            1 :                     aria-expanded={isExpanded}
     100            1 :                     onClick={click}
     101              :                 >
     102            1 :                     <JsonArrowSymbol isExpanded={isExpanded} />
     103            1 :                 </button>
     104              :             )}
     105            1 :             {(props.keyName || typeof props.keyName === 'number') && (
     106            1 :                 <JsonKeyName {...childProps} />
     107              :             )}
     108            1 :             <SetHeader value={props.initialValue} />
     109            1 :             <MapHeader value={props.initialValue} />
     110            1 :             <JsonBracketsOpen isBrackets={isArray || isMySet} />
     111            1 :             <JsonEllipsisSymbol
     112            1 :                 keyName={props.keyName!}
     113            1 :                 value={props.value}
     114            1 :                 isExpanded={isExpanded}
     115            1 :             />
     116            1 :             <JsonBracketsClose isVisible={!isExpanded || !showArrow} isBrackets={isArray || isMySet} />
     117            1 :             <JsonItemCount value={props.value} />
     118            1 :             <JsonCopyButton
     119            1 :                 keyName={props.keyName!}
     120            1 :                 value={props.value}
     121            1 :                 expandKey={props.expandKey}
     122            1 :                 parentValue={props.parentValue}
     123            1 :                 keyPath={props.keyPath}
     124            1 :             />
     125            1 :         </span>
     126              :     );
     127            1 : };
     128              : 
     129              : /// The explorer describes nested values instead of showing their brackets
     130            1 : function ExplorerSummary(props: { value: object; initialValue: object }) {
     131            1 :     const len = Object.keys(props.value ?? {}).length;
     132            1 :     if (len === 0) {
     133            1 :         return <span className={styles.explorer_placeholder}>empty</span>;
     134              :     }
     135            1 :     let noun: string;
     136            1 :     if (props.initialValue instanceof Map) {
     137            0 :         noun = len === 1 ? 'entry' : 'entries';
     138            1 :     } else if (Array.isArray(props.value) || props.initialValue instanceof Set) {
     139            1 :         noun = len === 1 ? 'item' : 'items';
     140              :     } else {
     141            1 :         noun = len === 1 ? 'field' : 'fields';
     142              :     }
     143            1 :     return <span className={styles.explorer_summary}>{len} {noun}</span>;
     144            1 : }
     145              : 
     146            1 : function SetHeader(props: { value: unknown; }) {
     147            1 :     const isSet = props.value instanceof Set;
     148            1 :     if (!isSet) return null;
     149            0 :     return <span className={styles.object_header_set} data-type="set">Set</span>;
     150            1 : };
     151              : 
     152            1 : function MapHeader(props: { value: unknown; }) {
     153            1 :     const isMap = props.value instanceof Map;
     154            1 :     if (!isMap) return null;
     155            0 :     return <span className={styles.object_header_set} data-type="map">Map</span>;
     156            1 : };
     157              : 
     158            1 : export function JsonBracketsOpen(props: { isBrackets: boolean }) {
     159            1 :     return props.isBrackets
     160              :         ? (
     161            1 :             <span className={styles.object_square_brackets_open}>
     162              :                 [
     163            1 :             </span>
     164              :         )
     165              :         : (
     166            1 :             <span className={styles.object_curly_brackets_open}>
     167            1 :                 {'{'}
     168            1 :             </span>
     169              :         );
     170            1 : };
     171              : 
     172            1 : export function JsonBracketsClose(props: { isBrackets: boolean, isVisible: boolean }) {
     173            1 :     if (!props.isVisible) return null;
     174            1 :     return props.isBrackets
     175              :         ? (
     176            1 :             <span className={styles.object_curly_brackets_open}>
     177              :                 ]
     178            1 :             </span>
     179              :         )
     180              :         : (
     181            1 :             <span className={styles.object_curly_brackets_close}>
     182            1 :                 {'}'}
     183            1 :             </span>
     184              :         );
     185            1 : };
        

Generated by: LCOV version 2.0-1