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 : }
|