@cauca-911/datagrid 21.3.0 → 21.3.2

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,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { ViewChild, HostBinding, Input, Optional, ViewEncapsulation, Component, EventEmitter, input, Output, Injectable, InjectionToken, signal, ContentChild, ContentChildren, Inject, ChangeDetectionStrategy, QueryList, inject, output, TemplateRef, forwardRef, computed, model } from '@angular/core';
2
+ import { ViewChild, HostBinding, Input, Optional, ViewEncapsulation, Component, EventEmitter, input, Output, Injectable, InjectionToken, signal, ContentChild, ContentChildren, Inject, ChangeDetectionStrategy, QueryList, inject, output, TemplateRef, DestroyRef, makeEnvironmentProviders, provideAppInitializer, forwardRef, computed, model } from '@angular/core';
3
3
  import * as i1 from '@angular/material/table';
4
4
  import { MatColumnDef, MatCellDef, MatHeaderCellDef, MatFooterCellDef, MatTable, MatHeaderRowDef, MatFooterRowDef, MatNoDataRow, MatRowDef, MatTableDataSource, MatHeaderCell, MatFooterCell, MatRow, MatHeaderRow, MatFooterRow, MatCell } from '@angular/material/table';
5
5
  import { CDK_DRAG_CONFIG, moveItemInArray, CdkDropList, CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
@@ -16,7 +16,7 @@ import { MatInputModule } from '@angular/material/input';
16
16
  import { IconButtonComponent, RaisedButtonComponent, BasicButtonComponent, StrokedButtonComponent, MenuItemComponent, StatusMessageComponent, IconComponent, Iso8601TimeSpanPipe } from '@cauca-911/material';
17
17
  import * as i1$3 from '@ngx-translate/core';
18
18
  import { TranslatePipe } from '@ngx-translate/core';
19
- import { Subject, debounceTime, takeUntil, BehaviorSubject, of, distinctUntilChanged } from 'rxjs';
19
+ import { Subject, debounceTime, takeUntil, BehaviorSubject, of, distinctUntilChanged, filter } from 'rxjs';
20
20
  import { MatSort, MatSortHeader } from '@angular/material/sort';
21
21
  import { CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
22
22
  import * as i1$4 from '@angular/material/card';
@@ -28,10 +28,11 @@ import { MatMenuModule } from '@angular/material/menu';
28
28
  import { NgTemplateOutlet, NgStyle, NgClass, DatePipe } from '@angular/common';
29
29
  import * as i1$5 from '@angular/material/progress-spinner';
30
30
  import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
31
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
32
+ import { Router, ActivatedRoute, NavigationEnd, RouterLink } from '@angular/router';
31
33
  import { ODataDataSource } from 'odata-data-source';
32
34
  import * as i1$6 from '@angular/common/http';
33
35
  import { MatTooltip } from '@angular/material/tooltip';
34
- import { RouterLink } from '@angular/router';
35
36
  import dayjs from 'dayjs';
36
37
 
37
38
  class DataGridRowDropEvent {
@@ -821,21 +822,141 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
821
822
  }]
822
823
  }], ctorParameters: () => [{ type: i1$6.HttpClient }] });
823
824
 
825
+ const PARAM_BASE_NAMES = {
826
+ filter: 'filter',
827
+ sort: 'sort',
828
+ direction: 'dir',
829
+ pageIndex: 'page',
830
+ pageSize: 'size',
831
+ };
832
+ /**
833
+ * Builds the query param name for a given base name and optional grid state key.
834
+ * With a key, the scheme is `<key><CapitalizedBaseName>` (e.g. key "alerts" + base "filter" -> "alertsFilter").
835
+ * Without a key, the plain base name is used (e.g. "filter").
836
+ */
837
+ function paramName(baseName, key) {
838
+ if (!key) {
839
+ return baseName;
840
+ }
841
+ return `${key}${baseName.charAt(0).toUpperCase()}${baseName.slice(1)}`;
842
+ }
843
+ function toQueryParams(state, key = '') {
844
+ const params = {};
845
+ if (state.filter) {
846
+ params[paramName(PARAM_BASE_NAMES.filter, key)] = state.filter;
847
+ }
848
+ if (state.sort) {
849
+ params[paramName(PARAM_BASE_NAMES.sort, key)] = state.sort;
850
+ }
851
+ if (state.direction) {
852
+ params[paramName(PARAM_BASE_NAMES.direction, key)] = state.direction;
853
+ }
854
+ if (state.pageIndex) {
855
+ params[paramName(PARAM_BASE_NAMES.pageIndex, key)] = state.pageIndex;
856
+ }
857
+ if (state.pageSize) {
858
+ params[paramName(PARAM_BASE_NAMES.pageSize, key)] = state.pageSize;
859
+ }
860
+ return params;
861
+ }
862
+ function fromQueryParams(map, key = '') {
863
+ const state = {};
864
+ const filter = map.get(paramName(PARAM_BASE_NAMES.filter, key));
865
+ if (filter) {
866
+ state.filter = filter;
867
+ }
868
+ const sort = map.get(paramName(PARAM_BASE_NAMES.sort, key));
869
+ if (sort) {
870
+ state.sort = sort;
871
+ }
872
+ const direction = map.get(paramName(PARAM_BASE_NAMES.direction, key));
873
+ if (direction === 'asc' || direction === 'desc') {
874
+ state.direction = direction;
875
+ }
876
+ const pageIndex = parsePositiveInteger(map.get(paramName(PARAM_BASE_NAMES.pageIndex, key)));
877
+ if (pageIndex !== undefined) {
878
+ state.pageIndex = pageIndex;
879
+ }
880
+ const pageSize = parsePositiveInteger(map.get(paramName(PARAM_BASE_NAMES.pageSize, key)));
881
+ if (pageSize !== undefined) {
882
+ state.pageSize = pageSize;
883
+ }
884
+ return state;
885
+ }
886
+ /**
887
+ * Returns a Params object with every grid-state param name mapped to `null` for the given key.
888
+ * Combined with `queryParamsHandling: 'merge'`, this lets the grid explicitly clear stale
889
+ * params (e.g. a previously-set filter) instead of leaving them stuck in the URL.
890
+ */
891
+ function emptyQueryParams(key = '') {
892
+ const params = {};
893
+ Object.values(PARAM_BASE_NAMES).forEach((baseName) => {
894
+ params[paramName(baseName, key)] = null;
895
+ });
896
+ return params;
897
+ }
898
+ function paramsEqual(a, b) {
899
+ const aKeys = Object.keys(a).filter((paramKey) => a[paramKey] !== null && a[paramKey] !== undefined);
900
+ const bKeys = Object.keys(b).filter((paramKey) => b[paramKey] !== null && b[paramKey] !== undefined);
901
+ if (aKeys.length !== bKeys.length) {
902
+ return false;
903
+ }
904
+ return aKeys.every((paramKey) => String(a[paramKey]) === String(b[paramKey]));
905
+ }
906
+ function parsePositiveInteger(value) {
907
+ if (!value) {
908
+ return undefined;
909
+ }
910
+ const parsed = Number(value);
911
+ if (Number.isNaN(parsed) || parsed < 0) {
912
+ return undefined;
913
+ }
914
+ return parsed;
915
+ }
916
+
824
917
  class OdataGridComponent extends BaseDataGridComponent {
825
918
  constructor(translateService, listing) {
826
919
  super(translateService, inject(PREFERENCE_SERVICE_TOKEN));
827
920
  this.listing = listing;
828
921
  this.showTableHeader = input(true, ...(ngDevMode ? [{ debugName: "showTableHeader" }] : []));
922
+ /** Master switch: when true, the grid's filter/sort/page are synced with the URL query params and restored when returning from a detail page. Off by default. */
923
+ this.persistState = input(false, ...(ngDevMode ? [{ debugName: "persistState" }] : []));
924
+ /** Optional prefix for the persisted query params, so two independent grids on the same route don't collide (e.g. "alerts" -> "alertsFilter"). */
925
+ this.stateKey = input('', ...(ngDevMode ? [{ debugName: "stateKey" }] : []));
829
926
  this.showHeader = true;
830
927
  this.idField = 'id';
831
928
  this.itemClick = new EventEmitter();
832
929
  this.refreshRequested = new EventEmitter();
833
930
  this.showRefreshButton = false;
834
931
  this.baseUrl = inject(CaucaCoreService).getConfig().apiUrl;
932
+ this.router = inject(Router, { optional: true });
933
+ this.route = inject(ActivatedRoute, { optional: true });
934
+ this.destroyRef = inject(DestroyRef);
935
+ this.lastWrittenParams = {};
835
936
  }
836
937
  ngOnDestroy() {
837
938
  this.listing.destroy();
838
939
  }
940
+ ngAfterViewInit() {
941
+ if (!this.canPersistState() || !this.header) {
942
+ return;
943
+ }
944
+ if (this.pendingRestoredFilter !== undefined) {
945
+ this.header.filterControl.setValue(this.pendingRestoredFilter, { emitEvent: false });
946
+ this.header.showClearButton = !!this.pendingRestoredFilter;
947
+ }
948
+ this.header.filterChange
949
+ .pipe(takeUntilDestroyed(this.destroyRef))
950
+ .subscribe(() => this.writeCurrentUrlState());
951
+ if (this.restoredState && this.hasRestoredState(this.restoredState)) {
952
+ // The initial fetch triggered during ngAfterContentInit (setUpFilterPredicate) can run
953
+ // before the paginator/sort are fully wired to the table, so it may go out with default
954
+ // skip/orderby/filter. Re-applying the restored state here and forcing a reload once the
955
+ // view is fully initialized mirrors the warm-navigation path and guarantees the first
956
+ // real fetch reflects the restored filter/sort/page on a cold (browser refresh) load.
957
+ this.applyUrlStateToGrid(this.restoredState);
958
+ }
959
+ }
839
960
  refreshData() {
840
961
  this.listing.refresh();
841
962
  this.refreshRequested.emit();
@@ -856,18 +977,25 @@ class OdataGridComponent extends BaseDataGridComponent {
856
977
  }
857
978
  refreshFilter() {
858
979
  if (this.filterPredicate) {
859
- this.listing.updateFilters(this.filterPredicate(toLowerCaseAndWithoutDiacritics(this.header.filterControl?.value)));
980
+ this.listing.updateFilters(this.filterPredicate(toLowerCaseAndWithoutDiacritics(this.header?.filterControl?.value)));
860
981
  }
861
982
  }
862
983
  setUpFilterPredicate() {
984
+ let filters = this.filterPredicate?.('') ?? [];
985
+ if (this.canPersistState()) {
986
+ filters = this.restoreStateBeforeInitialLoad() ?? filters;
987
+ }
863
988
  this.listing.initialize({
864
989
  baseUrl: this.baseUrl,
865
990
  resourceName: this.resourceName,
866
991
  paginator: this.paginator,
867
992
  sort: this.sort,
868
993
  initialSort: this.baseSort,
869
- filters: this.filterPredicate?.('') ?? [],
994
+ filters,
870
995
  });
996
+ if (this.canPersistState()) {
997
+ this.setUpUrlStatePersistence();
998
+ }
871
999
  }
872
1000
  setUpSort(sort) {
873
1001
  this.sort = sort;
@@ -900,8 +1028,96 @@ class OdataGridComponent extends BaseDataGridComponent {
900
1028
  }
901
1029
  this.allDisplayedColumns.set([...columns]);
902
1030
  }
1031
+ canPersistState() {
1032
+ return this.persistState() && !!this.router && !!this.route;
1033
+ }
1034
+ restoreStateBeforeInitialLoad() {
1035
+ this.defaultPageSize = this.paginator?.pageSize;
1036
+ const restoredState = fromQueryParams(this.route.snapshot.queryParamMap, this.stateKey());
1037
+ this.applySortAndPage(restoredState);
1038
+ this.pendingRestoredFilter = restoredState.filter;
1039
+ this.restoredState = restoredState;
1040
+ if (restoredState.filter === undefined || !this.filterPredicate) {
1041
+ return undefined;
1042
+ }
1043
+ return this.filterPredicate(toLowerCaseAndWithoutDiacritics(restoredState.filter));
1044
+ }
1045
+ setUpUrlStatePersistence() {
1046
+ this.lastWrittenParams = toQueryParams(fromQueryParams(this.route.snapshot.queryParamMap, this.stateKey()), this.stateKey());
1047
+ this.sort?.sortChange
1048
+ .pipe(takeUntilDestroyed(this.destroyRef))
1049
+ .subscribe(() => this.writeCurrentUrlState());
1050
+ this.paginator?.page
1051
+ .pipe(takeUntilDestroyed(this.destroyRef))
1052
+ .subscribe(() => this.writeCurrentUrlState());
1053
+ this.route.queryParamMap
1054
+ .pipe(takeUntilDestroyed(this.destroyRef))
1055
+ .subscribe((paramMap) => this.reactToExternalUrlStateChange(paramMap));
1056
+ }
1057
+ applySortAndPage(state) {
1058
+ if (state.sort && this.sort) {
1059
+ this.sort.active = state.sort;
1060
+ this.sort.direction = state.direction ?? '';
1061
+ }
1062
+ if (this.paginator) {
1063
+ if (state.pageIndex !== undefined) {
1064
+ this.paginator.pageIndex = state.pageIndex;
1065
+ }
1066
+ if (state.pageSize !== undefined) {
1067
+ this.paginator.pageSize = state.pageSize;
1068
+ }
1069
+ }
1070
+ }
1071
+ currentUrlState() {
1072
+ const pageSize = this.paginator?.pageSize;
1073
+ return {
1074
+ filter: this.header?.filterControl.value || undefined,
1075
+ sort: this.sort?.active || undefined,
1076
+ direction: this.sort?.direction || undefined,
1077
+ pageIndex: this.paginator?.pageIndex,
1078
+ pageSize: pageSize !== undefined && pageSize !== this.defaultPageSize ? pageSize : undefined,
1079
+ };
1080
+ }
1081
+ writeCurrentUrlState() {
1082
+ const params = toQueryParams(this.currentUrlState(), this.stateKey());
1083
+ if (paramsEqual(params, this.lastWrittenParams)) {
1084
+ return;
1085
+ }
1086
+ this.lastWrittenParams = params;
1087
+ this.router.navigate([], {
1088
+ relativeTo: this.route,
1089
+ queryParams: { ...emptyQueryParams(this.stateKey()), ...params },
1090
+ queryParamsHandling: 'merge',
1091
+ replaceUrl: true,
1092
+ });
1093
+ }
1094
+ reactToExternalUrlStateChange(paramMap) {
1095
+ const state = fromQueryParams(paramMap, this.stateKey());
1096
+ const params = toQueryParams(state, this.stateKey());
1097
+ if (paramsEqual(params, this.lastWrittenParams)) {
1098
+ return;
1099
+ }
1100
+ this.lastWrittenParams = params;
1101
+ this.applyUrlStateToGrid(state);
1102
+ }
1103
+ applyUrlStateToGrid(state) {
1104
+ this.applySortAndPage(state);
1105
+ if (this.header) {
1106
+ this.header.filterControl.setValue(state.filter ?? '', { emitEvent: false });
1107
+ this.header.showClearButton = !!state.filter;
1108
+ }
1109
+ if (this.filterPredicate) {
1110
+ this.refreshFilter();
1111
+ }
1112
+ else {
1113
+ this.listing.forceReloadFilter();
1114
+ }
1115
+ }
1116
+ hasRestoredState(state) {
1117
+ return state.filter !== undefined || state.sort !== undefined || state.pageIndex !== undefined || state.pageSize !== undefined;
1118
+ }
903
1119
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: OdataGridComponent, deps: [{ token: i1$3.TranslateService }, { token: OdataListingService }], target: i0.ɵɵFactoryTarget.Component }); }
904
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: OdataGridComponent, isStandalone: true, selector: "cauca-odata-grid", inputs: { showTableHeader: { classPropertyName: "showTableHeader", publicName: "showTableHeader", isSignal: true, isRequired: false, transformFunction: null }, baseSort: { classPropertyName: "baseSort", publicName: "baseSort", isSignal: false, isRequired: false, transformFunction: null }, filterPredicate: { classPropertyName: "filterPredicate", publicName: "filterPredicate", isSignal: false, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: false, isRequired: false, transformFunction: null }, selectedRowId: { classPropertyName: "selectedRowId", publicName: "selectedRowId", isSignal: false, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: false, isRequired: false, transformFunction: null }, showRefreshButton: { classPropertyName: "showRefreshButton", publicName: "showRefreshButton", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", refreshRequested: "refreshRequested" }, providers: [OdataListingService], queries: [{ propertyName: "rowDetailTemplate", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "@if (showHeader) {\n <cauca-data-grid-header\n [disableAddButton]=\"disableAddButton()\"\n [showRefreshButton]=\"showRefreshButton\" (refreshRequest)=\"refreshData()\"\n [showAddButton]=\"showAddButton\"\n [addButtonLabel]=\"addButtonLabel || defaultAddButtonLabel\"\n (addButtonClick)=\"addButtonClick.emit($event)\" (filterChange)=\"applyFilter($event)\">\n <ng-content select=\"[filterRow]\" ngProjectAs=\"[filterRow]\" />\n </cauca-data-grid-header>\n}\n\n<ng-content select=\"[customHeader]\" ngProjectAs=\"[customHeader]\" />\n\n@if (showTableOptions) {\n <cauca-data-grid-table-options [showColumnViewer]=\"showColumnViewer\" [gridName]=\"resourceName\" (densityChanged)=\"onDensityChanged($event)\" (columnsChanged)=\"onColumnsChanged($event)\" [allColumns]=\"regularColumns\">\n <ng-content select=\"[customFilter]\" ngProjectAs=\"[customFilter]\" />\n </cauca-data-grid-table-options>\n}\n\n<div class=\"relative\">\n <table mat-table [dataSource]=\"listing.dataSource\" aria-describedby=\"Data Grid\" multiTemplateDataRows>\n @if (showExpandButton()) {\n <ng-container matColumnDef=\"expand\">\n <th mat-header-cell *matHeaderCellDef></th>\n <td mat-cell *matCellDef=\"let row\">\n <div class=\"align-commands\">\n <cauca-icon-button (buttonClick)=\"onExpand(row)\" color=\"primary\" class=\"button-icon\" [icon]=\"row === expandedElement() ? 'keyboard_arrow_up' : 'keyboard_arrow_down'\" />\n </div>\n </td>\n </ng-container>\n }\n\n <ng-content></ng-content>\n @if(showTableHeader()) {\n <tr mat-header-row *matHeaderRowDef=\"allDisplayedColumns(); sticky: true\"></tr>\n }\n\n <tr mat-row *matRowDef=\"let row; columns: allDisplayedColumns()\"\n [class.showClick]=\"itemClick.observers.length > 0\"\n [class.compact]=\"preference.gridDensity === density.Compact\"\n [class.standard]=\"preference.gridDensity === density.Standard\"\n [class.comfortable]=\"preference.gridDensity === density.Comfortable\"\n [class.selected]=\"selectedRowId === row[idField]\"\n (click)=\"itemClick.emit(row)\"\n onkeypress=\"itemClick.emit(row)\">\n </tr>\n\n <ng-container matColumnDef=\"expandedDetail\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"allDisplayedColumns().length\">\n <div class=\"grid-element-detail-wrapper\" [class.grid-element-detail-wrapper-expanded]=\"row === expandedElement()\">\n <div class=\"grid-element-detail\">\n @if (row === expandedElement()) {\n <template [ngTemplateOutlet]=\"rowDetailTemplate\" [ngTemplateOutletContext]=\"{ row: expandedElement() }\"></template>\n }\n </div>\n </div>\n </td>\n </ng-container>\n\n <tr mat-row *matRowDef=\"let row; columns: ['expandedDetail']\" class=\"grid-detail-row\"></tr>\n\n <ng-container *matNoDataRow>\n @if(listing.loading() === false) {\n <ng-container>\n <tr class=\"mat-row\">\n @if (listing.error() === false) {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"noData\" />\n </th>\n } @else {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"error\" />\n </th>\n }\n </tr>\n </ng-container>\n }\n </ng-container>\n </table>\n\n <cauca-data-grid-progress-spiner [useBackground]=\"false\" [isLoading]=\"listing.loading()\" />\n\n</div>\n\n<mat-paginator [hidePageSize]=\"hidePageSize()\" [pageSizeOptions]=\"[5, 10, 25, 50]\" [pageSize]=\"10\" />\n", styles: ["table{width:100%;table-layout:fixed;--mat-table-header-container-height: 52px;--mat-table-footer-container-height: 48px;--mat-table-row-item-container-height: 48px;--mat-table-header-container-height: 56px}.relative{position:relative}.mat-column-expand{width:72px}cauca-data-grid-progress-spiner{left:50%;top:50%;transform:translate(-50%,-50%);position:absolute;z-index:101}tr.mat-mdc-row{transition:background-color .2s ease-out .1s}tr.mat-mdc-row:hover{background:#f5f5f5}tr.mat-mdc-row.selected{background:#249edb14}tr.mat-mdc-row.selected:hover{background:#249edb1a}cauca-data-grid-header{margin:16px}.compact{line-height:48px!important}.standard{line-height:56px!important}.comfortable{line-height:62px!important}.showClick{cursor:pointer}tr.grid-detail-row{height:0}.grid-element-row td{border-bottom-width:0;background-color:#fafafa}.mat-column-expandedDetail{background-color:#fafafa}.grid-element-detail-wrapper{overflow:hidden;display:grid;grid-template-rows:0fr;grid-template-columns:100%;transition:grid-template-rows 225ms cubic-bezier(.4,0,.2,1)}.grid-element-detail-wrapper-expanded{grid-template-rows:1fr}.grid-element-detail{display:flex;min-height:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type:
1120
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: OdataGridComponent, isStandalone: true, selector: "cauca-odata-grid", inputs: { showTableHeader: { classPropertyName: "showTableHeader", publicName: "showTableHeader", isSignal: true, isRequired: false, transformFunction: null }, persistState: { classPropertyName: "persistState", publicName: "persistState", isSignal: true, isRequired: false, transformFunction: null }, stateKey: { classPropertyName: "stateKey", publicName: "stateKey", isSignal: true, isRequired: false, transformFunction: null }, baseSort: { classPropertyName: "baseSort", publicName: "baseSort", isSignal: false, isRequired: false, transformFunction: null }, filterPredicate: { classPropertyName: "filterPredicate", publicName: "filterPredicate", isSignal: false, isRequired: false, transformFunction: null }, showHeader: { classPropertyName: "showHeader", publicName: "showHeader", isSignal: false, isRequired: false, transformFunction: null }, selectedRowId: { classPropertyName: "selectedRowId", publicName: "selectedRowId", isSignal: false, isRequired: false, transformFunction: null }, idField: { classPropertyName: "idField", publicName: "idField", isSignal: false, isRequired: false, transformFunction: null }, showRefreshButton: { classPropertyName: "showRefreshButton", publicName: "showRefreshButton", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", refreshRequested: "refreshRequested" }, providers: [OdataListingService], queries: [{ propertyName: "rowDetailTemplate", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true, static: true }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "@if (showHeader) {\n <cauca-data-grid-header\n [disableAddButton]=\"disableAddButton()\"\n [showRefreshButton]=\"showRefreshButton\" (refreshRequest)=\"refreshData()\"\n [showAddButton]=\"showAddButton\"\n [addButtonLabel]=\"addButtonLabel || defaultAddButtonLabel\"\n (addButtonClick)=\"addButtonClick.emit($event)\" (filterChange)=\"applyFilter($event)\">\n <ng-content select=\"[filterRow]\" ngProjectAs=\"[filterRow]\" />\n </cauca-data-grid-header>\n}\n\n<ng-content select=\"[customHeader]\" ngProjectAs=\"[customHeader]\" />\n\n@if (showTableOptions) {\n <cauca-data-grid-table-options [showColumnViewer]=\"showColumnViewer\" [gridName]=\"resourceName\" (densityChanged)=\"onDensityChanged($event)\" (columnsChanged)=\"onColumnsChanged($event)\" [allColumns]=\"regularColumns\">\n <ng-content select=\"[customFilter]\" ngProjectAs=\"[customFilter]\" />\n </cauca-data-grid-table-options>\n}\n\n<div class=\"relative\">\n <table mat-table [dataSource]=\"listing.dataSource\" aria-describedby=\"Data Grid\" multiTemplateDataRows>\n @if (showExpandButton()) {\n <ng-container matColumnDef=\"expand\">\n <th mat-header-cell *matHeaderCellDef></th>\n <td mat-cell *matCellDef=\"let row\">\n <div class=\"align-commands\">\n <cauca-icon-button (buttonClick)=\"onExpand(row)\" color=\"primary\" class=\"button-icon\" [icon]=\"row === expandedElement() ? 'keyboard_arrow_up' : 'keyboard_arrow_down'\" />\n </div>\n </td>\n </ng-container>\n }\n\n <ng-content></ng-content>\n @if(showTableHeader()) {\n <tr mat-header-row *matHeaderRowDef=\"allDisplayedColumns(); sticky: true\"></tr>\n }\n\n <tr mat-row *matRowDef=\"let row; columns: allDisplayedColumns()\"\n [class.showClick]=\"itemClick.observers.length > 0\"\n [class.compact]=\"preference.gridDensity === density.Compact\"\n [class.standard]=\"preference.gridDensity === density.Standard\"\n [class.comfortable]=\"preference.gridDensity === density.Comfortable\"\n [class.selected]=\"selectedRowId === row[idField]\"\n (click)=\"itemClick.emit(row)\"\n onkeypress=\"itemClick.emit(row)\">\n </tr>\n\n <ng-container matColumnDef=\"expandedDetail\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"allDisplayedColumns().length\">\n <div class=\"grid-element-detail-wrapper\" [class.grid-element-detail-wrapper-expanded]=\"row === expandedElement()\">\n <div class=\"grid-element-detail\">\n @if (row === expandedElement()) {\n <template [ngTemplateOutlet]=\"rowDetailTemplate\" [ngTemplateOutletContext]=\"{ row: expandedElement() }\"></template>\n }\n </div>\n </div>\n </td>\n </ng-container>\n\n <tr mat-row *matRowDef=\"let row; columns: ['expandedDetail']\" class=\"grid-detail-row\"></tr>\n\n <ng-container *matNoDataRow>\n @if(listing.loading() === false) {\n <ng-container>\n <tr class=\"mat-row\">\n @if (listing.error() === false) {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"noData\" />\n </th>\n } @else {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"error\" />\n </th>\n }\n </tr>\n </ng-container>\n }\n </ng-container>\n </table>\n\n <cauca-data-grid-progress-spiner [useBackground]=\"false\" [isLoading]=\"listing.loading()\" />\n\n</div>\n\n<mat-paginator [hidePageSize]=\"hidePageSize()\" [pageSizeOptions]=\"[5, 10, 25, 50]\" [pageSize]=\"10\" />\n", styles: ["table{width:100%;table-layout:fixed;--mat-table-header-container-height: 52px;--mat-table-footer-container-height: 48px;--mat-table-row-item-container-height: 48px;--mat-table-header-container-height: 56px}.relative{position:relative}.mat-column-expand{width:72px}cauca-data-grid-progress-spiner{left:50%;top:50%;transform:translate(-50%,-50%);position:absolute;z-index:101}tr.mat-mdc-row{transition:background-color .2s ease-out .1s}tr.mat-mdc-row:hover{background:#f5f5f5}tr.mat-mdc-row.selected{background:#249edb14}tr.mat-mdc-row.selected:hover{background:#249edb1a}cauca-data-grid-header{margin:16px}.compact{line-height:48px!important}.standard{line-height:56px!important}.comfortable{line-height:62px!important}.showClick{cursor:pointer}tr.grid-detail-row{height:0}.grid-element-row td{border-bottom-width:0;background-color:#fafafa}.mat-column-expandedDetail{background-color:#fafafa}.grid-element-detail-wrapper{overflow:hidden;display:grid;grid-template-rows:0fr;grid-template-columns:100%;transition:grid-template-rows 225ms cubic-bezier(.4,0,.2,1)}.grid-element-detail-wrapper-expanded{grid-template-rows:1fr}.grid-element-detail{display:flex;min-height:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type:
905
1121
  // Material table structure:
906
1122
  MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: MatColumnDef, selector: "[matColumnDef]", inputs: ["matColumnDef"] }, { kind: "directive", type: MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "directive", type: MatCellDef, selector: "[matCellDef]" }, { kind: "component", type: MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "directive", type: MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "component", type: MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "component", type:
907
1123
  // Material extras
@@ -927,7 +1143,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
927
1143
  DataGridProgressSpinerComponent,
928
1144
  StatusMessageComponent,
929
1145
  ], template: "@if (showHeader) {\n <cauca-data-grid-header\n [disableAddButton]=\"disableAddButton()\"\n [showRefreshButton]=\"showRefreshButton\" (refreshRequest)=\"refreshData()\"\n [showAddButton]=\"showAddButton\"\n [addButtonLabel]=\"addButtonLabel || defaultAddButtonLabel\"\n (addButtonClick)=\"addButtonClick.emit($event)\" (filterChange)=\"applyFilter($event)\">\n <ng-content select=\"[filterRow]\" ngProjectAs=\"[filterRow]\" />\n </cauca-data-grid-header>\n}\n\n<ng-content select=\"[customHeader]\" ngProjectAs=\"[customHeader]\" />\n\n@if (showTableOptions) {\n <cauca-data-grid-table-options [showColumnViewer]=\"showColumnViewer\" [gridName]=\"resourceName\" (densityChanged)=\"onDensityChanged($event)\" (columnsChanged)=\"onColumnsChanged($event)\" [allColumns]=\"regularColumns\">\n <ng-content select=\"[customFilter]\" ngProjectAs=\"[customFilter]\" />\n </cauca-data-grid-table-options>\n}\n\n<div class=\"relative\">\n <table mat-table [dataSource]=\"listing.dataSource\" aria-describedby=\"Data Grid\" multiTemplateDataRows>\n @if (showExpandButton()) {\n <ng-container matColumnDef=\"expand\">\n <th mat-header-cell *matHeaderCellDef></th>\n <td mat-cell *matCellDef=\"let row\">\n <div class=\"align-commands\">\n <cauca-icon-button (buttonClick)=\"onExpand(row)\" color=\"primary\" class=\"button-icon\" [icon]=\"row === expandedElement() ? 'keyboard_arrow_up' : 'keyboard_arrow_down'\" />\n </div>\n </td>\n </ng-container>\n }\n\n <ng-content></ng-content>\n @if(showTableHeader()) {\n <tr mat-header-row *matHeaderRowDef=\"allDisplayedColumns(); sticky: true\"></tr>\n }\n\n <tr mat-row *matRowDef=\"let row; columns: allDisplayedColumns()\"\n [class.showClick]=\"itemClick.observers.length > 0\"\n [class.compact]=\"preference.gridDensity === density.Compact\"\n [class.standard]=\"preference.gridDensity === density.Standard\"\n [class.comfortable]=\"preference.gridDensity === density.Comfortable\"\n [class.selected]=\"selectedRowId === row[idField]\"\n (click)=\"itemClick.emit(row)\"\n onkeypress=\"itemClick.emit(row)\">\n </tr>\n\n <ng-container matColumnDef=\"expandedDetail\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"allDisplayedColumns().length\">\n <div class=\"grid-element-detail-wrapper\" [class.grid-element-detail-wrapper-expanded]=\"row === expandedElement()\">\n <div class=\"grid-element-detail\">\n @if (row === expandedElement()) {\n <template [ngTemplateOutlet]=\"rowDetailTemplate\" [ngTemplateOutletContext]=\"{ row: expandedElement() }\"></template>\n }\n </div>\n </div>\n </td>\n </ng-container>\n\n <tr mat-row *matRowDef=\"let row; columns: ['expandedDetail']\" class=\"grid-detail-row\"></tr>\n\n <ng-container *matNoDataRow>\n @if(listing.loading() === false) {\n <ng-container>\n <tr class=\"mat-row\">\n @if (listing.error() === false) {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"noData\" />\n </th>\n } @else {\n <th class=\"mat-cell data-grid-message\"\n [attr.colspan]=\"allDisplayedColumns().length\">\n <cauca-status-message type=\"error\" />\n </th>\n }\n </tr>\n </ng-container>\n }\n </ng-container>\n </table>\n\n <cauca-data-grid-progress-spiner [useBackground]=\"false\" [isLoading]=\"listing.loading()\" />\n\n</div>\n\n<mat-paginator [hidePageSize]=\"hidePageSize()\" [pageSizeOptions]=\"[5, 10, 25, 50]\" [pageSize]=\"10\" />\n", styles: ["table{width:100%;table-layout:fixed;--mat-table-header-container-height: 52px;--mat-table-footer-container-height: 48px;--mat-table-row-item-container-height: 48px;--mat-table-header-container-height: 56px}.relative{position:relative}.mat-column-expand{width:72px}cauca-data-grid-progress-spiner{left:50%;top:50%;transform:translate(-50%,-50%);position:absolute;z-index:101}tr.mat-mdc-row{transition:background-color .2s ease-out .1s}tr.mat-mdc-row:hover{background:#f5f5f5}tr.mat-mdc-row.selected{background:#249edb14}tr.mat-mdc-row.selected:hover{background:#249edb1a}cauca-data-grid-header{margin:16px}.compact{line-height:48px!important}.standard{line-height:56px!important}.comfortable{line-height:62px!important}.showClick{cursor:pointer}tr.grid-detail-row{height:0}.grid-element-row td{border-bottom-width:0;background-color:#fafafa}.mat-column-expandedDetail{background-color:#fafafa}.grid-element-detail-wrapper{overflow:hidden;display:grid;grid-template-rows:0fr;grid-template-columns:100%;transition:grid-template-rows 225ms cubic-bezier(.4,0,.2,1)}.grid-element-detail-wrapper-expanded{grid-template-rows:1fr}.grid-element-detail{display:flex;min-height:0}\n"] }]
930
- }], ctorParameters: () => [{ type: i1$3.TranslateService }, { type: OdataListingService }], propDecorators: { showTableHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTableHeader", required: false }] }], baseSort: [{
1146
+ }], ctorParameters: () => [{ type: i1$3.TranslateService }, { type: OdataListingService }], propDecorators: { showTableHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTableHeader", required: false }] }], persistState: [{ type: i0.Input, args: [{ isSignal: true, alias: "persistState", required: false }] }], stateKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateKey", required: false }] }], baseSort: [{
931
1147
  type: Input
932
1148
  }], filterPredicate: [{
933
1149
  type: Input
@@ -951,6 +1167,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
951
1167
  args: [MatPaginator, { static: true }]
952
1168
  }] } });
953
1169
 
1170
+ /**
1171
+ * Re-injects a list page's remembered grid query params (filter/sort/page, written by
1172
+ * `OdataGridComponent` when `persistState` is enabled) when the user navigates back to that
1173
+ * list page from one of its own descendant (detail) routes. Any other way of reaching the
1174
+ * list page (menu, bookmark, direct link, refresh) leaves it clean, because the previous URL
1175
+ * won't be a descendant of the list path.
1176
+ */
1177
+ class GridStateRestorationService {
1178
+ constructor() {
1179
+ this.router = inject(Router);
1180
+ this.rememberedParamsByPath = new Map();
1181
+ this.previousPath = null;
1182
+ this.router.events
1183
+ .pipe(filter((event) => event instanceof NavigationEnd))
1184
+ .subscribe((event) => this.onNavigationEnd(event));
1185
+ }
1186
+ onNavigationEnd(event) {
1187
+ const currentPath = this.pathOnly(event.urlAfterRedirects);
1188
+ const currentParams = this.parseParams(event.urlAfterRedirects);
1189
+ const previousPath = this.previousPath;
1190
+ this.previousPath = currentPath;
1191
+ if (Object.keys(currentParams).length > 0) {
1192
+ this.rememberedParamsByPath.set(currentPath, currentParams);
1193
+ return;
1194
+ }
1195
+ if (this.restoreIfReturningFromDescendant(currentPath, previousPath)) {
1196
+ return;
1197
+ }
1198
+ this.rememberedParamsByPath.delete(currentPath);
1199
+ }
1200
+ restoreIfReturningFromDescendant(currentPath, previousPath) {
1201
+ if (!previousPath || !previousPath.startsWith(`${currentPath}/`)) {
1202
+ return false;
1203
+ }
1204
+ const remembered = this.rememberedParamsByPath.get(currentPath);
1205
+ if (!remembered) {
1206
+ return false;
1207
+ }
1208
+ this.router.navigate([currentPath], { queryParams: remembered, replaceUrl: true });
1209
+ return true;
1210
+ }
1211
+ pathOnly(url) {
1212
+ return url.split('?')[0].split('#')[0];
1213
+ }
1214
+ parseParams(url) {
1215
+ const queryString = url.split('?')[1]?.split('#')[0];
1216
+ if (!queryString) {
1217
+ return {};
1218
+ }
1219
+ const params = {};
1220
+ new URLSearchParams(queryString).forEach((value, key) => {
1221
+ params[key] = value;
1222
+ });
1223
+ return params;
1224
+ }
1225
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1226
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, providedIn: 'root' }); }
1227
+ }
1228
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, decorators: [{
1229
+ type: Injectable,
1230
+ args: [{
1231
+ providedIn: 'root',
1232
+ }]
1233
+ }], ctorParameters: () => [] });
1234
+
1235
+ /**
1236
+ * Activates the grid list-state restoration feature for the whole application. Add this once
1237
+ * in the app's root providers; it has no effect on pages whose grids don't opt in via
1238
+ * `[persistState]="true"`.
1239
+ */
1240
+ function provideGridStateRestoration() {
1241
+ return makeEnvironmentProviders([
1242
+ provideAppInitializer(() => {
1243
+ inject(GridStateRestorationService);
1244
+ }),
1245
+ ]);
1246
+ }
1247
+
954
1248
  class ColorDataGridColumnComponent extends BaseGridColumnComponent {
955
1249
  constructor(odata, grid) {
956
1250
  super((grid ?? odata)?.table);
@@ -1429,5 +1723,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1429
1723
  * Generated bundle index. Do not edit.
1430
1724
  */
1431
1725
 
1432
- export { BaseGridColumnComponent, ColorDataGridColumnComponent, DataGridCheckColumnComponent, DataGridColumnComponent, DataGridCommandIconColumnComponent, DataGridCommandsColumnComponent, DataGridComponent, DataGridHeaderComponent, DataGridIconColumnComponent, DataGridMenuColumnComponent, DataGridRowDropEvent, DateGridColumnComponent, Density, GridConfiguration, OdataGridComponent, OdataListingService, PREFERENCE_SERVICE_TOKEN, Preference, SortableDataGridCheckColumnComponent, SortableDataGridColumnComponent, TimeSpanDataGridColumnComponent, TranslationGridColumnComponent, provideCaucaGrid };
1726
+ export { BaseGridColumnComponent, ColorDataGridColumnComponent, DataGridCheckColumnComponent, DataGridColumnComponent, DataGridCommandIconColumnComponent, DataGridCommandsColumnComponent, DataGridComponent, DataGridHeaderComponent, DataGridIconColumnComponent, DataGridMenuColumnComponent, DataGridRowDropEvent, DateGridColumnComponent, Density, GridConfiguration, GridStateRestorationService, OdataGridComponent, OdataListingService, PREFERENCE_SERVICE_TOKEN, Preference, SortableDataGridCheckColumnComponent, SortableDataGridColumnComponent, TimeSpanDataGridColumnComponent, TranslationGridColumnComponent, emptyQueryParams, fromQueryParams, paramsEqual, provideCaucaGrid, provideGridStateRestoration, toQueryParams };
1433
1727
  //# sourceMappingURL=cauca-911-datagrid.mjs.map