LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - keyvalue_list.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 97 97
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import * as styles from './keyvalue_list.module.css';
       4              : 
       5              : import { Dispatch } from '../../utils/variant.js';
       6              : import { PlusIcon, XIcon } from './symbol_icon.js';
       7              : import { classNames } from '../../utils/classnames.js';
       8              : import { TextInput } from './text_input.js';
       9              : import { TextInputAction } from './text_input_action.js';
      10              : import { ButtonSize, ButtonVariant, IconButton } from './button.js';
      11              : import { CopyToClipboardButton } from './clipboard.js';
      12              : 
      13              : export interface KeyValueListElement {
      14              :     key: string;
      15              :     value: string;
      16              : }
      17              : 
      18              : export type UpdateKeyValueList = (prev: KeyValueListElement[]) => KeyValueListElement[];
      19              : 
      20              : interface Props {
      21              :     className?: string;
      22              :     title: string;
      23              :     caption: string;
      24              :     keyIcon: React.ElementType;
      25              :     valueIcon: React.ElementType;
      26              :     addButtonLabel: string;
      27              :     elements: readonly KeyValueListElement[];
      28              :     modifyElements: Dispatch<UpdateKeyValueList>;
      29              :     disabled?: boolean;
      30              :     readOnly?: boolean;
      31              :     sideBySide?: boolean;
      32              :     copyValue?: (element: KeyValueListElement) => string;
      33              :     copyLogContext?: string;
      34              : }
      35              : 
      36            1 : export const KeyValueListBuilder: React.FC<Props> = (props: Props) => {
      37            1 :     const appendElement = () => props.modifyElements(list => {
      38            1 :         const copy = [...list];
      39            1 :         copy.push({
      40            1 :             key: "",
      41            1 :             value: "",
      42            1 :         });
      43            1 :         return copy;
      44            1 :     });
      45            1 :     const deleteIndex = (index: number) => props.modifyElements(list => {
      46            1 :         const copy = [...list];
      47            1 :         copy.splice(index, 1);
      48            1 :         return copy;
      49            1 :     });
      50            1 :     const modifyElement = (index: number, key: string, value: string) => props.modifyElements(list => {
      51            1 :         const copy = [...list];
      52            1 :         copy[index] = { key, value };
      53            1 :         return copy;
      54            1 :     });
      55            1 :     return (
      56            1 :         <div className={classNames(props.className, styles.list, {
      57            1 :             [styles.list_side_by_side]: props.sideBySide,
      58            1 :         })}>
      59            1 :             <div className={styles.list_name}>
      60            1 :                 {props.title}
      61            1 :             </div>
      62            1 :             <div className={styles.list_caption}>
      63            1 :                 {props.caption}
      64            1 :             </div>
      65            1 :             {!props.readOnly && (
      66            1 :                 <IconButton
      67            1 :                     className={styles.add_button}
      68            1 :                     aria-label={props.addButtonLabel}
      69            1 :                     onClick={appendElement}
      70            1 :                     disabled={props.disabled}
      71            1 :                     size={ButtonSize.Small}
      72              :                 >
      73            1 :                     <PlusIcon />
      74            1 :                 </IconButton>
      75              :             )}
      76            1 :             <div className={styles.list_elements} role="list">
      77            1 :                 {props.elements.map((elem, i) => (
      78            1 :                     <div key={i} className={classNames(styles.element, {
      79            1 :                         [styles.element_side_by_side]: props.sideBySide,
      80            1 :                     })} role="listitem">
      81            1 :                         <TextInput
      82            1 :                             block
      83            1 :                             className={classNames(styles.path, {
      84            1 :                                 [styles.path_side_by_side]: props.sideBySide,
      85            1 :                             })}
      86            1 :                             value={elem.key}
      87            1 :                             onChange={(ev: any) => modifyElement(i, ev.target.value, elem.value)}
      88            1 :                             aria-label={`${props.title} name ${i + 1}`}
      89            1 :                             leadingVisual={props.keyIcon}
      90            1 :                             trailingAction={!props.readOnly ? (
      91            1 :                                 <TextInputAction
      92            1 :                                     aria-label="Clear input"
      93            1 :                                     aria-labelledby=""
      94            1 :                                     onClick={() => deleteIndex(i)}
      95              :                                 >
      96            1 :                                     <XIcon />
      97            1 :                                 </TextInputAction>
      98            1 :                             ) : undefined}
      99            1 :                             disabled={props.disabled}
     100            1 :                             readOnly={props.readOnly || props.disabled}
     101            1 :                         />
     102            1 :                         {!props.sideBySide && <div className={styles.aliaslink} />}
     103            1 :                         <TextInput
     104            1 :                             block
     105            1 :                             className={classNames(styles.alias, {
     106            1 :                                 [styles.alias_side_by_side]: props.sideBySide,
     107            1 :                             })}
     108            1 :                             value={elem.value}
     109            1 :                             onChange={(ev: any) => modifyElement(i, elem.key, ev.target.value)}
     110            1 :                             aria-label={`${props.title} value ${i + 1}`}
     111            1 :                             leadingVisual={props.valueIcon}
     112            1 :                             disabled={props.disabled}
     113            1 :                             readOnly={props.readOnly || props.disabled}
     114            1 :                         />
     115            1 :                         {props.copyValue && props.copyLogContext && (
     116            1 :                             <CopyToClipboardButton
     117            1 :                                 className={styles.copy_action}
     118            1 :                                 aria-label={`Copy ${props.title} ${i + 1}`}
     119            1 :                                 aria-labelledby=""
     120            1 :                                 data-testid="keyvalue-copy"
     121            1 :                                 value={props.copyValue(elem)}
     122            1 :                                 logContext={props.copyLogContext}
     123            1 :                                 variant={ButtonVariant.Invisible}
     124            1 :                                 size={ButtonSize.Small}
     125            1 :                             />
     126              :                         )}
     127            1 :                     </div>))}
     128            1 :             </div>
     129            1 :         </div>
     130              :     );
     131            1 : };
     132              : 
     133            1 : export function flattenKeyValueList(list: KeyValueListElement[]): Record<string, string> {
     134            1 :     const out: Record<string, string> = {};
     135            1 :     for (const kv of list) {
     136            1 :         out[kv.key] = kv.value;
     137              :     }
     138            1 :     return out;
     139            1 : }
        

Generated by: LCOV version 2.0-1