@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.
Files changed (25) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/templates/history-page/src/History.scss +15 -1
  4. package/templates/history-page/src/components/ChatMetadataPanel/ChatMetadataPanel.scss +18 -0
  5. package/templates/history-page/src/components/ChatMetadataPanel/index.tsx +206 -0
  6. package/templates/history-page/src/components/ChatMetadataPanelItem/index.tsx +17 -0
  7. package/templates/history-page/src/components/FilterTag/FilterTag.scss +42 -0
  8. package/templates/history-page/src/components/FilterTag/index.tsx +16 -0
  9. package/templates/history-page/src/components/HeaderCombobox/index.tsx +67 -0
  10. package/templates/history-page/src/components/QualitySettings/QualitySettings.scss +19 -0
  11. package/templates/history-page/src/components/QualitySettings/index.tsx +115 -0
  12. package/templates/history-page/src/components/SelectedFilterTags/SelectedFilterTags.scss +36 -0
  13. package/templates/history-page/src/components/SelectedFilterTags/index.tsx +224 -0
  14. package/templates/history-page/src/components/index.tsx +6 -0
  15. package/templates/history-page/src/index.tsx +946 -207
  16. package/templates/history-page/src/types/index.ts +17 -0
  17. package/translations/en/common.json +22 -2
  18. package/translations/et/common.json +22 -2
  19. package/types/chat.ts +3 -0
  20. package/ui-components/DataTable/index.tsx +0 -1
  21. package/ui-components/FormElements/FormCombobox/FormCombobox.scss +263 -0
  22. package/ui-components/FormElements/FormCombobox/index.tsx +393 -0
  23. package/ui-components/FormElements/index.tsx +1 -0
  24. package/ui-components/Icon/index.tsx +1 -0
  25. 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;
@@ -21,3 +21,4 @@ export {
21
21
  FormDatepicker,
22
22
  SwitchBox,
23
23
  };
24
+ export * from './FormCombobox';
@@ -14,6 +14,7 @@ const Icon = forwardRef<HTMLSpanElement, IconProps>(({ label, icon, size = 'smal
14
14
  const iconClasses = clsx(
15
15
  'icon',
16
16
  `icon--${size}`,
17
+ rest.className,
17
18
  );
18
19
 
19
20
  return (
@@ -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
  };