@adaptabletools/adaptable 23.0.16 → 23.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptabletools/adaptable",
3
- "version": "23.0.16",
3
+ "version": "23.0.18",
4
4
  "description": "Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements",
5
5
  "keywords": [
6
6
  "web-components",
@@ -190,6 +190,8 @@ export interface IAdaptable {
190
190
  column: AdaptableColumn;
191
191
  currentSearchValue: string;
192
192
  previousResult: InFilterValueResult | undefined;
193
+ offset?: number;
194
+ limit?: number;
193
195
  }): Promise<InFilterValueResult>;
194
196
  getGridCellFromRowNode(rowNode: IRowNode, columnId: string): GridCell | undefined;
195
197
  getRawValueFromRowNode(rowNode: IRowNode, columnId: string): any;
@@ -200,6 +200,12 @@ export interface InFilterValueResult {
200
200
  * If true, AdapTable will not filter the list using the current search value
201
201
  */
202
202
  skipDefaultSearch?: boolean;
203
+ /**
204
+ * When true, scrolling near the end of the list re-invokes `customInFilterValues`
205
+ * with a larger `offset`. New `values` are appended (same search).
206
+ * Search changes reset the list and set `offset` back to 0.
207
+ */
208
+ hasMore?: boolean;
203
209
  }
204
210
  /**
205
211
  * Information about items in the IN Column Filter
@@ -267,8 +273,21 @@ export interface CustomInFilterValuesContext<TData = any> extends AdaptableColum
267
273
  * The result returned by the previous invocation of `FilterOptions.customInFilterValues`, if any.
268
274
  *
269
275
  * Useful for avoiding expensive recomputations (e.g. when filtering is async or server-side).
276
+ * Opaque pagination cursors may also be stashed on this object and read on the next call.
270
277
  */
271
278
  previousFilterResult?: InFilterValueResult;
279
+ /**
280
+ * Zero-based index of the first value to return.
281
+ * Reset to 0 when the dropdown opens or `currentSearchValue` changes.
282
+ * Increased when loading the next page (`hasMore` is true on the return object - this means lazy pagination is enabled).
283
+ * @defaultValue 0
284
+ */
285
+ offset: number;
286
+ /**
287
+ * Suggested page size. Consumers may return fewer or more items. Useful when `hasMore` is true on the return object (lazy pagination).
288
+ * @defaultValue 1000
289
+ */
290
+ limit: number;
272
291
  }
273
292
  /**
274
293
  * List of Editors that can be used when creating the Grid Filter
@@ -53,6 +53,8 @@ export declare class ColumnFilterInternalApi extends ApiBase {
53
53
  getColumnFilterValues(options: {
54
54
  columnId: string;
55
55
  currentSearchValue: string;
56
+ offset?: number;
57
+ limit?: number;
56
58
  }): Promise<InFilterValueResult>;
57
59
  shouldManuallyApplyColumnFilter(columnId: string): boolean;
58
60
  getAdaptableFilterHandler(columnId: string): AdaptableFilterHandler | undefined;
@@ -199,6 +199,8 @@ export class ColumnFilterInternalApi extends ApiBase {
199
199
  return options.currentSearchValue;
200
200
  },
201
201
  previousResult: undefined,
202
+ offset: options.offset,
203
+ limit: options.limit,
202
204
  };
203
205
  const columnFilterHandler = this.getAgGridApi().getColumnFilterHandler(options.columnId);
204
206
  if (!columnFilterHandler) {
@@ -30,6 +30,8 @@ export declare class GridInternalApi extends ApiBase {
30
30
  columnId: string;
31
31
  currentSearchValue: string;
32
32
  previousResult: InFilterValueResult | undefined;
33
+ offset?: number;
34
+ limit?: number;
33
35
  }): Promise<InFilterValueResult>;
34
36
  getDistinctValuesForColumn(columnId: string): Promise<GridCell[]> | undefined;
35
37
  getDistinctEditDisplayValuesForColumn(options: {
@@ -28,6 +28,8 @@ export class GridInternalApi extends ApiBase {
28
28
  return options.currentSearchValue;
29
29
  },
30
30
  previousResult: options.previousResult,
31
+ offset: options.offset,
32
+ limit: options.limit,
31
33
  });
32
34
  return inFilterValues || { values: [] };
33
35
  }
@@ -1,5 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { ColumnValuesSelectProps } from '../../FilterForm/ListBoxFilterForm';
3
+ /** Suggested page size passed to `customInFilterValues` as `context.limit`. */
4
+ export declare const IN_FILTER_VALUES_PAGE_SIZE = 1000;
3
5
  export interface FloatingFilterValuesProps {
4
6
  columnId: string;
5
7
  disabled: boolean;
@@ -19,6 +21,7 @@ export declare function useDistinctFilterColumnValues(options: {
19
21
  searchValueRef?: React.MutableRefObject<string>;
20
22
  }): {
21
23
  isDistinctColumnValuesLoading: boolean;
24
+ isLoadingMore: boolean;
22
25
  quickFilterValues: {
23
26
  values: {
24
27
  value: any;
@@ -27,6 +30,7 @@ export declare function useDistinctFilterColumnValues(options: {
27
30
  dataLoadIsComplete: boolean;
28
31
  skipDefaultSearch: boolean;
29
32
  searchValueUsedInFilterValue: boolean;
33
+ hasMore: boolean;
30
34
  };
31
35
  setQuickFilterValues: React.Dispatch<React.SetStateAction<{
32
36
  values: {
@@ -36,6 +40,7 @@ export declare function useDistinctFilterColumnValues(options: {
36
40
  dataLoadIsComplete: boolean;
37
41
  skipDefaultSearch: boolean;
38
42
  searchValueUsedInFilterValue: boolean;
43
+ hasMore: boolean;
39
44
  }>>;
40
45
  triggerValuesLoad: (options?: {
41
46
  usePrevious: boolean;
@@ -12,6 +12,40 @@ const logOnce = (message, loggingFn) => {
12
12
  loggingFn(message);
13
13
  onceFlags[message] = true;
14
14
  };
15
+ export const IN_FILTER_VALUES_PAGE_SIZE = 1000;
16
+ const NEAR_END_PX = 100;
17
+ const SCROLL_DEBOUNCE_MS = 75;
18
+ const LOADING_MORE_INDICATOR_ATTR = 'data-adaptable-in-filter-loading-more';
19
+ const FILTER_VALUES_SCROLL_SELECTOR = '[role="dialog"][data-slot="combobox-content"] [class*="twa:overflow-y-auto"]';
20
+ const FILTER_VALUES_POPUP_SELECTOR = '[role="dialog"][data-slot="combobox-content"]';
21
+ const createLoadingMoreIndicator = () => {
22
+ const el = document.createElement('div');
23
+ el.setAttribute(LOADING_MORE_INDICATOR_ATTR, '');
24
+ el.setAttribute('role', 'status');
25
+ el.setAttribute('aria-live', 'polite');
26
+ el.className = 'ab-Loader';
27
+ el.style.cssText = [
28
+ 'position:absolute',
29
+ 'left:0',
30
+ 'right:0',
31
+ 'bottom:0',
32
+ 'z-index:2',
33
+ 'display:flex',
34
+ 'align-items:center',
35
+ 'justify-content:center',
36
+ 'gap:var(--ab-base-space)',
37
+ 'padding:calc(var(--ab-base-space) * 2) var(--ab-base-space)',
38
+ 'pointer-events:none',
39
+ 'background:color-mix(in oklab, var(--ab-color-popover) 92%, transparent)',
40
+ 'border-top:1px solid color-mix(in oklab, var(--ab-color-popover-foreground) 10%, transparent)',
41
+ ].join(';');
42
+ el.innerHTML =
43
+ '<div class="ab-Loader__Spinner" aria-hidden="true"></div><span class="ab-Loader__text">Loading...</span>';
44
+ return el;
45
+ };
46
+ const removeLoadingMoreIndicators = () => {
47
+ document.querySelectorAll(`[${LOADING_MORE_INDICATOR_ATTR}]`).forEach((node) => node.remove());
48
+ };
15
49
  export function useDistinctFilterColumnValues(options) {
16
50
  const { columnId, searchValueRef } = options;
17
51
  const { api } = useAdaptable();
@@ -27,14 +61,85 @@ export function useDistinctFilterColumnValues(options) {
27
61
  dataLoadIsComplete: false,
28
62
  skipDefaultSearch: false,
29
63
  searchValueUsedInFilterValue: false,
64
+ hasMore: false,
30
65
  });
31
66
  const [isDistinctColumnValuesLoading, setIsDistinctColumnValuesLoading] = React.useState(false);
67
+ const [isLoadingMore, setIsLoadingMore] = React.useState(false);
68
+ const isLoadingMoreRef = React.useRef(false);
69
+ const lastRequestedOffsetRef = React.useRef(null);
70
+ const accumulatedValuesRef = React.useRef(quickFilterValues.values);
71
+ accumulatedValuesRef.current = quickFilterValues.values;
72
+ const hasMoreRef = React.useRef(quickFilterValues.hasMore);
73
+ hasMoreRef.current = quickFilterValues.hasMore;
74
+ const searchValueUsedRef = React.useRef(false);
75
+ const loadMoreGenerationRef = React.useRef(0);
76
+ const setLoadingMore = React.useCallback((next) => {
77
+ isLoadingMoreRef.current = next;
78
+ setIsLoadingMore(next);
79
+ }, []);
80
+ const fetchFilterValues = React.useCallback((offset) => {
81
+ let searchValueUsedInFilterValue = false;
82
+ const columnFilterInternalApi = api.filterApi.columnFilterApi.internalApi;
83
+ return columnFilterInternalApi
84
+ .getColumnFilterValues({
85
+ columnId,
86
+ offset,
87
+ limit: IN_FILTER_VALUES_PAGE_SIZE,
88
+ get currentSearchValue() {
89
+ searchValueUsedInFilterValue = true;
90
+ return searchValueRef ? searchValueRef.current : '';
91
+ },
92
+ })
93
+ .then((result) => ({ result, searchValueUsedInFilterValue }));
94
+ }, [api, columnId, searchValueRef]);
95
+ const loadMoreValues = React.useCallback(() => {
96
+ if (!hasMoreRef.current || isLoadingMoreRef.current) {
97
+ return;
98
+ }
99
+ const offset = accumulatedValuesRef.current.length;
100
+ if (lastRequestedOffsetRef.current === offset) {
101
+ return;
102
+ }
103
+ setLoadingMore(true);
104
+ lastRequestedOffsetRef.current = offset;
105
+ const generation = loadMoreGenerationRef.current;
106
+ fetchFilterValues(offset)
107
+ .then(({ result, searchValueUsedInFilterValue, }) => {
108
+ if (generation !== loadMoreGenerationRef.current) {
109
+ return;
110
+ }
111
+ searchValueUsedRef.current = searchValueUsedInFilterValue;
112
+ const nextValues = [...accumulatedValuesRef.current, ...result.values];
113
+ accumulatedValuesRef.current = nextValues;
114
+ hasMoreRef.current = !!result.hasMore;
115
+ setQuickFilterValues((prev) => ({
116
+ ...prev,
117
+ values: nextValues,
118
+ skipDefaultSearch: !!result.skipDefaultSearch,
119
+ searchValueUsedInFilterValue: prev.searchValueUsedInFilterValue || searchValueUsedInFilterValue,
120
+ hasMore: !!result.hasMore,
121
+ dataLoadIsComplete: true,
122
+ }));
123
+ })
124
+ .finally(() => {
125
+ if (generation === loadMoreGenerationRef.current) {
126
+ setLoadingMore(false);
127
+ }
128
+ if (lastRequestedOffsetRef.current === offset &&
129
+ accumulatedValuesRef.current.length <= offset) {
130
+ lastRequestedOffsetRef.current = null;
131
+ }
132
+ });
133
+ }, [fetchFilterValues, setLoadingMore]);
32
134
  useEffect(() => {
33
135
  if (valuesLoadTrigger === 0) {
34
136
  return;
35
137
  }
36
138
  let ignore = false;
37
139
  setIsDistinctColumnValuesLoading(true);
140
+ loadMoreGenerationRef.current += 1;
141
+ setLoadingMore(false);
142
+ lastRequestedOffsetRef.current = null;
38
143
  let searchValueUsedInFilterValue = false;
39
144
  const columnFilterInternalApi = api.filterApi.columnFilterApi.internalApi;
40
145
  let promise = undefined;
@@ -42,18 +147,15 @@ export function useDistinctFilterColumnValues(options) {
42
147
  const previousResult = columnFilterInternalApi
43
148
  .getAdaptableFilterHandler(columnId)
44
149
  ?.getLastCachedFilterDisplayValues();
45
- if (previousResult) {
150
+ if (previousResult && !previousResult.hasMore) {
46
151
  promise = Promise.resolve(previousResult);
47
152
  }
48
153
  }
49
154
  usePreviousValuesProbablyBecauseOfAGGridUnnecessaryFilterRemountRef.current = false;
50
155
  if (!promise) {
51
- promise = columnFilterInternalApi.getColumnFilterValues({
52
- columnId,
53
- get currentSearchValue() {
54
- searchValueUsedInFilterValue = true;
55
- return searchValueRef ? searchValueRef.current : '';
56
- },
156
+ promise = fetchFilterValues(0).then(({ result, searchValueUsedInFilterValue: used, }) => {
157
+ searchValueUsedInFilterValue = used;
158
+ return result;
57
159
  });
58
160
  }
59
161
  promise.then((distinctFilterDisplayValues) => {
@@ -61,20 +163,155 @@ export function useDistinctFilterColumnValues(options) {
61
163
  return;
62
164
  }
63
165
  const values = distinctFilterDisplayValues.values;
166
+ hasMoreRef.current = !!distinctFilterDisplayValues.hasMore;
167
+ accumulatedValuesRef.current = values;
64
168
  setQuickFilterValues({
65
169
  values,
66
170
  dataLoadIsComplete: true,
67
171
  skipDefaultSearch: !!distinctFilterDisplayValues.skipDefaultSearch,
68
172
  searchValueUsedInFilterValue,
173
+ hasMore: !!distinctFilterDisplayValues.hasMore,
69
174
  });
175
+ lastRequestedOffsetRef.current = 0;
70
176
  setIsDistinctColumnValuesLoading(false);
71
177
  });
72
178
  return () => {
73
179
  ignore = true;
74
180
  };
75
- }, [valuesLoadTrigger, columnId]);
181
+ }, [valuesLoadTrigger, columnId, api, fetchFilterValues, setLoadingMore]);
182
+ useEffect(() => {
183
+ if (!quickFilterValues.hasMore || !quickFilterValues.dataLoadIsComplete) {
184
+ return;
185
+ }
186
+ let scrollEl = null;
187
+ let debounceTimer;
188
+ const onScroll = () => {
189
+ if (debounceTimer) {
190
+ clearTimeout(debounceTimer);
191
+ }
192
+ debounceTimer = setTimeout(() => {
193
+ if (!scrollEl) {
194
+ return;
195
+ }
196
+ const el = scrollEl;
197
+ const remaining = el.scrollHeight - el.scrollTop - el.clientHeight;
198
+ if (remaining > NEAR_END_PX) {
199
+ return;
200
+ }
201
+ loadMoreValues();
202
+ }, SCROLL_DEBOUNCE_MS);
203
+ };
204
+ const tryAttach = () => {
205
+ scrollEl = document.querySelector(FILTER_VALUES_SCROLL_SELECTOR);
206
+ if (scrollEl) {
207
+ scrollEl.addEventListener('scroll', onScroll, { passive: true });
208
+ return true;
209
+ }
210
+ return false;
211
+ };
212
+ let attached = tryAttach();
213
+ const raf = attached
214
+ ? undefined
215
+ : requestAnimationFrame(() => {
216
+ attached = tryAttach();
217
+ });
218
+ const timeout = attached
219
+ ? undefined
220
+ : setTimeout(() => {
221
+ tryAttach();
222
+ }, 50);
223
+ return () => {
224
+ if (raf != null) {
225
+ cancelAnimationFrame(raf);
226
+ }
227
+ if (timeout != null) {
228
+ clearTimeout(timeout);
229
+ }
230
+ if (debounceTimer) {
231
+ clearTimeout(debounceTimer);
232
+ }
233
+ if (scrollEl) {
234
+ scrollEl.removeEventListener('scroll', onScroll);
235
+ }
236
+ };
237
+ }, [
238
+ quickFilterValues.hasMore,
239
+ quickFilterValues.dataLoadIsComplete,
240
+ quickFilterValues.values.length,
241
+ loadMoreValues,
242
+ ]);
243
+ useEffect(() => {
244
+ if (!hasMoreRef.current ||
245
+ !quickFilterValues.dataLoadIsComplete ||
246
+ isLoadingMore ||
247
+ isDistinctColumnValuesLoading) {
248
+ return;
249
+ }
250
+ const scrollEl = document.querySelector(FILTER_VALUES_SCROLL_SELECTOR);
251
+ if (!scrollEl) {
252
+ return;
253
+ }
254
+ const remaining = scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight;
255
+ if (remaining > NEAR_END_PX) {
256
+ return;
257
+ }
258
+ loadMoreValues();
259
+ }, [
260
+ quickFilterValues.hasMore,
261
+ quickFilterValues.dataLoadIsComplete,
262
+ quickFilterValues.values.length,
263
+ isLoadingMore,
264
+ isDistinctColumnValuesLoading,
265
+ loadMoreValues,
266
+ ]);
267
+ useEffect(() => {
268
+ if (!isLoadingMore) {
269
+ removeLoadingMoreIndicators();
270
+ return;
271
+ }
272
+ let indicator = null;
273
+ let cancelled = false;
274
+ const tryAttach = () => {
275
+ if (cancelled) {
276
+ return false;
277
+ }
278
+ const popup = document.querySelector(FILTER_VALUES_POPUP_SELECTOR);
279
+ if (!popup) {
280
+ return false;
281
+ }
282
+ indicator = popup.querySelector(`[${LOADING_MORE_INDICATOR_ATTR}]`);
283
+ if (!indicator) {
284
+ indicator = createLoadingMoreIndicator();
285
+ popup.appendChild(indicator);
286
+ }
287
+ return true;
288
+ };
289
+ let attached = tryAttach();
290
+ const raf = attached
291
+ ? undefined
292
+ : requestAnimationFrame(() => {
293
+ attached = tryAttach();
294
+ });
295
+ const timeout = attached
296
+ ? undefined
297
+ : setTimeout(() => {
298
+ tryAttach();
299
+ }, 50);
300
+ return () => {
301
+ cancelled = true;
302
+ if (raf != null) {
303
+ cancelAnimationFrame(raf);
304
+ }
305
+ if (timeout != null) {
306
+ clearTimeout(timeout);
307
+ }
308
+ indicator?.remove();
309
+ removeLoadingMoreIndicators();
310
+ };
311
+ }, [isLoadingMore]);
76
312
  return {
77
313
  isDistinctColumnValuesLoading,
314
+ isLoadingMore,
78
315
  quickFilterValues,
79
316
  setQuickFilterValues,
80
317
  triggerValuesLoad,
@@ -144,6 +381,9 @@ export const FloatingFilterValues = (props) => {
144
381
  `, (msg) => console.warn(msg));
145
382
  }
146
383
  }
384
+ if (!currentColumn) {
385
+ return null;
386
+ }
147
387
  return (_jsx(ColumnValuesSelect, { selectProps: props.inline
148
388
  ? {
149
389
  ...props.selectProps,
@@ -283,6 +283,8 @@ export declare class AdaptableAgGrid implements IAdaptable {
283
283
  column: AdaptableColumn;
284
284
  currentSearchValue: string;
285
285
  previousResult: InFilterValueResult | undefined;
286
+ offset?: number;
287
+ limit?: number;
286
288
  }): Promise<InFilterValueResult>;
287
289
  getDistinctEditValuesForColumn(options: {
288
290
  column: AdaptableColumn;
@@ -2341,6 +2341,8 @@ export class AdaptableAgGrid {
2341
2341
  }
2342
2342
  async getDistinctFilterValuesForColumn(options) {
2343
2343
  const { column, previousResult } = options;
2344
+ const offset = options.offset ?? 0;
2345
+ const limit = options.limit ?? 1000;
2344
2346
  const self = this;
2345
2347
  let currentGridCells;
2346
2348
  const getCurrentGridCells = () => {
@@ -2389,6 +2391,8 @@ export class AdaptableAgGrid {
2389
2391
  ...this.api.internalApi.buildBaseContext(),
2390
2392
  column,
2391
2393
  previousFilterResult: previousResult,
2394
+ offset,
2395
+ limit,
2392
2396
  get currentSearchValue() {
2393
2397
  return options.currentSearchValue;
2394
2398
  },
@@ -14,9 +14,13 @@ export declare class AdaptableFilterHandler implements FilterHandler<any, any, C
14
14
  getLastCachedFilterDisplayValues(): InFilterValueResult | undefined;
15
15
  getFromCacheOrFetchFilterDisplayValues(options: {
16
16
  currentSearchValue: string;
17
+ offset?: number;
18
+ limit?: number;
17
19
  }): Promise<InFilterValueResult>;
18
20
  fetchFilterDisplayValues(options: {
19
21
  currentSearchValue: string;
22
+ offset?: number;
23
+ limit?: number;
20
24
  }): Promise<InFilterValueResult>;
21
25
  onNewRowsLoaded(): void;
22
26
  onAnyFilterChanged(): void;
@@ -51,6 +51,8 @@ export class AdaptableFilterHandler {
51
51
  return options.currentSearchValue;
52
52
  },
53
53
  previousResult: this.filterDisplayValuesResult,
54
+ offset: options.offset,
55
+ limit: options.limit,
54
56
  })
55
57
  .then((result) => {
56
58
  this.filterDisplayValuesResult = result;
@@ -25,7 +25,7 @@ export type MultiComboboxProps<T extends ComboboxItemType> = Omit<AdaptableCombo
25
25
  /**
26
26
  * Optional override for the Select All checkbox change handler.
27
27
  * When provided, replaces the default behaviour of selecting / deselecting
28
- * every item.
28
+ * the currently filtered items via `onValueChange`.
29
29
  */
30
30
  onSelectAllCheckboxChange?: (checked: boolean) => void;
31
31
  renderInputValues?: (items: T[]) => React.ReactNode;