@xcelsior/ui-spreadsheets 1.1.15 → 1.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-spreadsheets",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -89,6 +89,7 @@ export function Spreadsheet<T extends Record<string, any>>({
89
89
  onSelectionChange,
90
90
  onSortChange,
91
91
  onFilterChange,
92
+ afterFiltered,
92
93
  onRowClick,
93
94
  onRowDoubleClick,
94
95
  onAddCellComment,
@@ -286,6 +287,29 @@ export function Spreadsheet<T extends Record<string, any>>({
286
287
  [controlledPageSize, controlledCurrentPage, onPageChange]
287
288
  );
288
289
 
290
+ // Reset pagination to page 1 when filters change
291
+ const resetPaginationToFirstPage = useCallback(() => {
292
+ if (controlledCurrentPage === undefined) {
293
+ setInternalCurrentPage(1);
294
+ }
295
+ onPageChange?.(1, pageSize);
296
+ }, [controlledCurrentPage, onPageChange, pageSize]);
297
+
298
+ // Wrapper for handleFilterChange that resets pagination
299
+ const handleFilterChangeWithReset = useCallback(
300
+ (columnId: string, filter: Parameters<typeof handleFilterChange>[1]) => {
301
+ handleFilterChange(columnId, filter);
302
+ resetPaginationToFirstPage();
303
+ },
304
+ [handleFilterChange, resetPaginationToFirstPage]
305
+ );
306
+
307
+ // Wrapper for clearAllFilters that resets pagination
308
+ const clearAllFiltersWithReset = useCallback(() => {
309
+ clearAllFilters();
310
+ resetPaginationToFirstPage();
311
+ }, [clearAllFilters, resetPaginationToFirstPage]);
312
+
289
313
  // Sync sortConfig to spreadsheetSettings when sorting changes
290
314
  useEffect(() => {
291
315
  setSpreadsheetSettings((prev) => ({
@@ -525,6 +549,15 @@ export function Spreadsheet<T extends Record<string, any>>({
525
549
  }
526
550
  }, [totalPages, currentPage, serverSide]);
527
551
 
552
+ // Store afterFiltered in a ref to avoid re-running effect when callback changes
553
+ const afterFilteredRef = useRef(afterFiltered);
554
+ afterFilteredRef.current = afterFiltered;
555
+
556
+ // Call afterFiltered callback when filtered data changes
557
+ useEffect(() => {
558
+ afterFilteredRef.current?.(filteredData.toArray());
559
+ }, [filteredData]);
560
+
528
561
  // ==================== EVENT HANDLERS ====================
529
562
 
530
563
  const handleRowSelect = useCallback(
@@ -828,10 +861,10 @@ export function Spreadsheet<T extends Record<string, any>>({
828
861
  saveStatus={saveStatus}
829
862
  autoSave={spreadsheetSettings.autoSave}
830
863
  hasActiveFilters={hasActiveFilters}
831
- onClearFilters={clearAllFilters}
864
+ onClearFilters={clearAllFiltersWithReset}
832
865
  filters={filters}
833
866
  columns={columns}
834
- onClearFilter={(columnId) => handleFilterChange(columnId, undefined)}
867
+ onClearFilter={(columnId) => handleFilterChangeWithReset(columnId, undefined)}
835
868
  showFiltersPanel={showFiltersPanel}
836
869
  onToggleFiltersPanel={() => setShowFiltersPanel(!showFiltersPanel)}
837
870
  onZoomIn={() => setZoom((z) => Math.min(z + 10, 200))}
@@ -1013,7 +1046,10 @@ export function Spreadsheet<T extends Record<string, any>>({
1013
1046
  column={column}
1014
1047
  filter={filters[column.id]}
1015
1048
  onFilterChange={(filter) =>
1016
- handleFilterChange(column.id, filter)
1049
+ handleFilterChangeWithReset(
1050
+ column.id,
1051
+ filter
1052
+ )
1017
1053
  }
1018
1054
  onClose={() => setActiveFilterColumn(null)}
1019
1055
  />
@@ -154,235 +154,238 @@ export const SpreadsheetToolbar: React.FC<SpreadsheetToolbarProps> = ({
154
154
  className
155
155
  )}
156
156
  >
157
- {/* Left section: Primary actions */}
158
- <div className="flex items-center gap-2">
159
- {/* Undo/Redo buttons */}
160
- <div className="flex items-center gap-1">
161
- <button
162
- type={'button'}
163
- onClick={onUndo}
164
- disabled={!canUndo}
165
- className={cn(
166
- buttonBaseClasses,
167
- canUndo
168
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
169
- : 'bg-gray-50 text-gray-400'
170
- )}
171
- title={`Undo (${undoCount} changes)`}
172
- >
173
- <HiReply className="h-4 w-4" />
174
- </button>
175
- <button
176
- type={'button'}
177
- onClick={onRedo}
178
- disabled={!canRedo}
179
- className={cn(
180
- buttonBaseClasses,
181
- canRedo
182
- ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
183
- : 'bg-gray-50 text-gray-400'
184
- )}
185
- title={`Redo (${redoCount} changes)`}
186
- style={{ transform: 'scaleX(-1)' }}
187
- >
188
- <HiReply className="h-4 w-4" />
189
- </button>
190
- </div>
191
-
192
- {/* Zoom controls */}
193
- <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-100 rounded">
194
- <button
195
- type={'button'}
196
- onClick={onZoomOut}
197
- className="p-1 hover:bg-white rounded"
198
- title="Zoom out"
199
- >
200
- <HiZoomOut className="h-4 w-4 text-gray-600" />
201
- </button>
202
- <button
203
- type={'button'}
204
- onClick={onZoomReset}
205
- className="px-2 py-0.5 hover:bg-white rounded text-xs min-w-[45px] text-center text-gray-600"
206
- title="Reset zoom"
207
- >
208
- {zoom}%
209
- </button>
210
- <button
211
- type={'button'}
212
- onClick={onZoomIn}
213
- className="p-1 hover:bg-white rounded"
214
- title="Zoom in"
215
- >
216
- <HiZoomIn className="h-4 w-4 text-gray-600" />
217
- </button>
218
- </div>
219
- </div>
220
-
221
- {/* Center section: Status indicators */}
222
- <div className="flex items-center gap-2 flex-1 min-w-0">
223
- {/* Selected rows indicator */}
224
- {selectedRowCount > 0 && (
225
- <div className="flex items-center gap-2 px-2.5 py-1.5 bg-blue-600 text-white rounded">
226
- <span className="text-xs font-medium whitespace-nowrap">
227
- {selectedRowCount} row{selectedRowCount !== 1 ? 's' : ''} selected
228
- </span>
157
+ {/* Left section: Primary actions */}
158
+ <div className="flex items-center gap-2">
159
+ {/* Undo/Redo buttons */}
160
+ <div className="flex items-center gap-1">
229
161
  <button
230
162
  type={'button'}
231
- onClick={onClearSelection}
232
- className="p-0.5 hover:bg-blue-700 rounded"
233
- title="Clear selection"
163
+ onClick={onUndo}
164
+ disabled={!canUndo}
165
+ className={cn(
166
+ buttonBaseClasses,
167
+ canUndo
168
+ ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
169
+ : 'bg-gray-50 text-gray-400'
170
+ )}
171
+ title={`Undo (${undoCount} changes)`}
234
172
  >
235
- <HiX className="h-3 w-3" />
173
+ <HiReply className="h-4 w-4" />
174
+ </button>
175
+ <button
176
+ type={'button'}
177
+ onClick={onRedo}
178
+ disabled={!canRedo}
179
+ className={cn(
180
+ buttonBaseClasses,
181
+ canRedo
182
+ ? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
183
+ : 'bg-gray-50 text-gray-400'
184
+ )}
185
+ title={`Redo (${redoCount} changes)`}
186
+ style={{ transform: 'scaleX(-1)' }}
187
+ >
188
+ <HiReply className="h-4 w-4" />
236
189
  </button>
237
190
  </div>
238
- )}
239
-
240
- {/* Show filters button */}
241
- {hasActiveFilters && onToggleFiltersPanel && (
242
- <button
243
- type={'button'}
244
- onClick={onToggleFiltersPanel}
245
- className={cn(
246
- 'flex items-center gap-2 px-2.5 py-1.5 rounded transition-colors',
247
- showFiltersPanel
248
- ? 'bg-amber-600 text-white hover:bg-amber-700'
249
- : 'bg-amber-500 text-white hover:bg-amber-600'
250
- )}
251
- title={showFiltersPanel ? 'Hide active filters' : 'Show active filters'}
252
- >
253
- <HiFilter className="h-3.5 w-3.5" />
254
- <span className="text-xs font-medium whitespace-nowrap">
255
- {activeFilterCount} filter{activeFilterCount !== 1 ? 's' : ''} active
256
- </span>
257
- {showFiltersPanel ? (
258
- <HiChevronUp className="h-3 w-3" />
259
- ) : (
260
- <HiChevronDown className="h-3 w-3" />
261
- )}
262
- </button>
263
- )}
264
191
 
265
- {/* Summary badge */}
266
- {summary && (
267
- <div
268
- className={cn(
269
- 'flex items-center gap-2 px-2.5 py-1.5 rounded border text-xs',
270
- getSummaryVariantClasses(summary.variant)
271
- )}
272
- >
273
- <span className="font-semibold whitespace-nowrap">{summary.label}:</span>
274
- <span className="font-bold whitespace-nowrap">{summary.value}</span>
192
+ {/* Zoom controls */}
193
+ <div className="flex items-center gap-1 px-1.5 py-1 bg-gray-100 rounded">
194
+ <button
195
+ type={'button'}
196
+ onClick={onZoomOut}
197
+ className="p-1 hover:bg-white rounded"
198
+ title="Zoom out"
199
+ >
200
+ <HiZoomOut className="h-4 w-4 text-gray-600" />
201
+ </button>
202
+ <button
203
+ type={'button'}
204
+ onClick={onZoomReset}
205
+ className="px-2 py-0.5 hover:bg-white rounded text-xs min-w-[45px] text-center text-gray-600"
206
+ title="Reset zoom"
207
+ >
208
+ {zoom}%
209
+ </button>
210
+ <button
211
+ type={'button'}
212
+ onClick={onZoomIn}
213
+ className="p-1 hover:bg-white rounded"
214
+ title="Zoom in"
215
+ >
216
+ <HiZoomIn className="h-4 w-4 text-gray-600" />
217
+ </button>
275
218
  </div>
276
- )}
277
- </div>
219
+ </div>
278
220
 
279
- {/* Right section: Action buttons */}
280
- <div className="flex items-center gap-2">
281
- {/* Save status */}
282
- {saveStatusDisplay && (
283
- <span
284
- className={cn(
285
- 'text-xs flex items-center gap-1',
286
- saveStatusDisplay.className
287
- )}
288
- >
289
- {saveStatusDisplay.text}
290
- </span>
291
- )}
221
+ {/* Center section: Status indicators */}
222
+ <div className="flex items-center gap-2 flex-1 min-w-0">
223
+ {/* Selected rows indicator */}
224
+ {selectedRowCount > 0 && (
225
+ <div className="flex items-center gap-2 px-2.5 py-1.5 bg-blue-600 text-white rounded">
226
+ <span className="text-xs font-medium whitespace-nowrap">
227
+ {selectedRowCount} row{selectedRowCount !== 1 ? 's' : ''} selected
228
+ </span>
229
+ <button
230
+ type={'button'}
231
+ onClick={onClearSelection}
232
+ className="p-0.5 hover:bg-blue-700 rounded"
233
+ title="Clear selection"
234
+ >
235
+ <HiX className="h-3 w-3" />
236
+ </button>
237
+ </div>
238
+ )}
292
239
 
293
- {/* Manual save button (when auto-save is off) */}
294
- {!autoSave && onSave && (
295
- <button
296
- type={'button'}
297
- onClick={onSave}
298
- disabled={!hasUnsavedChanges}
299
- className={cn(
300
- 'px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center gap-1.5',
301
- 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600'
302
- )}
303
- >
304
- <HiCheck className="h-3.5 w-3.5" />
305
- Save
306
- </button>
307
- )}
240
+ {/* Show filters button */}
241
+ {hasActiveFilters && onToggleFiltersPanel && (
242
+ <button
243
+ type={'button'}
244
+ onClick={onToggleFiltersPanel}
245
+ className={cn(
246
+ 'flex items-center gap-2 px-2.5 py-1.5 rounded transition-colors',
247
+ showFiltersPanel
248
+ ? 'bg-amber-600 text-white hover:bg-amber-700'
249
+ : 'bg-amber-500 text-white hover:bg-amber-600'
250
+ )}
251
+ title={showFiltersPanel ? 'Hide active filters' : 'Show active filters'}
252
+ >
253
+ <HiFilter className="h-3.5 w-3.5" />
254
+ <span className="text-xs font-medium whitespace-nowrap">
255
+ {activeFilterCount} filter{activeFilterCount !== 1 ? 's' : ''}{' '}
256
+ active
257
+ </span>
258
+ {showFiltersPanel ? (
259
+ <HiChevronUp className="h-3 w-3" />
260
+ ) : (
261
+ <HiChevronDown className="h-3 w-3" />
262
+ )}
263
+ </button>
264
+ )}
308
265
 
309
- {/* More menu dropdown */}
310
- <div className="relative" ref={menuRef}>
311
- <button
312
- type={'button'}
313
- onClick={() => setShowMoreMenu(!showMoreMenu)}
314
- className="px-2.5 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center gap-1.5 text-xs"
315
- title="More actions"
316
- >
317
- <HiDotsVertical className="h-3.5 w-3.5" />
318
- <span className="hidden lg:inline">More</span>
319
- </button>
266
+ {/* Summary badge */}
267
+ {summary && (
268
+ <div
269
+ className={cn(
270
+ 'flex items-center gap-2 px-2.5 py-1.5 rounded border text-xs',
271
+ getSummaryVariantClasses(summary.variant)
272
+ )}
273
+ >
274
+ <span className="font-semibold whitespace-nowrap">
275
+ {summary.label}:
276
+ </span>
277
+ <span className="font-bold whitespace-nowrap">{summary.value}</span>
278
+ </div>
279
+ )}
280
+ </div>
320
281
 
321
- {/* Dropdown Menu */}
322
- {showMoreMenu && (
323
- <div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 shadow-lg rounded py-1 min-w-[180px] z-20">
324
- {onSettings && (
325
- <button
326
- type={'button'}
327
- onClick={() => {
328
- onSettings();
329
- setShowMoreMenu(false);
330
- }}
331
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
332
- >
333
- <HiCog className="h-3.5 w-3.5 text-gray-500" />
334
- <span className="text-gray-700">Settings</span>
335
- </button>
282
+ {/* Right section: Action buttons */}
283
+ <div className="flex items-center gap-2">
284
+ {/* Save status */}
285
+ {saveStatusDisplay && (
286
+ <span
287
+ className={cn(
288
+ 'text-xs flex items-center gap-1',
289
+ saveStatusDisplay.className
336
290
  )}
291
+ >
292
+ {saveStatusDisplay.text}
293
+ </span>
294
+ )}
337
295
 
338
- {onShowShortcuts && (
339
- <button
340
- type={'button'}
341
- onClick={() => {
342
- onShowShortcuts();
343
- setShowMoreMenu(false);
344
- }}
345
- className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
346
- >
347
- <HiOutlineQuestionMarkCircle className="h-3.5 w-3.5 text-gray-500" />
348
- <span className="text-gray-700">Keyboard Shortcuts</span>
349
- </button>
296
+ {/* Manual save button (when auto-save is off) */}
297
+ {!autoSave && onSave && (
298
+ <button
299
+ type={'button'}
300
+ onClick={onSave}
301
+ disabled={!hasUnsavedChanges}
302
+ className={cn(
303
+ 'px-3 py-1.5 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex items-center gap-1.5',
304
+ 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600'
350
305
  )}
306
+ >
307
+ <HiCheck className="h-3.5 w-3.5" />
308
+ Save
309
+ </button>
310
+ )}
311
+
312
+ {/* More menu dropdown */}
313
+ <div className="relative" ref={menuRef}>
314
+ <button
315
+ type={'button'}
316
+ onClick={() => setShowMoreMenu(!showMoreMenu)}
317
+ className="px-2.5 py-1.5 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors flex items-center gap-1.5 text-xs"
318
+ title="More actions"
319
+ >
320
+ <HiDotsVertical className="h-3.5 w-3.5" />
321
+ <span className="hidden lg:inline">More</span>
322
+ </button>
351
323
 
352
- {/* Custom menu items */}
353
- {menuItems &&
354
- menuItems.length > 0 &&
355
- (onSettings || onShowShortcuts) && (
356
- <div className="border-t border-gray-100 my-1" />
324
+ {/* Dropdown Menu */}
325
+ {showMoreMenu && (
326
+ <div className="absolute right-0 top-full mt-1 bg-white border border-gray-200 shadow-lg rounded py-1 min-w-[180px] z-50">
327
+ {onSettings && (
328
+ <button
329
+ type={'button'}
330
+ onClick={() => {
331
+ onSettings();
332
+ setShowMoreMenu(false);
333
+ }}
334
+ className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
335
+ >
336
+ <HiCog className="h-3.5 w-3.5 text-gray-500" />
337
+ <span className="text-gray-700">Settings</span>
338
+ </button>
357
339
  )}
358
340
 
359
- {menuItems?.map((item) => (
360
- <button
361
- key={item.id}
362
- type={'button'}
363
- disabled={item.disabled}
364
- onClick={() => {
365
- item.onClick();
366
- setShowMoreMenu(false);
367
- }}
368
- className={cn(
369
- 'w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors',
370
- item.disabled && 'opacity-50 cursor-not-allowed'
371
- )}
372
- >
373
- {item.icon && (
374
- <span className="h-3.5 w-3.5 text-gray-500 flex items-center justify-center">
375
- {item.icon}
376
- </span>
341
+ {onShowShortcuts && (
342
+ <button
343
+ type={'button'}
344
+ onClick={() => {
345
+ onShowShortcuts();
346
+ setShowMoreMenu(false);
347
+ }}
348
+ className="w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors"
349
+ >
350
+ <HiOutlineQuestionMarkCircle className="h-3.5 w-3.5 text-gray-500" />
351
+ <span className="text-gray-700">Keyboard Shortcuts</span>
352
+ </button>
353
+ )}
354
+
355
+ {/* Custom menu items */}
356
+ {menuItems &&
357
+ menuItems.length > 0 &&
358
+ (onSettings || onShowShortcuts) && (
359
+ <div className="border-t border-gray-100 my-1" />
377
360
  )}
378
- <span className="text-gray-700">{item.label}</span>
379
- </button>
380
- ))}
381
- </div>
382
- )}
361
+
362
+ {menuItems?.map((item) => (
363
+ <button
364
+ key={item.id}
365
+ type={'button'}
366
+ disabled={item.disabled}
367
+ onClick={() => {
368
+ item.onClick();
369
+ setShowMoreMenu(false);
370
+ }}
371
+ className={cn(
372
+ 'w-full px-3 py-2 text-left hover:bg-gray-50 flex items-center gap-2 text-xs transition-colors',
373
+ item.disabled && 'opacity-50 cursor-not-allowed'
374
+ )}
375
+ >
376
+ {item.icon && (
377
+ <span className="h-3.5 w-3.5 text-gray-500 flex items-center justify-center">
378
+ {item.icon}
379
+ </span>
380
+ )}
381
+ <span className="text-gray-700">{item.label}</span>
382
+ </button>
383
+ ))}
384
+ </div>
385
+ )}
386
+ </div>
383
387
  </div>
384
388
  </div>
385
- </div>
386
389
 
387
390
  {/* Active filters panel */}
388
391
  {showFiltersPanel && filters && columns && onClearFilter && onClearFilters && (
package/src/types.ts CHANGED
@@ -355,6 +355,8 @@ export interface SpreadsheetProps<T = any> {
355
355
  onSortChange?: (sortConfig: SpreadsheetSortConfig | null) => void;
356
356
  /** Callback when filters change */
357
357
  onFilterChange?: (filters: Record<string, SpreadsheetColumnFilter>) => void;
358
+ /** Callback with filtered data after filters are applied */
359
+ afterFiltered?: (filteredData: T[]) => void;
358
360
  /** Callback for row click */
359
361
  onRowClick?: (row: T, rowIndex: number) => void;
360
362
  /** Callback for row double click */