@bit.rhplus/ag-grid 0.0.38 → 0.0.40

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/Aggregations.js CHANGED
@@ -61,7 +61,7 @@ let cachedAggregations = null;
61
61
  let cachedRangeHash = null;
62
62
 
63
63
  // Jednoduchý hash funkce pro rychlé porovnání ranges
64
- const hashRanges = (ranges) => {
64
+ export const hashRanges = (ranges) => {
65
65
  if (!ranges || ranges.length === 0) return null;
66
66
  return ranges.map(r =>
67
67
  `${r.startRow?.rowIndex}-${r.endRow?.rowIndex}-${r.columns?.length}-${r.columns?.[0]?.colId}`
@@ -0,0 +1,295 @@
1
+ /* eslint-disable */
2
+ import React, { useState, useCallback, useMemo } from 'react';
3
+ import { Calendar, Button, Space, Tooltip } from 'antd';
4
+ import dayjs from 'dayjs';
5
+ import 'dayjs/locale/cs';
6
+ import styled from 'styled-components';
7
+ import Holidays from 'date-holidays';
8
+
9
+ dayjs.locale('cs');
10
+
11
+ const CalendarContainer = styled.div`
12
+ display: flex;
13
+ gap: 8px;
14
+ margin-bottom: 12px;
15
+ width: 900px;
16
+ justify-content: space-between;
17
+
18
+ .ant-picker-calendar {
19
+ max-width: 260px;
20
+ flex: 1;
21
+ }
22
+
23
+ .ant-picker-calendar-header {
24
+ padding: 8px 12px;
25
+ }
26
+
27
+ .ant-picker-cell-selected .ant-picker-cell-inner {
28
+ background: transparent !important;
29
+ color: inherit !important;
30
+ font-weight: normal !important;
31
+ }
32
+
33
+ .ant-picker-cell-disabled {
34
+ pointer-events: none;
35
+ }
36
+
37
+ .ant-picker-cell-disabled .ant-picker-cell-inner {
38
+ color: rgba(0, 0, 0, 0.25) !important;
39
+ background: transparent !important;
40
+ }
41
+
42
+ .ant-picker-cell:not(.ant-picker-cell-in-view) .ant-picker-cell-inner {
43
+ color: rgba(0, 0, 0, 0.25) !important;
44
+ background: transparent !important;
45
+ }
46
+ `;
47
+
48
+ const BulkEditDatePicker = ({
49
+ value,
50
+ onChange,
51
+ onSubmit,
52
+ onCancel,
53
+ loading,
54
+ format = 'DD.MM.YYYY',
55
+ showTime = false,
56
+ }) => {
57
+ const today = dayjs();
58
+ const [selectedDate, setSelectedDate] = useState(value ? dayjs(value) : null);
59
+ const [baseMonth, setBaseMonth] = useState(today);
60
+
61
+ const month1 = baseMonth;
62
+ const month2 = baseMonth.add(1, 'month');
63
+ const month3 = baseMonth.add(2, 'month');
64
+
65
+ const hd = useMemo(() => new Holidays('CZ'), []);
66
+
67
+ const handleDateSelect = (date) => {
68
+ setSelectedDate(date);
69
+ if (showTime) {
70
+ onChange(date.toISOString());
71
+ } else {
72
+ onChange(date.format('YYYY-MM-DD'));
73
+ }
74
+ };
75
+
76
+ const getHoliday = useCallback((date) => {
77
+ const holidays = hd.isHoliday(date.toDate());
78
+ return holidays && holidays.length > 0 ? holidays[0].name : null;
79
+ }, [hd]);
80
+
81
+ const getVisibleHolidays = useCallback(() => {
82
+ const holidays = [];
83
+ const months = [month1, month2, month3];
84
+
85
+ months.forEach(month => {
86
+ const startOfMonth = month.startOf('month');
87
+ const endOfMonth = month.endOf('month');
88
+ let current = startOfMonth;
89
+
90
+ while (current.isBefore(endOfMonth) || current.isSame(endOfMonth, 'day')) {
91
+ const holidayName = getHoliday(current);
92
+ if (holidayName) {
93
+ holidays.push({
94
+ date: current.format('DD.MM.YYYY'),
95
+ name: holidayName,
96
+ });
97
+ }
98
+ current = current.add(1, 'day');
99
+ }
100
+ });
101
+
102
+ return holidays;
103
+ }, [month1, month2, month3, getHoliday]);
104
+
105
+ const visibleHolidays = useMemo(() => getVisibleHolidays(), [getVisibleHolidays]);
106
+
107
+ const createCellRender = useCallback((monthValue) => {
108
+ return (current, info) => {
109
+ const isSelected = selectedDate &&
110
+ current.date() === selectedDate.date() &&
111
+ current.month() === selectedDate.month() &&
112
+ current.year() === selectedDate.year();
113
+ const isToday = current.isSame(today, 'day');
114
+ const holidayName = getHoliday(current);
115
+
116
+ if (isSelected) {
117
+ return (
118
+ <div className="ant-picker-cell-inner" style={{
119
+ background: '#1890ff',
120
+ color: 'white',
121
+ fontWeight: 600,
122
+ }}>
123
+ {current.date()}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ if (holidayName) {
129
+ return (
130
+ <div className="ant-picker-cell-inner" style={{
131
+ background: '#fff1f0',
132
+ color: '#cf1322',
133
+ fontWeight: 600,
134
+ }}>
135
+ {current.date()}
136
+ </div>
137
+ );
138
+ }
139
+
140
+ if (isToday) {
141
+ return (
142
+ <div className="ant-picker-cell-inner" style={{
143
+ background: '#e6f7ff',
144
+ color: '#1890ff',
145
+ fontWeight: 600,
146
+ border: '1px solid #1890ff',
147
+ }}>
148
+ {current.date()}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ return (
154
+ <div className="ant-picker-cell-inner">
155
+ {current.date()}
156
+ </div>
157
+ );
158
+ };
159
+ }, [selectedDate, today, getHoliday]);
160
+
161
+ return (
162
+ <>
163
+ <CalendarContainer onClick={(e) => e.stopPropagation()}>
164
+ <Calendar
165
+ fullscreen={false}
166
+ value={month1}
167
+ onSelect={handleDateSelect}
168
+ fullCellRender={createCellRender(month1)}
169
+ headerRender={({ value: currentValue }) => (
170
+ <div style={{
171
+ padding: '8px 12px',
172
+ fontWeight: 500,
173
+ textAlign: 'center',
174
+ display: 'flex',
175
+ alignItems: 'center',
176
+ justifyContent: 'space-between'
177
+ }}>
178
+ <span
179
+ style={{ cursor: 'pointer', padding: '0 4px' }}
180
+ onClick={() => setBaseMonth(baseMonth.subtract(1, 'month'))}
181
+ >
182
+
183
+ </span>
184
+ <span>{currentValue.format('MMMM YYYY')}</span>
185
+ <span
186
+ style={{ cursor: 'pointer', padding: '0 4px' }}
187
+ onClick={() => setBaseMonth(baseMonth.add(1, 'month'))}
188
+ >
189
+
190
+ </span>
191
+ </div>
192
+ )}
193
+ />
194
+ <Calendar
195
+ fullscreen={false}
196
+ value={month2}
197
+ onSelect={handleDateSelect}
198
+ fullCellRender={createCellRender(month2)}
199
+ headerRender={({ value: currentValue }) => (
200
+ <div style={{
201
+ padding: '8px 12px',
202
+ fontWeight: 500,
203
+ textAlign: 'center',
204
+ display: 'flex',
205
+ alignItems: 'center',
206
+ justifyContent: 'space-between'
207
+ }}>
208
+ <span
209
+ style={{ cursor: 'pointer', padding: '0 4px' }}
210
+ onClick={() => setBaseMonth(baseMonth.subtract(1, 'month'))}
211
+ >
212
+
213
+ </span>
214
+ <span>{currentValue.format('MMMM YYYY')}</span>
215
+ <span
216
+ style={{ cursor: 'pointer', padding: '0 4px' }}
217
+ onClick={() => setBaseMonth(baseMonth.add(1, 'month'))}
218
+ >
219
+
220
+ </span>
221
+ </div>
222
+ )}
223
+ />
224
+ <Calendar
225
+ fullscreen={false}
226
+ value={month3}
227
+ onSelect={handleDateSelect}
228
+ fullCellRender={createCellRender(month3)}
229
+ headerRender={({ value: currentValue }) => (
230
+ <div style={{
231
+ padding: '8px 12px',
232
+ fontWeight: 500,
233
+ textAlign: 'center',
234
+ display: 'flex',
235
+ alignItems: 'center',
236
+ justifyContent: 'space-between'
237
+ }}>
238
+ <span
239
+ style={{ cursor: 'pointer', padding: '0 4px' }}
240
+ onClick={() => setBaseMonth(baseMonth.subtract(1, 'month'))}
241
+ >
242
+
243
+ </span>
244
+ <span>{currentValue.format('MMMM YYYY')}</span>
245
+ <span
246
+ style={{ cursor: 'pointer', padding: '0 4px' }}
247
+ onClick={() => setBaseMonth(baseMonth.add(1, 'month'))}
248
+ >
249
+
250
+ </span>
251
+ </div>
252
+ )}
253
+ />
254
+ </CalendarContainer>
255
+
256
+ {visibleHolidays.length > 0 && (
257
+ <div style={{ marginBottom: '12px', fontSize: '11px', color: '#666' }}>
258
+ {visibleHolidays.map((holiday, index) => {
259
+ const isSelectedHoliday = selectedDate && holiday.date === selectedDate.format('DD.MM.YYYY');
260
+ return (
261
+ <div
262
+ key={index}
263
+ style={{
264
+ marginBottom: '2px',
265
+ background: isSelectedHoliday ? '#e6f7ff' : 'transparent',
266
+ padding: isSelectedHoliday ? '2px 4px' : '0',
267
+ borderRadius: '2px',
268
+ fontWeight: isSelectedHoliday ? 600 : 'normal',
269
+ }}
270
+ >
271
+ <span style={{ color: '#cf1322', fontWeight: 500 }}>{holiday.date}</span> - {holiday.name}
272
+ </div>
273
+ );
274
+ })}
275
+ </div>
276
+ )}
277
+
278
+ <Space style={{ width: '100%', justifyContent: 'flex-end' }}>
279
+ <Button size="small" onClick={onCancel} disabled={loading}>
280
+ Zrušit
281
+ </Button>
282
+ <Button
283
+ type="primary"
284
+ size="small"
285
+ onClick={() => onSubmit(value)}
286
+ loading={loading}
287
+ >
288
+ Použít
289
+ </Button>
290
+ </Space>
291
+ </>
292
+ );
293
+ };
294
+
295
+ export default BulkEditDatePicker;
@@ -163,7 +163,7 @@ const BulkEditPopover = ({
163
163
  padding: '12px',
164
164
  boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
165
165
  minWidth: '280px',
166
- maxWidth: '320px',
166
+ maxWidth: '1050px',
167
167
  }}
168
168
  >
169
169
  <div style={{ marginBottom: '8px' }}>
@@ -218,7 +218,7 @@ const BulkEditPopover = ({
218
218
  padding: '12px',
219
219
  boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
220
220
  minWidth: '280px',
221
- maxWidth: '320px',
221
+ maxWidth: '1050px',
222
222
  }}
223
223
  >
224
224
  <div style={{ marginBottom: '8px' }}>
package/BulkEdit/index.js CHANGED
@@ -2,4 +2,8 @@
2
2
  export { useBulkCellEdit } from './useBulkCellEdit';
3
3
  export { default as BulkEditButton } from './BulkEditButton';
4
4
  export { default as BulkEditPopover } from './BulkEditPopover';
5
+ export { default as BulkEditSelect } from './BulkEditSelect';
6
+ export { default as BulkEditDatePicker } from './BulkEditDatePicker';
7
+
5
8
  export * from './utils';
9
+
@@ -44,27 +44,111 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
44
44
  });
45
45
 
46
46
  const scrollListenerRef = useRef(null);
47
- const rangeTimeoutRef = useRef(null);
47
+ const debounceTimeoutRef = useRef(null);
48
+ const pendingEventRef = useRef(null);
48
49
 
49
50
  /**
50
- * Handler pro změnu range selection
51
+ * Helper funkce pro výpočet pozice buttonu
52
+ */
53
+ const calculateButtonPosition = useCallback((cellPosition) => {
54
+ let buttonX, buttonY;
55
+
56
+ switch (buttonPosition) {
57
+ case 'bottom-right':
58
+ buttonX = cellPosition.right + buttonOffset.x;
59
+ buttonY = cellPosition.bottom + buttonOffset.y;
60
+ break;
61
+ case 'bottom-left':
62
+ buttonX = cellPosition.left - buttonOffset.x;
63
+ buttonY = cellPosition.bottom + buttonOffset.y;
64
+ break;
65
+ case 'top-right':
66
+ buttonX = cellPosition.right + buttonOffset.x;
67
+ buttonY = cellPosition.top - buttonOffset.y;
68
+ break;
69
+ case 'top-left':
70
+ buttonX = cellPosition.left - buttonOffset.x;
71
+ buttonY = cellPosition.top - buttonOffset.y;
72
+ break;
73
+ default:
74
+ buttonX = cellPosition.right + buttonOffset.x;
75
+ buttonY = cellPosition.bottom + buttonOffset.y;
76
+ }
77
+
78
+ return { x: buttonX, y: buttonY };
79
+ }, [buttonPosition, buttonOffset]);
80
+
81
+ /**
82
+ * Handler pro změnu range selection s okamžitou + debounced validací
51
83
  */
52
84
  const handleRangeChange = useCallback(
53
85
  (event) => {
54
- // Clear předchozí timeout
55
- if (rangeTimeoutRef.current) {
56
- clearTimeout(rangeTimeoutRef.current);
57
- }
58
-
59
- // 1. Kontrola enableBulkEdit
86
+ // Lightweight pre-checks - okamžitě bez debounce
60
87
  if (!enabled) {
61
88
  setFloatingButton({ visible: false });
62
89
  return;
63
90
  }
64
91
 
65
92
  const ranges = event.api.getCellRanges();
93
+ if (!ranges || ranges.length === 0) {
94
+ setFloatingButton({ visible: false });
95
+ return;
96
+ }
97
+
98
+ const range = ranges[0];
99
+
100
+ // ✨ OKAŽITÁ lightweight validace - zobraz button HNED
101
+ if (allowMultiColumn || range.columns.length === 1) {
102
+ const cellCount = Math.abs(range.endRow.rowIndex - range.startRow.rowIndex) + 1;
103
+
104
+ if (cellCount >= minCells) {
105
+ // Kontrola zda má sloupec bulkEditApi (rychlá kontrola)
106
+ const column = range.columns[0];
107
+ const colDef = column?.getColDef();
108
+ const hasBulkEditApi = colDef?.bulkEditApi || colDef?.bulkEditPopover;
109
+
110
+ // Zobraz button jen pokud má bulkEditApi nebo bulkEditPopover
111
+ if (hasBulkEditApi) {
112
+ const cellPosition = getLastCellPosition(range, event.api);
113
+ if (cellPosition) {
114
+ setFloatingButton({
115
+ visible: true,
116
+ position: calculateButtonPosition(cellPosition),
117
+ range,
118
+ column: column,
119
+ cellCount,
120
+ });
121
+ }
122
+ } else {
123
+ // Skryj button pokud sloupec nemá bulk edit podporu
124
+ setFloatingButton({ visible: false });
125
+ }
126
+ }
127
+ }
128
+
129
+ // Uložit event pro debounced těžkou validaci (editable check)
130
+ pendingEventRef.current = event;
66
131
 
67
- // 2. Kontrola existence ranges
132
+ // Debounce logika - těžká validace proběhne na pozadí (50ms)
133
+ if (debounceTimeoutRef.current) {
134
+ clearTimeout(debounceTimeoutRef.current);
135
+ }
136
+
137
+ debounceTimeoutRef.current = setTimeout(() => {
138
+ if (pendingEventRef.current) {
139
+ executeRangeValidation(pendingEventRef.current);
140
+ }
141
+ }, 50);
142
+ },
143
+ [enabled, allowMultiColumn, minCells, calculateButtonPosition]
144
+ );
145
+
146
+ /**
147
+ * Těžké validace a výpočty v debounced callbacku
148
+ */
149
+ const executeRangeValidation = useCallback(
150
+ (event) => {
151
+ const ranges = event.api.getCellRanges();
68
152
  if (!ranges || ranges.length === 0) {
69
153
  setFloatingButton({ visible: false });
70
154
  return;
@@ -72,70 +156,44 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
72
156
 
73
157
  const range = ranges[0];
74
158
 
75
- // 3. Kontrola single column (pokud není povoleno multi-column)
159
+ // Kontrola single column
76
160
  if (!allowMultiColumn && range.columns.length !== 1) {
77
161
  setFloatingButton({ visible: false });
78
162
  return;
79
163
  }
80
164
 
81
- // 4. Výpočet počtu buněk
165
+ // Výpočet počtu buněk
82
166
  const cellCount =
83
167
  Math.abs(range.endRow.rowIndex - range.startRow.rowIndex) + 1;
84
168
 
85
- // 5. Kontrola min počtu buněk
169
+ // Kontrola min počtu buněk
86
170
  if (cellCount < minCells) {
87
171
  setFloatingButton({ visible: false });
88
172
  return;
89
173
  }
90
174
 
91
- // 6. Kontrola editable sloupce
175
+ // Těžká validace - editable sloupce
92
176
  if (!validateRangeEditable(range, event.api)) {
93
177
  setFloatingButton({ visible: false });
94
178
  return;
95
179
  }
96
180
 
97
- // 7-9. Debounce výpočet pozice a zobrazení buttonu (500ms)
98
- rangeTimeoutRef.current = setTimeout(() => {
99
- const cellPosition = getLastCellPosition(range, event.api);
100
- if (!cellPosition) {
101
- setFloatingButton({ visible: false });
102
- return;
103
- }
104
-
105
- let buttonX, buttonY;
106
-
107
- switch (buttonPosition) {
108
- case 'bottom-right':
109
- buttonX = cellPosition.right + buttonOffset.x;
110
- buttonY = cellPosition.bottom + buttonOffset.y;
111
- break;
112
- case 'bottom-left':
113
- buttonX = cellPosition.left - buttonOffset.x;
114
- buttonY = cellPosition.bottom + buttonOffset.y;
115
- break;
116
- case 'top-right':
117
- buttonX = cellPosition.right + buttonOffset.x;
118
- buttonY = cellPosition.top - buttonOffset.y;
119
- break;
120
- case 'top-left':
121
- buttonX = cellPosition.left - buttonOffset.x;
122
- buttonY = cellPosition.top - buttonOffset.y;
123
- break;
124
- default:
125
- buttonX = cellPosition.right + buttonOffset.x;
126
- buttonY = cellPosition.bottom + buttonOffset.y;
127
- }
181
+ // Těžký výpočet - pozice buňky
182
+ const cellPosition = getLastCellPosition(range, event.api);
183
+ if (!cellPosition) {
184
+ setFloatingButton({ visible: false });
185
+ return;
186
+ }
128
187
 
129
- setFloatingButton({
130
- visible: true,
131
- position: { x: buttonX, y: buttonY },
132
- range,
133
- column: range.columns[0],
134
- cellCount,
135
- });
136
- }, 500);
188
+ setFloatingButton({
189
+ visible: true,
190
+ position: calculateButtonPosition(cellPosition),
191
+ range,
192
+ column: range.columns[0],
193
+ cellCount,
194
+ });
137
195
  },
138
- [enabled, minCells, allowMultiColumn, buttonPosition, buttonOffset]
196
+ [minCells, allowMultiColumn, buttonPosition, buttonOffset]
139
197
  );
140
198
 
141
199
  /**
@@ -310,8 +368,8 @@ export const useBulkCellEdit = (gridRef, options = {}) => {
310
368
  */
311
369
  useEffect(() => {
312
370
  return () => {
313
- if (rangeTimeoutRef.current) {
314
- clearTimeout(rangeTimeoutRef.current);
371
+ if (debounceTimeoutRef.current) {
372
+ clearTimeout(debounceTimeoutRef.current);
315
373
  }
316
374
  };
317
375
  }, []);
package/BulkEdit/utils.js CHANGED
@@ -386,13 +386,28 @@ export const applyBulkChangesWithApi = async (
386
386
  };
387
387
  }
388
388
 
389
- // Sestavit request podle bulkEditApi.mapRequest
390
- const requestData = bulkEditApi.mapRequest
391
- ? bulkEditApi.mapRequest(ids, newValue, field, rows)
392
- : {
393
- ids,
394
- patch: [{ op: 'replace', path: `/${field}`, value: newValue }]
395
- };
389
+ // Sestavit request podle bulkEditApi konfigurace
390
+ let requestData;
391
+ if (bulkEditApi.lookup) {
392
+ // Lookup pattern - automaticky vytvořit PATCH request
393
+ requestData = {
394
+ ids,
395
+ patch: [{
396
+ op: 'replace',
397
+ path: bulkEditApi.lookup.patchPath,
398
+ value: newValue
399
+ }]
400
+ };
401
+ } else if (bulkEditApi.mapRequest) {
402
+ // Custom mapRequest funkce
403
+ requestData = bulkEditApi.mapRequest(ids, newValue, field, rows);
404
+ } else {
405
+ // Fallback default
406
+ requestData = {
407
+ ids,
408
+ patch: [{ op: 'replace', path: `/${field}`, value: newValue }]
409
+ };
410
+ }
396
411
 
397
412
  // Zavolat API
398
413
  const response = await fetchDataUIAsync(
@@ -404,7 +419,32 @@ export const applyBulkChangesWithApi = async (
404
419
  if (response.success) {
405
420
  // Update grid - použít data z response nebo lokálně
406
421
  let updatedData;
407
- if (bulkEditApi.mapResponse) {
422
+ if (bulkEditApi.lookup) {
423
+ // Lookup pattern - najít objekt v lookupData a aktualizovat rows
424
+ let selectedItem = null;
425
+
426
+ // Pokud je newValue prázdné ("" nebo null), nastavit selectedItem na null
427
+ if (newValue !== "" && newValue !== null && newValue !== undefined) {
428
+ selectedItem = bulkEditApi.lookup.data.find(item => item.id === newValue);
429
+
430
+ if (!selectedItem) {
431
+ console.warn(`⚠️ ${bulkEditApi.lookup.objectField} nenalezen pro ID:`, newValue);
432
+ }
433
+ }
434
+
435
+ // Aktualizovat rows - i když je selectedItem null (= clear hodnoty)
436
+ updatedData = rows.map(row => {
437
+ if (ids.includes(row.id)) {
438
+ return {
439
+ ...row,
440
+ [bulkEditApi.lookup.objectField]: selectedItem,
441
+ [bulkEditApi.lookup.idField]: selectedItem ? selectedItem.id : null
442
+ };
443
+ }
444
+ return row;
445
+ });
446
+ } else if (bulkEditApi.mapResponse) {
447
+ // Custom mapResponse funkce
408
448
  updatedData = bulkEditApi.mapResponse(response.data, ids, rows, field);
409
449
  } else if (response.data?.records) {
410
450
  // Mapovat response records (id + value) na existující rows
@@ -1 +1,2 @@
1
1
  export default function Aggregations(gridRef: any): any;
2
+ export function hashRanges(ranges: any): any;
@@ -53,7 +53,7 @@ const isValidDate = value => {
53
53
  let cachedAggregations = null;
54
54
  let cachedRangeHash = null;
55
55
  // Jednoduchý hash funkce pro rychlé porovnání ranges
56
- const hashRanges = (ranges) => {
56
+ export const hashRanges = (ranges) => {
57
57
  if (!ranges || ranges.length === 0)
58
58
  return null;
59
59
  return ranges.map(r => `${r.startRow?.rowIndex}-${r.endRow?.rowIndex}-${r.columns?.length}-${r.columns?.[0]?.colId}`).join('|');