ag-grid-url-sync 0.3.0 → 0.5.0

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.
Files changed (42) hide show
  1. package/README.md +144 -14
  2. package/dist/core/ag-grid-url-sync.d.ts +13 -0
  3. package/dist/core/ag-grid-url-sync.js +10 -55
  4. package/dist/core/ag-grid-url-sync.js.map +1 -1
  5. package/dist/core/grid-integration.js +5 -67
  6. package/dist/core/grid-integration.js.map +1 -1
  7. package/dist/core/index.js +0 -5
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/individual-params.d.ts +19 -0
  10. package/dist/core/individual-params.js +260 -0
  11. package/dist/core/individual-params.js.map +1 -0
  12. package/dist/core/serialization/formats.d.ts +8 -9
  13. package/dist/core/serialization/formats.js +63 -65
  14. package/dist/core/serialization/formats.js.map +1 -1
  15. package/dist/core/serialization/grouped.d.ts +10 -5
  16. package/dist/core/serialization/grouped.js +4 -51
  17. package/dist/core/serialization/grouped.js.map +1 -1
  18. package/dist/core/serialization/index.js +0 -4
  19. package/dist/core/serialization/index.js.map +1 -1
  20. package/dist/core/types.d.ts +42 -9
  21. package/dist/core/types.js +0 -118
  22. package/dist/core/types.js.map +1 -1
  23. package/dist/core/url-generator.d.ts +10 -7
  24. package/dist/core/url-generator.js +25 -165
  25. package/dist/core/url-generator.js.map +1 -1
  26. package/dist/core/url-parser.d.ts +19 -7
  27. package/dist/core/url-parser.js +28 -294
  28. package/dist/core/url-parser.js.map +1 -1
  29. package/dist/core/validation.d.ts +6 -6
  30. package/dist/core/validation.js +1 -78
  31. package/dist/core/validation.js.map +1 -1
  32. package/dist/core/view-storage.d.ts +63 -0
  33. package/dist/core/view-storage.js +112 -0
  34. package/dist/core/view-storage.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +0 -6
  37. package/dist/index.js.map +1 -1
  38. package/dist/react/index.d.ts +1 -0
  39. package/dist/react/types.d.ts +103 -1
  40. package/dist/react/use-ag-grid-url-sync.js +406 -46
  41. package/dist/react/use-ag-grid-url-sync.js.map +1 -1
  42. package/package.json +98 -31
package/README.md CHANGED
@@ -196,12 +196,11 @@ import type {
196
196
  ```typescript
197
197
  import { parseUrlFilters } from 'ag-grid-url-sync/url-parser'
198
198
  import { DEFAULT_CONFIG } from 'ag-grid-url-sync/validation'
199
- import type { FilterState } from 'ag-grid-url-sync/types'
199
+ import type { FilterState, ParseConfig } from 'ag-grid-url-sync/types'
200
200
 
201
201
  function getFiltersFromUrl(url: string): FilterState {
202
- const config = {
203
- gridApi: null, // Not needed for parsing
204
- ...DEFAULT_CONFIG,
202
+ const config: ParseConfig = {
203
+ ...DEFAULT_CONFIG, // gridApi is not needed for parsing
205
204
  onParseError: error => console.error('Parse error:', error)
206
205
  }
207
206
 
@@ -209,18 +208,32 @@ function getFiltersFromUrl(url: string): FilterState {
209
208
  }
210
209
  ```
211
210
 
211
+ `parseUrlFilters` reads the input with these rules, in this order:
212
+
213
+ | # | Input | Result | Example |
214
+ | --- | --------------------------- | ------------------------ | ----------------------------------------- |
215
+ | 1 | A host with no scheme | Throws `InvalidURLError` | `example.com?…`, `localhost:3000?…` |
216
+ | 2 | Starts with a scheme | Absolute URL | `https://example.com/users?f_name_eq=bob` |
217
+ | 3 | Starts with `/`, `?` or `#` | Relative URL | `/users?f_name_eq=bob`, `?f_name_eq=bob` |
218
+ | 4 | Anything else | Raw query string | `f_name_eq=bob` |
219
+
220
+ - A relative path must start with `/`. `users?f_name_eq=bob` matches rule 4, so it gives no filters.
221
+ - A bare `localhost?…` with no port cannot be told apart from a raw query, so it matches rule 4.
222
+ - In a raw query string, a `?` or `#` is part of a param name or value.
223
+
224
+ `InvalidURLError` is thrown for rule 1, and for any input that the `URL` constructor rejects under rules 2 and 3 (for example `http://` or `//[`). A param that is not valid is skipped and sent to `onParseError`.
225
+
212
226
  #### Example: Custom URL Generator
213
227
 
214
228
  ```typescript
215
- import { generateUrl, serializeFilters } from 'ag-grid-url-sync/url-generator'
216
- import type { FilterState } from 'ag-grid-url-sync/types'
229
+ import { generateUrl } from 'ag-grid-url-sync/url-generator'
230
+ import { DEFAULT_CONFIG } from 'ag-grid-url-sync/validation'
231
+ import type { FilterState, ParseConfig } from 'ag-grid-url-sync/types'
217
232
 
218
233
  function createShareableUrl(filters: FilterState, baseUrl: string): string {
219
- const config = {
220
- gridApi: null, // Not needed for generation
221
- paramPrefix: 'f_',
222
- maxValueLength: 200,
223
- onParseError: () => {}
234
+ const config: ParseConfig = {
235
+ ...DEFAULT_CONFIG, // gridApi is not needed for generation
236
+ maxValueLength: 500
224
237
  }
225
238
 
226
239
  return generateUrl(baseUrl, filters, config)
@@ -300,6 +313,10 @@ interface AGGridUrlSyncConfig {
300
313
  }
301
314
  ```
302
315
 
316
+ When the library reads a URL, it reads only the `groupedParam` name and decodes it with the configured `format`. It does not guess the format. This works in both modes, so a grouped link opens in an app in individual mode when the name and format match. If the param is present and not empty, the prefixed params are not read.
317
+
318
+ > **Note:** A URL written with a different `groupedParam` or `format` does not decode. If you change either option, links that were shared before the change stop working.
319
+
303
320
  ### Format Options
304
321
 
305
322
  #### QueryString Format (Default)
@@ -420,7 +437,7 @@ import {
420
437
 
421
438
  ### Error Handling for Grouped Serialization
422
439
 
423
- You can provide a custom error handler to gracefully handle malformed grouped parameters or serialization errors. The library will throw an `InvalidSerializationError` for invalid formats:
440
+ You can provide a custom error handler to gracefully handle malformed grouped parameters or serialization errors. In grouped mode, `onParseError` receives an `InvalidSerializationError` for invalid formats:
424
441
 
425
442
  ```typescript
426
443
  import { InvalidSerializationError } from 'ag-grid-url-sync/types'
@@ -440,6 +457,18 @@ const urlSync = createUrlSync(gridApi, {
440
457
  })
441
458
  ```
442
459
 
460
+ In every grouped format (`querystring`, `json`, `base64`), a text value over `maxValueLength` (default 200) is dropped and reported through `onParseError`, both when the URL is generated and when it is parsed.
461
+
462
+ In grouped mode, an error that comes from the grouped URL value is an `InvalidSerializationError`. This includes an error for one bad parameter inside a grouped querystring value, such as a value over `maxValueLength`. The original error is in `error.cause`. In individual mode, the same failure arrives as the original error, with no wrapper. To handle a specific error in both modes, check the error and its cause:
463
+
464
+ ```typescript
465
+ import { InvalidFilterError } from 'ag-grid-url-sync/types'
466
+
467
+ const isFilterError = (error: Error) =>
468
+ error instanceof InvalidFilterError ||
469
+ error.cause instanceof InvalidFilterError
470
+ ```
471
+
443
472
  ### Exports
444
473
 
445
474
  The package supports granular subpath exports for advanced usage and optimal bundle size:
@@ -450,6 +479,7 @@ The package supports granular subpath exports for advanced usage and optimal bun
450
479
  - `ag-grid-url-sync/url-parser` – URL parsing utilities
451
480
  - `ag-grid-url-sync/url-generator` – URL generation utilities
452
481
  - `ag-grid-url-sync/grid-integration` – AG Grid integration helpers
482
+ - `ag-grid-url-sync/view-storage` – localStorage-backed saved views
453
483
  - `ag-grid-url-sync/types` – TypeScript types
454
484
 
455
485
  Refer to the [package.json](./package.json) for the full export map.
@@ -577,6 +607,9 @@ interface UseAGGridUrlSyncOptions {
577
607
  autoApplyOnMount?: boolean // Default: false
578
608
  enabledWhenReady?: boolean // Default: true
579
609
  onError?: (error: Error, context: string) => void // Advanced error handling
610
+
611
+ // Saved views: setting a key enables the feature and namespaces its storage
612
+ storageKey?: string // Default: undefined (saved views disabled)
580
613
  }
581
614
  ```
582
615
 
@@ -590,22 +623,110 @@ interface UseAGGridUrlSyncOptions {
590
623
  | `clearFilters` | `() => void` | Clear all text filters |
591
624
  | `isReady` | `boolean` | Whether grid API is available and hook is ready |
592
625
  | `currentUrl` | `string` | Current URL with filters applied |
593
- | `hasFilters` | `boolean` | Whether grid has any active text filters |
626
+ | `hasFilters` | `boolean` | Whether the grid has supported filters |
594
627
  | `parseUrlFilters` | `(url: string) => FilterState` | Parse filters from URL without applying |
595
628
  | `applyFilters` | `(filters: FilterState) => void` | Apply filter state object to grid |
596
629
  | `getFiltersAsFormat` | `(format: 'querystring' | 'json' | 'base64') => string` | Serialize filters to any supported format (for sharing/export) |
597
630
  | `getCurrentFormat` | `() => 'individual' | 'grouped'` | Get the current serialization mode (individual or grouped) |
598
631
 
632
+ Saved views (inert unless `storageKey` is set):
633
+
634
+ | Property | Type | Description |
635
+ | -------------- | ------------------------------------ | ----------------------------------------------------------------------- |
636
+ | `views` | `GridView[]` | Saved views for this grid, in save order. Empty when views are disabled |
637
+ | `activeViewId` | `string \| null` | Id of the view currently applied to the grid, or `null` when none is |
638
+ | `saveView` | `(name: string) => GridView \| null` | Save current filters under a name; overwrites an existing name in place |
639
+ | `loadView` | `(id: string \| null) => void` | Apply a saved view, or pass `null` to reset to unfiltered |
640
+ | `deleteView` | `(id: string) => void` | Delete a saved view |
641
+
599
642
  > **Note:** `getFiltersAsFormat` and `getCurrentFormat` are especially useful for grouped serialization, format conversion, and advanced sharing scenarios.
600
643
 
644
+ ### Saved Views
645
+
646
+ Pass a `storageKey` to let users name and revisit filter sets. The key both enables
647
+ the feature and namespaces its storage, so two grids on the same origin must use
648
+ different keys:
649
+
650
+ ```tsx
651
+ const { views, activeViewId, saveView, loadView, deleteView } =
652
+ useAGGridUrlSync(gridApi, { storageKey: 'employee-grid' })
653
+
654
+ // One handler each, as a user drives them: save whatever is currently filtered,
655
+ // then apply or remove a view by id in a later interaction.
656
+ const handleSave = () => saveView('Engineering, high salary')
657
+ const handleLoad = (id: string) => loadView(id)
658
+ const handleDelete = (id: string) => deleteView(id)
659
+ const handleReset = () => loadView(null) // back to an unfiltered grid
660
+ ```
661
+
662
+ Render the list from `views` and drive the buttons from it. `views[i].id` is only
663
+ stale immediately after a mutation, not during render. If you do need the new
664
+ view's id in the same handler that saved it, use `saveView`'s return value rather
665
+ than reading it back out of `views`: that array is React state, so it does not
666
+ update until the next render.
667
+
668
+ Only the filter model is stored, using AG Grid's native format so set filters and
669
+ combined conditions survive a round trip. Column order, row selection and expanded
670
+ groups are not stored.
671
+
672
+ Behaviour worth knowing:
673
+
674
+ - **Names are unique.** Saving over an existing name updates that view in place,
675
+ keeping its id and list position, so "load, adjust, save under the same name" is
676
+ how you update one. Names are trimmed and must not be empty.
677
+ - **`activeViewId` describes the live grid**, not what is persisted. A stored view
678
+ is only applied on mount when `autoApplyOnMount` is set, so with it off this
679
+ stays `null` until you call `loadView` or `saveView`.
680
+ - **Filtering away from a view unloads it.** `activeViewId` returns to `null` as
681
+ soon as the grid stops showing that view, including through routes that never
682
+ mention views: `clearFilters`, `applyFilters`, `applyUrlFilters`, or the user
683
+ editing a filter in the grid's own UI. The stored pointer is cleared with it,
684
+ so `autoApplyOnMount` will not bring the view back on the next load. The grid
685
+ is compared against what it reported once the view was applied, so a view AG
686
+ Grid normalises (one naming a column the grid no longer has, say) stays loaded
687
+ and keeps its stored pointer. A write AG Grid defers, such as a restore made
688
+ before column data types are inferred, is recognised when it lands by the
689
+ event's `source: 'api'`; on AG Grid versions that do not report a source, a
690
+ deferred view the grid also normalises unloads when it lands. An API change
691
+ that filters a column the view does not, such as your own `setFilterModel`
692
+ over a view the grid ignored, is not taken for the view and unloads it. One
693
+ that filters only the view's columns cannot be told from the view landing.
694
+ Render the
695
+ active state from `activeViewId` and it follows the grid on its own. Replacing
696
+ `gridApi` also clears `activeViewId`, but keeps the stored pointer, so with
697
+ `autoApplyOnMount` the view is restored against the new grid.
698
+ - **URL filters win on mount.** With `autoApplyOnMount` set, an incoming URL that
699
+ carries filters takes precedence over the stored view, so a shared link shows the
700
+ sender's filters rather than the recipient's saved default. It wins once per
701
+ grid: switching `storageKey` later restores the new namespace's stored view
702
+ instead of applying the link again.
703
+ - **Deleting is conservative.** `deleteView` clears the grid only if it still shows
704
+ exactly that view's filters, so hand-edited filters survive.
705
+ - **Failures are reported, not silent.** A save that cannot be persisted (quota
706
+ exhausted, storage blocked by policy) returns `null` and reports through
707
+ `onError` with context `'save-view'`. Reads degrade to an empty list instead of
708
+ throwing, so server rendering is safe.
709
+
710
+ For non-React consumers the same store is available directly:
711
+
712
+ ```ts
713
+ import { createViewStore } from 'ag-grid-url-sync/view-storage'
714
+
715
+ const store = createViewStore('employee-grid')
716
+ store.saveView('Engineering', gridApi.getFilterModel())
717
+ ```
718
+
601
719
  ### React Features
602
720
 
603
721
  - **🔄 Automatic State Management**: Tracks grid state and filter changes automatically
604
722
  - **⚡ Ready State**: `isReady` indicates when the grid API is available
605
- - **🎯 Auto-Apply**: `autoApplyOnMount` applies URL filters when component mounts
723
+ - **🎯 Auto-Apply**: `autoApplyOnMount` applies URL filters when component mounts.
724
+ It is attempted once per grid: a failure is reported through `onError` and
725
+ `onParseError` and is not retried on later renders
606
726
  - **🧹 Cleanup**: Automatically cleans up resources when component unmounts
607
727
  - **🛡️ Error Boundaries**: Graceful error handling with configurable callbacks
608
728
  - **📊 Filter Status**: `hasFilters` tracks whether any filters are active
729
+ - **💾 Saved Views**: `storageKey` enables naming and revisiting filter sets
609
730
 
610
731
  ### React with Grouped Serialization
611
732
 
@@ -797,6 +918,15 @@ const url = urlSync.generateUrl('https://app.com/page')
797
918
  // https://app.com/page?f_name_contains=john
798
919
  ```
799
920
 
921
+ ##### `getUrlState(baseUrl?: string): { url: string; hasFilters: boolean }`
922
+
923
+ Generates the same URL as `generateUrl`, and tells if the grid has filters that the library supports. It reads the grid one time for both values. `hasFilters` comes from the filter state, not from the URL, because the URL also keeps the page's own params.
924
+
925
+ ```typescript
926
+ const { url, hasFilters } = urlSync.getUrlState('https://app.com/page?tab=1')
927
+ // url: https://app.com/page?tab=1 (no filters), hasFilters: false
928
+ ```
929
+
800
930
  ##### `getQueryParams(): string`
801
931
 
802
932
  Gets the current filter state as URL query parameters.
@@ -17,6 +17,19 @@ export declare class AGGridUrlSync {
17
17
  * @returns URL string with filter parameters
18
18
  */
19
19
  generateUrl(baseUrl?: string): string;
20
+ /**
21
+ * Generates a URL with the current filter state, and tells if the grid has
22
+ * filters that the library supports.
23
+ *
24
+ * Reads the grid filter model one time, so an error in it is reported one
25
+ * time. `hasFilters` comes from the filter state, not from the URL: the URL
26
+ * also keeps the page's own params (#215).
27
+ * @param baseUrl - Optional base URL (defaults to current URL)
28
+ */
29
+ getUrlState(baseUrl?: string): {
30
+ url: string;
31
+ hasFilters: boolean;
32
+ };
20
33
  /**
21
34
  * Gets the current filter state as URL query parameters
22
35
  * @returns Query parameter string
@@ -3,107 +3,62 @@ import { parseUrlFilters } from './url-parser.js';
3
3
  import { generateUrl } from './url-generator.js';
4
4
  import { getFilterModel, applyFilterModel } from './grid-integration.js';
5
5
  import { serializeGrouped } from './serialization/grouped.js';
6
- /**
7
- * AGGridUrlSync class for synchronizing AG Grid text filters with URL parameters
8
- */
9
6
  export class AGGridUrlSync {
10
7
  config;
11
- /**
12
- * Creates a new instance of AGGridUrlSync
13
- * @param gridApi - AG Grid API instance
14
- * @param config - Optional configuration options
15
- */
16
8
  constructor(gridApi, config = {}) {
9
+ const definedConfig = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
17
10
  this.config = {
18
11
  gridApi,
19
12
  ...DEFAULT_CONFIG,
20
- ...config
13
+ ...definedConfig
21
14
  };
22
15
  }
23
- /**
24
- * Generates a URL with the current filter state
25
- * @param baseUrl - Optional base URL (defaults to current URL)
26
- * @returns URL string with filter parameters
27
- */
28
16
  generateUrl(baseUrl) {
17
+ return this.getUrlState(baseUrl).url;
18
+ }
19
+ getUrlState(baseUrl) {
29
20
  const filterState = getFilterModel(this.config);
30
21
  const url = baseUrl ?? window.location.href;
31
- return generateUrl(url, filterState, this.config);
22
+ return {
23
+ url: generateUrl(url, filterState, this.config),
24
+ hasFilters: Object.keys(filterState).length > 0
25
+ };
32
26
  }
33
- /**
34
- * Gets the current filter state as URL query parameters
35
- * @returns Query parameter string
36
- */
37
27
  getQueryParams() {
38
28
  const url = this.generateUrl();
39
29
  const urlObj = new URL(url);
40
30
  return urlObj.search;
41
31
  }
42
- /**
43
- * Applies filters from a URL to the grid
44
- * @param url - Optional URL to parse (defaults to current URL)
45
- */
46
32
  applyFromUrl(url) {
47
33
  const urlToParse = url ?? window.location.href;
48
34
  const filterState = parseUrlFilters(urlToParse, this.config);
49
35
  this.applyFilters(filterState);
50
36
  }
51
- /**
52
- * Applies a filter state object to the grid
53
- * @param filterState - Filter state to apply
54
- */
55
37
  applyFilters(filterState) {
56
38
  applyFilterModel(filterState, this.config);
57
39
  }
58
- /**
59
- * Clears all text filters from the grid
60
- */
61
40
  clearFilters() {
62
41
  this.applyFilters({});
63
42
  }
64
- /**
65
- * Cleans up any resources or event listeners
66
- */
67
43
  destroy() {
68
- // Currently no cleanup needed, but included for future use
69
44
  }
70
- /**
71
- * Gets the current serialization mode
72
- * @returns The current serialization mode
73
- */
74
45
  getSerializationMode() {
75
46
  return this.config.serialization;
76
47
  }
77
- /**
78
- * Gets the current format (for grouped mode)
79
- * @returns The current format
80
- */
81
48
  getCurrentFormat() {
82
49
  return this.config.format;
83
- } /**
84
- * Gets filters in a specific format (useful for sharing/export)
85
- * @param format - The format to serialize to
86
- * @returns Serialized filter string
87
- */
50
+ }
88
51
  getFiltersAsFormat(format) {
89
52
  const filterState = getFilterModel(this.config);
90
- // Create temporary config for the requested format
91
53
  const tempConfig = {
92
54
  ...this.config,
93
55
  format,
94
56
  serialization: 'grouped'
95
57
  };
96
- // Use the serialization module
97
58
  const result = serializeGrouped(filterState, tempConfig);
98
59
  return result.value;
99
60
  }
100
61
  }
101
- /**
102
- * Factory function to create a new AGGridUrlSync instance
103
- * @param gridApi - AG Grid API instance
104
- * @param config - Optional configuration options
105
- * @returns AGGridUrlSync instance
106
- */
107
62
  export function createUrlSync(gridApi, config) {
108
63
  return new AGGridUrlSync(gridApi, config);
109
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ag-grid-url-sync.js","sourceRoot":"","sources":["../../src/core/ag-grid-url-sync.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAE7D;;GAEG;AACH,MAAM,OAAO,aAAa;IAChB,MAAM,CAAgB;IAE9B;;;;OAIG;IACH,YAAY,OAAgB,EAAE,SAA8B,EAAE;QAC5D,IAAI,CAAC,MAAM,GAAG;YACZ,OAAO;YACP,GAAG,cAAc;YACjB,GAAG,MAAM;SACV,CAAA;IACH,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,OAAgB;QAC1B,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC/C,MAAM,GAAG,GAAG,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;QAC3C,OAAO,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IACnD,CAAC;IAED;;;OAGG;IACH,cAAc;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QAC9B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,OAAO,MAAM,CAAC,MAAM,CAAA;IACtB,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,GAAY;QACvB,MAAM,UAAU,GAAG,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;QAC9C,MAAM,WAAW,GAAG,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAC5D,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAA;IAChC,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,WAAwB;QACnC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC5C,CAAC;IAED;;OAEG;IACH,YAAY;QACV,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IACvB,CAAC;IAED;;OAEG;IACH,OAAO;QACL,2DAA2D;IAC7D,CAAC;IAED;;;OAGG;IACH,oBAAoB;QAClB,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAA;IAClC,CAAC;IAED;;;OAGG;IACH,gBAAgB;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;IAC3B,CAAC,CAAC;;;;OAIC;IACH,kBAAkB,CAAC,MAA2B;QAC5C,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAE/C,mDAAmD;QACnD,MAAM,UAAU,GAAG;YACjB,GAAG,IAAI,CAAC,MAAM;YACd,MAAM;YACN,aAAa,EAAE,SAAkB;SAClC,CAAA;QAED,+BAA+B;QAC/B,MAAM,MAAM,GAAG,gBAAgB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;QAExD,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,MAA4B;IAE5B,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;AAC3C,CAAC"}
1
+ {"version":3,"file":"ag-grid-url-sync.js","sourceRoot":"","sources":["../../src/core/ag-grid-url-sync.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,MAAM,OAAO,aAAa;IAChB,MAAM,CAAgB;IAO9B,YAAY,OAAgB,EAAE,MAAM,GAAwB,EAAE;QAI5D,MAAM,aAAa,GAAG,MAAM,CAAC,WAAW,CACtC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAC3C,CAAA;QAExB,IAAI,CAAC,MAAM,GAAG;YACZ,OAAO;YACP,GAAG,cAAc;YACjB,GAAG,aAAa;SACjB,CAAA;IACH,CAAC;IAOD,WAAW,CAAC,OAAgB;QAC1B,OAAO,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,GAAG,CAAA;IACtC,CAAC;IAWD,WAAW,CAAC,OAAgB;QAC1B,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC/C,MAAM,GAAG,GAAG,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;QAC3C,OAAO;YACL,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC;YAC/C,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC;SAChD,CAAA;IACH,CAAC;IAMD,cAAc;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAA;QAC9B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;QAC3B,OAAO,MAAM,CAAC,MAAM,CAAA;IACtB,CAAC;IAMD,YAAY,CAAC,GAAY;QACvB,MAAM,UAAU,GAAG,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;QAC9C,MAAM,WAAW,GAAG,eAAe,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAC5D,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAA;IAChC,CAAC;IAMD,YAAY,CAAC,WAAwB;QACnC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAC5C,CAAC;IAKD,YAAY;QACV,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAA;IACvB,CAAC;IAKD,OAAO;IAEP,CAAC;IAMD,oBAAoB;QAClB,OAAO,IAAI,CAAC,MAAM,CAAC,aAAa,CAAA;IAClC,CAAC;IAMD,gBAAgB;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAA;IAC3B,CAAC;IAKD,kBAAkB,CAAC,MAA2B;QAC5C,MAAM,WAAW,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAG/C,MAAM,UAAU,GAAG;YACjB,GAAG,IAAI,CAAC,MAAM;YACd,MAAM;YACN,aAAa,EAAE,SAAkB;SAClC,CAAA;QAGD,MAAM,MAAM,GAAG,gBAAgB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAA;QAExD,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC;CACF;AAQD,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,MAA4B;IAE5B,OAAO,IAAI,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;AAC3C,CAAC"}
@@ -1,6 +1,4 @@
1
1
  import { AG_GRID_OPERATION_NAMES, REVERSE_AG_GRID_OPERATION_NAMES, TEXT_FILTER_OPERATIONS, NUMBER_FILTER_OPERATIONS, DATE_FILTER_OPERATIONS } from './types.js';
2
- // Map AG Grid number operations to our internal number operations
3
- // Since AG Grid uses generic names, we need to be explicit about number operations
4
2
  const AG_GRID_TO_NUMBER_OPERATION_MAP = {
5
3
  lessThan: 'lessThan',
6
4
  lessThanOrEqual: 'lessThanOrEqual',
@@ -12,74 +10,48 @@ const AG_GRID_TO_NUMBER_OPERATION_MAP = {
12
10
  blank: 'blank',
13
11
  notBlank: 'notBlank'
14
12
  };
15
- /**
16
- * Type guard to check if a value is a valid number operation key
17
- */
18
13
  function isValidNumberOperation(operation) {
19
14
  return Boolean(operation && operation in AG_GRID_TO_NUMBER_OPERATION_MAP);
20
15
  }
21
- /**
22
- * Converts a date string to ISO format (YYYY-MM-DD)
23
- * Handles both date strings and undefined values
24
- * Returns an empty string if the input is undefined or invalid
25
- */
26
16
  function toIsoDateString(val) {
27
17
  if (!val)
28
18
  return '';
29
19
  const match = val.match(/^(\d{4}-\d{2}-\d{2})/);
30
20
  return match?.[1] ? match[1] : val || '';
31
21
  }
32
- /**
33
- * Type predicate to check if an operation is a valid text filter operation
34
- */
35
22
  function isTextFilterOperation(operation) {
36
23
  return TEXT_FILTER_OPERATIONS.includes(operation);
37
24
  }
38
- /**
39
- * Type predicate to check if an operation is a valid number filter operation
40
- */
41
25
  function isNumberFilterOperation(operation) {
42
26
  return NUMBER_FILTER_OPERATIONS.includes(operation);
43
27
  }
44
- /**
45
- * Type predicate to check if an operation is a valid date filter operation
46
- */
47
28
  function isDateFilterOperation(operation) {
48
29
  return DATE_FILTER_OPERATIONS.includes(operation);
49
30
  }
50
- /**
51
- * Detects the expected filter type for a column based on AG Grid configuration
52
- */
53
31
  export function detectColumnFilterType(gridApi, columnId) {
54
32
  try {
55
33
  const column = gridApi.getColumn(columnId);
56
34
  if (!column)
57
- return 'text'; // Default to text for unknown columns
35
+ return 'text';
58
36
  const colDef = column.getColDef();
59
- // 1. Explicit filter configuration takes priority
60
37
  if (colDef.filter === 'agDateColumnFilter')
61
38
  return 'date';
62
39
  if (colDef.filter === 'agNumberColumnFilter')
63
40
  return 'number';
64
41
  if (colDef.filter === 'agTextColumnFilter')
65
42
  return 'text';
66
- // 2. Cell data type configuration
67
43
  if (colDef.cellDataType === 'date')
68
44
  return 'date';
69
45
  if (colDef.cellDataType === 'number')
70
46
  return 'number';
71
47
  if (colDef.cellDataType === 'text')
72
48
  return 'text';
73
- // 3. Default to text for backward compatibility
74
49
  return 'text';
75
50
  }
76
51
  catch {
77
- return 'text'; // Safe fallback
52
+ return 'text';
78
53
  }
79
54
  }
80
- /**
81
- * Gets the current filter model from AG Grid
82
- */
83
55
  export function getFilterModel(config) {
84
56
  try {
85
57
  const model = config.gridApi.getFilterModel();
@@ -91,28 +63,20 @@ export function getFilterModel(config) {
91
63
  if (!filter || typeof filter !== 'object')
92
64
  continue;
93
65
  const { filterType, type, filter: value, filterTo, dateFrom, dateTo } = filter;
94
- // Handle text filters
95
66
  if (filterType === 'text') {
96
- // Map AG Grid operations back to our internal operations using centralized mapping
97
67
  const internalOperation = REVERSE_AG_GRID_OPERATION_NAMES[type];
98
- // Use type predicate to validate and narrow the type
99
68
  if (internalOperation && isTextFilterOperation(internalOperation)) {
100
69
  filterState[column] = {
101
70
  filterType: 'text',
102
- type: internalOperation, // Now properly typed as TextFilterOperation
71
+ type: internalOperation,
103
72
  filter: String(value || '')
104
73
  };
105
74
  }
106
75
  }
107
- // Handle number filters
108
76
  else if (filterType === 'number') {
109
- // For number filters, we need to handle the mapping carefully since
110
- // AG Grid uses the same operation names for numbers and dates
111
- // Use the explicit number mapping instead of the reverse AG Grid mapping
112
77
  const internalOperation = isValidNumberOperation(type)
113
78
  ? AG_GRID_TO_NUMBER_OPERATION_MAP[type]
114
79
  : null;
115
- // If the operation type is not recognized, report the error
116
80
  if (!internalOperation) {
117
81
  if (config.onParseError) {
118
82
  config.onParseError(new Error(`Unknown number filter operation '${type}' for column '${column}'. ` +
@@ -120,20 +84,16 @@ export function getFilterModel(config) {
120
84
  }
121
85
  continue;
122
86
  }
123
- // Use type predicate to validate and narrow the type
124
87
  if (internalOperation && isNumberFilterOperation(internalOperation)) {
125
- // Validate that the filter value is actually a number
126
88
  const numericValue = typeof value === 'number' ? value : Number(value);
127
- // Skip invalid number filters
128
89
  if (isNaN(numericValue)) {
129
90
  continue;
130
91
  }
131
92
  const numberFilter = {
132
93
  filterType: 'number',
133
- type: internalOperation, // Now properly typed as NumberFilterOperation
94
+ type: internalOperation,
134
95
  filter: numericValue
135
96
  };
136
- // Handle range operations
137
97
  if (internalOperation === 'inRange' && typeof filterTo === 'number') {
138
98
  ;
139
99
  numberFilter.filterTo = filterTo;
@@ -141,14 +101,9 @@ export function getFilterModel(config) {
141
101
  filterState[column] = numberFilter;
142
102
  }
143
103
  }
144
- // Handle date filters
145
104
  else if (filterType === 'date') {
146
- // Map AG Grid date operations back to our internal operations
147
105
  const internalOperation = REVERSE_AG_GRID_OPERATION_NAMES[type];
148
- // For date operations, we need to handle the special case where AG Grid uses
149
- // generic operation names but we have date-specific internal names
150
106
  let mappedOperation = internalOperation;
151
- // Map AG Grid generic date operations to our date-specific operations
152
107
  const AG_GRID_TO_DATE_OPERATION_MAP = {
153
108
  lessThan: 'dateBefore',
154
109
  lessThanOrEqual: 'dateBeforeOrEqual',
@@ -159,14 +114,12 @@ export function getFilterModel(config) {
159
114
  mappedOperation = type
160
115
  ? AG_GRID_TO_DATE_OPERATION_MAP[type] || internalOperation
161
116
  : internalOperation;
162
- // Use type predicate to validate and narrow the type
163
117
  if (mappedOperation && isDateFilterOperation(mappedOperation)) {
164
118
  const dateFilter = {
165
119
  filterType: 'date',
166
120
  type: mappedOperation,
167
121
  filter: toIsoDateString(dateFrom || String(value || ''))
168
122
  };
169
- // Handle date range operations (AG Grid uses dateFrom/dateTo)
170
123
  if (mappedOperation === 'dateRange' && dateTo) {
171
124
  ;
172
125
  dateFilter.filterTo = toIsoDateString(dateTo);
@@ -184,21 +137,16 @@ export function getFilterModel(config) {
184
137
  return {};
185
138
  }
186
139
  }
187
- /**
188
- * Converts internal filter to AG Grid filter format
189
- */
190
140
  function convertToAGGridFilter(filter) {
191
141
  if (filter.filterType === 'date') {
192
- // Handle date range operations
193
142
  if (filter.type === 'dateRange') {
194
143
  return {
195
144
  filterType: 'date',
196
- type: 'inRange', // AG Grid uses 'inRange' for date ranges
145
+ type: 'inRange',
197
146
  dateFrom: filter.filter,
198
147
  dateTo: filter.filterTo || null
199
148
  };
200
149
  }
201
- // Handle blank operations for dates
202
150
  if (filter.type === 'blank' || filter.type === 'notBlank') {
203
151
  return {
204
152
  filterType: 'date',
@@ -207,7 +155,6 @@ function convertToAGGridFilter(filter) {
207
155
  dateTo: null
208
156
  };
209
157
  }
210
- // Map date-specific operations to AG Grid generic operations
211
158
  const dateOperationMapping = {
212
159
  dateBefore: 'lessThan',
213
160
  dateBeforeOrEqual: 'lessThanOrEqual',
@@ -231,7 +178,6 @@ function convertToAGGridFilter(filter) {
231
178
  type: agGridOperation,
232
179
  filter: filter.filter
233
180
  };
234
- // Only include filterTo if it's defined
235
181
  return filter.filterTo !== undefined
236
182
  ? { ...baseFilter, filterTo: filter.filterTo }
237
183
  : baseFilter;
@@ -242,7 +188,6 @@ function convertToAGGridFilter(filter) {
242
188
  filter: filter.filter
243
189
  };
244
190
  }
245
- // Text filter handling
246
191
  const agGridOperation = AG_GRID_OPERATION_NAMES[filter.type];
247
192
  return {
248
193
  filterType: 'text',
@@ -250,18 +195,13 @@ function convertToAGGridFilter(filter) {
250
195
  filter: filter.filter
251
196
  };
252
197
  }
253
- /**
254
- * Applies a filter state to AG Grid
255
- */
256
198
  export function applyFilterModel(filterState, config) {
257
199
  try {
258
200
  const model = {};
259
201
  for (const [column, filter] of Object.entries(filterState)) {
260
202
  if (!filter || typeof filter !== 'object')
261
203
  continue;
262
- // Detect expected filter type for this column
263
204
  const expectedType = detectColumnFilterType(config.gridApi, column);
264
- // Validate filter type compatibility
265
205
  if (filter.filterType === 'date' && expectedType !== 'date') {
266
206
  console.warn(`Column '${column}' expects a ${expectedType} filter but received a date filter. This may indicate a configuration mismatch in the column definition or filter application.`);
267
207
  continue;
@@ -270,13 +210,11 @@ export function applyFilterModel(filterState, config) {
270
210
  console.warn(`Column '${column}' expects a ${expectedType} filter but received a number filter. This may indicate a configuration mismatch in the column definition or filter application.`);
271
211
  continue;
272
212
  }
273
- // Convert to AG Grid filter model
274
213
  const agGridFilter = convertToAGGridFilter(filter);
275
214
  if (agGridFilter) {
276
215
  model[column] = agGridFilter;
277
216
  }
278
217
  }
279
- // Apply the filter model to AG Grid
280
218
  config.gridApi.setFilterModel(model);
281
219
  }
282
220
  catch (error) {