@buerokratt-ria/common-gui-components 0.0.59 → 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 +6 -0
- package/package.json +1 -1
- package/templates/history-page/src/components/HeaderCombobox/index.tsx +1 -0
- package/templates/history-page/src/index.tsx +85 -80
- package/ui-components/DataTable/index.tsx +0 -1
- package/ui-components/FormElements/FormCombobox/FormCombobox.scss +11 -0
- package/ui-components/FormElements/FormCombobox/index.tsx +109 -50
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## Template [MajorVersion.MediterraneanVersion.MinorVersion] - DD-MM-YYYY
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
## [0.0.60] - 09.06.2026
|
|
9
|
+
|
|
10
|
+
- History table: theme, follow up, quality sorting
|
|
11
|
+
- History table: header filter positioning
|
|
12
|
+
|
|
7
13
|
## [0.0.59] - 05.06.2026
|
|
8
14
|
|
|
9
15
|
- Quality measurements in chat history view
|
package/package.json
CHANGED
|
@@ -163,10 +163,15 @@ const formatChatAnalysisCell = (
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
const ALL_COLUMNS_VALUE = '__all__';
|
|
166
|
-
|
|
166
|
+
// Boolean -> truthy values before falsy
|
|
167
|
+
// For other columns -> desc before asc - populated before empty
|
|
168
|
+
const NON_EMPTY_FIRST_SORT_COLUMN_IDS = new Set([
|
|
167
169
|
'authenticatedPerson',
|
|
168
170
|
'istest',
|
|
169
171
|
'isPreserve',
|
|
172
|
+
'followUpStatus',
|
|
173
|
+
'responseQuality',
|
|
174
|
+
'theme',
|
|
170
175
|
]);
|
|
171
176
|
const CHAT_STATUSES = [
|
|
172
177
|
CHAT_EVENTS.ACCEPTED,
|
|
@@ -184,7 +189,7 @@ const getEndedChatsSortBy = (sorting: SortingState) => {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
const [sortingObject] = sorting;
|
|
187
|
-
const sortType =
|
|
192
|
+
const sortType = NON_EMPTY_FIRST_SORT_COLUMN_IDS.has(sortingObject.id)
|
|
188
193
|
? sortingObject.desc ? 'asc' : 'desc'
|
|
189
194
|
: sortingObject.desc ? 'desc' : 'asc';
|
|
190
195
|
|
|
@@ -1455,87 +1460,87 @@ const ChatHistory: FC<PropsWithChildren<HistoryProps>> = ({
|
|
|
1455
1460
|
sortDescFirst: false,
|
|
1456
1461
|
}),
|
|
1457
1462
|
...isChatAnalysisEnabled ? [
|
|
1458
|
-
columnHelper.
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
label={t('chat.history.theme')}
|
|
1463
|
-
options={themeOptions}
|
|
1464
|
-
value={theme}
|
|
1465
|
-
onChange={(value) => {
|
|
1466
|
-
setTableHeaderValue(
|
|
1467
|
-
'theme',
|
|
1468
|
-
normalizeAllOptionFilterValues(
|
|
1469
|
-
value,
|
|
1470
|
-
theme,
|
|
1471
|
-
getAllStringFilterValues(realThemeOptions)
|
|
1472
|
-
)
|
|
1473
|
-
);
|
|
1474
|
-
}}
|
|
1475
|
-
/>
|
|
1463
|
+
columnHelper.accessor(
|
|
1464
|
+
(row) => formatChatAnalysisCell(
|
|
1465
|
+
row.theme,
|
|
1466
|
+
t('chat.quality.selectionEmptied')
|
|
1476
1467
|
),
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1468
|
+
{
|
|
1469
|
+
id: 'theme',
|
|
1470
|
+
header: () => (
|
|
1471
|
+
<HeaderCombobox
|
|
1472
|
+
label={t('chat.history.theme')}
|
|
1473
|
+
options={themeOptions}
|
|
1474
|
+
value={theme}
|
|
1475
|
+
onChange={(value) => {
|
|
1476
|
+
setTableHeaderValue(
|
|
1477
|
+
'theme',
|
|
1478
|
+
normalizeAllOptionFilterValues(
|
|
1479
|
+
value,
|
|
1480
|
+
theme,
|
|
1481
|
+
getAllStringFilterValues(realThemeOptions)
|
|
1482
|
+
)
|
|
1483
|
+
);
|
|
1484
|
+
}}
|
|
1485
|
+
/>
|
|
1486
|
+
),
|
|
1487
|
+
enableSorting: true,
|
|
1488
|
+
}
|
|
1489
|
+
),
|
|
1490
|
+
columnHelper.accessor(
|
|
1491
|
+
(row) => formatChatAnalysisCell(
|
|
1492
|
+
row.responseQuality,
|
|
1493
|
+
t('chat.quality.selectionEmptied')
|
|
1503
1494
|
),
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1495
|
+
{
|
|
1496
|
+
id: 'responseQuality',
|
|
1497
|
+
header: () => (
|
|
1498
|
+
<HeaderCombobox
|
|
1499
|
+
label={t('chat.history.responseQuality')}
|
|
1500
|
+
options={responseQualityOptions}
|
|
1501
|
+
value={responseQuality}
|
|
1502
|
+
onChange={(value) => {
|
|
1503
|
+
setTableHeaderValue(
|
|
1504
|
+
'responseQuality',
|
|
1505
|
+
normalizeAllOptionFilterValues(
|
|
1506
|
+
value,
|
|
1507
|
+
responseQuality,
|
|
1508
|
+
getAllStringFilterValues(realResponseQualityOptions)
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
}}
|
|
1512
|
+
/>
|
|
1513
|
+
),
|
|
1514
|
+
enableSorting: true,
|
|
1515
|
+
}
|
|
1516
|
+
),
|
|
1517
|
+
columnHelper.accessor(
|
|
1518
|
+
(row) => formatChatAnalysisCell(
|
|
1519
|
+
row.followUpStatus,
|
|
1520
|
+
t('chat.quality.selectionEmptied')
|
|
1530
1521
|
),
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1522
|
+
{
|
|
1523
|
+
id: 'followUpStatus',
|
|
1524
|
+
header: () => (
|
|
1525
|
+
<HeaderCombobox
|
|
1526
|
+
label={t('chat.history.followUpStatus')}
|
|
1527
|
+
options={followUpStatusOptions}
|
|
1528
|
+
value={followUpStatus}
|
|
1529
|
+
onChange={(value) => {
|
|
1530
|
+
setTableHeaderValue(
|
|
1531
|
+
'followUpStatus',
|
|
1532
|
+
normalizeAllOptionFilterValues(
|
|
1533
|
+
value,
|
|
1534
|
+
followUpStatus,
|
|
1535
|
+
getAllStringFilterValues(realFollowUpStatusOptions)
|
|
1536
|
+
)
|
|
1537
|
+
);
|
|
1538
|
+
}}
|
|
1539
|
+
/>
|
|
1540
|
+
),
|
|
1541
|
+
enableSorting: true,
|
|
1542
|
+
}
|
|
1543
|
+
),
|
|
1539
1544
|
] : [],
|
|
1540
1545
|
columnHelper.display({
|
|
1541
1546
|
id: 'detail',
|
|
@@ -210,7 +210,6 @@ const DataTable: FC<DataTableProps> = ({
|
|
|
210
210
|
header.column.columnDef.meta?.sticky === 'right'
|
|
211
211
|
? `${header.column.getAfter('right') * 0.675}px`
|
|
212
212
|
: undefined,
|
|
213
|
-
backgroundColor: 'white',
|
|
214
213
|
zIndex: header.column.columnDef.meta?.sticky ? 1 : 0,
|
|
215
214
|
}}
|
|
216
215
|
>
|
|
@@ -159,6 +159,17 @@
|
|
|
159
159
|
margin-bottom: 3px;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
&__menu--portal {
|
|
163
|
+
display: block;
|
|
164
|
+
position: fixed;
|
|
165
|
+
top: 100%;
|
|
166
|
+
bottom: auto;
|
|
167
|
+
right: auto;
|
|
168
|
+
margin-top: 3px;
|
|
169
|
+
margin-bottom: 0;
|
|
170
|
+
z-index: 10000;
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
&__options {
|
|
163
174
|
max-height: 224px;
|
|
164
175
|
overflow: auto;
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
ReactNode,
|
|
6
6
|
useEffect,
|
|
7
7
|
useId,
|
|
8
|
+
useLayoutEffect,
|
|
8
9
|
useMemo,
|
|
9
10
|
useRef,
|
|
10
11
|
useState
|
|
11
12
|
} from 'react';
|
|
13
|
+
import { createPortal } from 'react-dom';
|
|
12
14
|
import clsx from 'clsx';
|
|
13
15
|
import { useTranslation } from 'react-i18next';
|
|
14
16
|
import { MdArrowDropDown, MdExpandMore, MdSearch } from 'react-icons/md';
|
|
@@ -32,6 +34,7 @@ type FormComboboxBaseProps = {
|
|
|
32
34
|
readonly style?: CSSProperties;
|
|
33
35
|
readonly isSearchEnabled?: boolean;
|
|
34
36
|
readonly hideInputStyle?: boolean;
|
|
37
|
+
readonly isMenuPortaled?: boolean;
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
type FormComboboxSingleProps = FormComboboxBaseProps & {
|
|
@@ -132,6 +135,7 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
132
135
|
style,
|
|
133
136
|
isSearchEnabled = false,
|
|
134
137
|
hideInputStyle = false,
|
|
138
|
+
isMenuPortaled = false,
|
|
135
139
|
...props
|
|
136
140
|
}) => {
|
|
137
141
|
const id = useId();
|
|
@@ -144,7 +148,10 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
144
148
|
const [internalMultipleValue, setInternalMultipleValue] = useState<string[]>(
|
|
145
149
|
getInitialMultipleValue(props)
|
|
146
150
|
);
|
|
151
|
+
const [menuStyle, setMenuStyle] = useState<CSSProperties>({});
|
|
147
152
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
153
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
154
|
+
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
148
155
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
|
149
156
|
|
|
150
157
|
const isMultiple = props.multiple === true;
|
|
@@ -167,6 +174,34 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
167
174
|
options.filter((option) => selectedValues.includes(option.value))
|
|
168
175
|
), [options, selectedValues]);
|
|
169
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
|
+
|
|
170
205
|
useEffect(() => {
|
|
171
206
|
if (isOpen) {
|
|
172
207
|
searchInputRef.current?.focus();
|
|
@@ -175,7 +210,12 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
175
210
|
|
|
176
211
|
useEffect(() => {
|
|
177
212
|
const handleDocumentClick = (event: MouseEvent) => {
|
|
178
|
-
|
|
213
|
+
const target = event.target as Node;
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
!wrapperRef.current?.contains(target) &&
|
|
217
|
+
!menuRef.current?.contains(target)
|
|
218
|
+
) {
|
|
179
219
|
setIsOpen(false);
|
|
180
220
|
setQuery('');
|
|
181
221
|
}
|
|
@@ -189,7 +229,12 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
189
229
|
}, []);
|
|
190
230
|
|
|
191
231
|
const closeOnFocusOutside = (event: ReactFocusEvent<HTMLDivElement>) => {
|
|
192
|
-
|
|
232
|
+
const relatedTarget = event.relatedTarget as Node | null;
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
!event.currentTarget.contains(relatedTarget) &&
|
|
236
|
+
!menuRef.current?.contains(relatedTarget)
|
|
237
|
+
) {
|
|
193
238
|
setIsOpen(false);
|
|
194
239
|
setQuery('');
|
|
195
240
|
}
|
|
@@ -254,11 +299,70 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
254
299
|
hideInputStyle && 'select--plain',
|
|
255
300
|
);
|
|
256
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
|
+
|
|
257
360
|
return (
|
|
258
361
|
<div ref={wrapperRef} className={selectClasses} style={style} onBlur={closeOnFocusOutside}>
|
|
259
362
|
{label && !hideLabel && <label htmlFor={id} className='select__label'>{label}</label>}
|
|
260
363
|
<div className='select__wrapper'>
|
|
261
364
|
<button
|
|
365
|
+
ref={triggerRef}
|
|
262
366
|
id={id}
|
|
263
367
|
type='button'
|
|
264
368
|
className='select__trigger'
|
|
@@ -277,54 +381,9 @@ export const FormCombobox: FC<FormComboboxProps> = ({
|
|
|
277
381
|
</button>
|
|
278
382
|
|
|
279
383
|
{isOpen && (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<Icon
|
|
284
|
-
label='Search icon'
|
|
285
|
-
size='medium'
|
|
286
|
-
className='select__search-icon'
|
|
287
|
-
icon={<MdSearch className='search__icon-size' color='#5D6071' />}
|
|
288
|
-
/>
|
|
289
|
-
<input
|
|
290
|
-
ref={searchInputRef}
|
|
291
|
-
className='select__search-input'
|
|
292
|
-
value={query}
|
|
293
|
-
onChange={(event) => setQuery(event.target.value)}
|
|
294
|
-
placeholder={searchPlaceholder ?? t('global.search')}
|
|
295
|
-
disabled={disabled}
|
|
296
|
-
/>
|
|
297
|
-
</div>
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
<ul className='select__options' role='listbox' aria-label={searchPlaceholder ?? t('global.search')}>
|
|
301
|
-
{filteredOptions.length > 0 ? (
|
|
302
|
-
filteredOptions.map((option) => {
|
|
303
|
-
const isSelected = selectedValues.includes(option.value);
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<li
|
|
307
|
-
key={option.value}
|
|
308
|
-
role='option'
|
|
309
|
-
aria-selected={isSelected}
|
|
310
|
-
className='select__option select__option--combobox'
|
|
311
|
-
onMouseDown={(event) => event.preventDefault()}
|
|
312
|
-
onClick={() => selectOption(option)}
|
|
313
|
-
>
|
|
314
|
-
<input
|
|
315
|
-
type={isMultiple ? 'checkbox' : 'radio'}
|
|
316
|
-
checked={isSelected}
|
|
317
|
-
value={option.value}
|
|
318
|
-
onChange={() => null}
|
|
319
|
-
onClick={(event) => event.preventDefault()}
|
|
320
|
-
/>
|
|
321
|
-
<span>{option.label}</span>
|
|
322
|
-
</li>
|
|
323
|
-
);
|
|
324
|
-
})
|
|
325
|
-
) : null}
|
|
326
|
-
</ul>
|
|
327
|
-
</div>
|
|
384
|
+
isMenuPortaled && typeof document !== 'undefined'
|
|
385
|
+
? createPortal(menu, document.body)
|
|
386
|
+
: menu
|
|
328
387
|
)}
|
|
329
388
|
</div>
|
|
330
389
|
</div>
|