@bit.rhplus/ui2.module-dropdown-list 0.1.111 → 0.1.112
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/dist/index.d.ts +14 -1
- package/dist/index.js +402 -151
- package/dist/index.js.map +1 -1
- package/index.jsx +653 -395
- package/package.json +6 -7
- package/ModuleInput.css +0 -259
- package/dist/ModuleInput.css +0 -259
- package/dist/store/recentItemsStore.d.ts +0 -11
- package/dist/store/recentItemsStore.js +0 -29
- package/dist/store/recentItemsStore.js.map +0 -1
- package/dist/useModuleDropdownList.d.ts +0 -26
- package/dist/useModuleDropdownList.js +0 -171
- package/dist/useModuleDropdownList.js.map +0 -1
- package/store/recentItemsStore.js +0 -37
- package/useModuleDropdownList.js +0 -198
- /package/dist/{preview-1773922763074.js → preview-1774279607741.js} +0 -0
package/index.jsx
CHANGED
|
@@ -1,395 +1,653 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import React, { useEffect, useRef, useCallback } from 'react';
|
|
3
|
-
import
|
|
4
|
-
import Select from 'antd/es/select';
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { SearchOutlined } from '@ant-design/icons';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
3
|
+
import AutoComplete from 'antd/es/auto-complete';
|
|
4
|
+
import Select from 'antd/es/select';
|
|
5
|
+
import Button from 'antd/es/button';
|
|
6
|
+
import Input from 'antd/es/input';
|
|
7
|
+
import { SearchOutlined, LinkOutlined, EditOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons';
|
|
8
|
+
import { AgGridReact } from 'ag-grid-react';
|
|
9
|
+
import DraggableModal from '@bit.rhplus/draggable-modal';
|
|
10
|
+
import useData from '@bit.rhplus/data';
|
|
11
|
+
import { useOidcAccessToken } from '@axa-fr/react-oidc';
|
|
12
|
+
|
|
13
|
+
// Session-level cache — data načtena jednou per moduleName per session
|
|
14
|
+
const _cache = {};
|
|
15
|
+
|
|
16
|
+
// ─── Detail panel subkomponenta ───────────────────────────────────────────────
|
|
17
|
+
const DetailPanel = ({ definition, item }) => {
|
|
18
|
+
if (!definition || !item) return null;
|
|
19
|
+
if (typeof definition.render === 'function') return definition.render(item);
|
|
20
|
+
if (!Array.isArray(definition.fields)) return null;
|
|
21
|
+
if (!definition.fields.length) return null;
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
style={{
|
|
25
|
+
background: '#fafafa',
|
|
26
|
+
border: '1px solid #d9d9d9',
|
|
27
|
+
borderRadius: 6,
|
|
28
|
+
padding: '8px 12px',
|
|
29
|
+
display: 'grid',
|
|
30
|
+
gridTemplateColumns: '1fr 1fr',
|
|
31
|
+
gap: '4px 16px',
|
|
32
|
+
fontSize: 12,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{definition.fields.map((f) => (
|
|
36
|
+
<div key={f.key} style={{ display: 'flex', gap: 4, minWidth: 0 }}>
|
|
37
|
+
<span style={{ color: '#8c8c8c', flexShrink: 0 }}>{f.label}:</span>
|
|
38
|
+
<span
|
|
39
|
+
style={{
|
|
40
|
+
fontWeight: 500,
|
|
41
|
+
overflow: 'hidden',
|
|
42
|
+
textOverflow: 'ellipsis',
|
|
43
|
+
whiteSpace: 'nowrap',
|
|
44
|
+
color: item[f.key] ? 'inherit' : '#bfbfbf',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{item[f.key] ?? '—'}
|
|
48
|
+
</span>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ModuleDropdownList = ({
|
|
56
|
+
value,
|
|
57
|
+
onChange,
|
|
58
|
+
onFreeTextCommit,
|
|
59
|
+
placeholder = 'Vyberte',
|
|
60
|
+
disabled = false,
|
|
61
|
+
style = {},
|
|
62
|
+
allowFreeText = true,
|
|
63
|
+
createNewOnMismatch = false,
|
|
64
|
+
showArrow = true,
|
|
65
|
+
showAddButton = true,
|
|
66
|
+
getContainer = () => document.body,
|
|
67
|
+
moduleDefinition,
|
|
68
|
+
}) => {
|
|
69
|
+
const moduleName = moduleDefinition?.moduleName;
|
|
70
|
+
|
|
71
|
+
const { fetchDataUIAsync } = useData();
|
|
72
|
+
const { accessToken } = useOidcAccessToken();
|
|
73
|
+
|
|
74
|
+
// ─── Data a načítání ──────────────────────────────────────────────────────────
|
|
75
|
+
const [allItems, setAllItems] = useState(() => _cache[moduleName] || []);
|
|
76
|
+
const [allItemsLoading, setAllItemsLoading] = useState(!_cache[moduleName]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!moduleName || !moduleDefinition) return;
|
|
80
|
+
if (_cache[moduleName]) {
|
|
81
|
+
setAllItems(_cache[moduleName]);
|
|
82
|
+
setAllItemsLoading(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setAllItemsLoading(true);
|
|
86
|
+
moduleDefinition
|
|
87
|
+
.fetchData('', fetchDataUIAsync, accessToken)
|
|
88
|
+
.then((data) => {
|
|
89
|
+
const items = Array.isArray(data) ? data : [];
|
|
90
|
+
_cache[moduleName] = items;
|
|
91
|
+
setAllItems(items);
|
|
92
|
+
})
|
|
93
|
+
.catch(() => setAllItems([]))
|
|
94
|
+
.finally(() => setAllItemsLoading(false));
|
|
95
|
+
}, []); // pouze při mount
|
|
96
|
+
|
|
97
|
+
// ─── Free-text stav ──────────────────────────────────────────────────────────
|
|
98
|
+
const initialText = useMemo(() => {
|
|
99
|
+
if (!allowFreeText) return '';
|
|
100
|
+
if (typeof value === 'string') return value;
|
|
101
|
+
return '';
|
|
102
|
+
}, []); // pouze při mount
|
|
103
|
+
|
|
104
|
+
const [inputText, setInputText] = useState(initialText);
|
|
105
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
106
|
+
// null = prázdno/psaní, true = katalog (🔗), false = volný text (✏️)
|
|
107
|
+
const [isLinked, setIsLinked] = useState(null);
|
|
108
|
+
const [selectedItem, setSelectedItem] = useState(null);
|
|
109
|
+
const [detailOpen, setDetailOpen] = useState(false);
|
|
110
|
+
const justSelectedRef = useRef(false);
|
|
111
|
+
const freeTextWrapperRef = useRef(null);
|
|
112
|
+
const inputTextRef = useRef(initialText);
|
|
113
|
+
inputTextRef.current = inputText;
|
|
114
|
+
const isLinkedRef = useRef(null);
|
|
115
|
+
isLinkedRef.current = isLinked;
|
|
116
|
+
// Ref na aktuální suggestions — přístupný v capture handleru bez stale closure
|
|
117
|
+
const suggestionsRef = useRef([]);
|
|
118
|
+
// Ref na handleAutoCompleteSelect — přístupný v capture handleru bez stale closure
|
|
119
|
+
const handleAutoCompleteSelectRef = useRef(null);
|
|
120
|
+
|
|
121
|
+
// ─── Sync externího value (načtení formuláře) ────────────────────────────────
|
|
122
|
+
// Když se value změní zvenku (form load), najde položku v katalogu a zobrazí ji.
|
|
123
|
+
// Pokud položka není v allItems, zkusí fetchById fallback.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
console.log('[ModuleDropdownList] sync value:', { value, allItemsCount: allItems.length, allItemsLoading, moduleName });
|
|
126
|
+
if (!value) return;
|
|
127
|
+
// Pokud allItems ještě loading, počkáme na další run
|
|
128
|
+
if (allItemsLoading) return;
|
|
129
|
+
|
|
130
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
131
|
+
|
|
132
|
+
const showItem = (item) => {
|
|
133
|
+
console.log('[ModuleDropdownList] showItem:', { id: item?.[vf], displayValue: moduleDefinition?.getDisplayValue?.(item) });
|
|
134
|
+
const displayText =
|
|
135
|
+
moduleDefinition?.getDisplayValue?.(item) || item.code || item.name || '';
|
|
136
|
+
setInputText(displayText);
|
|
137
|
+
setIsLinked(true);
|
|
138
|
+
setSelectedItem(item);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (allItems.length) {
|
|
142
|
+
const item = allItems.find((i) => String(i[vf]) === String(value));
|
|
143
|
+
if (item) {
|
|
144
|
+
showItem(item);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback: fetchById pro položky, které nejsou v seznamu
|
|
150
|
+
if (moduleDefinition?.fetchById) {
|
|
151
|
+
moduleDefinition.fetchById(value, fetchDataUIAsync, accessToken).then((item) => {
|
|
152
|
+
if (item) {
|
|
153
|
+
showItem(item);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}, [value, allItems, allItemsLoading]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
158
|
+
|
|
159
|
+
// ─── Create-new modal ─────────────────────────────────────────────────────────
|
|
160
|
+
const [createNewModalOpen, setCreateNewModalOpen] = useState(false);
|
|
161
|
+
const [createNewInitialName, setCreateNewInitialName] = useState('');
|
|
162
|
+
|
|
163
|
+
// ─── Modaly ───────────────────────────────────────────────────────────────────
|
|
164
|
+
const [catalogFilter, setCatalogFilter] = useState('');
|
|
165
|
+
const [catalogModalOpen, setCatalogModalOpen] = useState(false);
|
|
166
|
+
const [multiMatchData, setMultiMatchData] = useState([]);
|
|
167
|
+
const [multiMatchModalOpen, setMultiMatchModalOpen] = useState(false);
|
|
168
|
+
const catalogModalOpenRef = useRef(false);
|
|
169
|
+
catalogModalOpenRef.current = catalogModalOpen;
|
|
170
|
+
const multiMatchModalOpenRef = useRef(false);
|
|
171
|
+
multiMatchModalOpenRef.current = multiMatchModalOpen;
|
|
172
|
+
|
|
173
|
+
// ─── Lokální filtrování návrhů ────────────────────────────────────────────────
|
|
174
|
+
const suggestions = useMemo(() => {
|
|
175
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
176
|
+
const source =
|
|
177
|
+
!inputText || !inputText.trim()
|
|
178
|
+
? allItems
|
|
179
|
+
: allItems.filter((item) => {
|
|
180
|
+
const label =
|
|
181
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
182
|
+
item.code ||
|
|
183
|
+
item.name ||
|
|
184
|
+
'';
|
|
185
|
+
return label.toLowerCase().includes(inputText.toLowerCase());
|
|
186
|
+
});
|
|
187
|
+
return source.slice(0, 15).map((item) => ({
|
|
188
|
+
value: String(item[vf]),
|
|
189
|
+
label:
|
|
190
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
191
|
+
item.code ||
|
|
192
|
+
item.name ||
|
|
193
|
+
'',
|
|
194
|
+
item,
|
|
195
|
+
}));
|
|
196
|
+
}, [allItems, inputText, moduleDefinition]);
|
|
197
|
+
suggestionsRef.current = suggestions;
|
|
198
|
+
|
|
199
|
+
// Catalog-only: options pro Select
|
|
200
|
+
const catalogOptions = useMemo(() => {
|
|
201
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
202
|
+
return allItems.map((item) => ({
|
|
203
|
+
value: item[vf],
|
|
204
|
+
label:
|
|
205
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
206
|
+
item.code ||
|
|
207
|
+
item.name ||
|
|
208
|
+
String(item[vf]),
|
|
209
|
+
}));
|
|
210
|
+
}, [allItems, moduleDefinition]);
|
|
211
|
+
|
|
212
|
+
// ─── Výběr z katalogu (shared) ────────────────────────────────────────────────
|
|
213
|
+
const commitCatalogItem = useCallback(
|
|
214
|
+
(item) => {
|
|
215
|
+
if (!item) return;
|
|
216
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
217
|
+
const id = item[vf];
|
|
218
|
+
setSelectedItem(item);
|
|
219
|
+
if (typeof onChange === 'function') {
|
|
220
|
+
onChange(id, item);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
[moduleDefinition, onChange]
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const handleCreateSuccess = useCallback(
|
|
227
|
+
(newItem) => {
|
|
228
|
+
if (newItem) {
|
|
229
|
+
_cache[moduleName] = [...(_cache[moduleName] || []), newItem];
|
|
230
|
+
setAllItems((prev) => [...prev, newItem]);
|
|
231
|
+
const displayText =
|
|
232
|
+
moduleDefinition?.getDisplayValue?.(newItem) || newItem.name || '';
|
|
233
|
+
setInputText(displayText);
|
|
234
|
+
setIsLinked(true);
|
|
235
|
+
commitCatalogItem(newItem);
|
|
236
|
+
}
|
|
237
|
+
setCreateNewModalOpen(false);
|
|
238
|
+
},
|
|
239
|
+
[moduleName, moduleDefinition, commitCatalogItem]
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// ─── Catalog-only: výběr ze Select ───────────────────────────────────────────
|
|
243
|
+
const handleCatalogSelectChange = useCallback(
|
|
244
|
+
(selectedId) => {
|
|
245
|
+
if (!selectedId) {
|
|
246
|
+
if (typeof onChange === 'function') onChange(null, null);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
250
|
+
const item = allItems.find((i) => String(i[vf]) === String(selectedId));
|
|
251
|
+
if (item) commitCatalogItem(item);
|
|
252
|
+
},
|
|
253
|
+
[allItems, moduleDefinition, commitCatalogItem, onChange]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// ─── Výběr z catalog modalu ───────────────────────────────────────────────────
|
|
257
|
+
const handleCatalogModalSelect = useCallback(
|
|
258
|
+
(params) => {
|
|
259
|
+
if (!params.data) return;
|
|
260
|
+
const item = params.data;
|
|
261
|
+
if (allowFreeText) {
|
|
262
|
+
const displayText =
|
|
263
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
264
|
+
item.code ||
|
|
265
|
+
item.name ||
|
|
266
|
+
'';
|
|
267
|
+
setInputText(displayText);
|
|
268
|
+
}
|
|
269
|
+
isLinkedRef.current = true; // synchronní update před zavřením modalu (onCancel race condition)
|
|
270
|
+
setIsLinked(true);
|
|
271
|
+
commitCatalogItem(item);
|
|
272
|
+
setCatalogModalOpen(false);
|
|
273
|
+
},
|
|
274
|
+
[allowFreeText, moduleDefinition, commitCatalogItem]
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// ─── Free-text: výběr návrhu z AutoComplete ───────────────────────────────────
|
|
278
|
+
const handleAutoCompleteSelect = useCallback(
|
|
279
|
+
(optionValue, option) => {
|
|
280
|
+
justSelectedRef.current = true;
|
|
281
|
+
const item = option.item;
|
|
282
|
+
const displayText =
|
|
283
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
284
|
+
item.code ||
|
|
285
|
+
item.name ||
|
|
286
|
+
'';
|
|
287
|
+
setInputText(displayText);
|
|
288
|
+
setDropdownOpen(false);
|
|
289
|
+
setIsLinked(true);
|
|
290
|
+
commitCatalogItem(item);
|
|
291
|
+
// Ztratit focus z inputu po výběru
|
|
292
|
+
freeTextWrapperRef.current?.querySelector('input')?.blur();
|
|
293
|
+
},
|
|
294
|
+
[moduleDefinition, commitCatalogItem]
|
|
295
|
+
);
|
|
296
|
+
handleAutoCompleteSelectRef.current = handleAutoCompleteSelect;
|
|
297
|
+
|
|
298
|
+
// ─── Free-text: validace po Enter / blur ──────────────────────────────────────
|
|
299
|
+
const handleFreeTextCommit = useCallback(
|
|
300
|
+
(text) => {
|
|
301
|
+
if (!text || !text.trim()) {
|
|
302
|
+
if (typeof onChange === 'function') onChange(null, null);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const searchLower = text.trim().toLowerCase();
|
|
307
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
308
|
+
const localMatches = allItems.filter((item) => {
|
|
309
|
+
const label =
|
|
310
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
311
|
+
item.code ||
|
|
312
|
+
item.name ||
|
|
313
|
+
'';
|
|
314
|
+
return label.toLowerCase() === searchLower;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (localMatches.length === 0) {
|
|
318
|
+
if (createNewOnMismatch && moduleDefinition?.createFormComponent) {
|
|
319
|
+
// Create-new mód — otevřít formulář pro vytvoření nového záznamu
|
|
320
|
+
setCreateNewInitialName(text.trim());
|
|
321
|
+
setCreateNewModalOpen(true);
|
|
322
|
+
} else {
|
|
323
|
+
// Volný text — žádná shoda v katalogu
|
|
324
|
+
setIsLinked(false);
|
|
325
|
+
setSelectedItem(null);
|
|
326
|
+
setDetailOpen(false);
|
|
327
|
+
if (typeof onFreeTextCommit === 'function') {
|
|
328
|
+
onFreeTextCommit(text.trim());
|
|
329
|
+
}
|
|
330
|
+
if (typeof onChange === 'function') onChange(null, null);
|
|
331
|
+
freeTextWrapperRef.current?.querySelector('input')?.blur();
|
|
332
|
+
}
|
|
333
|
+
} else if (localMatches.length === 1) {
|
|
334
|
+
// Přesná jedna shoda → auto-výběr
|
|
335
|
+
const item = localMatches[0];
|
|
336
|
+
const displayText =
|
|
337
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
338
|
+
item.code ||
|
|
339
|
+
item.name ||
|
|
340
|
+
text.trim();
|
|
341
|
+
setInputText(displayText);
|
|
342
|
+
setIsLinked(true);
|
|
343
|
+
commitCatalogItem(item);
|
|
344
|
+
} else {
|
|
345
|
+
// Více shod → multi-match modal
|
|
346
|
+
setMultiMatchData(localMatches);
|
|
347
|
+
setMultiMatchModalOpen(true);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
[allItems, moduleDefinition, onChange, onFreeTextCommit, commitCatalogItem]
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// ─── Free-text: blur ──────────────────────────────────────────────────────────
|
|
354
|
+
const handleBlur = useCallback(() => {
|
|
355
|
+
if (justSelectedRef.current) {
|
|
356
|
+
justSelectedRef.current = false;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// Pokud už byla hodnota commitnuta (katalog / autocomplete), neoverwritovat
|
|
360
|
+
if (isLinkedRef.current === true) return;
|
|
361
|
+
// Refy místo state — vždy aktuální hodnota, bez stale closure problémů
|
|
362
|
+
if (inputTextRef.current && !multiMatchModalOpenRef.current && !catalogModalOpenRef.current) {
|
|
363
|
+
handleFreeTextCommit(inputTextRef.current);
|
|
364
|
+
}
|
|
365
|
+
}, [handleFreeTextCommit]);
|
|
366
|
+
|
|
367
|
+
// ─── Free-text: výběr z multi-match modalu ────────────────────────────────────
|
|
368
|
+
const handleMultiMatchSelect = useCallback(
|
|
369
|
+
(params) => {
|
|
370
|
+
if (!params.data) return;
|
|
371
|
+
const item = params.data;
|
|
372
|
+
const displayText =
|
|
373
|
+
moduleDefinition?.getDisplayValue?.(item) ||
|
|
374
|
+
item.code ||
|
|
375
|
+
item.name ||
|
|
376
|
+
'';
|
|
377
|
+
setInputText(displayText);
|
|
378
|
+
isLinkedRef.current = true; // synchronní update před zavřením modalu (onCancel race condition)
|
|
379
|
+
setIsLinked(true);
|
|
380
|
+
setMultiMatchModalOpen(false);
|
|
381
|
+
commitCatalogItem(item);
|
|
382
|
+
},
|
|
383
|
+
[moduleDefinition, commitCatalogItem]
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// ─── Native capture Enter handler (rc-select stopPropagation obrana) ─────────
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
if (!allowFreeText) return;
|
|
389
|
+
const el = freeTextWrapperRef.current;
|
|
390
|
+
if (!el) return;
|
|
391
|
+
const input = el.querySelector('input');
|
|
392
|
+
if (!input) return;
|
|
393
|
+
|
|
394
|
+
const handler = (e) => {
|
|
395
|
+
if (e.key !== 'Enter') return;
|
|
396
|
+
|
|
397
|
+
// Pokud je v dropdown označená položka šipkou, ručně ji vybereme
|
|
398
|
+
const activeOptionEl = document.querySelector('.ant-select-item-option-active');
|
|
399
|
+
if (activeOptionEl) {
|
|
400
|
+
e.stopImmediatePropagation();
|
|
401
|
+
e.preventDefault();
|
|
402
|
+
const labelText = activeOptionEl.querySelector('.ant-select-item-option-content')?.textContent?.trim();
|
|
403
|
+
if (labelText) {
|
|
404
|
+
const matched = suggestionsRef.current.find((s) => s.label === labelText);
|
|
405
|
+
if (matched) {
|
|
406
|
+
handleAutoCompleteSelectRef.current(matched.value, matched);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Žádná aktivní položka → volný text
|
|
414
|
+
const text = inputTextRef.current || '';
|
|
415
|
+
if (!text.trim()) return;
|
|
416
|
+
e.stopImmediatePropagation();
|
|
417
|
+
e.preventDefault();
|
|
418
|
+
handleFreeTextCommit(text);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
input.addEventListener('keydown', handler, true);
|
|
422
|
+
return () => input.removeEventListener('keydown', handler, true);
|
|
423
|
+
}, [allowFreeText, handleFreeTextCommit]);
|
|
424
|
+
|
|
425
|
+
// ─── Shared: create-new modal ────────────────────────────────────────────────
|
|
426
|
+
const createNewModalContent = moduleDefinition?.createFormComponent ? (
|
|
427
|
+
<DraggableModal
|
|
428
|
+
open={createNewModalOpen}
|
|
429
|
+
title={moduleDefinition.createModalTitle || 'Nový záznam'}
|
|
430
|
+
width={moduleDefinition.createModalWidth || 600}
|
|
431
|
+
onCancel={() => setCreateNewModalOpen(false)}
|
|
432
|
+
maskClosable={false}
|
|
433
|
+
getContainer={getContainer}
|
|
434
|
+
zIndex={99999999}
|
|
435
|
+
footer={null}
|
|
436
|
+
>
|
|
437
|
+
{React.createElement(moduleDefinition.createFormComponent, {
|
|
438
|
+
initialName: createNewInitialName,
|
|
439
|
+
onSuccess: handleCreateSuccess,
|
|
440
|
+
onCancel: () => setCreateNewModalOpen(false),
|
|
441
|
+
fetchDataUIAsync,
|
|
442
|
+
accessToken,
|
|
443
|
+
createRecord: moduleDefinition.createRecord,
|
|
444
|
+
})}
|
|
445
|
+
</DraggableModal>
|
|
446
|
+
) : null;
|
|
447
|
+
|
|
448
|
+
const addButton = showAddButton && moduleDefinition?.createFormComponent ? (
|
|
449
|
+
<Button
|
|
450
|
+
icon={<PlusOutlined />}
|
|
451
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
452
|
+
onClick={() => {
|
|
453
|
+
setCreateNewInitialName('');
|
|
454
|
+
setCreateNewModalOpen(true);
|
|
455
|
+
}}
|
|
456
|
+
disabled={disabled}
|
|
457
|
+
/>
|
|
458
|
+
) : null;
|
|
459
|
+
|
|
460
|
+
const searchButton = (onClickFn) => (
|
|
461
|
+
<Button
|
|
462
|
+
icon={<SearchOutlined />}
|
|
463
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
464
|
+
onClick={onClickFn}
|
|
465
|
+
disabled={disabled}
|
|
466
|
+
/>
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// ─── Shared: catalog modal ────────────────────────────────────────────────────
|
|
470
|
+
const catalogModalContent = (
|
|
471
|
+
<DraggableModal
|
|
472
|
+
open={catalogModalOpen}
|
|
473
|
+
title={
|
|
474
|
+
moduleDefinition?.modalTitle ||
|
|
475
|
+
`Vybrat z modulu: ${moduleDefinition?.moduleName}`
|
|
476
|
+
}
|
|
477
|
+
width={moduleDefinition?.modalWidth || 800}
|
|
478
|
+
onCancel={() => {
|
|
479
|
+
setCatalogModalOpen(false);
|
|
480
|
+
setCatalogFilter('');
|
|
481
|
+
// Pokud uživatel zavřel modal bez výběru a text není katalogově propojený, commitni ho
|
|
482
|
+
if (allowFreeText && inputTextRef.current && isLinkedRef.current !== true) {
|
|
483
|
+
setTimeout(() => handleFreeTextCommit(inputTextRef.current), 0);
|
|
484
|
+
}
|
|
485
|
+
}}
|
|
486
|
+
maskClosable={false}
|
|
487
|
+
getContainer={getContainer}
|
|
488
|
+
zIndex={99999999}
|
|
489
|
+
footer={null}
|
|
490
|
+
>
|
|
491
|
+
{allItemsLoading ? (
|
|
492
|
+
<div style={{ padding: 24, textAlign: 'center' }}>Načítání...</div>
|
|
493
|
+
) : (
|
|
494
|
+
<>
|
|
495
|
+
<Input
|
|
496
|
+
prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />}
|
|
497
|
+
placeholder="Hledat..."
|
|
498
|
+
value={catalogFilter}
|
|
499
|
+
onChange={(e) => setCatalogFilter(e.target.value)}
|
|
500
|
+
allowClear
|
|
501
|
+
style={{ marginBottom: 8 }}
|
|
502
|
+
/>
|
|
503
|
+
<div className="ag-theme-alpine" style={{ height: 400, width: '100%' }}>
|
|
504
|
+
<AgGridReact
|
|
505
|
+
columnDefs={moduleDefinition?.columnDefs || []}
|
|
506
|
+
rowData={allItems}
|
|
507
|
+
quickFilterText={catalogFilter}
|
|
508
|
+
onRowDoubleClicked={(params) => {
|
|
509
|
+
handleCatalogModalSelect(params);
|
|
510
|
+
setCatalogFilter('');
|
|
511
|
+
}}
|
|
512
|
+
/>
|
|
513
|
+
</div>
|
|
514
|
+
</>
|
|
515
|
+
)}
|
|
516
|
+
</DraggableModal>
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
// ─── Render: catalog-only mód ─────────────────────────────────────────────────
|
|
520
|
+
if (!allowFreeText) {
|
|
521
|
+
const vf = moduleDefinition?.valueField || 'id';
|
|
522
|
+
const currentId =
|
|
523
|
+
value && typeof value === 'object' ? value[vf] : value;
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
<div style={{ display: 'flex', gap: 4, ...style }}>
|
|
527
|
+
<Select
|
|
528
|
+
style={{ flex: 1 }}
|
|
529
|
+
value={currentId}
|
|
530
|
+
options={catalogOptions}
|
|
531
|
+
loading={allItemsLoading}
|
|
532
|
+
showSearch
|
|
533
|
+
filterOption={(input, option) =>
|
|
534
|
+
String(option?.label || '')
|
|
535
|
+
.toLowerCase()
|
|
536
|
+
.includes(input.toLowerCase())
|
|
537
|
+
}
|
|
538
|
+
onChange={handleCatalogSelectChange}
|
|
539
|
+
getPopupContainer={() => document.body}
|
|
540
|
+
dropdownStyle={{ zIndex: 9999 }}
|
|
541
|
+
placeholder={placeholder}
|
|
542
|
+
disabled={disabled}
|
|
543
|
+
/>
|
|
544
|
+
{addButton}
|
|
545
|
+
{searchButton(() => {
|
|
546
|
+
catalogModalOpenRef.current = true;
|
|
547
|
+
setCatalogModalOpen(true);
|
|
548
|
+
})}
|
|
549
|
+
{catalogModalContent}
|
|
550
|
+
{createNewModalContent}
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ─── Render: free-text mód ────────────────────────────────────────────────────
|
|
556
|
+
return (
|
|
557
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
558
|
+
{/* Hlavní řádek — omezený stylem z props */}
|
|
559
|
+
<div style={{ display: 'flex', gap: 4, alignItems: 'center', ...style }}>
|
|
560
|
+
<div ref={freeTextWrapperRef} style={{ flex: 1, position: 'relative' }}>
|
|
561
|
+
{moduleDefinition?.detailPanel && showArrow && (
|
|
562
|
+
<DownOutlined
|
|
563
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
564
|
+
onClick={() => selectedItem && setDetailOpen((v) => !v)}
|
|
565
|
+
style={{
|
|
566
|
+
position: 'absolute',
|
|
567
|
+
left: -18,
|
|
568
|
+
top: '50%',
|
|
569
|
+
transform: `translateY(-50%) rotate(${detailOpen ? 180 : 0}deg)`,
|
|
570
|
+
transition: 'transform 0.2s',
|
|
571
|
+
fontSize: 11,
|
|
572
|
+
cursor: selectedItem ? 'pointer' : 'default',
|
|
573
|
+
color: selectedItem ? 'rgba(0,0,0,0.45)' : '#d9d9d9',
|
|
574
|
+
zIndex: 1,
|
|
575
|
+
pointerEvents: selectedItem ? 'auto' : 'none',
|
|
576
|
+
}}
|
|
577
|
+
/>
|
|
578
|
+
)}
|
|
579
|
+
<AutoComplete
|
|
580
|
+
style={{ width: '100%' }}
|
|
581
|
+
value={inputText}
|
|
582
|
+
options={suggestions}
|
|
583
|
+
open={dropdownOpen && suggestions.length > 0}
|
|
584
|
+
defaultActiveFirstOption={false}
|
|
585
|
+
allowClear
|
|
586
|
+
onChange={(text) => {
|
|
587
|
+
setInputText(text || '');
|
|
588
|
+
setIsLinked(null);
|
|
589
|
+
setSelectedItem(null);
|
|
590
|
+
setDetailOpen(false);
|
|
591
|
+
}}
|
|
592
|
+
onSelect={handleAutoCompleteSelect}
|
|
593
|
+
onFocus={() => setDropdownOpen(true)}
|
|
594
|
+
onBlur={() => {
|
|
595
|
+
setTimeout(() => setDropdownOpen(false), 150);
|
|
596
|
+
handleBlur();
|
|
597
|
+
}}
|
|
598
|
+
notFoundContent={allItemsLoading ? 'Načítání...' : null}
|
|
599
|
+
placeholder={placeholder}
|
|
600
|
+
disabled={disabled}
|
|
601
|
+
getPopupContainer={() => document.body}
|
|
602
|
+
dropdownStyle={{ zIndex: 9999 }}
|
|
603
|
+
/>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
{!createNewOnMismatch && isLinked === true && (
|
|
607
|
+
<LinkOutlined style={{ color: '#52c41a', alignSelf: 'center', fontSize: 14, flexShrink: 0 }} />
|
|
608
|
+
)}
|
|
609
|
+
{!createNewOnMismatch && isLinked === false && inputText && (
|
|
610
|
+
<EditOutlined style={{ color: '#faad14', alignSelf: 'center', fontSize: 14, flexShrink: 0 }} />
|
|
611
|
+
)}
|
|
612
|
+
|
|
613
|
+
{addButton}
|
|
614
|
+
{searchButton(() => {
|
|
615
|
+
catalogModalOpenRef.current = true;
|
|
616
|
+
setCatalogModalOpen(true);
|
|
617
|
+
})}
|
|
618
|
+
</div>
|
|
619
|
+
|
|
620
|
+
{/* Detail panel — 100% šířky parent containeru, nezávisle na šířce inputu */}
|
|
621
|
+
{moduleDefinition?.detailPanel && detailOpen && selectedItem && (
|
|
622
|
+
<DetailPanel definition={moduleDefinition.detailPanel} item={selectedItem} />
|
|
623
|
+
)}
|
|
624
|
+
|
|
625
|
+
{catalogModalContent}
|
|
626
|
+
{createNewModalContent}
|
|
627
|
+
|
|
628
|
+
{/* Multi-match modal — více shod na zadaný text */}
|
|
629
|
+
<DraggableModal
|
|
630
|
+
open={multiMatchModalOpen}
|
|
631
|
+
title="Nalezeno více shod"
|
|
632
|
+
width={moduleDefinition?.modalWidth || 800}
|
|
633
|
+
onCancel={() => setMultiMatchModalOpen(false)}
|
|
634
|
+
maskClosable={false}
|
|
635
|
+
getContainer={getContainer}
|
|
636
|
+
zIndex={99999999}
|
|
637
|
+
footer={null}
|
|
638
|
+
>
|
|
639
|
+
<div className="ag-theme-alpine" style={{ height: 400, width: '100%' }}>
|
|
640
|
+
<AgGridReact
|
|
641
|
+
columnDefs={moduleDefinition?.columnDefs || []}
|
|
642
|
+
rowData={multiMatchData}
|
|
643
|
+
onRowDoubleClicked={handleMultiMatchSelect}
|
|
644
|
+
/>
|
|
645
|
+
</div>
|
|
646
|
+
</DraggableModal>
|
|
647
|
+
|
|
648
|
+
</div>
|
|
649
|
+
);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
export default ModuleDropdownList;
|
|
653
|
+
/* eslint-enable */
|