@gooddata/sdk-ui-pivot 11.52.0-alpha.4 → 11.52.0-alpha.6

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.
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // (C) 2007-2026 GoodData Corporation
3
- import { Component } from "react";
3
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
4
4
  import { AllCommunityModule, ModuleRegistry, provideGlobalGridOptions, } from "ag-grid-community";
5
5
  import { AgGridReact } from "ag-grid-react";
6
6
  import cx from "classnames";
@@ -36,7 +36,38 @@ ModuleRegistry.registerModules([AllCommunityModule]);
36
36
  // Mark all grids as using legacy themes
37
37
  provideGlobalGridOptions({ theme: "legacy" });
38
38
  /**
39
- * This class implements pivot table using the community version of ag-grid.
39
+ * Applies the table default props. This is the functional component equivalent of the `defaultProps` static
40
+ * that this component used to have while it was implemented as a class component.
41
+ */
42
+ function resolveDefaultProps(props) {
43
+ return {
44
+ ...props,
45
+ locale: props.locale ?? PIVOT_TABLE_DEFAULT_PROPS.locale,
46
+ drillableItems: props.drillableItems ?? PIVOT_TABLE_DEFAULT_PROPS.drillableItems,
47
+ afterRender: props.afterRender ?? PIVOT_TABLE_DEFAULT_PROPS.afterRender,
48
+ pushData: props.pushData ?? PIVOT_TABLE_DEFAULT_PROPS.pushData,
49
+ onExportReady: props.onExportReady ?? PIVOT_TABLE_DEFAULT_PROPS.onExportReady,
50
+ onLoadingChanged: props.onLoadingChanged ?? PIVOT_TABLE_DEFAULT_PROPS.onLoadingChanged,
51
+ onError: props.onError ?? PIVOT_TABLE_DEFAULT_PROPS.onError,
52
+ onDataView: props.onDataView ?? PIVOT_TABLE_DEFAULT_PROPS.onDataView,
53
+ onDrill: props.onDrill ?? PIVOT_TABLE_DEFAULT_PROPS.onDrill,
54
+ ErrorComponent: props.ErrorComponent ?? PIVOT_TABLE_DEFAULT_PROPS.ErrorComponent,
55
+ LoadingComponent: props.LoadingComponent ?? PIVOT_TABLE_DEFAULT_PROPS.LoadingComponent,
56
+ pageSize: props.pageSize ?? PIVOT_TABLE_DEFAULT_PROPS.pageSize,
57
+ config: props.config ?? PIVOT_TABLE_DEFAULT_PROPS.config,
58
+ onColumnResized: props.onColumnResized ?? PIVOT_TABLE_DEFAULT_PROPS.onColumnResized,
59
+ };
60
+ }
61
+ function stopEventWhenResizeHeader(e) {
62
+ // Do not propagate event when it originates from the table resizer.
63
+ // This means for example that we can resize columns without triggering drag in the application.
64
+ if (e.target?.className?.includes?.("ag-header-cell-resize")) {
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ }
68
+ }
69
+ /**
70
+ * This component implements pivot table using the community version of ag-grid.
40
71
  *
41
72
  * Bear in mind that this is not a typical, standard React component implementation; the main reason
42
73
  * behind that is that while ag-grid comes with a React component the ag-grid itself is not a React component
@@ -46,10 +77,14 @@ provideGlobalGridOptions({ theme: "legacy" });
46
77
  * Instead of looking at this implementation as a typical React component, look at it like a adapter between
47
78
  * React and ag-grid which is used to render data obtained using GD executions.
48
79
  *
49
- * The code in this class is built to reflect the adapter nature of the integration. The responsibility of this
80
+ * The code in this component is built to reflect the adapter nature of the integration. The responsibility of this
50
81
  * component is to correctly handle the component lifecycle and orchestrate integration of React and ag-grid, React
51
82
  * and GoodData, React and GoodData and ag-grid.
52
83
  *
84
+ * Note on the implementation: because the table is driven imperatively, all the non-react state lives in refs and
85
+ * all the functions that the table hands over to its sub-components are stable for the entire lifetime of the
86
+ * component; they read the current props, state and internal state through refs.
87
+ *
53
88
  * Lifecycle
54
89
  * ---------
55
90
  *
@@ -76,7 +111,7 @@ provideGlobalGridOptions({ theme: "legacy" });
76
111
  * prepared execution, add the extra settings and re-drive the execution. Once done, it will update the internal
77
112
  * state and ping ag-grid API to re-render the table.
78
113
  *
79
- * In case the client changes props (say modifying the prepared execution) the componentDidUpdate will determine
114
+ * In case the client changes props (say modifying the prepared execution) the update effect will determine
80
115
  * whether the full reinitialization is in order. If so the entire existing ag-grid and all our internal state
81
116
  * is thrown out and table is initialized from scratch.
82
117
  *
@@ -142,36 +177,18 @@ provideGlobalGridOptions({ theme: "legacy" });
142
177
  *
143
178
  * @internal
144
179
  */
145
- export class CorePivotTableAgImpl extends Component {
146
- static defaultProps = PIVOT_TABLE_DEFAULT_PROPS;
147
- errorMap;
148
- containerRef;
149
- pivotTableId;
150
- internal;
151
- boundAgGridCallbacks;
152
- // Event handler instances
153
- gridEventHandlers;
154
- dataRenderHandlers;
155
- scrollEventHandlers;
156
- resizeEventHandlers;
157
- sortingEventHandlers;
158
- aggregationEventHandlers;
159
- dataLoadingEventHandlers;
160
- // Config accessor instance
161
- configAccessors;
162
- configDelegates;
163
- // Sticky row manager instance
164
- stickyRowManager;
165
- // Height calculation manager instance
166
- heightCalculationManager;
167
- // Execution abort manager instance
168
- executionAbortManager;
169
- // Component update analyzer instance
170
- componentUpdateAnalyzer;
171
- constructor(props) {
172
- super(props);
173
- const { execution, config } = props;
174
- this.state = {
180
+ export function CorePivotTableAgImpl(props) {
181
+ const resolvedProps = resolveDefaultProps(props);
182
+ /*
183
+ * Vast majority of the code below runs outside of the React rendering (ag-grid callbacks, timeouts, promise
184
+ * continuations). Props and state are therefore mirrored into refs so that this code always sees the current
185
+ * values - exactly like `this.props` and `this.state` used to do.
186
+ */
187
+ const propsRef = useRef(resolvedProps);
188
+ propsRef.current = resolvedProps;
189
+ const [state, setStateInternal] = useState(() => {
190
+ const { execution, config } = resolvedProps;
191
+ return {
175
192
  readyToRender: false,
176
193
  columnTotals: cloneDeep(sanitizeDefTotals(execution.definition)),
177
194
  rowTotals: getTotalsForColumnsBucket(execution.definition),
@@ -180,18 +197,480 @@ export class CorePivotTableAgImpl extends Component {
180
197
  tempExecution: execution,
181
198
  isLoading: false,
182
199
  };
183
- this.errorMap = newErrorMapping(props.intl);
184
- this.internal = new InternalTableState();
185
- this.boundAgGridCallbacks = this.createBoundAgGridCallbacks();
186
- this.pivotTableId = uuidv4().replace(/-/g, "");
187
- this.onLoadingChanged = this.onLoadingChanged.bind(this);
188
- // Initialize execution abort manager
189
- this.executionAbortManager = new ExecutionAbortManager({
190
- enableExecutionCancelling: props.config?.enableExecutionCancelling,
200
+ });
201
+ const stateRef = useRef(state);
202
+ stateRef.current = state;
203
+ /*
204
+ * Callbacks passed to the `setState(update, callback)` calls; they must run after the commit of the state
205
+ * update that registered them - the same way React does it for class components.
206
+ *
207
+ * Callbacks of the state updates that are already reflected in the state being rendered are moved to the
208
+ * 'committed' queue below, which is then drained by an effect after the commit.
209
+ */
210
+ const enqueuedStateUpdateCallbacks = useRef([]);
211
+ const committedStateUpdateCallbacks = useRef([]);
212
+ if (enqueuedStateUpdateCallbacks.current.length > 0) {
213
+ committedStateUpdateCallbacks.current.push(...enqueuedStateUpdateCallbacks.current);
214
+ enqueuedStateUpdateCallbacks.current = [];
215
+ }
216
+ /**
217
+ * Drop-in replacement of `React.Component.setState`: merges the partial state (or the result of the state
218
+ * updater function) into the current state and optionally calls a callback once the new state is committed.
219
+ */
220
+ const setState = useCallback((update, callback) => {
221
+ if (callback) {
222
+ enqueuedStateUpdateCallbacks.current.push(callback);
223
+ }
224
+ setStateInternal((prevState) => ({
225
+ ...prevState,
226
+ ...(typeof update === "function" ? update(prevState) : update),
227
+ }));
228
+ }, []);
229
+ const errorMap = useMemo(() => newErrorMapping(resolvedProps.intl), [resolvedProps.intl]);
230
+ const pivotTableId = useMemo(() => uuidv4().replace(/-/g, ""), []);
231
+ /*
232
+ * The internal table state is intentionally kept out of the React state; it is replaced whenever the table
233
+ * gets re-initialized. Always access it through the ref so that the current instance is used.
234
+ */
235
+ const internalRef = useRef(new InternalTableState());
236
+ const containerRef = useRef(undefined);
237
+ // Execution abort manager instance; created once for the whole lifetime of the component
238
+ const [executionAbortManager] = useState(() => new ExecutionAbortManager({
239
+ enableExecutionCancelling: resolvedProps.config?.enableExecutionCancelling,
240
+ }));
241
+ /*
242
+ * The imperative helpers below are created during the first render (this is what the constructor used to do)
243
+ * and are replaced whenever the table gets re-initialized.
244
+ */
245
+ const eventHandlersRef = useRef(null);
246
+ const configAccessorsRef = useRef(null);
247
+ const componentUpdateAnalyzerRef = useRef(null);
248
+ const boundAgGridCallbacksRef = useRef(null);
249
+ /*
250
+ * The table methods handed over to the sub-components need to be able to trigger the table
251
+ * re-initialization; the re-initialization in turn needs the table methods. This ref breaks that cycle.
252
+ */
253
+ const reinitializeRef = useRef(() => { });
254
+ /**
255
+ * Getter of the current internal table state. It is essential to always obtain the internal state through
256
+ * this getter because the internal state gets replaced on table re-initialization.
257
+ */
258
+ const getInternal = useCallback(() => internalRef.current, []);
259
+ const getContainerRef = useCallback(() => containerRef.current, []);
260
+ const getEventHandlers = useCallback(() => {
261
+ const eventHandlers = eventHandlersRef.current;
262
+ invariant(eventHandlers);
263
+ return eventHandlers;
264
+ }, []);
265
+ const getConfigAccessors = useCallback(() => {
266
+ const configAccessors = configAccessorsRef.current;
267
+ invariant(configAccessors);
268
+ return configAccessors;
269
+ }, []);
270
+ //
271
+ // Table configuration accessors
272
+ //
273
+ const getLastSortedColId = useCallback(() => internalRef.current.lastSortedColId, []);
274
+ const setLastSortedColId = useCallback((colId) => {
275
+ internalRef.current.lastSortedColId = colId;
276
+ }, []);
277
+ // Delegate to config accessors
278
+ const getColumnTotals = useCallback(() => getConfigAccessors().configDelegates.getColumnTotals(), [getConfigAccessors]);
279
+ const getRowTotals = useCallback(() => getConfigAccessors().configDelegates.getRowTotals(), [getConfigAccessors]);
280
+ const getExecutionDefinition = useCallback(() => getConfigAccessors().configDelegates.getExecutionDefinition(), [getConfigAccessors]);
281
+ const getGroupRows = useCallback(() => getConfigAccessors().configDelegates.getGroupRows(), [getConfigAccessors]);
282
+ const getMeasureGroupDimension = useCallback(() => getConfigAccessors().configDelegates.getMeasureGroupDimension(), [getConfigAccessors]);
283
+ const getColumnHeadersPosition = useCallback(() => getConfigAccessors().configDelegates.getColumnHeadersPosition(), [getConfigAccessors]);
284
+ const getMenuConfig = useCallback(() => getConfigAccessors().configDelegates.getMenuConfig(), [getConfigAccessors]);
285
+ const getDefaultWidth = useCallback(() => getConfigAccessors().configDelegates.getDefaultWidth(), [getConfigAccessors]);
286
+ const isColumnAutoresizeEnabled = useCallback(() => getConfigAccessors().configDelegates.isColumnAutoresizeEnabled(), [getConfigAccessors]);
287
+ const isGrowToFitEnabled = useCallback((props = propsRef.current) => getConfigAccessors().configDelegates.isGrowToFitEnabled(props), [getConfigAccessors]);
288
+ const getColumnWidths = useCallback((props) => getConfigAccessors().configDelegates.getColumnWidths(props), [getConfigAccessors]);
289
+ const hasColumnWidths = useCallback(() => getConfigAccessors().configDelegates.hasColumnWidths(), [getConfigAccessors]);
290
+ const getDefaultWidthFromProps = useCallback((props) => getConfigAccessors().configDelegates.getDefaultWidthFromProps(props), [getConfigAccessors]);
291
+ const shouldAutoResizeColumns = useCallback(() => getConfigAccessors().configDelegates.shouldAutoResizeColumns(), [getConfigAccessors]);
292
+ const getResizingConfig = useCallback(() => {
293
+ const currentProps = propsRef.current;
294
+ const internal = internalRef.current;
295
+ return {
296
+ defaultWidth: getDefaultWidth(),
297
+ growToFit: isGrowToFitEnabled(),
298
+ columnAutoresizeOption: getDefaultWidthFromProps(currentProps),
299
+ widths: getColumnWidths(currentProps),
300
+ isAltKeyPressed: internal.isAltKeyPressed,
301
+ isMetaOrCtrlKeyPressed: internal.isMetaOrCtrlKeyPressed,
302
+ // use clientWidth of the viewport container to accommodate for vertical scrollbars if needed
303
+ clientWidth: containerRef.current?.getElementsByClassName("ag-body-viewport")[0]?.clientWidth ?? 0,
304
+ containerRef: containerRef.current,
305
+ separators: currentProps?.config?.separators,
306
+ onColumnResized: currentProps.onColumnResized,
307
+ };
308
+ }, [getColumnWidths, getDefaultWidth, getDefaultWidthFromProps, isGrowToFitEnabled]);
309
+ /**
310
+ * All pushData calls done by the table must go through this guard.
311
+ *
312
+ * TODO: The guard should ensure a 'disconnect' between push data handling and the calling function processing.
313
+ * When the pushData is handled by the application, it MAY (and in our case it DOES) trigger processing that
314
+ * lands back in the table. This opens additional set of invariants to check / be prepared for in order to
315
+ * optimize the renders and re-renders.
316
+ */
317
+ const pushDataGuard = useCallback((data) => {
318
+ propsRef.current.pushData?.(data);
319
+ /*
320
+ * TODO: Switch to this on in FET-715.
321
+ setTimeout(() => {
322
+ propsRef.current.pushData?.(data);
323
+ }, 0);
324
+ */
325
+ }, []);
326
+ //
327
+ // Sticky row handling - delegated to StickyRowManager
328
+ //
329
+ const updateStickyRow = useCallback(() => {
330
+ getConfigAccessors().stickyRowManager.updateStickyRow();
331
+ }, [getConfigAccessors]);
332
+ const updateStickyRowContent = useCallback((scrollPosition) => {
333
+ getConfigAccessors().stickyRowManager.updateStickyRowContent(scrollPosition);
334
+ }, [getConfigAccessors]);
335
+ //
336
+ // Desired height updating - delegated to HeightCalculationManager
337
+ //
338
+ const updateDesiredHeight = useCallback(() => {
339
+ getConfigAccessors().heightCalculationManager.updateDesiredHeight();
340
+ }, [getConfigAccessors]);
341
+ //
342
+ // Table resizing
343
+ //
344
+ const growToFit = useCallback(() => {
345
+ const internal = internalRef.current;
346
+ invariant(internal.table);
347
+ if (!isGrowToFitEnabled()) {
348
+ return;
349
+ }
350
+ internal.table.growToFit(getResizingConfig());
351
+ if (!stateRef.current.resized && !internal.table.isResizing()) {
352
+ setState({
353
+ resized: true,
354
+ });
355
+ }
356
+ }, [getResizingConfig, isGrowToFitEnabled, setState]);
357
+ const autoresizeColumns = useCallback(async (force = false) => {
358
+ if (stateRef.current.resized && !force) {
359
+ return;
360
+ }
361
+ const didResize = await internalRef.current.table?.autoresizeColumns(getResizingConfig(), force);
362
+ if (didResize) {
363
+ // after column resizing, horizontal scroolbar may change and table height may need resizing
364
+ updateDesiredHeight();
365
+ }
366
+ if (didResize && !stateRef.current.resized) {
367
+ setState({
368
+ resized: true,
369
+ });
370
+ }
371
+ }, [getResizingConfig, setState, updateDesiredHeight]);
372
+ const stopWatchingTableRendered = useCallback(() => {
373
+ internalRef.current.stopWatching();
374
+ propsRef.current.afterRender();
375
+ }, []);
376
+ const startWatchingTableRendered = useCallback(() => {
377
+ const internal = internalRef.current;
378
+ if (!internal.table) {
379
+ return;
380
+ }
381
+ const missingContainerRef = !containerRef.current; // table having no data will be unmounted, it causes ref null
382
+ const isTableRendered = shouldAutoResizeColumns()
383
+ ? stateRef.current.resized
384
+ : internal.table.isPivotTableReady();
385
+ const shouldCallAutoresizeColumns = internal.table.isPivotTableReady() && !stateRef.current.resized && !internal.table.isResizing();
386
+ if (shouldAutoResizeColumns() && shouldCallAutoresizeColumns) {
387
+ void autoresizeColumns();
388
+ }
389
+ if (missingContainerRef || isTableRendered) {
390
+ stopWatchingTableRendered();
391
+ }
392
+ }, [autoresizeColumns, shouldAutoResizeColumns, stopWatchingTableRendered]);
393
+ //
394
+ // event handlers
395
+ //
396
+ const onGridReady = useCallback((event) => {
397
+ getEventHandlers().gridEventHandlers.onGridReady(event);
398
+ }, [getEventHandlers]);
399
+ const onFirstDataRendered = useCallback(async (_event) => {
400
+ await getEventHandlers().dataRenderHandlers.onFirstDataRendered(_event);
401
+ }, [getEventHandlers]);
402
+ const onModelUpdated = useCallback((event) => {
403
+ getEventHandlers().gridEventHandlers.onModelUpdated(event);
404
+ }, [getEventHandlers]);
405
+ const onGridColumnsChanged = useCallback(() => {
406
+ getEventHandlers().gridEventHandlers.onGridColumnsChanged();
407
+ }, [getEventHandlers]);
408
+ const onGridSizeChanged = useCallback((gridSizeChangedEvent) => {
409
+ getEventHandlers().resizeEventHandlers.onGridSizeChanged(gridSizeChangedEvent);
410
+ }, [getEventHandlers]);
411
+ const onGridColumnResized = useCallback((columnEvent) => {
412
+ getEventHandlers().resizeEventHandlers.onGridColumnResized(columnEvent);
413
+ }, [getEventHandlers]);
414
+ const onSortChanged = useCallback((event) => {
415
+ getEventHandlers().sortingEventHandlers.onSortChanged(event);
416
+ }, [getEventHandlers]);
417
+ const onPinnedRowDataChanged = useCallback(async (event) => {
418
+ await getEventHandlers().resizeEventHandlers.onPinnedRowDataChanged(event);
419
+ }, [getEventHandlers]);
420
+ const onBodyScroll = useCallback((event) => {
421
+ getEventHandlers().scrollEventHandlers.onBodyScroll(event);
422
+ }, [getEventHandlers]);
423
+ const onContainerMouseDown = useCallback((event) => {
424
+ getEventHandlers().scrollEventHandlers.onContainerMouseDown(event);
425
+ }, [getEventHandlers]);
426
+ const onPageLoaded = useCallback((dv, newResult) => {
427
+ getEventHandlers().dataLoadingEventHandlers.onPageLoaded(dv, newResult);
428
+ }, [getEventHandlers]);
429
+ const onMenuAggregationClick = useCallback((menuAggregationClickConfig) => {
430
+ getEventHandlers().aggregationEventHandlers.onMenuAggregationClick(menuAggregationClickConfig);
431
+ }, [getEventHandlers]);
432
+ const onLoadingChanged = useCallback((loadingState) => {
433
+ getEventHandlers().dataLoadingEventHandlers.onLoadingChanged(loadingState);
434
+ }, [getEventHandlers]);
435
+ const onError = useCallback((error, execution = propsRef.current.execution) => {
436
+ getEventHandlers().dataLoadingEventHandlers.onError(error, execution);
437
+ }, [getEventHandlers]);
438
+ /**
439
+ * This will be called when user changes sorts or adds totals. This means complete re-execution with
440
+ * new sorts or totals. Loading indicators will be shown instead of all rendered rows thanks to the
441
+ * LoadingRenderer used in all cells of the left-most column.
442
+ *
443
+ * The table must take care to remove the sticky (top-pinned) row - it is treated differently by
444
+ * ag-grid and will be literally sticking there on its own with the loading indicators.
445
+ *
446
+ * Once transformation finishes - indicated by call to onPageLoaded, table can re-instance the sticky row.
447
+ *
448
+ * @param newExecution - the new execution which is being run and will be used to populate the table
449
+ */
450
+ const onExecutionTransformed = useCallback((newExecution) => {
451
+ // Handle the reinitialize call directly here since we can't pass it to the handler
452
+ const internal = internalRef.current;
453
+ if (!internal.table) {
454
+ return;
455
+ }
456
+ internal.table.clearStickyRow();
457
+ // Force double execution only when totals/subtotals for columns change, so table is render properly.
458
+ if (!isEqual(stateRef.current.tempExecution.definition.buckets[2], newExecution.definition.buckets[2])) {
459
+ setState({
460
+ tempExecution: newExecution,
461
+ });
462
+ reinitializeRef.current(newExecution);
463
+ }
464
+ }, [setState]);
465
+ /**
466
+ * All callback functions that the table passes to ag-grid must be bound to the current internal state of the table. The
467
+ * callback functions MUST be noop if the internal state at the time of call is different from the internal state
468
+ * at the time of creation.
469
+ *
470
+ * This is essential to prevent errors stemming for racy behavior triggered by ag-grid. ag-grid often triggers
471
+ * event callbacks using setTimeout(). It can happen, that once the event is actually processed the ag-grid table
472
+ * which caused it is unmounted. Doing anything with the unmounted table's gridApi leads to errors.
473
+ *
474
+ * Without this, table may trigger ag-grid errors such as this:
475
+ *
476
+ * https://github.com/ag-grid/ag-grid/issues/3457
477
+ *
478
+ * or this:
479
+ *
480
+ * https://github.com/ag-grid/ag-grid/issues/3334
481
+ */
482
+ const createBoundAgGridCallbacks = useCallback(() => {
483
+ const internal = internalRef.current;
484
+ const debouncedGridSizeChanged = createDebouncedCallback(createStateBoundCallback(onGridSizeChanged, internal, getInternal), AGGRID_ON_RESIZE_TIMEOUT);
485
+ return {
486
+ onGridReady: createStateBoundCallback(onGridReady, internal, getInternal),
487
+ onFirstDataRendered: createStateBoundCallback(onFirstDataRendered, internal, getInternal),
488
+ onBodyScroll: createStateBoundCallback(onBodyScroll, internal, getInternal),
489
+ onModelUpdated: createStateBoundCallback(onModelUpdated, internal, getInternal),
490
+ onGridColumnsChanged: createStateBoundCallback(onGridColumnsChanged, internal, getInternal),
491
+ onGridColumnResized: createStateBoundCallback(onGridColumnResized, internal, getInternal),
492
+ onSortChanged: createStateBoundCallback(onSortChanged, internal, getInternal),
493
+ onGridSizeChanged: debouncedGridSizeChanged,
494
+ onPinnedRowDataChanged: createStateBoundCallback(onPinnedRowDataChanged, internal, getInternal),
495
+ };
496
+ }, [
497
+ getInternal,
498
+ onBodyScroll,
499
+ onFirstDataRendered,
500
+ onGridColumnResized,
501
+ onGridColumnsChanged,
502
+ onGridReady,
503
+ onGridSizeChanged,
504
+ onModelUpdated,
505
+ onPinnedRowDataChanged,
506
+ onSortChanged,
507
+ ]);
508
+ const getTableMethods = useCallback(() => {
509
+ const boundAgGridCallbacks = boundAgGridCallbacksRef.current;
510
+ invariant(boundAgGridCallbacks);
511
+ return {
512
+ hasColumnWidths: hasColumnWidths(),
513
+ getExecutionDefinition,
514
+ getMenuConfig,
515
+ getGroupRows,
516
+ getColumnTotals,
517
+ getRowTotals,
518
+ getColumnHeadersPosition,
519
+ getMeasureGroupDimension,
520
+ getResizingConfig,
521
+ onLoadingChanged,
522
+ onError,
523
+ onExportReady: propsRef.current.onExportReady ?? (() => { }),
524
+ pushData: pushDataGuard,
525
+ onPageLoaded,
526
+ onExecutionTransformed,
527
+ onMenuAggregationClick,
528
+ setLastSortedColId,
529
+ ...boundAgGridCallbacks,
530
+ };
531
+ }, [
532
+ getColumnHeadersPosition,
533
+ getColumnTotals,
534
+ getExecutionDefinition,
535
+ getGroupRows,
536
+ getMeasureGroupDimension,
537
+ getMenuConfig,
538
+ getResizingConfig,
539
+ getRowTotals,
540
+ hasColumnWidths,
541
+ onError,
542
+ onExecutionTransformed,
543
+ onLoadingChanged,
544
+ onMenuAggregationClick,
545
+ onPageLoaded,
546
+ pushDataGuard,
547
+ setLastSortedColId,
548
+ ]);
549
+ //
550
+ // Initialization of the imperative parts of the table
551
+ //
552
+ const initializeEventHandlers = useCallback(() => {
553
+ const internal = internalRef.current;
554
+ const currentProps = propsRef.current;
555
+ // Create event handler instances with proper context
556
+ eventHandlersRef.current = {
557
+ gridEventHandlers: new GridEventHandlers({
558
+ internal,
559
+ props: currentProps,
560
+ updateDesiredHeight,
561
+ onFirstDataRendered,
562
+ updateStickyRow,
563
+ getGroupRows,
564
+ getLastSortedColId,
565
+ setLastSortedColId,
566
+ }),
567
+ dataRenderHandlers: new DataRenderHandlers({
568
+ internal,
569
+ isColumnAutoresizeEnabled,
570
+ isGrowToFitEnabled,
571
+ autoresizeColumns,
572
+ growToFit,
573
+ updateStickyRow,
574
+ startWatchingTableRendered,
575
+ }),
576
+ scrollEventHandlers: new ScrollEventHandlers({
577
+ internal,
578
+ updateStickyRowContent,
579
+ }),
580
+ resizeEventHandlers: new ResizeEventHandlers({
581
+ internal,
582
+ updateDesiredHeight,
583
+ autoresizeColumns,
584
+ getResizingConfig,
585
+ }),
586
+ sortingEventHandlers: new SortingEventHandlers({
587
+ internal,
588
+ getExecutionDefinition,
589
+ pushDataGuard,
590
+ setState,
591
+ }),
592
+ aggregationEventHandlers: new AggregationEventHandlers({
593
+ internal,
594
+ getExecutionDefinition,
595
+ getColumnTotals,
596
+ getRowTotals,
597
+ pushDataGuard,
598
+ setState,
599
+ }),
600
+ dataLoadingEventHandlers: new DataLoadingEventHandlers({
601
+ internal,
602
+ props: currentProps,
603
+ updateStickyRow,
604
+ updateDesiredHeight,
605
+ setState,
606
+ }),
607
+ };
608
+ }, [
609
+ autoresizeColumns,
610
+ getColumnTotals,
611
+ getExecutionDefinition,
612
+ getGroupRows,
613
+ getLastSortedColId,
614
+ getResizingConfig,
615
+ getRowTotals,
616
+ growToFit,
617
+ isColumnAutoresizeEnabled,
618
+ isGrowToFitEnabled,
619
+ onFirstDataRendered,
620
+ pushDataGuard,
621
+ setLastSortedColId,
622
+ setState,
623
+ startWatchingTableRendered,
624
+ updateDesiredHeight,
625
+ updateStickyRow,
626
+ updateStickyRowContent,
627
+ ]);
628
+ const createComponentUpdateAnalyzer = useCallback(() => new ComponentUpdateAnalyzer({
629
+ props: propsRef.current,
630
+ state: stateRef.current,
631
+ internal: internalRef.current,
632
+ executionAbortManager,
633
+ }), [executionAbortManager]);
634
+ const initializeConfigAccessors = useCallback(() => {
635
+ const internal = internalRef.current;
636
+ const currentProps = propsRef.current;
637
+ const currentState = stateRef.current;
638
+ const configAccessors = new TableConfigAccessors({
639
+ props: currentProps,
640
+ state: currentState,
191
641
  });
192
- // Initialize event handlers and config accessors
193
- this.initializeEventHandlers();
194
- this.initializeConfigAccessors();
642
+ configAccessorsRef.current = {
643
+ configAccessors,
644
+ configDelegates: createConfigDelegates(configAccessors),
645
+ // Initialize sticky row manager
646
+ stickyRowManager: new StickyRowManager({
647
+ internal,
648
+ props: currentProps,
649
+ state: currentState,
650
+ getGroupRows,
651
+ }),
652
+ // Initialize height calculation manager
653
+ heightCalculationManager: new HeightCalculationManager({
654
+ internal,
655
+ props: currentProps,
656
+ state: currentState,
657
+ setState,
658
+ getContainerRef,
659
+ }),
660
+ };
661
+ // Initialize component update analyzer
662
+ componentUpdateAnalyzerRef.current = createComponentUpdateAnalyzer();
663
+ }, [createComponentUpdateAnalyzer, getContainerRef, getGroupRows, setState]);
664
+ /*
665
+ * Mirrors what the class constructor used to do: create the event handlers, config accessors and the
666
+ * ag-grid callbacks before anything gets rendered.
667
+ */
668
+ const isInitializedRef = useRef(false);
669
+ if (!isInitializedRef.current) {
670
+ isInitializedRef.current = true;
671
+ initializeEventHandlers();
672
+ initializeConfigAccessors();
673
+ boundAgGridCallbacksRef.current = createBoundAgGridCallbacks();
195
674
  }
196
675
  //
197
676
  // Lifecycle
@@ -210,26 +689,26 @@ export class CorePivotTableAgImpl extends Component {
210
689
  *
211
690
  * @param execution - prepared execution to drive
212
691
  */
213
- initialize = (execution) => {
214
- this.internal.abandonInitialization();
215
- this.executionAbortManager.refresh({
216
- isLoading: this.state.isLoading,
217
- readyToRender: this.state.readyToRender,
692
+ const initialize = useCallback((execution) => {
693
+ internalRef.current.abandonInitialization();
694
+ executionAbortManager.refresh({
695
+ isLoading: stateRef.current.isLoading,
696
+ readyToRender: stateRef.current.readyToRender,
218
697
  });
219
- const initializer = new TableFacadeInitializer(execution, this.getTableMethods(), this.props, () => this.executionAbortManager.getCurrentController());
698
+ const initializer = new TableFacadeInitializer(execution, getTableMethods(), propsRef.current, () => executionAbortManager.getCurrentController());
220
699
  void initializer.initialize().then((result) => {
221
- if (!result || this.internal.initializer !== result.initializer) {
700
+ if (!result || internalRef.current.initializer !== result.initializer) {
222
701
  /*
223
702
  * This particular initialization was abandoned.
224
703
  */
225
704
  return;
226
705
  }
227
- this.internal.initializer = undefined;
228
- this.internal.table = result.table;
229
- this.setState({ readyToRender: true });
706
+ internalRef.current.initializer = undefined;
707
+ internalRef.current.table = result.table;
708
+ setState({ readyToRender: true });
230
709
  });
231
710
  return initializer;
232
- };
711
+ }, [executionAbortManager, getTableMethods, setState]);
233
712
  /**
234
713
  * Completely re-initializes the table in order to show data for the provided prepared execution. At this point
235
714
  * code has determined that the table layout for the other prepared execution differs from what is currently
@@ -237,53 +716,52 @@ export class CorePivotTableAgImpl extends Component {
237
716
  *
238
717
  * This will reset all React state and non-react state and start table initialization process.
239
718
  */
240
- reinitialize = (execution) => {
241
- this.setState({
719
+ const reinitialize = useCallback((execution) => {
720
+ setState({
242
721
  readyToRender: false,
243
722
  columnTotals: cloneDeep(sanitizeDefTotals(execution.definition)),
244
723
  rowTotals: getTotalsForColumnsBucket(execution.definition),
245
724
  error: undefined,
246
- desiredHeight: this.props.config.maxHeight,
725
+ desiredHeight: propsRef.current.config.maxHeight,
247
726
  resized: false,
248
727
  }, () => {
249
- this.internal.destroy();
250
- this.internal = new InternalTableState();
251
- this.initializeEventHandlers();
252
- this.initializeConfigAccessors();
253
- this.boundAgGridCallbacks = this.createBoundAgGridCallbacks();
254
- this.internal.initializer = this.initialize(execution);
728
+ internalRef.current.destroy();
729
+ internalRef.current = new InternalTableState();
730
+ initializeEventHandlers();
731
+ initializeConfigAccessors();
732
+ boundAgGridCallbacksRef.current = createBoundAgGridCallbacks();
733
+ internalRef.current.initializer = initialize(execution);
255
734
  });
256
- };
257
- componentDidMount() {
258
- this.internal.initializer = this.initialize(this.props.execution);
259
- }
260
- componentWillUnmount() {
261
- this.executionAbortManager.destroy();
262
- if (this.containerRef) {
263
- this.containerRef.removeEventListener("mousedown", this.onContainerMouseDown);
264
- }
265
- // this ensures any events emitted during the async initialization will be sunk. they are no longer needed.
266
- this.internal.destroy();
267
- }
268
- componentDidUpdate(prevProps) {
735
+ }, [
736
+ createBoundAgGridCallbacks,
737
+ initialize,
738
+ initializeConfigAccessors,
739
+ initializeEventHandlers,
740
+ setState,
741
+ ]);
742
+ reinitializeRef.current = reinitialize;
743
+ /**
744
+ * Handles the component update; this is the functional equivalent of what used to live in
745
+ * `componentDidUpdate`.
746
+ */
747
+ const handleUpdate = useCallback((prevProps) => {
748
+ const currentProps = propsRef.current;
749
+ const previousAnalyzer = componentUpdateAnalyzerRef.current;
750
+ invariant(previousAnalyzer);
269
751
  // Update execution abort manager config if it changed
270
- if (this.componentUpdateAnalyzer.hasExecutionCancellingChanged(prevProps)) {
271
- this.executionAbortManager.updateConfig({
272
- enableExecutionCancelling: this.props.config?.enableExecutionCancelling,
752
+ if (previousAnalyzer.hasExecutionCancellingChanged(prevProps)) {
753
+ executionAbortManager.updateConfig({
754
+ enableExecutionCancelling: currentProps.config?.enableExecutionCancelling,
273
755
  });
274
756
  }
275
757
  // Update component update analyzer context
276
- this.componentUpdateAnalyzer = new ComponentUpdateAnalyzer({
277
- props: this.props,
278
- state: this.state,
279
- internal: this.internal,
280
- executionAbortManager: this.executionAbortManager,
281
- });
758
+ const componentUpdateAnalyzer = createComponentUpdateAnalyzer();
759
+ componentUpdateAnalyzerRef.current = componentUpdateAnalyzer;
282
760
  // reinit in progress
283
- if (this.componentUpdateAnalyzer.shouldSkipUpdate()) {
761
+ if (componentUpdateAnalyzer.shouldSkipUpdate()) {
284
762
  return;
285
763
  }
286
- if (this.componentUpdateAnalyzer.isReinitNeeded(prevProps)) {
764
+ if (componentUpdateAnalyzer.isReinitNeeded(prevProps)) {
287
765
  /*
288
766
  * This triggers when execution changes (new measures / attributes). In that case,
289
767
  * a complete re-init of the table is in order.
@@ -294,8 +772,8 @@ export class CorePivotTableAgImpl extends Component {
294
772
  * Note: compared to v7 version of the table, this only happens if someone actually changes the
295
773
  * execution-related props of the table. This branch will not hit any other time.
296
774
  */
297
- this.componentUpdateAnalyzer.logReinitDebug(prevProps.execution);
298
- this.reinitialize(this.props.execution);
775
+ componentUpdateAnalyzer.logReinitDebug(prevProps.execution);
776
+ reinitialize(currentProps.execution);
299
777
  }
300
778
  else {
301
779
  /*
@@ -305,424 +783,110 @@ export class CorePivotTableAgImpl extends Component {
305
783
  * The currentResult and visibleData _will_ be available at this point because the component is definitely
306
784
  * after a successful execution and initialization.
307
785
  */
308
- if (this.componentUpdateAnalyzer.shouldRefreshHeader(prevProps)) {
309
- this.internal.table?.refreshHeader();
786
+ if (componentUpdateAnalyzer.shouldRefreshHeader(prevProps)) {
787
+ internalRef.current.table?.refreshHeader();
310
788
  }
311
- if (this.componentUpdateAnalyzer.hasGrowToFitEnabledChanged(prevProps, this.isGrowToFitEnabled)) {
312
- this.growToFit();
789
+ if (componentUpdateAnalyzer.hasGrowToFitEnabledChanged(prevProps, isGrowToFitEnabled)) {
790
+ growToFit();
313
791
  }
314
- if (this.componentUpdateAnalyzer.hasColumnWidthsChanged(prevProps, this.getColumnWidths)) {
315
- this.internal.table?.applyColumnSizes(this.getResizingConfig());
792
+ if (componentUpdateAnalyzer.hasColumnWidthsChanged(prevProps, getColumnWidths)) {
793
+ internalRef.current.table?.applyColumnSizes(getResizingConfig());
316
794
  }
317
- if (this.componentUpdateAnalyzer.hasMaxHeightChanged(prevProps)) {
318
- this.updateDesiredHeight();
795
+ if (componentUpdateAnalyzer.hasMaxHeightChanged(prevProps)) {
796
+ updateDesiredHeight();
319
797
  }
320
798
  }
321
- }
322
- stopEventWhenResizeHeader(e) {
323
- // Do not propagate event when it originates from the table resizer.
324
- // This means for example that we can resize columns without triggering drag in the application.
325
- if (e.target?.className?.includes?.("ag-header-cell-resize")) {
326
- e.preventDefault();
327
- e.stopPropagation();
328
- }
329
- }
330
- //
331
- // Render
332
- //
333
- setContainerRef = (container) => {
334
- this.containerRef = container;
335
- if (this.containerRef) {
336
- this.containerRef.addEventListener("mousedown", this.onContainerMouseDown);
337
- }
338
- };
339
- renderLoading() {
340
- const { LoadingComponent, theme } = this.props;
341
- return _jsx(PivotTableLoading, { LoadingComponent: LoadingComponent, theme: theme });
342
- }
343
- render() {
344
- const { ErrorComponent } = this.props;
345
- const { desiredHeight, error } = this.state;
346
- if (error) {
347
- return _jsx(PivotTableError, { error: error, errorMap: this.errorMap, ErrorComponent: ErrorComponent });
348
- }
349
- const style = {
350
- height: desiredHeight || "100%",
351
- position: "relative",
352
- overflow: "hidden",
799
+ }, [
800
+ createComponentUpdateAnalyzer,
801
+ executionAbortManager,
802
+ getColumnWidths,
803
+ getResizingConfig,
804
+ growToFit,
805
+ isGrowToFitEnabled,
806
+ reinitialize,
807
+ updateDesiredHeight,
808
+ ]);
809
+ // componentDidMount & componentWillUnmount
810
+ useLayoutEffect(() => {
811
+ internalRef.current.initializer = initialize(propsRef.current.execution);
812
+ return () => {
813
+ executionAbortManager.destroy();
814
+ if (containerRef.current) {
815
+ containerRef.current.removeEventListener("mousedown", onContainerMouseDown);
816
+ }
817
+ // this ensures any events emitted during the async initialization will be sunk. they are no longer needed.
818
+ internalRef.current.destroy();
353
819
  };
354
- if (!this.state.readyToRender) {
355
- return (_jsx("div", { className: "gd-table-component", style: style, id: this.pivotTableId, children: this.renderLoading() }));
356
- }
357
- // when table is ready, then the table facade must be set. if this bombs then there is a bug
358
- // in the initialization logic
359
- invariant(this.internal.table);
360
- if (!this.internal.gridOptions) {
361
- this.internal.gridOptions = createGridOptions(this.internal.table, this.getTableMethods(), this.props);
362
- }
363
- /*
364
- * Show loading overlay until all the resizing is done. This is because the various resizing operations
365
- * have to happen asynchronously - they must wait until ag-grid fires onFirstDataRendered, before our code
366
- * can start reliably interfacing with the autosizing features.
367
- */
368
- const shouldRenderLoadingOverlay = (this.isColumnAutoresizeEnabled() || this.isGrowToFitEnabled()) && !this.state.resized;
369
- const classNames = cx("gd-table-component", {
370
- "gd-table-header-hide": this.props.config?.columnHeadersPosition === "left" &&
371
- this.internal.table.tableDescriptor.isTransposed(),
372
- });
373
- return (_jsx("div", { className: classNames, style: style, id: this.pivotTableId, onMouseDown: this.stopEventWhenResizeHeader, onDragStart: this.stopEventWhenResizeHeader, children: _jsxs("div", { className: "gd-table ag-theme-balham s-pivot-table", style: style, ref: this.setContainerRef, children: [
374
- _jsx(AgGridReact, { ...this.internal.gridOptions, modules: [AllCommunityModule] }), shouldRenderLoadingOverlay ? this.renderLoading() : null] }) }));
375
- }
376
- //
377
- // Initialize event handlers
378
- //
379
- initializeEventHandlers = () => {
380
- // Create event handler instances with proper context
381
- this.gridEventHandlers = new GridEventHandlers({
382
- internal: this.internal,
383
- props: this.props,
384
- updateDesiredHeight: this.updateDesiredHeight,
385
- onFirstDataRendered: this.onFirstDataRendered,
386
- updateStickyRow: this.updateStickyRow,
387
- getGroupRows: this.getGroupRows,
388
- getLastSortedColId: this.getLastSortedColId,
389
- setLastSortedColId: this.setLastSortedColId,
390
- });
391
- this.dataRenderHandlers = new DataRenderHandlers({
392
- internal: this.internal,
393
- isColumnAutoresizeEnabled: this.isColumnAutoresizeEnabled,
394
- isGrowToFitEnabled: this.isGrowToFitEnabled,
395
- autoresizeColumns: this.autoresizeColumns,
396
- growToFit: this.growToFit,
397
- updateStickyRow: this.updateStickyRow,
398
- startWatchingTableRendered: this.startWatchingTableRendered,
399
- });
400
- this.scrollEventHandlers = new ScrollEventHandlers({
401
- internal: this.internal,
402
- updateStickyRowContent: this.updateStickyRowContent,
403
- });
404
- this.resizeEventHandlers = new ResizeEventHandlers({
405
- internal: this.internal,
406
- updateDesiredHeight: this.updateDesiredHeight,
407
- autoresizeColumns: this.autoresizeColumns,
408
- getResizingConfig: this.getResizingConfig,
409
- });
410
- this.sortingEventHandlers = new SortingEventHandlers({
411
- internal: this.internal,
412
- getExecutionDefinition: this.getExecutionDefinition,
413
- pushDataGuard: this.pushDataGuard,
414
- setState: this.setState.bind(this),
415
- });
416
- this.aggregationEventHandlers = new AggregationEventHandlers({
417
- internal: this.internal,
418
- getExecutionDefinition: this.getExecutionDefinition,
419
- getColumnTotals: this.getColumnTotals,
420
- getRowTotals: this.getRowTotals,
421
- pushDataGuard: this.pushDataGuard,
422
- setState: this.setState.bind(this),
423
- });
424
- this.dataLoadingEventHandlers = new DataLoadingEventHandlers({
425
- internal: this.internal,
426
- props: this.props,
427
- updateStickyRow: this.updateStickyRow,
428
- updateDesiredHeight: this.updateDesiredHeight,
429
- setState: this.setState.bind(this),
430
- });
431
- };
432
- //
433
- // event handlers
434
- //
435
- onGridReady = (event) => {
436
- this.gridEventHandlers.onGridReady(event);
437
- };
438
- onFirstDataRendered = async (_event) => {
439
- await this.dataRenderHandlers.onFirstDataRendered(_event);
440
- };
441
- onModelUpdated = (event) => {
442
- this.gridEventHandlers.onModelUpdated(event);
443
- };
444
- onGridColumnsChanged = () => {
445
- this.gridEventHandlers.onGridColumnsChanged();
446
- };
447
- onGridSizeChanged = (gridSizeChangedEvent) => {
448
- this.resizeEventHandlers.onGridSizeChanged(gridSizeChangedEvent);
449
- };
450
- onGridColumnResized = (columnEvent) => {
451
- this.resizeEventHandlers.onGridColumnResized(columnEvent);
452
- };
453
- onSortChanged = (event) => {
454
- this.sortingEventHandlers.onSortChanged(event);
455
- };
456
- onPinnedRowDataChanged = async (event) => {
457
- await this.resizeEventHandlers.onPinnedRowDataChanged(event);
458
- };
459
- onBodyScroll = (event) => {
460
- this.scrollEventHandlers.onBodyScroll(event);
461
- };
462
- onContainerMouseDown = (event) => {
463
- this.scrollEventHandlers.onContainerMouseDown(event);
464
- };
465
- onPageLoaded = (dv, newResult) => {
466
- this.dataLoadingEventHandlers.onPageLoaded(dv, newResult);
467
- };
468
- /**
469
- * This will be called when user changes sorts or adds totals. This means complete re-execution with
470
- * new sorts or totals. Loading indicators will be shown instead of all rendered rows thanks to the
471
- * LoadingRenderer used in all cells of the left-most column.
472
- *
473
- * The table must take care to remove the sticky (top-pinned) row - it is treated differently by
474
- * ag-grid and will be literally sticking there on its own with the loading indicators.
475
- *
476
- * Once transformation finishes - indicated by call to onPageLoaded, table can re-instance the sticky row.
477
- *
478
- * @param newExecution - the new execution which is being run and will be used to populate the table
479
- */
480
- onExecutionTransformed = (newExecution) => {
481
- // Handle the reinitialize call directly here since we can't pass it to the handler
482
- if (!this.internal.table) {
820
+ // the table must be initialized on mount only; all the functions used here are stable
821
+ // eslint-disable-next-line react-hooks/exhaustive-deps
822
+ }, []);
823
+ // componentDidUpdate
824
+ const prevPropsRef = useRef(resolvedProps);
825
+ const isMountedRef = useRef(false);
826
+ useLayoutEffect(() => {
827
+ const prevProps = prevPropsRef.current;
828
+ prevPropsRef.current = resolvedProps;
829
+ if (!isMountedRef.current) {
830
+ // the update handling must not run for the initial render
831
+ isMountedRef.current = true;
483
832
  return;
484
833
  }
485
- this.internal.table.clearStickyRow();
486
- // Force double execution only when totals/subtotals for columns change, so table is render properly.
487
- if (!isEqual(this.state.tempExecution.definition.buckets[2], newExecution.definition.buckets[2])) {
488
- this.setState({
489
- tempExecution: newExecution,
490
- });
491
- this.reinitialize(newExecution);
492
- }
493
- };
494
- onMenuAggregationClick = (menuAggregationClickConfig) => {
495
- this.aggregationEventHandlers.onMenuAggregationClick(menuAggregationClickConfig);
496
- };
497
- onLoadingChanged = (loadingState) => {
498
- this.dataLoadingEventHandlers.onLoadingChanged(loadingState);
499
- };
500
- onError = (error, execution = this.props.execution) => {
501
- this.dataLoadingEventHandlers.onError(error, execution);
502
- };
503
- //
504
- // Table resizing
505
- //
506
- growToFit = () => {
507
- invariant(this.internal.table);
508
- if (!this.isGrowToFitEnabled()) {
834
+ handleUpdate(prevProps);
835
+ });
836
+ // drain the setState callbacks; they run right after the commit of the state update that registered them
837
+ useLayoutEffect(() => {
838
+ if (committedStateUpdateCallbacks.current.length === 0) {
509
839
  return;
510
840
  }
511
- this.internal.table.growToFit(this.getResizingConfig());
512
- if (!this.state.resized && !this.internal.table.isResizing()) {
513
- this.setState({
514
- resized: true,
515
- });
516
- }
517
- };
518
- autoresizeColumns = async (force = false) => {
519
- if (this.state.resized && !force) {
520
- return;
521
- }
522
- const didResize = await this.internal.table?.autoresizeColumns(this.getResizingConfig(), force);
523
- if (didResize) {
524
- // after column resizing, horizontal scroolbar may change and table height may need resizing
525
- this.updateDesiredHeight();
526
- }
527
- if (didResize && !this.state.resized) {
528
- this.setState({
529
- resized: true,
530
- });
531
- }
532
- };
533
- shouldAutoResizeColumns = () => {
534
- return this.configDelegates.shouldAutoResizeColumns();
535
- };
536
- startWatchingTableRendered = () => {
537
- if (!this.internal.table) {
538
- return;
539
- }
540
- const missingContainerRef = !this.containerRef; // table having no data will be unmounted, it causes ref null
541
- const isTableRendered = this.shouldAutoResizeColumns()
542
- ? this.state.resized
543
- : this.internal.table.isPivotTableReady();
544
- const shouldCallAutoresizeColumns = this.internal.table.isPivotTableReady() &&
545
- !this.state.resized &&
546
- !this.internal.table.isResizing();
547
- if (this.shouldAutoResizeColumns() && shouldCallAutoresizeColumns) {
548
- void this.autoresizeColumns();
549
- }
550
- if (missingContainerRef || isTableRendered) {
551
- this.stopWatchingTableRendered();
552
- }
553
- };
554
- stopWatchingTableRendered = () => {
555
- this.internal.stopWatching();
556
- this.props.afterRender();
557
- };
558
- //
559
- // Sticky row handling - delegated to StickyRowManager
560
- //
561
- updateStickyRow = () => {
562
- this.stickyRowManager.updateStickyRow();
563
- };
564
- updateStickyRowContent = (scrollPosition) => {
565
- this.stickyRowManager.updateStickyRowContent(scrollPosition);
566
- };
567
- //
568
- // Desired height updating - delegated to HeightCalculationManager
569
- //
570
- updateDesiredHeight = () => {
571
- this.heightCalculationManager.updateDesiredHeight();
572
- };
573
- //
574
- // Initialize config accessors
575
- //
576
- initializeConfigAccessors = () => {
577
- this.configAccessors = new TableConfigAccessors({
578
- props: this.props,
579
- state: this.state,
580
- });
581
- this.configDelegates = createConfigDelegates(this.configAccessors);
582
- // Initialize sticky row manager
583
- this.stickyRowManager = new StickyRowManager({
584
- internal: this.internal,
585
- props: this.props,
586
- state: this.state,
587
- getGroupRows: this.getGroupRows,
588
- });
589
- // Initialize height calculation manager
590
- this.heightCalculationManager = new HeightCalculationManager({
591
- internal: this.internal,
592
- props: this.props,
593
- state: this.state,
594
- setState: (state, callback) => {
595
- this.setState(state, callback);
596
- },
597
- getContainerRef: () => this.containerRef,
598
- });
599
- // Initialize component update analyzer
600
- this.componentUpdateAnalyzer = new ComponentUpdateAnalyzer({
601
- props: this.props,
602
- state: this.state,
603
- internal: this.internal,
604
- executionAbortManager: this.executionAbortManager,
605
- });
606
- };
841
+ const callbacks = committedStateUpdateCallbacks.current;
842
+ committedStateUpdateCallbacks.current = [];
843
+ callbacks.forEach((callback) => callback());
844
+ });
607
845
  //
608
- // Table configuration accessors
846
+ // Render
609
847
  //
610
- getLastSortedColId = () => {
611
- return this.internal.lastSortedColId;
612
- };
613
- setLastSortedColId = (colId) => {
614
- this.internal.lastSortedColId = colId;
615
- };
616
- // Delegate to config accessors
617
- getColumnTotals = () => this.configDelegates.getColumnTotals();
618
- getRowTotals = () => this.configDelegates.getRowTotals();
619
- getExecutionDefinition = () => this.configDelegates.getExecutionDefinition();
620
- getGroupRows = () => this.configDelegates.getGroupRows();
621
- getMeasureGroupDimension = () => this.configDelegates.getMeasureGroupDimension();
622
- getColumnHeadersPosition = () => this.configDelegates.getColumnHeadersPosition();
623
- getMenuConfig = () => this.configDelegates.getMenuConfig();
624
- getDefaultWidth = () => this.configDelegates.getDefaultWidth();
625
- isColumnAutoresizeEnabled = () => this.configDelegates.isColumnAutoresizeEnabled();
626
- isGrowToFitEnabled = (props = this.props) => this.configDelegates.isGrowToFitEnabled(props);
627
- getColumnWidths = (props) => this.configDelegates.getColumnWidths(props);
628
- hasColumnWidths = () => this.configDelegates.hasColumnWidths();
629
- getDefaultWidthFromProps = (props) => this.configDelegates.getDefaultWidthFromProps(props);
630
- /**
631
- * All pushData calls done by the table must go through this guard.
632
- *
633
- * TODO: The guard should ensure a 'disconnect' between push data handling and the calling function processing.
634
- * When the pushData is handled by the application, it MAY (and in our case it DOES) trigger processing that
635
- * lands back in the table. This opens additional set of invariants to check / be prepared for in order to
636
- * optimize the renders and re-renders.
637
- */
638
- pushDataGuard = (data) => {
639
- this.props.pushData?.(data);
640
- /*
641
- * TODO: Switch to this on in FET-715.
642
- setTimeout(() => {
643
- this.props.pushData?.(data);
644
- }, 0);
645
- */
848
+ const setContainerRef = useCallback((container) => {
849
+ containerRef.current = container ?? undefined;
850
+ if (containerRef.current) {
851
+ containerRef.current.addEventListener("mousedown", onContainerMouseDown);
852
+ }
853
+ }, [onContainerMouseDown]);
854
+ const renderLoading = () => {
855
+ const { LoadingComponent, theme } = resolvedProps;
856
+ return _jsx(PivotTableLoading, { LoadingComponent: LoadingComponent, theme: theme });
646
857
  };
647
- /**
648
- * Wraps the provided callback function with a guard that checks whether the current table state is the same
649
- * as the state snapshot at the time of callback creation. If the state differs, the wrapped function WILL NOT
650
- * be called.
651
- *
652
- * @param callback - function to wrap with state guard
653
- */
654
- // oxlint-disable-next-line @typescript-eslint/no-unsafe-function-type
655
- stateBoundCallback = (callback) => {
656
- return createStateBoundCallback(callback, this.internal, () => this.internal);
858
+ const { ErrorComponent } = resolvedProps;
859
+ const { desiredHeight, error } = state;
860
+ if (error) {
861
+ return _jsx(PivotTableError, { error: error, errorMap: errorMap, ErrorComponent: ErrorComponent });
862
+ }
863
+ const style = {
864
+ height: desiredHeight || "100%",
865
+ position: "relative",
866
+ overflow: "hidden",
657
867
  };
658
- /**
659
- * All callback functions that the table passes to ag-grid must be bound to the current internal state of the table. The
660
- * callback functions MUST be noop if the internal state at the time of call is different from the internal state
661
- * at the time of creation.
662
- *
663
- * This is essential to prevent errors stemming for racy behavior triggered by ag-grid. ag-grid often triggers
664
- * event callbacks using setTimeout(). It can happen, that once the event is actually processed the ag-grid table
665
- * which caused it is unmounted. Doing anything with the unmounted table's gridApi leads to errors.
666
- *
667
- * Without this, table may trigger ag-grid errors such as this:
668
- *
669
- * https://github.com/ag-grid/ag-grid/issues/3457
670
- *
671
- * or this:
672
- *
673
- * https://github.com/ag-grid/ag-grid/issues/3334
868
+ if (!state.readyToRender) {
869
+ return (_jsx("div", { className: "gd-table-component", style: style, id: pivotTableId, children: renderLoading() }));
870
+ }
871
+ const internal = internalRef.current;
872
+ // when table is ready, then the table facade must be set. if this bombs then there is a bug
873
+ // in the initialization logic
874
+ invariant(internal.table);
875
+ if (!internal.gridOptions) {
876
+ internal.gridOptions = createGridOptions(internal.table, getTableMethods(), resolvedProps);
877
+ }
878
+ /*
879
+ * Show loading overlay until all the resizing is done. This is because the various resizing operations
880
+ * have to happen asynchronously - they must wait until ag-grid fires onFirstDataRendered, before our code
881
+ * can start reliably interfacing with the autosizing features.
674
882
  */
675
- createBoundAgGridCallbacks = () => {
676
- const debouncedGridSizeChanged = createDebouncedCallback(this.stateBoundCallback(this.onGridSizeChanged), AGGRID_ON_RESIZE_TIMEOUT);
677
- return {
678
- onGridReady: this.stateBoundCallback(this.onGridReady),
679
- onFirstDataRendered: this.stateBoundCallback(this.onFirstDataRendered),
680
- onBodyScroll: this.stateBoundCallback(this.onBodyScroll),
681
- onModelUpdated: this.stateBoundCallback(this.onModelUpdated),
682
- onGridColumnsChanged: this.stateBoundCallback(this.onGridColumnsChanged),
683
- onGridColumnResized: this.stateBoundCallback(this.onGridColumnResized),
684
- onSortChanged: this.stateBoundCallback(this.onSortChanged),
685
- onGridSizeChanged: debouncedGridSizeChanged,
686
- onPinnedRowDataChanged: this.stateBoundCallback(this.onPinnedRowDataChanged),
687
- };
688
- };
689
- getTableMethods = () => {
690
- return {
691
- hasColumnWidths: this.hasColumnWidths(),
692
- getExecutionDefinition: this.getExecutionDefinition,
693
- getMenuConfig: this.getMenuConfig,
694
- getGroupRows: this.getGroupRows,
695
- getColumnTotals: this.getColumnTotals,
696
- getRowTotals: this.getRowTotals,
697
- getColumnHeadersPosition: this.getColumnHeadersPosition,
698
- getMeasureGroupDimension: this.getMeasureGroupDimension,
699
- getResizingConfig: this.getResizingConfig,
700
- onLoadingChanged: this.onLoadingChanged,
701
- onError: this.onError,
702
- onExportReady: this.props.onExportReady ?? (() => { }),
703
- pushData: this.pushDataGuard,
704
- onPageLoaded: this.onPageLoaded,
705
- onExecutionTransformed: this.onExecutionTransformed,
706
- onMenuAggregationClick: this.onMenuAggregationClick,
707
- setLastSortedColId: this.setLastSortedColId,
708
- ...this.boundAgGridCallbacks,
709
- };
710
- };
711
- getResizingConfig = () => {
712
- return {
713
- defaultWidth: this.getDefaultWidth(),
714
- growToFit: this.isGrowToFitEnabled(),
715
- columnAutoresizeOption: this.getDefaultWidthFromProps(this.props),
716
- widths: this.getColumnWidths(this.props),
717
- isAltKeyPressed: this.internal.isAltKeyPressed,
718
- isMetaOrCtrlKeyPressed: this.internal.isMetaOrCtrlKeyPressed,
719
- // use clientWidth of the viewport container to accommodate for vertical scrollbars if needed
720
- clientWidth: this.containerRef?.getElementsByClassName("ag-body-viewport")[0]?.clientWidth ?? 0,
721
- containerRef: this.containerRef,
722
- separators: this.props?.config?.separators,
723
- onColumnResized: this.props.onColumnResized,
724
- };
725
- };
883
+ const shouldRenderLoadingOverlay = (isColumnAutoresizeEnabled() || isGrowToFitEnabled()) && !state.resized;
884
+ const classNames = cx("gd-table-component", {
885
+ "gd-table-header-hide": resolvedProps.config?.columnHeadersPosition === "left" &&
886
+ internal.table.tableDescriptor.isTransposed(),
887
+ });
888
+ return (_jsx("div", { className: classNames, style: style, id: pivotTableId, onMouseDown: stopEventWhenResizeHeader, onDragStart: stopEventWhenResizeHeader, children: _jsxs("div", { className: "gd-table ag-theme-balham s-pivot-table", style: style, ref: setContainerRef, children: [
889
+ _jsx(AgGridReact, { ...internal.gridOptions, modules: [AllCommunityModule] }), shouldRenderLoadingOverlay ? renderLoading() : null] }) }));
726
890
  }
727
891
  const CorePivotTableWithIntl = injectIntl(withTheme(CorePivotTableAgImpl));
728
892
  /**