basesite-shared-grid-lib 21.0.0-beta.4 → 21.0.0-beta.5

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.
@@ -549,11 +549,49 @@ class OdataProvider {
549
549
  if (beforeRequest) {
550
550
  beforeRequest(options);
551
551
  }
552
- me.callApi(me.toQuery(options)).then((x) => {
553
- if (x) {
554
- let values = me.getOdataResult(x);
555
- callback(values.map((y) => y[field]));
552
+ // OData wraps nested fields with '/' (e.g. parent/child); the response
553
+ // mirrors that shape, so we walk the path instead of using the raw key.
554
+ // We also fall back to a case-insensitive lookup because some OData
555
+ // services return PascalCase property names regardless of the request
556
+ // casing (e.g. asked for `dacStatus`, response is `DacStatus`).
557
+ const readField = (row) => {
558
+ if (row == null)
559
+ return undefined;
560
+ if (Object.prototype.hasOwnProperty.call(row, field))
561
+ return row[field];
562
+ const segments = String(field).split('.');
563
+ let current = row;
564
+ for (const seg of segments) {
565
+ if (current == null)
566
+ return undefined;
567
+ if (Object.prototype.hasOwnProperty.call(current, seg)) {
568
+ current = current[seg];
569
+ continue;
570
+ }
571
+ // Case-insensitive key match as a last resort.
572
+ const lower = seg.toLowerCase();
573
+ const match = Object.keys(current).find(k => k.toLowerCase() === lower);
574
+ if (!match)
575
+ return undefined;
576
+ current = current[match];
577
+ }
578
+ return current;
579
+ };
580
+ me.callApi(me.toQuery(options))
581
+ .then((x) => {
582
+ const values = x ? me.getOdataResult(x) || [] : [];
583
+ const mapped = values.map((y) => readField(y));
584
+ console.log('[OdataProvider] getFilterValuesParams response for', field, '— rows:', values.length, '— sample keys:', values[0] ? Object.keys(values[0]) : '(none)', '— mapped sample:', mapped.slice(0, 5));
585
+ // Helpful diagnostic when the response shape doesn't match the field.
586
+ if (values.length > 0 && mapped.every((v) => v === undefined)) {
587
+ console.warn('[OdataProvider] getFilterValuesParams: response had', values.length, 'rows but none contained field', field, '— sample row keys:', Object.keys(values[0] || {}));
556
588
  }
589
+ callback(mapped);
590
+ })
591
+ .catch((err) => {
592
+ console.error('[OdataProvider] getFilterValuesParams failed for field', field, err);
593
+ // Always invoke the callback so AG Grid's set filter doesn't hang.
594
+ callback([]);
557
595
  });
558
596
  };
559
597
  /**
@@ -2106,6 +2144,10 @@ class GridLibraryComponent {
2106
2144
  this.tooltipShowDelay = 500;
2107
2145
  this.tooltipHideDelay = 4000;
2108
2146
  this.rowModelType = 'clientSide';
2147
+ // Per-field cache of distinct values for fill-cell set filters. Populated
2148
+ // by the OData groupby fallback in _getFillFilterValues and cleared when
2149
+ // the underlying grid data is refreshed.
2150
+ this._fillFilterValuesCache = new Map();
2109
2151
  this._Design_Manager = 'Design Manager';
2110
2152
  this._Integration_Lead = 'Integration Lead';
2111
2153
  this._Tool_Owner = 'Tool Owner';
@@ -2405,6 +2447,9 @@ class GridLibraryComponent {
2405
2447
  // handles case-insensitive matching server-side via tolower().
2406
2448
  values: (params) => this._getFillFilterValues(res, params),
2407
2449
  refreshValuesOnOpen: true,
2450
+ // Open with nothing checked so a user picking one option triggers
2451
+ // a single backend request instead of "uncheck all + pick one".
2452
+ defaultToNothingSelected: true,
2408
2453
  buttons: ['reset'],
2409
2454
  // Pretty label for each checkbox; falls back to the value itself
2410
2455
  valueFormatter: (p) => {
@@ -2547,7 +2592,31 @@ class GridLibraryComponent {
2547
2592
  }
2548
2593
  const field = res?.field;
2549
2594
  if (this.enableServerSidePaging && this.odataProvider) {
2550
- this.odataProvider.getFilterValuesParams(field, (data) => params.success((data || []).filter(v => v !== null && v !== undefined && v !== '')), undefined);
2595
+ // Cache OData distinct-values per field so repeated filter-opens for
2596
+ // fill-cell columns don't re-hit the backend. Cache is invalidated on
2597
+ // grid data refresh (see _invalidateFillFilterCache).
2598
+ const cached = this._fillFilterValuesCache.get(field);
2599
+ if (cached) {
2600
+ params.success(cached);
2601
+ return;
2602
+ }
2603
+ try {
2604
+ this.odataProvider.getFilterValuesParams(field, (data) => {
2605
+ const cleaned = (data || []).filter(v => v !== null && v !== undefined && v !== '');
2606
+ // Only cache non-empty successful responses so a transient
2607
+ // failure doesn't permanently stick an empty list.
2608
+ if (cleaned.length > 0) {
2609
+ this._fillFilterValuesCache.set(field, cleaned);
2610
+ }
2611
+ // Never leave the set-filter spinning: always invoke success,
2612
+ // even with an empty list.
2613
+ params.success(cleaned);
2614
+ }, undefined);
2615
+ }
2616
+ catch (err) {
2617
+ console.error('[grid-library] getFilterValuesParams threw for field', field, err);
2618
+ params.success([]);
2619
+ }
2551
2620
  return;
2552
2621
  }
2553
2622
  const source = Array.isArray(this.rowData) ? this.rowData : [];
@@ -2602,20 +2671,86 @@ class GridLibraryComponent {
2602
2671
  return false;
2603
2672
  }
2604
2673
  setExternalFilters(options) {
2605
- var serverUrl = '';
2606
- if (this.serverDataUrl.indexOf('filter') >= 0 &&
2607
- options.indexOf('filter') >= 0) {
2608
- let slug = this.serverDataUrl.split('?$filter=');
2609
- if (slug && slug.length > 0) {
2610
- options = options + ' and ' + slug[1];
2611
- serverUrl = slug[0];
2612
- }
2613
- }
2614
- else if (this.serverDataUrl.indexOf('?') >= 0) {
2615
- serverUrl = this.serverDataUrl;
2616
- options = options.replace('?', '&');
2674
+ // Extract any caller-supplied $filter predicate from the configured
2675
+ // serverDataUrl so we can merge it correctly with whatever the provider
2676
+ // built. Without this, requests like `$apply=groupby((dacStatus))` get a
2677
+ // sibling `$filter=siteId eq 5` appended, which OData rejects because
2678
+ // after `groupby` the only remaining property is the grouped one.
2679
+ let serverUrl = this.serverDataUrl;
2680
+ let externalFilter = '';
2681
+ const filterIdx = serverUrl.indexOf('$filter=');
2682
+ if (filterIdx >= 0) {
2683
+ // Find the end of the $filter value (next & or end of string).
2684
+ const after = serverUrl.indexOf('&', filterIdx);
2685
+ externalFilter = serverUrl.substring(filterIdx + '$filter='.length, after === -1 ? serverUrl.length : after);
2686
+ // Strip the $filter clause (and its leading ? or &) from the base URL.
2687
+ const before = serverUrl.charAt(filterIdx - 1); // '?' or '&'
2688
+ const removeStart = filterIdx - 1;
2689
+ const removeEnd = after === -1 ? serverUrl.length : after;
2690
+ serverUrl =
2691
+ serverUrl.substring(0, removeStart) +
2692
+ (before === '?' && after !== -1 ? '?' : '') +
2693
+ serverUrl.substring(removeEnd + (before === '?' && after !== -1 ? 1 : 0));
2694
+ }
2695
+ // `options` always starts with '?' from the provider.
2696
+ // If we still need to attach to a URL that already has '?', flip it to '&'.
2697
+ if (serverUrl.indexOf('?') >= 0 && options.startsWith('?')) {
2698
+ options = '&' + options.substring(1);
2699
+ }
2700
+ if (!externalFilter) {
2701
+ return `${serverUrl}${options}`;
2702
+ }
2703
+ // Decode for matching; we'll re-emit the predicate as-is since OData
2704
+ // accepts both encoded and decoded forms within a query string.
2705
+ const decodedFilter = (() => {
2706
+ try {
2707
+ return decodeURIComponent(externalFilter);
2708
+ }
2709
+ catch {
2710
+ return externalFilter;
2711
+ }
2712
+ })();
2713
+ // Case 1: provider used $apply (e.g. groupby for set-filter values, or
2714
+ // any aggregation). The external predicate must run BEFORE the aggregation
2715
+ // as a filter() transformation, otherwise its columns no longer exist.
2716
+ const applyMatch = options.match(/([?&])\$apply=([^&]+)/);
2717
+ if (applyMatch) {
2718
+ const sep = applyMatch[1];
2719
+ const applyValue = applyMatch[2];
2720
+ const decodedApply = (() => {
2721
+ try {
2722
+ return decodeURIComponent(applyValue);
2723
+ }
2724
+ catch {
2725
+ return applyValue;
2726
+ }
2727
+ })();
2728
+ const merged = `filter(${decodedFilter})/${decodedApply}`;
2729
+ options = options.replace(`${sep}$apply=${applyValue}`, `${sep}$apply=${encodeURIComponent(merged)}`);
2730
+ return `${serverUrl}${options}`;
2731
+ }
2732
+ // Case 2: provider already has its own $filter. AND them together.
2733
+ const filterMatch = options.match(/([?&])\$filter=([^&]+)/);
2734
+ if (filterMatch) {
2735
+ const sep = filterMatch[1];
2736
+ const existing = filterMatch[2];
2737
+ const decodedExisting = (() => {
2738
+ try {
2739
+ return decodeURIComponent(existing);
2740
+ }
2741
+ catch {
2742
+ return existing;
2743
+ }
2744
+ })();
2745
+ const merged = `(${decodedExisting}) and (${decodedFilter})`;
2746
+ options = options.replace(`${sep}$filter=${existing}`, `${sep}$filter=${encodeURIComponent(merged)}`);
2747
+ return `${serverUrl}${options}`;
2617
2748
  }
2618
- return `${serverUrl}${options}`;
2749
+ // Case 3: no provider filter and no $apply — just add the external $filter.
2750
+ const joiner = options.length === 0
2751
+ ? (serverUrl.indexOf('?') >= 0 ? '&' : '?')
2752
+ : '&';
2753
+ return `${serverUrl}${options}${joiner}$filter=${encodeURIComponent(decodedFilter)}`;
2619
2754
  }
2620
2755
  onGridReady(params) {
2621
2756
  this.gridAPI = params.api;
@@ -2659,6 +2794,7 @@ class GridLibraryComponent {
2659
2794
  if (!hasRetried) {
2660
2795
  hasRetried = true;
2661
2796
  setTimeout(() => {
2797
+ this._fillFilterValuesCache.clear();
2662
2798
  params.api.refreshServerSide({ purge: true });
2663
2799
  }, 2000);
2664
2800
  }
@@ -2675,6 +2811,7 @@ class GridLibraryComponent {
2675
2811
  if (!hasRetried) {
2676
2812
  hasRetried = true;
2677
2813
  setTimeout(() => {
2814
+ this._fillFilterValuesCache.clear();
2678
2815
  this.gridAPI.refreshServerSide({ purge: true });
2679
2816
  }, 2000);
2680
2817
  }
@@ -2770,6 +2907,7 @@ class GridLibraryComponent {
2770
2907
  this.gridAPI.showLoadingOverlay();
2771
2908
  this.gridAPI.resetColumnState();
2772
2909
  this.gridAPI.setFilterModel(null);
2910
+ this._fillFilterValuesCache.clear();
2773
2911
  this.gridColumnState = JSON.parse(overrideStateData);
2774
2912
  this.setGridState();
2775
2913
  setTimeout(() => { this.gridAPI.hideOverlay(); }, 500);