@gooddata/sdk-ui-pivot 11.52.0-alpha.5 → 11.52.0-alpha.7

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