@bit.rhplus/ag-grid 0.0.123 → 0.0.124
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/Editors/ModuleLookupEditor.jsx +76 -22
- package/Editors/RouteCellEditor.jsx +124 -0
- package/Editors/index.jsx +1 -0
- package/OnCellValueChanged.js +1 -2
- package/Renderers/RouteCellRenderer.jsx +76 -0
- package/Renderers/SelectCellRenderer.jsx +33 -7
- package/Renderers/index.jsx +1 -0
- package/dist/Editors/ModuleLookupEditor.js +64 -23
- package/dist/Editors/ModuleLookupEditor.js.map +1 -1
- package/dist/Editors/RouteCellEditor.d.ts +21 -0
- package/dist/Editors/RouteCellEditor.js +79 -0
- package/dist/Editors/RouteCellEditor.js.map +1 -0
- package/dist/Editors/index.js +1 -0
- package/dist/Editors/index.js.map +1 -1
- package/dist/OnCellValueChanged.js +1 -1
- package/dist/OnCellValueChanged.js.map +1 -1
- package/dist/Renderers/RouteCellRenderer.d.ts +9 -0
- package/dist/Renderers/RouteCellRenderer.js +48 -0
- package/dist/Renderers/RouteCellRenderer.js.map +1 -0
- package/dist/Renderers/SelectCellRenderer.d.ts +1 -5
- package/dist/Renderers/SelectCellRenderer.js +23 -5
- package/dist/Renderers/SelectCellRenderer.js.map +1 -1
- package/dist/Renderers/index.js +1 -0
- package/dist/Renderers/index.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/index.jsx +8 -0
- package/package.json +6 -5
- /package/dist/{preview-1775594979481.js → preview-1775722983848.js} +0 -0
|
@@ -62,6 +62,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
62
62
|
moduleDefinition,
|
|
63
63
|
allowFreeText = false,
|
|
64
64
|
rowHeight = 42,
|
|
65
|
+
autoOpen = true,
|
|
65
66
|
}, _ref) => {
|
|
66
67
|
|
|
67
68
|
// ─── Hooks (vždy voláno) ──────────────────────────────────────────────────────
|
|
@@ -109,6 +110,12 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
109
110
|
|
|
110
111
|
const [inputText, setInputText] = useState(initialText);
|
|
111
112
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (autoOpen) {
|
|
116
|
+
setDropdownOpen(true);
|
|
117
|
+
}
|
|
118
|
+
}, [autoOpen]);
|
|
112
119
|
const [multiMatchData, setMultiMatchData] = useState([]);
|
|
113
120
|
const [multiMatchModalOpen, setMultiMatchModalOpen] = useState(false);
|
|
114
121
|
const catalogModalOpenRef = useRef(false);
|
|
@@ -117,7 +124,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
117
124
|
multiMatchModalOpenRef.current = multiMatchModalOpen;
|
|
118
125
|
const currentTextRef = useRef(initialText);
|
|
119
126
|
const inputTextRef = useRef(initialText); // vždy aktuální inputText pro native handler
|
|
120
|
-
const
|
|
127
|
+
const editorWrapperRef = useRef(null); // ref na wrapper div → querySelector('input')
|
|
121
128
|
const isStoppedRef = useRef(false);
|
|
122
129
|
const justSelectedRef = useRef(false);
|
|
123
130
|
// Nastaven synchronně před každým stopEditing() voláním.
|
|
@@ -133,6 +140,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
133
140
|
// Ref na suggestions a handleAutoCompleteSelect — přístupné v capture handleru bez stale closure
|
|
134
141
|
const suggestionsRef = useRef([]);
|
|
135
142
|
const handleAutoCompleteSelectRef = useRef(null);
|
|
143
|
+
const handleCatalogSelectChangeRef = useRef(null);
|
|
136
144
|
|
|
137
145
|
// ─── Lokální filtrování návrhů (bez API volání) ───────────────────────────────
|
|
138
146
|
const suggestions = React.useMemo(() => {
|
|
@@ -143,12 +151,22 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
143
151
|
const label = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || '';
|
|
144
152
|
return label.toLowerCase().includes(inputText.toLowerCase());
|
|
145
153
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
const result = source.map(item => {
|
|
155
|
+
const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
|
|
156
|
+
const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
|
|
157
|
+
return {
|
|
158
|
+
value: String(item[vf]),
|
|
159
|
+
label: (
|
|
160
|
+
<div data-mle-value={String(item[vf])} style={{ display: 'contents' }}>
|
|
161
|
+
{originalLabel}
|
|
162
|
+
</div>
|
|
163
|
+
),
|
|
164
|
+
displayText,
|
|
165
|
+
item,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
return allowFreeText ? result.slice(0, 50) : result;
|
|
169
|
+
}, [allItems, inputText, moduleDefinition, allowFreeText]);
|
|
152
170
|
suggestionsRef.current = suggestions;
|
|
153
171
|
|
|
154
172
|
// ─── AG Grid useGridCellEditor ───────────────────────────────────────────────
|
|
@@ -215,6 +233,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
215
233
|
if (onValueChange) onValueChange(item);
|
|
216
234
|
setTimeout(() => stopEditing(), 0);
|
|
217
235
|
}, [allItems, moduleDefinition, onValueChange, stopEditing]);
|
|
236
|
+
handleCatalogSelectChangeRef.current = handleCatalogSelectChange;
|
|
218
237
|
|
|
219
238
|
const catalogCurrentId = React.useMemo(() => {
|
|
220
239
|
if (allowFreeText || !value) return null;
|
|
@@ -358,8 +377,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
358
377
|
// rc-select volá stopPropagation() na Enter v bubble fázi → náš React onKeyDown
|
|
359
378
|
// na wrapperu ho nikdy nedostane. Capture-phase native listener se spustí jako první.
|
|
360
379
|
useEffect(() => {
|
|
361
|
-
|
|
362
|
-
const el = freeTextWrapperRef.current;
|
|
380
|
+
const el = editorWrapperRef.current;
|
|
363
381
|
if (!el) return;
|
|
364
382
|
const input = el.querySelector('input');
|
|
365
383
|
if (!input) return;
|
|
@@ -383,17 +401,41 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
383
401
|
if (activeOptionEl) {
|
|
384
402
|
e.stopImmediatePropagation();
|
|
385
403
|
e.preventDefault();
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
|
|
391
|
-
|
|
404
|
+
const contentEl = activeOptionEl.querySelector('.ant-select-item-option-content');
|
|
405
|
+
if (contentEl) {
|
|
406
|
+
const mleValue = contentEl.querySelector('[data-mle-value]')?.getAttribute('data-mle-value');
|
|
407
|
+
if (mleValue) {
|
|
408
|
+
const matched = suggestionsRef.current.find((s) => s.value === mleValue);
|
|
409
|
+
if (matched) {
|
|
410
|
+
if (allowFreeText) {
|
|
411
|
+
handleAutoCompleteSelectRef.current?.(matched.value, matched);
|
|
412
|
+
} else {
|
|
413
|
+
handleCatalogSelectChangeRef.current?.(matched.value);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const spanText = contentEl.querySelector('span')?.textContent?.trim();
|
|
420
|
+
const fullText = contentEl.textContent?.trim();
|
|
421
|
+
const labelText = spanText || fullText;
|
|
422
|
+
if (labelText) {
|
|
423
|
+
const matched = suggestionsRef.current.find((s) => s.displayText === labelText || String(s.label) === labelText);
|
|
424
|
+
if (matched) {
|
|
425
|
+
if (allowFreeText) {
|
|
426
|
+
handleAutoCompleteSelectRef.current?.(matched.value, matched);
|
|
427
|
+
} else {
|
|
428
|
+
handleCatalogSelectChangeRef.current?.(matched.value);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
392
432
|
}
|
|
393
433
|
}
|
|
394
434
|
return;
|
|
395
435
|
}
|
|
396
436
|
|
|
437
|
+
if (!allowFreeText) return;
|
|
438
|
+
|
|
397
439
|
const text = inputTextRef.current || '';
|
|
398
440
|
// Prázdný input bez předchozí hodnoty → nechat AG Grid zpracovat (cancel)
|
|
399
441
|
if (!text.trim() && pendingValueRef.current === null && !initialText) return;
|
|
@@ -442,27 +484,39 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
442
484
|
// ─── Render: catalog-only mód ─────────────────────────────────────────────────
|
|
443
485
|
if (!allowFreeText) {
|
|
444
486
|
const vf = moduleDefinition?.valueField || 'id';
|
|
445
|
-
const selectOptions = allItems.map(item =>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
487
|
+
const selectOptions = allItems.map(item => {
|
|
488
|
+
const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
|
|
489
|
+
const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
|
|
490
|
+
return {
|
|
491
|
+
value: item[vf],
|
|
492
|
+
label: (
|
|
493
|
+
<div data-mle-value={String(item[vf])} style={{ display: 'contents' }}>
|
|
494
|
+
{originalLabel}
|
|
495
|
+
</div>
|
|
496
|
+
),
|
|
497
|
+
displayText,
|
|
498
|
+
};
|
|
499
|
+
});
|
|
449
500
|
|
|
450
501
|
return (
|
|
451
|
-
<div style={{ width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }}>
|
|
502
|
+
<div style={{ width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }} ref={editorWrapperRef}>
|
|
452
503
|
<div style={{ display: 'flex', flex: 1, height: '100%' }}>
|
|
453
504
|
<Select
|
|
454
505
|
style={{ flex: 1, height: '100%' }}
|
|
455
506
|
value={catalogCurrentId}
|
|
456
507
|
options={selectOptions}
|
|
508
|
+
optionLabelProp="displayText"
|
|
457
509
|
loading={allItemsLoading}
|
|
458
510
|
showSearch
|
|
459
511
|
filterOption={(input, option) =>
|
|
460
|
-
String(option?.label || '').toLowerCase().includes(input.toLowerCase())
|
|
512
|
+
String(option?.displayText || option?.label || '').toLowerCase().includes(input.toLowerCase())
|
|
461
513
|
}
|
|
462
514
|
onChange={handleCatalogSelectChange}
|
|
515
|
+
onSearch={handleInputChange}
|
|
463
516
|
getPopupContainer={() => document.body}
|
|
464
517
|
dropdownStyle={{ zIndex: 9999 }}
|
|
465
518
|
placeholder={moduleDefinition?.placeholder || 'Vyberte...'}
|
|
519
|
+
defaultOpen={autoOpen}
|
|
466
520
|
autoFocus
|
|
467
521
|
/>
|
|
468
522
|
<Button icon={<SearchOutlined />} onMouseDown={e => e.preventDefault()} onClick={() => setCatalogModalOpen(true)} type="primary" style={{ height: '100%', borderRadius: 0 }} />
|
|
@@ -475,7 +529,7 @@ const ModuleLookupEditor = React.forwardRef(({
|
|
|
475
529
|
// ─── Render: free-text mód ────────────────────────────────────────────────────
|
|
476
530
|
return (
|
|
477
531
|
<div style={{ width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }}>
|
|
478
|
-
<div ref={
|
|
532
|
+
<div ref={editorWrapperRef} style={{ display: 'flex', flex: 1, height: '100%' }}>
|
|
479
533
|
<AutoComplete
|
|
480
534
|
style={{ flex: 1 }}
|
|
481
535
|
value={inputText}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useGridCellEditor } from 'ag-grid-react';
|
|
4
|
+
import DraggableModal from '@bit.rhplus/draggable-modal';
|
|
5
|
+
import { useSharedGridItems } from '@bit.rhplus/shared-grid-form';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Vnitřní komponenta editoru — přístup k Jotai SharedGridForm kontextu.
|
|
9
|
+
*/
|
|
10
|
+
const RouteEditorInner = ({
|
|
11
|
+
instanceId,
|
|
12
|
+
initialData,
|
|
13
|
+
gridComponent: GridComponent,
|
|
14
|
+
onCancel,
|
|
15
|
+
onSave,
|
|
16
|
+
}) => {
|
|
17
|
+
const { effectiveItems, setItems } = useSharedGridItems(instanceId);
|
|
18
|
+
const distanceKmRef = useRef(null);
|
|
19
|
+
|
|
20
|
+
// Inicializace dat při mountu
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (Array.isArray(initialData)) {
|
|
23
|
+
setItems(initialData);
|
|
24
|
+
}
|
|
25
|
+
}, []); // Pouze při mountu
|
|
26
|
+
|
|
27
|
+
const handleRouteCalculated = useCallback((info) => {
|
|
28
|
+
if (info?.distance != null && info.distance > 0) {
|
|
29
|
+
distanceKmRef.current = Math.round(info.distance / 1000);
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const handleOk = useCallback(() => {
|
|
34
|
+
onSave(effectiveItems, distanceKmRef.current);
|
|
35
|
+
}, [effectiveItems, onSave]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<DraggableModal
|
|
39
|
+
title="Editace trasy"
|
|
40
|
+
open={true}
|
|
41
|
+
onOk={handleOk}
|
|
42
|
+
onCancel={onCancel}
|
|
43
|
+
width={1100}
|
|
44
|
+
height="80vh"
|
|
45
|
+
resizable={true}
|
|
46
|
+
getContainer={() => document.body}
|
|
47
|
+
destroyOnClose
|
|
48
|
+
styles={{ body: { height: '100%', overflow: 'hidden', padding: 0 } }}
|
|
49
|
+
centered
|
|
50
|
+
maskClosable={false}
|
|
51
|
+
>
|
|
52
|
+
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|
53
|
+
<GridComponent instanceId={instanceId} showAddButton={true} onRouteCalculated={handleRouteCalculated} />
|
|
54
|
+
</div>
|
|
55
|
+
</DraggableModal>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* RouteCellEditor — AG Grid 35 Cell Editor pro správu tras.
|
|
61
|
+
*
|
|
62
|
+
* Používá useGridCellEditor hook (povinné v AG Grid 31+).
|
|
63
|
+
* Props jsou předány přímo z AG Grid (value, onValueChange, stopEditing + cellEditorParams).
|
|
64
|
+
*/
|
|
65
|
+
/**
|
|
66
|
+
* RouteCellEditor — AG Grid 35 Cell Editor pro správu tras.
|
|
67
|
+
*
|
|
68
|
+
* Používá useGridCellEditor hook (povinné v AG Grid 31+).
|
|
69
|
+
* Props jsou předány přímo z AG Grid (value, onValueChange, stopEditing + cellEditorParams).
|
|
70
|
+
*
|
|
71
|
+
* @param {Function} transformValue - volitelná funkce z cellEditorParams pro transformaci
|
|
72
|
+
* effectiveItems před uložením (např. pro převod temp ID na null a type string na transportTypeId).
|
|
73
|
+
* Pokud není zadána, data se uloží bez transformace.
|
|
74
|
+
*/
|
|
75
|
+
const RouteCellEditor = ({ value, onValueChange, stopEditing, data, gridComponent: GridComponent, transformValue }) => {
|
|
76
|
+
const [visible, setVisible] = useState(true);
|
|
77
|
+
|
|
78
|
+
// Ref pro synchronní přístup z getValue() — AG Grid 35 volá getValue() při stopEditing()
|
|
79
|
+
const valueRef = useRef(value);
|
|
80
|
+
|
|
81
|
+
// AG Grid 35: useGridCellEditor je povinné pro správné getValue() při stopEditing()
|
|
82
|
+
useGridCellEditor({
|
|
83
|
+
getValue: () => valueRef.current,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Stabilní instanceId pro celý editační proces
|
|
87
|
+
const [instanceId] = useState(() => `route-editor-${data?.id || Date.now()}`);
|
|
88
|
+
|
|
89
|
+
const handleSave = useCallback((items, distanceKm) => {
|
|
90
|
+
// Transformace dat před uložením — připraví formát kompatibilní s backendem
|
|
91
|
+
const prepared = transformValue ? transformValue(items) : items;
|
|
92
|
+
const payload = distanceKm != null ? { routes: prepared, destination: distanceKm } : prepared;
|
|
93
|
+
valueRef.current = payload;
|
|
94
|
+
if (onValueChange) onValueChange(payload);
|
|
95
|
+
setVisible(false);
|
|
96
|
+
stopEditing();
|
|
97
|
+
}, [transformValue, onValueChange, stopEditing]);
|
|
98
|
+
|
|
99
|
+
const handleCancel = useCallback(() => {
|
|
100
|
+
setVisible(false);
|
|
101
|
+
setTimeout(() => stopEditing(true), 0); // true = cancel, necommitovat
|
|
102
|
+
}, [stopEditing]);
|
|
103
|
+
|
|
104
|
+
if (!visible) return null;
|
|
105
|
+
|
|
106
|
+
if (!GridComponent) {
|
|
107
|
+
console.error('RouteCellEditor: gridComponent missing in cellEditorParams');
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<RouteEditorInner
|
|
113
|
+
instanceId={instanceId}
|
|
114
|
+
initialData={value}
|
|
115
|
+
gridComponent={GridComponent}
|
|
116
|
+
onSave={handleSave}
|
|
117
|
+
onCancel={handleCancel}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
RouteCellEditor.displayName = 'RouteCellEditor';
|
|
123
|
+
|
|
124
|
+
export default RouteCellEditor;
|
package/Editors/index.jsx
CHANGED
package/OnCellValueChanged.js
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { MapPin, Flag, ChevronRight } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RouteCellRenderer — zobrazuje trasu se šipkami a ikonami pro nakládku/vykládku.
|
|
7
|
+
* Bucovice => slavkov u Brna => brno => praha
|
|
8
|
+
*/
|
|
9
|
+
const RouteCellRenderer = (params) => {
|
|
10
|
+
const routes = params.value || params.data?.routes || [];
|
|
11
|
+
if (!Array.isArray(routes) || routes.length === 0) {
|
|
12
|
+
return <span style={{ color: '#bfbfbf', fontStyle: 'italic' }}>Nezadána</span>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div style={{
|
|
18
|
+
display: 'flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
gap: '4px',
|
|
21
|
+
overflow: 'hidden',
|
|
22
|
+
height: '100%',
|
|
23
|
+
padding: '0 4px'
|
|
24
|
+
}}>
|
|
25
|
+
{routes.map((route, index) => {
|
|
26
|
+
const isLast = index === routes.length - 1;
|
|
27
|
+
const isFirst = index === 0;
|
|
28
|
+
|
|
29
|
+
// Typ: 1 = loading (nakládka), 2 = unloading (vykládka)
|
|
30
|
+
const transportTypeId = route.transportTypeId || (route.type === 'loading' ? 1 : 2);
|
|
31
|
+
|
|
32
|
+
const isLoading = transportTypeId === 1;
|
|
33
|
+
const color = isLoading ? '#1890ff' : '#52c41a';
|
|
34
|
+
const Icon = isLoading ? MapPin : Flag;
|
|
35
|
+
|
|
36
|
+
const name = route.project?.name || route.name || route.address || '???';
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<React.Fragment key={route.id || index}>
|
|
40
|
+
<div style={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: '2px',
|
|
44
|
+
flexShrink: 0,
|
|
45
|
+
maxWidth: '120px'
|
|
46
|
+
}}>
|
|
47
|
+
<Icon
|
|
48
|
+
size={14}
|
|
49
|
+
color={color}
|
|
50
|
+
fill={isFirst || isLast ? `${color}33` : 'none'}
|
|
51
|
+
style={{ flexShrink: 0 }}
|
|
52
|
+
/>
|
|
53
|
+
<span style={{
|
|
54
|
+
fontSize: '12px',
|
|
55
|
+
fontWeight: isFirst || isLast ? '500' : '400',
|
|
56
|
+
overflow: 'hidden',
|
|
57
|
+
textOverflow: 'ellipsis',
|
|
58
|
+
whiteSpace: 'nowrap',
|
|
59
|
+
color: '#262626'
|
|
60
|
+
}}>
|
|
61
|
+
{name}
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
{!isLast && (
|
|
65
|
+
<ChevronRight size={12} color="#bfbfbf" style={{ flexShrink: 0 }} />
|
|
66
|
+
)}
|
|
67
|
+
</React.Fragment>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
RouteCellRenderer.displayName = 'RouteCellRenderer';
|
|
75
|
+
|
|
76
|
+
export default RouteCellRenderer;
|
|
@@ -2,16 +2,37 @@
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import * as LucideIcons from 'lucide-react';
|
|
4
4
|
|
|
5
|
-
export default (
|
|
5
|
+
export default (params) => {
|
|
6
|
+
const { value, data, displayField = 'name', imageField = null, icon, iconColor } = params;
|
|
6
7
|
if (value == null) return null;
|
|
8
|
+
|
|
7
9
|
const displayText = typeof value === 'object'
|
|
8
10
|
? (value[displayField] ?? value.name ?? '')
|
|
9
11
|
: String(value);
|
|
10
|
-
|
|
12
|
+
|
|
11
13
|
const imageUrl = imageField && typeof value === 'object' ? value[imageField] : null;
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
+
// Lucide ikona z hodnoty (hodnota má pole icon s názvem Lucide komponenty)
|
|
16
|
+
const lucideIconName = value?.icon;
|
|
17
|
+
const LucideIcon = !imageUrl && !icon && lucideIconName && LucideIcons[lucideIconName]
|
|
18
|
+
? LucideIcons[lucideIconName]
|
|
19
|
+
: null;
|
|
20
|
+
|
|
21
|
+
// Custom ikona předaná přes cellRendererParams.icon — může být element, funkce nebo komponenta
|
|
22
|
+
let iconElement = null;
|
|
23
|
+
if (!imageUrl && icon) {
|
|
24
|
+
const resolvedColor = typeof iconColor === 'function' ? iconColor({ data, value }) : iconColor;
|
|
25
|
+
const colorStyle = resolvedColor ? { color: resolvedColor } : {};
|
|
26
|
+
|
|
27
|
+
if (React.isValidElement(icon)) {
|
|
28
|
+
iconElement = React.cloneElement(icon, { style: { ...colorStyle, ...icon.props.style } });
|
|
29
|
+
} else if (typeof icon === 'function') {
|
|
30
|
+
const rendered = icon({ data, value });
|
|
31
|
+
iconElement = resolvedColor && React.isValidElement(rendered)
|
|
32
|
+
? React.cloneElement(rendered, { style: { ...colorStyle, ...rendered.props?.style } })
|
|
33
|
+
: rendered;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
return (
|
|
17
38
|
<div style={{
|
|
@@ -29,14 +50,19 @@ export default ({value, displayField = 'name', imageField = null}) => {
|
|
|
29
50
|
onError={(e) => { e.currentTarget.style.display = 'none'; }}
|
|
30
51
|
/>
|
|
31
52
|
)}
|
|
32
|
-
{
|
|
53
|
+
{iconElement && (
|
|
54
|
+
<span style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
|
55
|
+
{iconElement}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
{!iconElement && LucideIcon && (
|
|
33
59
|
<span style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
|
34
|
-
<
|
|
60
|
+
<LucideIcon size={16} />
|
|
35
61
|
</span>
|
|
36
62
|
)}
|
|
37
63
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
38
64
|
{displayText}
|
|
39
65
|
</span>
|
|
40
66
|
</div>
|
|
41
|
-
)
|
|
67
|
+
);
|
|
42
68
|
}
|
package/Renderers/index.jsx
CHANGED
|
@@ -2,3 +2,4 @@ export {default as SelectCellRenderer} from './SelectCellRenderer';
|
|
|
2
2
|
export {default as ModuleLookupRenderer} from './ModuleLookupRenderer';
|
|
3
3
|
export {default as AddressLookupRenderer} from './AddressLookupRenderer';
|
|
4
4
|
export {default as NumberedListRenderer} from './NumberedListRenderer';
|
|
5
|
+
export {default as RouteCellRenderer} from './RouteCellRenderer';
|
|
@@ -53,7 +53,7 @@ document.addEventListener('focusin', (e) => {
|
|
|
53
53
|
* 1 shoda → auto-výběr + ikona 🔗
|
|
54
54
|
* 2+ shody → multi-match modal → uživatel vybere → 🔗
|
|
55
55
|
*/
|
|
56
|
-
const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing, moduleDefinition, allowFreeText = false, rowHeight = 42, }, _ref) => {
|
|
56
|
+
const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing, moduleDefinition, allowFreeText = false, rowHeight = 42, autoOpen = true, }, _ref) => {
|
|
57
57
|
// ─── Hooks (vždy voláno) ──────────────────────────────────────────────────────
|
|
58
58
|
const { fetchDataUIAsync } = useData();
|
|
59
59
|
const { accessToken } = useOidcAccessToken();
|
|
@@ -97,6 +97,11 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
97
97
|
}, []); // jen při mount
|
|
98
98
|
const [inputText, setInputText] = useState(initialText);
|
|
99
99
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (autoOpen) {
|
|
102
|
+
setDropdownOpen(true);
|
|
103
|
+
}
|
|
104
|
+
}, [autoOpen]);
|
|
100
105
|
const [multiMatchData, setMultiMatchData] = useState([]);
|
|
101
106
|
const [multiMatchModalOpen, setMultiMatchModalOpen] = useState(false);
|
|
102
107
|
const catalogModalOpenRef = useRef(false);
|
|
@@ -105,7 +110,7 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
105
110
|
multiMatchModalOpenRef.current = multiMatchModalOpen;
|
|
106
111
|
const currentTextRef = useRef(initialText);
|
|
107
112
|
const inputTextRef = useRef(initialText); // vždy aktuální inputText pro native handler
|
|
108
|
-
const
|
|
113
|
+
const editorWrapperRef = useRef(null); // ref na wrapper div → querySelector('input')
|
|
109
114
|
const isStoppedRef = useRef(false);
|
|
110
115
|
const justSelectedRef = useRef(false);
|
|
111
116
|
// Nastaven synchronně před každým stopEditing() voláním.
|
|
@@ -119,6 +124,7 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
119
124
|
// Ref na suggestions a handleAutoCompleteSelect — přístupné v capture handleru bez stale closure
|
|
120
125
|
const suggestionsRef = useRef([]);
|
|
121
126
|
const handleAutoCompleteSelectRef = useRef(null);
|
|
127
|
+
const handleCatalogSelectChangeRef = useRef(null);
|
|
122
128
|
// ─── Lokální filtrování návrhů (bez API volání) ───────────────────────────────
|
|
123
129
|
const suggestions = React.useMemo(() => {
|
|
124
130
|
const vf = moduleDefinition?.valueField || 'id';
|
|
@@ -128,12 +134,18 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
128
134
|
const label = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || '';
|
|
129
135
|
return label.toLowerCase().includes(inputText.toLowerCase());
|
|
130
136
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
const result = source.map(item => {
|
|
138
|
+
const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
|
|
139
|
+
const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
|
|
140
|
+
return {
|
|
141
|
+
value: String(item[vf]),
|
|
142
|
+
label: (_jsx("div", { "data-mle-value": String(item[vf]), style: { display: 'contents' }, children: originalLabel })),
|
|
143
|
+
displayText,
|
|
144
|
+
item,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
return allowFreeText ? result.slice(0, 50) : result;
|
|
148
|
+
}, [allItems, inputText, moduleDefinition, allowFreeText]);
|
|
137
149
|
suggestionsRef.current = suggestions;
|
|
138
150
|
// ─── AG Grid useGridCellEditor ───────────────────────────────────────────────
|
|
139
151
|
// AG Grid 35 vyžaduje getValue() pro získání výsledné hodnoty při stopEditing().
|
|
@@ -195,6 +207,7 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
195
207
|
onValueChange(item);
|
|
196
208
|
setTimeout(() => stopEditing(), 0);
|
|
197
209
|
}, [allItems, moduleDefinition, onValueChange, stopEditing]);
|
|
210
|
+
handleCatalogSelectChangeRef.current = handleCatalogSelectChange;
|
|
198
211
|
const catalogCurrentId = React.useMemo(() => {
|
|
199
212
|
if (allowFreeText || !value)
|
|
200
213
|
return null;
|
|
@@ -343,9 +356,7 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
343
356
|
// rc-select volá stopPropagation() na Enter v bubble fázi → náš React onKeyDown
|
|
344
357
|
// na wrapperu ho nikdy nedostane. Capture-phase native listener se spustí jako první.
|
|
345
358
|
useEffect(() => {
|
|
346
|
-
|
|
347
|
-
return;
|
|
348
|
-
const el = freeTextWrapperRef.current;
|
|
359
|
+
const el = editorWrapperRef.current;
|
|
349
360
|
if (!el)
|
|
350
361
|
return;
|
|
351
362
|
const input = el.querySelector('input');
|
|
@@ -369,16 +380,41 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
369
380
|
if (activeOptionEl) {
|
|
370
381
|
e.stopImmediatePropagation();
|
|
371
382
|
e.preventDefault();
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
|
|
377
|
-
|
|
383
|
+
const contentEl = activeOptionEl.querySelector('.ant-select-item-option-content');
|
|
384
|
+
if (contentEl) {
|
|
385
|
+
const mleValue = contentEl.querySelector('[data-mle-value]')?.getAttribute('data-mle-value');
|
|
386
|
+
if (mleValue) {
|
|
387
|
+
const matched = suggestionsRef.current.find((s) => s.value === mleValue);
|
|
388
|
+
if (matched) {
|
|
389
|
+
if (allowFreeText) {
|
|
390
|
+
handleAutoCompleteSelectRef.current?.(matched.value, matched);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
handleCatalogSelectChangeRef.current?.(matched.value);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const spanText = contentEl.querySelector('span')?.textContent?.trim();
|
|
399
|
+
const fullText = contentEl.textContent?.trim();
|
|
400
|
+
const labelText = spanText || fullText;
|
|
401
|
+
if (labelText) {
|
|
402
|
+
const matched = suggestionsRef.current.find((s) => s.displayText === labelText || String(s.label) === labelText);
|
|
403
|
+
if (matched) {
|
|
404
|
+
if (allowFreeText) {
|
|
405
|
+
handleAutoCompleteSelectRef.current?.(matched.value, matched);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
handleCatalogSelectChangeRef.current?.(matched.value);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
378
412
|
}
|
|
379
413
|
}
|
|
380
414
|
return;
|
|
381
415
|
}
|
|
416
|
+
if (!allowFreeText)
|
|
417
|
+
return;
|
|
382
418
|
const text = inputTextRef.current || '';
|
|
383
419
|
// Prázdný input bez předchozí hodnoty → nechat AG Grid zpracovat (cancel)
|
|
384
420
|
if (!text.trim() && pendingValueRef.current === null && !initialText)
|
|
@@ -402,14 +438,19 @@ const ModuleLookupEditor = React.forwardRef(({ value, onValueChange, stopEditing
|
|
|
402
438
|
// ─── Render: catalog-only mód ─────────────────────────────────────────────────
|
|
403
439
|
if (!allowFreeText) {
|
|
404
440
|
const vf = moduleDefinition?.valueField || 'id';
|
|
405
|
-
const selectOptions = allItems.map(item =>
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
441
|
+
const selectOptions = allItems.map(item => {
|
|
442
|
+
const displayText = moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || String(item[vf] || '');
|
|
443
|
+
const originalLabel = moduleDefinition?.renderOption ? moduleDefinition.renderOption(item, displayText) : displayText;
|
|
444
|
+
return {
|
|
445
|
+
value: item[vf],
|
|
446
|
+
label: (_jsx("div", { "data-mle-value": String(item[vf]), style: { display: 'contents' }, children: originalLabel })),
|
|
447
|
+
displayText,
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
return (_jsxs("div", { style: { width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }, ref: editorWrapperRef, children: [_jsxs("div", { style: { display: 'flex', flex: 1, height: '100%' }, children: [_jsx(Select, { style: { flex: 1, height: '100%' }, value: catalogCurrentId, options: selectOptions, optionLabelProp: "displayText", loading: allItemsLoading, showSearch: true, filterOption: (input, option) => String(option?.displayText || option?.label || '').toLowerCase().includes(input.toLowerCase()), onChange: handleCatalogSelectChange, onSearch: handleInputChange, getPopupContainer: () => document.body, dropdownStyle: { zIndex: 9999 }, placeholder: moduleDefinition?.placeholder || 'Vyberte...', defaultOpen: autoOpen, autoFocus: true }), _jsx(Button, { icon: _jsx(SearchOutlined, {}), onMouseDown: e => e.preventDefault(), onClick: () => setCatalogModalOpen(true), type: "primary", style: { height: '100%', borderRadius: 0 } })] }), catalogModalContent] }));
|
|
410
451
|
}
|
|
411
452
|
// ─── Render: free-text mód ────────────────────────────────────────────────────
|
|
412
|
-
return (_jsxs("div", { style: { width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }, children: [_jsxs("div", { ref:
|
|
453
|
+
return (_jsxs("div", { style: { width: '100%', minWidth: 250, height: rowHeight, display: 'flex', alignItems: 'stretch' }, children: [_jsxs("div", { ref: editorWrapperRef, style: { display: 'flex', flex: 1, height: '100%' }, children: [_jsx(AutoComplete, { style: { flex: 1 }, value: inputText, options: suggestions, open: dropdownOpen && suggestions.length > 0, defaultActiveFirstOption: false, allowClear: true, onChange: handleInputChange, onClear: handleClear, onSelect: handleAutoCompleteSelect, onFocus: () => setDropdownOpen(true), onBlur: () => {
|
|
413
454
|
setTimeout(() => setDropdownOpen(false), 150);
|
|
414
455
|
handleBlur();
|
|
415
456
|
}, notFoundContent: allItemsLoading ? 'Načítání...' : null, placeholder: moduleDefinition?.placeholder || 'Zadejte nebo vyberte...', autoFocus: true }), _jsx(Button, { icon: _jsx(SearchOutlined, {}), onMouseDown: e => e.preventDefault(), onClick: () => {
|