@cauca-911/datagrid 21.3.0 → 21.3.1

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,133 @@ 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
+ }
839
952
  refreshData() {
840
953
  this.listing.refresh();
841
954
  this.refreshRequested.emit();
@@ -856,18 +969,25 @@ class OdataGridComponent extends BaseDataGridComponent {
856
969
  }
857
970
  refreshFilter() {
858
971
  if (this.filterPredicate) {
859
- this.listing.updateFilters(this.filterPredicate(toLowerCaseAndWithoutDiacritics(this.header.filterControl?.value)));
972
+ this.listing.updateFilters(this.filterPredicate(toLowerCaseAndWithoutDiacritics(this.header?.filterControl?.value)));
860
973
  }
861
974
  }
862
975
  setUpFilterPredicate() {
976
+ let filters = this.filterPredicate?.('') ?? [];
977
+ if (this.canPersistState()) {
978
+ filters = this.restoreStateBeforeInitialLoad() ?? filters;
979
+ }
863
980
  this.listing.initialize({
864
981
  baseUrl: this.baseUrl,
865
982
  resourceName: this.resourceName,
866
983
  paginator: this.paginator,
867
984
  sort: this.sort,
868
985
  initialSort: this.baseSort,
869
- filters: this.filterPredicate?.('') ?? [],
986
+ filters,
870
987
  });
988
+ if (this.canPersistState()) {
989
+ this.setUpUrlStatePersistence();
990
+ }
871
991
  }
872
992
  setUpSort(sort) {
873
993
  this.sort = sort;
@@ -900,8 +1020,89 @@ class OdataGridComponent extends BaseDataGridComponent {
900
1020
  }
901
1021
  this.allDisplayedColumns.set([...columns]);
902
1022
  }
1023
+ canPersistState() {
1024
+ return this.persistState() && !!this.router && !!this.route;
1025
+ }
1026
+ restoreStateBeforeInitialLoad() {
1027
+ this.defaultPageSize = this.paginator?.pageSize;
1028
+ const restoredState = fromQueryParams(this.route.snapshot.queryParamMap, this.stateKey());
1029
+ this.applySortAndPage(restoredState);
1030
+ this.pendingRestoredFilter = restoredState.filter;
1031
+ if (restoredState.filter === undefined || !this.filterPredicate) {
1032
+ return undefined;
1033
+ }
1034
+ return this.filterPredicate(toLowerCaseAndWithoutDiacritics(restoredState.filter));
1035
+ }
1036
+ setUpUrlStatePersistence() {
1037
+ this.lastWrittenParams = toQueryParams(fromQueryParams(this.route.snapshot.queryParamMap, this.stateKey()), this.stateKey());
1038
+ this.sort?.sortChange
1039
+ .pipe(takeUntilDestroyed(this.destroyRef))
1040
+ .subscribe(() => this.writeCurrentUrlState());
1041
+ this.paginator?.page
1042
+ .pipe(takeUntilDestroyed(this.destroyRef))
1043
+ .subscribe(() => this.writeCurrentUrlState());
1044
+ this.route.queryParamMap
1045
+ .pipe(takeUntilDestroyed(this.destroyRef))
1046
+ .subscribe((paramMap) => this.reactToExternalUrlStateChange(paramMap));
1047
+ }
1048
+ applySortAndPage(state) {
1049
+ if (state.sort && this.sort) {
1050
+ this.sort.active = state.sort;
1051
+ this.sort.direction = state.direction ?? '';
1052
+ }
1053
+ if (this.paginator) {
1054
+ if (state.pageIndex !== undefined) {
1055
+ this.paginator.pageIndex = state.pageIndex;
1056
+ }
1057
+ if (state.pageSize !== undefined) {
1058
+ this.paginator.pageSize = state.pageSize;
1059
+ }
1060
+ }
1061
+ }
1062
+ currentUrlState() {
1063
+ const pageSize = this.paginator?.pageSize;
1064
+ return {
1065
+ filter: this.header?.filterControl.value || undefined,
1066
+ sort: this.sort?.active || undefined,
1067
+ direction: this.sort?.direction || undefined,
1068
+ pageIndex: this.paginator?.pageIndex,
1069
+ pageSize: pageSize !== undefined && pageSize !== this.defaultPageSize ? pageSize : undefined,
1070
+ };
1071
+ }
1072
+ writeCurrentUrlState() {
1073
+ const params = toQueryParams(this.currentUrlState(), this.stateKey());
1074
+ if (paramsEqual(params, this.lastWrittenParams)) {
1075
+ return;
1076
+ }
1077
+ this.lastWrittenParams = params;
1078
+ this.router.navigate([], {
1079
+ relativeTo: this.route,
1080
+ queryParams: { ...emptyQueryParams(this.stateKey()), ...params },
1081
+ queryParamsHandling: 'merge',
1082
+ replaceUrl: true,
1083
+ });
1084
+ }
1085
+ reactToExternalUrlStateChange(paramMap) {
1086
+ const state = fromQueryParams(paramMap, this.stateKey());
1087
+ const params = toQueryParams(state, this.stateKey());
1088
+ if (paramsEqual(params, this.lastWrittenParams)) {
1089
+ return;
1090
+ }
1091
+ this.lastWrittenParams = params;
1092
+ this.applySortAndPage(state);
1093
+ if (this.header) {
1094
+ this.header.filterControl.setValue(state.filter ?? '', { emitEvent: false });
1095
+ this.header.showClearButton = !!state.filter;
1096
+ }
1097
+ if (this.filterPredicate) {
1098
+ this.refreshFilter();
1099
+ }
1100
+ else {
1101
+ this.listing.forceReloadFilter();
1102
+ }
1103
+ }
903
1104
  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:
1105
+ 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
1106
  // Material table structure:
906
1107
  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
1108
  // Material extras
@@ -927,7 +1128,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
927
1128
  DataGridProgressSpinerComponent,
928
1129
  StatusMessageComponent,
929
1130
  ], 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: [{
1131
+ }], 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
1132
  type: Input
932
1133
  }], filterPredicate: [{
933
1134
  type: Input
@@ -951,6 +1152,84 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
951
1152
  args: [MatPaginator, { static: true }]
952
1153
  }] } });
953
1154
 
1155
+ /**
1156
+ * Re-injects a list page's remembered grid query params (filter/sort/page, written by
1157
+ * `OdataGridComponent` when `persistState` is enabled) when the user navigates back to that
1158
+ * list page from one of its own descendant (detail) routes. Any other way of reaching the
1159
+ * list page (menu, bookmark, direct link, refresh) leaves it clean, because the previous URL
1160
+ * won't be a descendant of the list path.
1161
+ */
1162
+ class GridStateRestorationService {
1163
+ constructor() {
1164
+ this.router = inject(Router);
1165
+ this.rememberedParamsByPath = new Map();
1166
+ this.previousPath = null;
1167
+ this.router.events
1168
+ .pipe(filter((event) => event instanceof NavigationEnd))
1169
+ .subscribe((event) => this.onNavigationEnd(event));
1170
+ }
1171
+ onNavigationEnd(event) {
1172
+ const currentPath = this.pathOnly(event.urlAfterRedirects);
1173
+ const currentParams = this.parseParams(event.urlAfterRedirects);
1174
+ const previousPath = this.previousPath;
1175
+ this.previousPath = currentPath;
1176
+ if (Object.keys(currentParams).length > 0) {
1177
+ this.rememberedParamsByPath.set(currentPath, currentParams);
1178
+ return;
1179
+ }
1180
+ if (this.restoreIfReturningFromDescendant(currentPath, previousPath)) {
1181
+ return;
1182
+ }
1183
+ this.rememberedParamsByPath.delete(currentPath);
1184
+ }
1185
+ restoreIfReturningFromDescendant(currentPath, previousPath) {
1186
+ if (!previousPath || !previousPath.startsWith(`${currentPath}/`)) {
1187
+ return false;
1188
+ }
1189
+ const remembered = this.rememberedParamsByPath.get(currentPath);
1190
+ if (!remembered) {
1191
+ return false;
1192
+ }
1193
+ this.router.navigate([currentPath], { queryParams: remembered, replaceUrl: true });
1194
+ return true;
1195
+ }
1196
+ pathOnly(url) {
1197
+ return url.split('?')[0].split('#')[0];
1198
+ }
1199
+ parseParams(url) {
1200
+ const queryString = url.split('?')[1]?.split('#')[0];
1201
+ if (!queryString) {
1202
+ return {};
1203
+ }
1204
+ const params = {};
1205
+ new URLSearchParams(queryString).forEach((value, key) => {
1206
+ params[key] = value;
1207
+ });
1208
+ return params;
1209
+ }
1210
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
1211
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, providedIn: 'root' }); }
1212
+ }
1213
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: GridStateRestorationService, decorators: [{
1214
+ type: Injectable,
1215
+ args: [{
1216
+ providedIn: 'root',
1217
+ }]
1218
+ }], ctorParameters: () => [] });
1219
+
1220
+ /**
1221
+ * Activates the grid list-state restoration feature for the whole application. Add this once
1222
+ * in the app's root providers; it has no effect on pages whose grids don't opt in via
1223
+ * `[persistState]="true"`.
1224
+ */
1225
+ function provideGridStateRestoration() {
1226
+ return makeEnvironmentProviders([
1227
+ provideAppInitializer(() => {
1228
+ inject(GridStateRestorationService);
1229
+ }),
1230
+ ]);
1231
+ }
1232
+
954
1233
  class ColorDataGridColumnComponent extends BaseGridColumnComponent {
955
1234
  constructor(odata, grid) {
956
1235
  super((grid ?? odata)?.table);
@@ -1429,5 +1708,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImpor
1429
1708
  * Generated bundle index. Do not edit.
1430
1709
  */
1431
1710
 
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 };
1711
+ 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
1712
  //# sourceMappingURL=cauca-911-datagrid.mjs.map