@buerokratt-ria/common-gui-components 0.0.58 → 0.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/templates/history-page/src/History.scss +15 -1
- package/templates/history-page/src/components/ChatMetadataPanel/ChatMetadataPanel.scss +18 -0
- package/templates/history-page/src/components/ChatMetadataPanel/index.tsx +206 -0
- package/templates/history-page/src/components/ChatMetadataPanelItem/index.tsx +17 -0
- package/templates/history-page/src/components/FilterTag/FilterTag.scss +42 -0
- package/templates/history-page/src/components/FilterTag/index.tsx +16 -0
- package/templates/history-page/src/components/HeaderCombobox/index.tsx +67 -0
- package/templates/history-page/src/components/QualitySettings/QualitySettings.scss +19 -0
- package/templates/history-page/src/components/QualitySettings/index.tsx +115 -0
- package/templates/history-page/src/components/SelectedFilterTags/SelectedFilterTags.scss +36 -0
- package/templates/history-page/src/components/SelectedFilterTags/index.tsx +224 -0
- package/templates/history-page/src/components/index.tsx +6 -0
- package/templates/history-page/src/index.tsx +946 -207
- package/templates/history-page/src/types/index.ts +17 -0
- package/translations/en/common.json +22 -2
- package/translations/et/common.json +22 -2
- package/types/chat.ts +3 -0
- package/ui-components/DataTable/index.tsx +0 -1
- package/ui-components/FormElements/FormCombobox/FormCombobox.scss +263 -0
- package/ui-components/FormElements/FormCombobox/index.tsx +393 -0
- package/ui-components/FormElements/index.tsx +1 -0
- package/ui-components/Icon/index.tsx +1 -0
- package/ui-components/index.tsx +2 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
FC,
|
|
4
|
+
FocusEvent as ReactFocusEvent,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useEffect,
|
|
7
|
+
useId,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
useState
|
|
12
|
+
} from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
14
|
+
import clsx from 'clsx';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
16
|
+
import { MdArrowDropDown, MdExpandMore, MdSearch } from 'react-icons/md';
|
|
17
|
+
|
|
18
|
+
import { Icon } from '../..';
|
|
19
|
+
import './FormCombobox.scss';
|
|
20
|
+
|
|
21
|
+
type FormComboboxOption = {
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly value: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type FormComboboxBaseProps = {
|
|
27
|
+
readonly label?: ReactNode;
|
|
28
|
+
readonly name?: string;
|
|
29
|
+
readonly placeholder?: string;
|
|
30
|
+
readonly searchPlaceholder?: string;
|
|
31
|
+
readonly options: FormComboboxOption[];
|
|
32
|
+
readonly hideLabel?: boolean;
|
|
33
|
+
readonly disabled?: boolean;
|
|
34
|
+
readonly style?: CSSProperties;
|
|
35
|
+
readonly isSearchEnabled?: boolean;
|
|
36
|
+
readonly hideInputStyle?: boolean;
|
|
37
|
+
readonly isMenuPortaled?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type FormComboboxSingleProps = FormComboboxBaseProps & {
|
|
41
|
+
readonly multiple?: false;
|
|
42
|
+
readonly value?: string;
|
|
43
|
+
readonly defaultValue?: string;
|
|
44
|
+
readonly onChange?: (value: string) => void;
|
|
45
|
+
readonly onSelectionChange?: (selection: FormComboboxOption | null) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type FormComboboxMultipleProps = FormComboboxBaseProps & {
|
|
49
|
+
readonly multiple: true;
|
|
50
|
+
readonly value?: string[];
|
|
51
|
+
readonly defaultValue?: string[];
|
|
52
|
+
readonly onChange?: (value: string[]) => void;
|
|
53
|
+
readonly onSelectionChange?: (selection: FormComboboxOption[] | null) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type FormComboboxProps = FormComboboxSingleProps | FormComboboxMultipleProps;
|
|
57
|
+
|
|
58
|
+
type FormComboboxValueProps =
|
|
59
|
+
| {
|
|
60
|
+
readonly multiple?: false;
|
|
61
|
+
readonly value?: string;
|
|
62
|
+
readonly defaultValue?: string;
|
|
63
|
+
}
|
|
64
|
+
| {
|
|
65
|
+
readonly multiple: true;
|
|
66
|
+
readonly value?: string[];
|
|
67
|
+
readonly defaultValue?: string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getInitialSingleValue = (props: FormComboboxValueProps): string => {
|
|
71
|
+
if (props.multiple === true) {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return props.defaultValue ?? '';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getInitialMultipleValue = (props: FormComboboxValueProps): string[] => {
|
|
79
|
+
if (!props.multiple) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return props.defaultValue ?? [];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getSelectedValues = (
|
|
87
|
+
props: FormComboboxValueProps,
|
|
88
|
+
internalSingleValue: string,
|
|
89
|
+
internalMultipleValue: string[]
|
|
90
|
+
): string[] => {
|
|
91
|
+
if (props.multiple === true) {
|
|
92
|
+
return props.value ?? internalMultipleValue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (props.value) {
|
|
96
|
+
return [props.value];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (internalSingleValue) {
|
|
100
|
+
return [internalSingleValue];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const orderSelectedOptionsFirst = (
|
|
107
|
+
options: FormComboboxOption[],
|
|
108
|
+
selectedValues: string[]
|
|
109
|
+
): FormComboboxOption[] => {
|
|
110
|
+
if (selectedValues.length === 0) return options;
|
|
111
|
+
|
|
112
|
+
const selectedValueSet = new Set(selectedValues);
|
|
113
|
+
const selectedOptions: FormComboboxOption[] = [];
|
|
114
|
+
const unselectedOptions: FormComboboxOption[] = [];
|
|
115
|
+
|
|
116
|
+
options.forEach((option) => {
|
|
117
|
+
if (selectedValueSet.has(option.value)) {
|
|
118
|
+
selectedOptions.push(option);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
unselectedOptions.push(option);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return [...selectedOptions, ...unselectedOptions];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const FormCombobox: FC<FormComboboxProps> = ({
|
|
129
|
+
label,
|
|
130
|
+
hideLabel,
|
|
131
|
+
placeholder,
|
|
132
|
+
searchPlaceholder,
|
|
133
|
+
options,
|
|
134
|
+
disabled,
|
|
135
|
+
style,
|
|
136
|
+
isSearchEnabled = false,
|
|
137
|
+
hideInputStyle = false,
|
|
138
|
+
isMenuPortaled = false,
|
|
139
|
+
...props
|
|
140
|
+
}) => {
|
|
141
|
+
const id = useId();
|
|
142
|
+
const { t } = useTranslation();
|
|
143
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
144
|
+
const [query, setQuery] = useState('');
|
|
145
|
+
const [internalSingleValue, setInternalSingleValue] = useState<string>(
|
|
146
|
+
getInitialSingleValue(props)
|
|
147
|
+
);
|
|
148
|
+
const [internalMultipleValue, setInternalMultipleValue] = useState<string[]>(
|
|
149
|
+
getInitialMultipleValue(props)
|
|
150
|
+
);
|
|
151
|
+
const [menuStyle, setMenuStyle] = useState<CSSProperties>({});
|
|
152
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
153
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
154
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
155
|
+
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
156
|
+
|
|
157
|
+
const isMultiple = props.multiple === true;
|
|
158
|
+
const selectedValues = getSelectedValues(props, internalSingleValue, internalMultipleValue);
|
|
159
|
+
|
|
160
|
+
const orderedOptions = useMemo(() => (
|
|
161
|
+
orderSelectedOptionsFirst(options, selectedValues)
|
|
162
|
+
), [options, selectedValues]);
|
|
163
|
+
|
|
164
|
+
const filteredOptions = useMemo(() => {
|
|
165
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
166
|
+
if (!normalizedQuery) return orderedOptions;
|
|
167
|
+
|
|
168
|
+
return orderedOptions.filter((option) => (
|
|
169
|
+
option.label.toLowerCase().includes(normalizedQuery)
|
|
170
|
+
));
|
|
171
|
+
}, [orderedOptions, query]);
|
|
172
|
+
|
|
173
|
+
const selectedOptions = useMemo(() => (
|
|
174
|
+
options.filter((option) => selectedValues.includes(option.value))
|
|
175
|
+
), [options, selectedValues]);
|
|
176
|
+
|
|
177
|
+
const updateMenuPosition = () => {
|
|
178
|
+
const triggerRect = triggerRef.current?.getBoundingClientRect();
|
|
179
|
+
|
|
180
|
+
if (!triggerRect) return;
|
|
181
|
+
|
|
182
|
+
const offset = 3;
|
|
183
|
+
|
|
184
|
+
setMenuStyle({
|
|
185
|
+
left: triggerRect.left,
|
|
186
|
+
minWidth: hideInputStyle ? 296 : triggerRect.width,
|
|
187
|
+
top: triggerRect.bottom + offset,
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
useLayoutEffect(() => {
|
|
192
|
+
if (!isOpen || !isMenuPortaled) return;
|
|
193
|
+
|
|
194
|
+
updateMenuPosition();
|
|
195
|
+
|
|
196
|
+
window.addEventListener('resize', updateMenuPosition);
|
|
197
|
+
document.addEventListener('scroll', updateMenuPosition, true);
|
|
198
|
+
|
|
199
|
+
return () => {
|
|
200
|
+
window.removeEventListener('resize', updateMenuPosition);
|
|
201
|
+
document.removeEventListener('scroll', updateMenuPosition, true);
|
|
202
|
+
};
|
|
203
|
+
}, [hideInputStyle, isMenuPortaled, isOpen]);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (isOpen) {
|
|
207
|
+
searchInputRef.current?.focus();
|
|
208
|
+
}
|
|
209
|
+
}, [isOpen]);
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const handleDocumentClick = (event: MouseEvent) => {
|
|
213
|
+
const target = event.target as Node;
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
!wrapperRef.current?.contains(target) &&
|
|
217
|
+
!menuRef.current?.contains(target)
|
|
218
|
+
) {
|
|
219
|
+
setIsOpen(false);
|
|
220
|
+
setQuery('');
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
document.addEventListener('mousedown', handleDocumentClick);
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
document.removeEventListener('mousedown', handleDocumentClick);
|
|
228
|
+
};
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
const closeOnFocusOutside = (event: ReactFocusEvent<HTMLDivElement>) => {
|
|
232
|
+
const relatedTarget = event.relatedTarget as Node | null;
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
!event.currentTarget.contains(relatedTarget) &&
|
|
236
|
+
!menuRef.current?.contains(relatedTarget)
|
|
237
|
+
) {
|
|
238
|
+
setIsOpen(false);
|
|
239
|
+
setQuery('');
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const setSingleValue = (option: FormComboboxOption) => {
|
|
244
|
+
if (props.multiple) return;
|
|
245
|
+
const singleProps = props as FormComboboxSingleProps;
|
|
246
|
+
const nextValue = selectedValues.includes(option.value) ? '' : option.value;
|
|
247
|
+
const nextSelection = nextValue ? option : null;
|
|
248
|
+
|
|
249
|
+
if (singleProps.value === undefined) {
|
|
250
|
+
setInternalSingleValue(nextValue);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
singleProps.onChange?.(nextValue);
|
|
254
|
+
singleProps.onSelectionChange?.(nextSelection);
|
|
255
|
+
setIsOpen(false);
|
|
256
|
+
setQuery('');
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const setMultipleValue = (option: FormComboboxOption) => {
|
|
260
|
+
if (!props.multiple) return;
|
|
261
|
+
const multipleProps = props as FormComboboxMultipleProps;
|
|
262
|
+
|
|
263
|
+
const nextValues = selectedValues.includes(option.value)
|
|
264
|
+
? selectedValues.filter((value) => value !== option.value)
|
|
265
|
+
: [...selectedValues, option.value];
|
|
266
|
+
const nextSelection = options.filter((item) => nextValues.includes(item.value));
|
|
267
|
+
|
|
268
|
+
if (multipleProps.value === undefined) {
|
|
269
|
+
setInternalMultipleValue(nextValues);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
multipleProps.onChange?.(nextValues);
|
|
273
|
+
multipleProps.onSelectionChange?.(nextSelection.length ? nextSelection : null);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const selectOption = (option: FormComboboxOption) => {
|
|
277
|
+
if (disabled) return;
|
|
278
|
+
|
|
279
|
+
if (props.multiple) {
|
|
280
|
+
setMultipleValue(option);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
setSingleValue(option);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const placeholderValue = placeholder || t('global.choose');
|
|
288
|
+
const triggerLabel = isMultiple
|
|
289
|
+
? selectedOptions.length > 0
|
|
290
|
+
? selectedOptions.map((option) => option.label).join(', ')
|
|
291
|
+
: placeholderValue
|
|
292
|
+
: selectedOptions[0]?.label ?? placeholderValue;
|
|
293
|
+
const triggerContent = hideInputStyle ? label ?? triggerLabel : triggerLabel;
|
|
294
|
+
|
|
295
|
+
const selectClasses = clsx(
|
|
296
|
+
'select',
|
|
297
|
+
disabled && 'select--disabled',
|
|
298
|
+
isOpen && 'select--open',
|
|
299
|
+
hideInputStyle && 'select--plain',
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const menu = (
|
|
303
|
+
<div
|
|
304
|
+
ref={menuRef}
|
|
305
|
+
className={clsx(
|
|
306
|
+
'select__menu select__menu--combobox',
|
|
307
|
+
isMenuPortaled && 'select__menu--portal'
|
|
308
|
+
)}
|
|
309
|
+
style={isMenuPortaled ? menuStyle : undefined}
|
|
310
|
+
>
|
|
311
|
+
{
|
|
312
|
+
isSearchEnabled && <div className='select__search'>
|
|
313
|
+
<Icon
|
|
314
|
+
label='Search icon'
|
|
315
|
+
size='medium'
|
|
316
|
+
className='select__search-icon'
|
|
317
|
+
icon={<MdSearch className='search__icon-size' color='#5D6071' />}
|
|
318
|
+
/>
|
|
319
|
+
<input
|
|
320
|
+
ref={searchInputRef}
|
|
321
|
+
className='select__search-input'
|
|
322
|
+
value={query}
|
|
323
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
324
|
+
placeholder={searchPlaceholder ?? t('global.search')}
|
|
325
|
+
disabled={disabled}
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
<ul className='select__options' role='listbox' aria-label={searchPlaceholder ?? t('global.search')}>
|
|
331
|
+
{filteredOptions.length > 0 ? (
|
|
332
|
+
filteredOptions.map((option) => {
|
|
333
|
+
const isSelected = selectedValues.includes(option.value);
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<li
|
|
337
|
+
key={option.value}
|
|
338
|
+
role='option'
|
|
339
|
+
aria-selected={isSelected}
|
|
340
|
+
className='select__option select__option--combobox'
|
|
341
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
342
|
+
onClick={() => selectOption(option)}
|
|
343
|
+
>
|
|
344
|
+
<input
|
|
345
|
+
type={isMultiple ? 'checkbox' : 'radio'}
|
|
346
|
+
checked={isSelected}
|
|
347
|
+
value={option.value}
|
|
348
|
+
onChange={() => null}
|
|
349
|
+
onClick={(event) => event.preventDefault()}
|
|
350
|
+
/>
|
|
351
|
+
<span>{option.label}</span>
|
|
352
|
+
</li>
|
|
353
|
+
);
|
|
354
|
+
})
|
|
355
|
+
) : null}
|
|
356
|
+
</ul>
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<div ref={wrapperRef} className={selectClasses} style={style} onBlur={closeOnFocusOutside}>
|
|
362
|
+
{label && !hideLabel && <label htmlFor={id} className='select__label'>{label}</label>}
|
|
363
|
+
<div className='select__wrapper'>
|
|
364
|
+
<button
|
|
365
|
+
ref={triggerRef}
|
|
366
|
+
id={id}
|
|
367
|
+
type='button'
|
|
368
|
+
className='select__trigger'
|
|
369
|
+
aria-label={hideInputStyle && typeof label === 'string' ? label : undefined}
|
|
370
|
+
aria-haspopup='listbox'
|
|
371
|
+
aria-expanded={isOpen}
|
|
372
|
+
disabled={disabled}
|
|
373
|
+
onClick={() => setIsOpen((open) => !open)}
|
|
374
|
+
>
|
|
375
|
+
<span className='select__trigger-text'>{triggerContent}</span>
|
|
376
|
+
<Icon
|
|
377
|
+
label='Dropdown icon'
|
|
378
|
+
size='medium'
|
|
379
|
+
icon={hideInputStyle ? <MdExpandMore color='#5D6071' /> : <MdArrowDropDown color='#5D6071' />}
|
|
380
|
+
/>
|
|
381
|
+
</button>
|
|
382
|
+
|
|
383
|
+
{isOpen && (
|
|
384
|
+
isMenuPortaled && typeof document !== 'undefined'
|
|
385
|
+
? createPortal(menu, document.body)
|
|
386
|
+
: menu
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
export default FormCombobox;
|
package/ui-components/index.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
FormTextarea,
|
|
13
13
|
Switch,
|
|
14
14
|
SwitchBox,
|
|
15
|
+
FormCombobox,
|
|
15
16
|
} from './FormElements';
|
|
16
17
|
import DataTable from './DataTable';
|
|
17
18
|
import Tooltip from './Tooltip';
|
|
@@ -52,4 +53,5 @@ export {
|
|
|
52
53
|
Toast,
|
|
53
54
|
Label,
|
|
54
55
|
ClearFiltersButton,
|
|
56
|
+
FormCombobox,
|
|
55
57
|
};
|